Skip to content

Commit 1e84417

Browse files
committed
feat: sync opened projects through server events
1 parent f7482cc commit 1e84417

18 files changed

Lines changed: 943 additions & 157 deletions

File tree

packages/app/src/app.tsx

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ import { LanguageProvider, type Locale, useLanguage } from "@/context/language"
3737
import { LayoutProvider } from "@/context/layout"
3838
import { ModelsProvider } from "@/context/models"
3939
import { NotificationProvider } from "@/context/notification"
40+
import { OpenedProjectsProvider } from "@/context/opened-projects"
4041
import { PermissionProvider } from "@/context/permission"
4142
import { PromptProvider } from "@/context/prompt"
4243
import { ServerConnection, ServerProvider, serverName, useServer } from "@/context/server"
@@ -310,16 +311,18 @@ export function AppInterface(props: {
310311
<QueryProvider>
311312
<GlobalSDKProvider>
312313
<GlobalSyncProvider>
313-
<Dynamic
314-
component={props.router ?? Router}
315-
root={(routerProps) => <RouterRoot appChildren={props.children}>{routerProps.children}</RouterRoot>}
316-
>
317-
<Route path="/" component={HomeRoute} />
318-
<Route path="/:dir" component={DirectoryLayout}>
319-
<Route path="/" component={SessionIndexRoute} />
320-
<Route path="/session/:id?" component={SessionRoute} />
321-
</Route>
322-
</Dynamic>
314+
<OpenedProjectsProvider>
315+
<Dynamic
316+
component={props.router ?? Router}
317+
root={(routerProps) => <RouterRoot appChildren={props.children}>{routerProps.children}</RouterRoot>}
318+
>
319+
<Route path="/" component={HomeRoute} />
320+
<Route path="/:dir" component={DirectoryLayout}>
321+
<Route path="/" component={SessionIndexRoute} />
322+
<Route path="/session/:id?" component={SessionRoute} />
323+
</Route>
324+
</Dynamic>
325+
</OpenedProjectsProvider>
323326
</GlobalSyncProvider>
324327
</GlobalSDKProvider>
325328
</QueryProvider>

packages/app/src/components/dialog-edit-project.tsx

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@ import { Icon } from "@opencode-ai/ui/icon"
77
import { createMemo, For, Show } from "solid-js"
88
import { createStore } from "solid-js/store"
99
import { useGlobalSDK } from "@/context/global-sdk"
10-
import { useGlobalSync } from "@/context/global-sync"
1110
import { type LocalProject, getAvatarColors } from "@/context/layout"
11+
import { useOpenedProjects } from "@/context/opened-projects"
1212
import { getFilename } from "@opencode-ai/core/util/path"
1313
import { Avatar } from "@opencode-ai/ui/avatar"
1414
import { useLanguage } from "@/context/language"
@@ -19,7 +19,7 @@ const AVATAR_COLOR_KEYS = ["pink", "mint", "orange", "purple", "cyan", "lime"] a
1919
export function DialogEditProject(props: { project: LocalProject }) {
2020
const dialog = useDialog()
2121
const globalSDK = useGlobalSDK()
22-
const globalSync = useGlobalSync()
22+
const openedProjects = useOpenedProjects()
2323
const language = useLanguage()
2424

2525
const folderName = createMemo(() => getFilename(props.project.worktree))
@@ -77,6 +77,16 @@ export function DialogEditProject(props: { project: LocalProject }) {
7777
const name = store.name.trim() === folderName() ? "" : store.name.trim()
7878
const start = store.startup.trim()
7979

80+
// Always write name/icon to config — this is the primary source for cross-client sync
81+
openedProjects.updateMeta(props.project.worktree, {
82+
name: name || undefined,
83+
icon: {
84+
color: store.color || undefined,
85+
override: store.iconOverride || undefined,
86+
},
87+
})
88+
89+
// Also update the project database when we have an ID (needed for startup commands)
8090
if (props.project.id && props.project.id !== "global") {
8191
await globalSDK.client.project.update({
8292
projectID: props.project.id,
@@ -85,16 +95,8 @@ export function DialogEditProject(props: { project: LocalProject }) {
8595
icon: { color: store.color || "", override: store.iconOverride || "" },
8696
commands: { start },
8797
})
88-
globalSync.project.icon(props.project.worktree, store.iconOverride || undefined)
89-
dialog.close()
90-
return
9198
}
9299

93-
globalSync.project.meta(props.project.worktree, {
94-
name,
95-
icon: { color: store.color || undefined, override: store.iconOverride || undefined },
96-
commands: { start: start || undefined },
97-
})
98100
dialog.close()
99101
},
100102
}))

packages/app/src/context/layout.tsx

Lines changed: 33 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,7 @@ import { batch, createEffect, createMemo, onCleanup, onMount, type Accessor } fr
33
import { createSimpleContext } from "@opencode-ai/ui/context"
44
import { makeEventListener } from "@solid-primitives/event-listener"
55
import { useGlobalSync } from "./global-sync"
6-
import { useGlobalSDK } from "./global-sdk"
7-
import { useServer } from "./server"
6+
import { useOpenedProjects } from "./opened-projects"
87
import { usePlatform } from "./platform"
98
import { Project } from "@opencode-ai/sdk/v2"
109
import { Persist, persisted, removePersisted } from "@/utils/persist"
@@ -135,9 +134,8 @@ const normalizeStoredSessionTabs = (key: string, tabs: SessionTabs) => {
135134
export const { use: useLayout, provider: LayoutProvider } = createSimpleContext({
136135
name: "Layout",
137136
init: () => {
138-
const globalSdk = useGlobalSDK()
139137
const globalSync = useGlobalSync()
140-
const server = useServer()
138+
const openedProjects = useOpenedProjects()
141139
const platform = usePlatform()
142140

143141
const isRecord = (value: unknown): value is Record<string, unknown> =>
@@ -384,21 +382,26 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
384382
return available[Math.floor(Math.random() * available.length)]
385383
}
386384

387-
function enrich(project: { worktree: string; expanded: boolean }) {
385+
function enrich(project: { worktree: string; expanded: boolean; name?: string; icon?: { color?: string; override?: string } }) {
388386
const [childStore] = globalSync.child(project.worktree, { bootstrap: false })
389387
const projectID = childStore.project
390-
const metadata = projectID
388+
const dbProject = projectID
391389
? globalSync.data.project.find((x) => x.id === projectID)
392390
: globalSync.data.project.find((x) => x.worktree === project.worktree)
393391

394-
// Preserve local icon override from per-workspace localStorage cache (childStore.icon).
395-
// Without this, different subdirectories of the same git repo would share the same
396-
// icon from the database instead of using their individual overrides.
397-
const base = { ...metadata, ...project }
398-
if (childStore.icon) {
399-
return { ...base, icon: { ...base.icon, override: childStore.icon } }
400-
}
401-
return base
392+
// Config data (name, icon.color, icon.override) is authoritative for display.
393+
// Database data supplements with id, sandboxes, icon.url (auto-discovered favicon).
394+
return {
395+
...dbProject,
396+
worktree: project.worktree,
397+
expanded: project.expanded,
398+
name: project.name ?? dbProject?.name,
399+
icon: {
400+
url: dbProject?.icon?.url,
401+
color: project.icon?.color,
402+
override: project.icon?.override ?? childStore.icon,
403+
},
404+
} as LocalProject
402405
}
403406

404407
const roots = createMemo(() => {
@@ -435,27 +438,27 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
435438
}
436439

437440
createEffect(() => {
438-
const projects = server.projects.list()
441+
const projects = openedProjects.list()
439442
const seen = new Set(projects.map((project) => project.worktree))
440443

441444
batch(() => {
442445
for (const project of projects) {
443446
const root = rootFor(project.worktree)
444447
if (root === project.worktree) continue
445448

446-
server.projects.close(project.worktree)
449+
openedProjects.close(project.worktree)
447450

448451
if (!seen.has(root)) {
449-
server.projects.open(root)
452+
openedProjects.open(root)
450453
seen.add(root)
451454
}
452455

453-
if (project.expanded) server.projects.expand(root)
456+
if (project.expanded) openedProjects.expand(root)
454457
}
455458
})
456459
})
457460

458-
const enriched = createMemo(() => server.projects.list().map(enrich))
461+
const enriched = createMemo(() => openedProjects.list().map(enrich))
459462
const list = createMemo(() => {
460463
const projects = enriched()
461464
return projects.map((project) => {
@@ -501,22 +504,13 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
501504
used.add(color)
502505
setColors(worktree, color)
503506
}
504-
if (!project.id) continue
505507

506508
const requested = colorRequested.get(worktree)
507509
if (requested === color) continue
508510
colorRequested.set(worktree, color)
509511

510-
if (project.id === "global") {
511-
globalSync.project.meta(worktree, { icon: { color } })
512-
continue
513-
}
514-
515-
void globalSdk.client.project
516-
.update({ projectID: project.id, directory: worktree, icon: { color } })
517-
.catch(() => {
518-
if (colorRequested.get(worktree) === color) colorRequested.delete(worktree)
519-
})
512+
// Write color to config so all clients stay in sync
513+
openedProjects.updateMeta(worktree, { icon: { color } })
520514
}
521515
})
522516

@@ -529,7 +523,7 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
529523
sessionTimer = window.setTimeout(() => {
530524
sessionTimer = undefined
531525
void Promise.all(
532-
server.projects.list().map((project) => {
526+
openedProjects.list().map((project) => {
533527
return globalSync.project.loadSessions(project.worktree)
534528
}),
535529
)
@@ -558,21 +552,23 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
558552
list,
559553
open(directory: string) {
560554
const root = rootFor(directory)
561-
if (server.projects.list().find((x) => x.worktree === root)) return
555+
if (openedProjects.list().find((x) => x.worktree === root)) return
556+
// Bootstrap with bootstrap: true to trigger project discovery on the backend
557+
globalSync.child(root, { bootstrap: true })
562558
void globalSync.project.loadSessions(root)
563-
server.projects.open(root)
559+
openedProjects.open(root)
564560
},
565561
close(directory: string) {
566-
server.projects.close(directory)
562+
openedProjects.close(directory)
567563
},
568564
expand(directory: string) {
569-
server.projects.expand(directory)
565+
openedProjects.expand(directory)
570566
},
571567
collapse(directory: string) {
572-
server.projects.collapse(directory)
568+
openedProjects.collapse(directory)
573569
},
574570
move(directory: string, toIndex: number) {
575-
server.projects.move(directory, toIndex)
571+
openedProjects.move(directory, toIndex)
576572
},
577573
},
578574
sidebar: {

0 commit comments

Comments
 (0)