diff options
| author | Luke Parker <[email protected]> | 2026-04-30 08:39:19 +1000 |
|---|---|---|
| committer | GitHub <[email protected]> | 2026-04-29 22:39:19 +0000 |
| commit | d7b7be1909d614a4022b345bdbeef0c1ec32e159 (patch) | |
| tree | b25640c571a0333b62cf7f06a0ee7ea24b8d3c68 /packages/app/src/pages | |
| parent | a740d2c66782ef3371146cd55d70920ae9b94daf (diff) | |
| download | opencode-d7b7be1909d614a4022b345bdbeef0c1ec32e159.tar.gz opencode-d7b7be1909d614a4022b345bdbeef0c1ec32e159.zip | |
fix(desktop): Path mismatches cause sessions missing + strong ID + existing data fix (#25013)
Diffstat (limited to 'packages/app/src/pages')
| -rw-r--r-- | packages/app/src/pages/layout.tsx | 76 | ||||
| -rw-r--r-- | packages/app/src/pages/layout/helpers.test.ts | 16 | ||||
| -rw-r--r-- | packages/app/src/pages/layout/helpers.ts | 17 | ||||
| -rw-r--r-- | packages/app/src/pages/layout/sidebar-workspace.tsx | 5 |
4 files changed, 51 insertions, 63 deletions
diff --git a/packages/app/src/pages/layout.tsx b/packages/app/src/pages/layout.tsx index d9ce87a02..27eae67c0 100644 --- a/packages/app/src/pages/layout.tsx +++ b/packages/app/src/pages/layout.tsx @@ -64,14 +64,8 @@ import { DebugBar } from "@/components/debug-bar" import { Titlebar } from "@/components/titlebar" import { useServer } from "@/context/server" import { useLanguage, type Locale } from "@/context/language" -import { - displayName, - effectiveWorkspaceOrder, - errorMessage, - latestRootSession, - sortedRootSessions, - workspaceKey, -} from "./layout/helpers" +import { pathKey } from "@/utils/path-key" +import { displayName, effectiveWorkspaceOrder, errorMessage, latestRootSession, sortedRootSessions } from "./layout/helpers" import { collectNewSessionDeepLinks, collectOpenProjectDeepLinks, @@ -164,7 +158,7 @@ export default function Layout(props: ParentProps) { const editor = createInlineEditorController() const setBusy = (directory: string, value: boolean) => { - const key = workspaceKey(directory) + const key = pathKey(directory) if (value) { setState("busyWorkspaces", key, true) return @@ -176,7 +170,7 @@ export default function Layout(props: ParentProps) { }), ) } - const isBusy = (directory: string) => !!state.busyWorkspaces[workspaceKey(directory)] + const isBusy = (directory: string) => !!state.busyWorkspaces[pathKey(directory)] const navLeave = { current: undefined as number | undefined } const sortNow = () => state.sortNow let sizet: number | undefined @@ -497,8 +491,8 @@ export default function Layout(props: ParentProps) { } const currentSession = params.id - if (workspaceKey(directory) === workspaceKey(currentDir()) && props.sessionID === currentSession) return - if (workspaceKey(directory) === workspaceKey(currentDir()) && session?.parentID === currentSession) return + if (pathKey(directory) === pathKey(currentDir()) && props.sessionID === currentSession) return + if (pathKey(directory) === pathKey(currentDir()) && session?.parentID === currentSession) return dismissSessionAlert(sessionKey) @@ -556,14 +550,14 @@ export default function Layout(props: ParentProps) { const currentProject = createMemo(() => { const directory = currentDir() if (!directory) return - const key = workspaceKey(directory) + const key = pathKey(directory) const projects = layout.projects.list() - const sandbox = projects.find((p) => p.sandboxes?.some((item) => workspaceKey(item) === key)) + const sandbox = projects.find((p) => p.sandboxes?.some((item) => pathKey(item) === key)) if (sandbox) return sandbox - const direct = projects.find((p) => workspaceKey(p.worktree) === key) + const direct = projects.find((p) => pathKey(p.worktree) === key) if (direct) return direct const [child] = globalSync.child(directory, { bootstrap: false }) @@ -596,7 +590,7 @@ export default function Layout(props: ParentProps) { }) const workspaceName = (directory: string, projectId?: string, branch?: string) => { - const key = workspaceKey(directory) + const key = pathKey(directory) const direct = store.workspaceName[key] ?? store.workspaceName[directory] if (direct) return direct if (!projectId) return @@ -605,7 +599,7 @@ export default function Layout(props: ParentProps) { } const setWorkspaceName = (directory: string, next: string, projectId?: string, branch?: string) => { - const key = workspaceKey(directory) + const key = pathKey(directory) setStore("workspaceName", key, next) if (!projectId) return if (!branch) return @@ -633,7 +627,7 @@ export default function Layout(props: ParentProps) { const activeDir = currentDir() return workspaceIds(project).filter((directory) => { const expanded = store.workspaceExpanded[directory] ?? directory === project.worktree - const active = workspaceKey(directory) === workspaceKey(activeDir) + const active = pathKey(directory) === pathKey(activeDir) return expanded || active }) }) @@ -644,10 +638,10 @@ export default function Layout(props: ParentProps) { const projects = layout.projects.list() for (const [directory, expanded] of Object.entries(store.workspaceExpanded)) { if (!expanded) continue - const key = workspaceKey(directory) + const key = pathKey(directory) const project = projects.find( (item) => - workspaceKey(item.worktree) === key || item.sandboxes?.some((sandbox) => workspaceKey(sandbox) === key), + pathKey(item.worktree) === key || item.sandboxes?.some((sandbox) => pathKey(sandbox) === key), ) if (!project) continue if (project.vcs === "git" && layout.sidebar.workspaces(project.worktree)()) continue @@ -700,7 +694,7 @@ export default function Layout(props: ParentProps) { seen: lru, keep: sessionID, limit: PREFETCH_MAX_SESSIONS_PER_DIR, - preserve: params.id && workspaceKey(directory) === workspaceKey(currentDir()) ? [params.id] : undefined, + preserve: params.id && pathKey(directory) === pathKey(currentDir()) ? [params.id] : undefined, }) } @@ -1221,17 +1215,17 @@ export default function Layout(props: ParentProps) { } function projectRoot(directory: string) { - const key = workspaceKey(directory) + const key = pathKey(directory) const project = layout.projects .list() .find( (item) => - workspaceKey(item.worktree) === key || item.sandboxes?.some((sandbox) => workspaceKey(sandbox) === key), + pathKey(item.worktree) === key || item.sandboxes?.some((sandbox) => pathKey(sandbox) === key), ) if (project) return project.worktree const known = Object.entries(store.workspaceOrder).find( - ([root, dirs]) => workspaceKey(root) === key || dirs.some((item) => workspaceKey(item) === key), + ([root, dirs]) => pathKey(root) === key || dirs.some((item) => pathKey(item) === key), ) if (known) return known[0] @@ -1283,7 +1277,7 @@ export default function Layout(props: ParentProps) { : [root] const canOpen = (value: string | undefined) => { if (!value) return false - return dirs.some((item) => workspaceKey(item) === workspaceKey(value)) + return dirs.some((item) => pathKey(item) === pathKey(value)) } const refreshDirs = async (target?: string) => { if (!target || target === root || canOpen(target)) return canOpen(target) @@ -1409,9 +1403,9 @@ export default function Layout(props: ParentProps) { function closeProject(directory: string) { const list = layout.projects.list() - const key = workspaceKey(directory) - const index = list.findIndex((x) => workspaceKey(x.worktree) === key) - const active = workspaceKey(currentProject()?.worktree ?? "") === key + const key = pathKey(directory) + const index = list.findIndex((x) => pathKey(x.worktree) === key) + const active = pathKey(currentProject()?.worktree ?? "") === key if (index === -1) return const next = list[index + 1] @@ -1485,8 +1479,8 @@ export default function Layout(props: ParentProps) { if (directory === root) return const current = currentDir() - const currentKey = workspaceKey(current) - const deletedKey = workspaceKey(directory) + const currentKey = pathKey(current) + const deletedKey = pathKey(directory) const shouldLeave = leaveDeletedWorkspace || (!!params.dir && currentKey === deletedKey) if (!leaveDeletedWorkspace && shouldLeave) { navigateWithSidebarReset(`/${base64Encode(root)}/session`) @@ -1509,7 +1503,7 @@ export default function Layout(props: ParentProps) { if (!result) return - if (workspaceKey(store.lastProjectSession[root]?.directory ?? "") === workspaceKey(directory)) { + if (pathKey(store.lastProjectSession[root]?.directory ?? "") === pathKey(directory)) { clearLastProjectSession(root) } @@ -1529,12 +1523,12 @@ export default function Layout(props: ParentProps) { if (shouldLeave) return const nextCurrent = currentDir() - const nextKey = workspaceKey(nextCurrent) + const nextKey = pathKey(nextCurrent) const project = layout.projects.list().find((item) => item.worktree === root) const dirs = project ? effectiveWorkspaceOrder(root, [root, ...(project.sandboxes ?? [])], store.workspaceOrder[root]) : [root] - const valid = dirs.some((item) => workspaceKey(item) === nextKey) + const valid = dirs.some((item) => pathKey(item) === nextKey) if (params.dir && projectRoot(nextCurrent) === root && !valid) { navigateWithSidebarReset(`/${base64Encode(root)}/session`) @@ -1640,7 +1634,7 @@ export default function Layout(props: ParentProps) { }) const handleDelete = () => { - const leaveDeletedWorkspace = !!params.dir && workspaceKey(currentDir()) === workspaceKey(props.directory) + const leaveDeletedWorkspace = !!params.dir && pathKey(currentDir()) === pathKey(props.directory) if (leaveDeletedWorkspace) { navigateWithSidebarReset(`/${base64Encode(props.root)}/session`) } @@ -1867,11 +1861,11 @@ export default function Layout(props: ParentProps) { const local = project.worktree const dirs = [local, ...(project.sandboxes ?? [])] const active = currentProject() - const directory = workspaceKey(active?.worktree ?? "") === workspaceKey(project.worktree) ? currentDir() : undefined + const directory = pathKey(active?.worktree ?? "") === pathKey(project.worktree) ? currentDir() : undefined const extra = directory && - workspaceKey(directory) !== workspaceKey(local) && - !dirs.some((item) => workspaceKey(item) === workspaceKey(directory)) + pathKey(directory) !== pathKey(local) && + !dirs.some((item) => pathKey(item) === pathKey(directory)) ? directory : undefined const pending = extra ? WorktreeState.get(extra)?.status === "pending" : false @@ -1916,7 +1910,7 @@ export default function Layout(props: ParentProps) { setStore( "workspaceOrder", project.worktree, - result.filter((directory) => workspaceKey(directory) !== workspaceKey(project.worktree)), + result.filter((directory) => pathKey(directory) !== pathKey(project.worktree)), ) } @@ -1942,8 +1936,8 @@ export default function Layout(props: ParentProps) { setWorkspaceName(created.directory, created.branch, project.id, created.branch) const local = project.worktree - const key = workspaceKey(created.directory) - const root = workspaceKey(local) + const key = pathKey(created.directory) + const root = pathKey(local) setBusy(created.directory, true) WorktreeState.pending(created.directory) @@ -1954,7 +1948,7 @@ export default function Layout(props: ParentProps) { setStore("workspaceOrder", project.worktree, (prev) => { const existing = prev ?? [] const next = existing.filter((item) => { - const id = workspaceKey(item) + const id = pathKey(item) return id !== root && id !== key }) return [created.directory, ...next] diff --git a/packages/app/src/pages/layout/helpers.test.ts b/packages/app/src/pages/layout/helpers.test.ts index 988332ab7..9cf302482 100644 --- a/packages/app/src/pages/layout/helpers.test.ts +++ b/packages/app/src/pages/layout/helpers.test.ts @@ -14,8 +14,8 @@ import { errorMessage, hasProjectPermissions, latestRootSession, - workspaceKey, } from "./helpers" +import { pathKey } from "@/utils/path-key" const session = (input: Partial<Session> & Pick<Session, "id" | "directory">) => ({ @@ -104,16 +104,16 @@ describe("layout deep links", () => { describe("layout workspace helpers", () => { test("normalizes trailing slash in workspace key", () => { - expect(workspaceKey("/tmp/demo///")).toBe("/tmp/demo") - expect(workspaceKey("C:\\tmp\\demo\\\\")).toBe("C:/tmp/demo") + expect(String(pathKey("/tmp/demo///"))).toBe("/tmp/demo") + expect(String(pathKey("C:\\tmp\\demo\\\\"))).toBe("C:/tmp/demo") }) test("preserves posix and drive roots in workspace key", () => { - expect(workspaceKey("/")).toBe("/") - expect(workspaceKey("///")).toBe("/") - expect(workspaceKey("C:\\")).toBe("C:/") - expect(workspaceKey("C://")).toBe("C:/") - expect(workspaceKey("C:///")).toBe("C:/") + expect(String(pathKey("/"))).toBe("/") + expect(String(pathKey("///"))).toBe("/") + expect(String(pathKey("C:\\"))).toBe("C:/") + expect(String(pathKey("C://"))).toBe("C:/") + expect(String(pathKey("C:///"))).toBe("C:/") }) test("keeps local first while preserving known order", () => { diff --git a/packages/app/src/pages/layout/helpers.ts b/packages/app/src/pages/layout/helpers.ts index 4bc5254d9..d53381e40 100644 --- a/packages/app/src/pages/layout/helpers.ts +++ b/packages/app/src/pages/layout/helpers.ts @@ -1,19 +1,12 @@ import { getFilename } from "@opencode-ai/core/util/path" import { type Session } from "@opencode-ai/sdk/v2/client" +import { pathKey } from "@/utils/path-key" type SessionStore = { session?: Session[] path: { directory: string } } -export const workspaceKey = (directory: string) => { - const value = directory.replaceAll("\\", "/") - const drive = value.match(/^([A-Za-z]:)\/+$/) - if (drive) return `${drive[1]}/` - if (/^\/+$/i.test(value)) return "/" - return value.replace(/\/+$/, "") -} - function sortSessions(now: number) { const oneMinuteAgo = now - 60 * 1000 return (a: Session, b: Session) => { @@ -29,7 +22,7 @@ function sortSessions(now: number) { } const isRootVisibleSession = (session: Session, directory: string) => - workspaceKey(session.directory) === workspaceKey(directory) && !session.parentID && !session.time?.archived + pathKey(session.directory) === pathKey(directory) && !session.parentID && !session.time?.archived export const roots = (store: SessionStore) => (store.session ?? []).filter((session) => isRootVisibleSession(session, store.path.directory)) @@ -72,11 +65,11 @@ export const errorMessage = (err: unknown, fallback: string) => { } export const effectiveWorkspaceOrder = (local: string, dirs: string[], persisted?: string[]) => { - const root = workspaceKey(local) + const root = pathKey(local) const live = new Map<string, string>() for (const dir of dirs) { - const key = workspaceKey(dir) + const key = pathKey(dir) if (key === root) continue if (!live.has(key)) live.set(key, dir) } @@ -85,7 +78,7 @@ export const effectiveWorkspaceOrder = (local: string, dirs: string[], persisted const result = [local] for (const dir of persisted) { - const key = workspaceKey(dir) + const key = pathKey(dir) if (key === root) continue const match = live.get(key) if (!match) continue diff --git a/packages/app/src/pages/layout/sidebar-workspace.tsx b/packages/app/src/pages/layout/sidebar-workspace.tsx index 0a3fc7f41..d2e887b44 100644 --- a/packages/app/src/pages/layout/sidebar-workspace.tsx +++ b/packages/app/src/pages/layout/sidebar-workspace.tsx @@ -16,8 +16,9 @@ import { type Session } from "@opencode-ai/sdk/v2/client" import { type LocalProject } from "@/context/layout" import { loadSessionsQuery, useGlobalSync } from "@/context/global-sync" import { useLanguage } from "@/context/language" +import { pathKey } from "@/utils/path-key" import { NewSessionItem, SessionItem, SessionSkeleton } from "./sidebar-items" -import { sortedRootSessions, workspaceKey } from "./helpers" +import { sortedRootSessions } from "./helpers" import { useQuery } from "@tanstack/solid-query" type InlineEditorComponent = (props: { @@ -309,7 +310,7 @@ export const SortableWorkspace = (props: { const slug = createMemo(() => base64Encode(props.directory)) const sessions = createMemo(() => sortedRootSessions(workspaceStore, props.sortNow())) const local = createMemo(() => props.directory === props.project.worktree) - const active = createMemo(() => workspaceKey(props.ctx.currentDir()) === workspaceKey(props.directory)) + const active = createMemo(() => pathKey(props.ctx.currentDir()) === pathKey(props.directory)) const workspaceValue = createMemo(() => { const branch = workspaceStore.vcs?.branch const name = branch ?? getFilename(props.directory) |
