fix(desktop): Path mismatches cause sessions missing + strong ID + existing data fix (#25013)
This commit is contained in:
parent
a740d2c667
commit
d7b7be1909
14 changed files with 485 additions and 202 deletions
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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", () => {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue