diff --git a/packages/ui/src/sync/session-directory-adoption.test.ts b/packages/ui/src/sync/session-directory-adoption.test.ts index 81821dc0..4857872c 100644 --- a/packages/ui/src/sync/session-directory-adoption.test.ts +++ b/packages/ui/src/sync/session-directory-adoption.test.ts @@ -19,10 +19,14 @@ const PARENT = "/repo" const WORKTREE = "/repo/.worktrees/feature" const SESSION_ID = "ses_directory_adoption" -const indexSessionIn = (manager: ChildStoreManager, directory: string): void => { +const indexSessionIn = ( + manager: ChildStoreManager, + directory: string, + recordDirectory: string = directory, +): void => { const store = manager.ensureChild(directory, { bootstrap: false }) store.setState({ - session: [{ id: SESSION_ID, directory, title: "test" } as never], + session: [{ id: SESSION_ID, directory: recordDirectory, title: "test" } as never], }) } @@ -45,6 +49,18 @@ describe("adoptAuthoritativeSessionDirectory", () => { expect(useSessionUIStore.getState().currentSessionDirectory).toBe(WORKTREE) }) + test("believes the session record over the store that merely holds it", () => { + // A project's session list includes the sessions of its worktrees so the + // sidebar can group them, so the parent store holds this session while the + // session itself reports the worktree. Ownership comes from the record. + useSessionUIStore.getState().setCurrentSession(SESSION_ID) + indexSessionIn(manager, PARENT, WORKTREE) + + useSessionUIStore.getState().adoptAuthoritativeSessionDirectory() + + expect(useSessionUIStore.getState().currentSessionDirectory).toBe(WORKTREE) + }) + test("does nothing while the owning directory is still unknown", () => { useSessionUIStore.getState().setCurrentSession(SESSION_ID) const before = useSessionUIStore.getState().currentSessionDirectory diff --git a/packages/ui/src/sync/session-ui-store.ts b/packages/ui/src/sync/session-ui-store.ts index 87721194..ef450f54 100644 --- a/packages/ui/src/sync/session-ui-store.ts +++ b/packages/ui/src/sync/session-ui-store.ts @@ -411,11 +411,24 @@ const getAttachmentForSession = (sessionId: string | null | undefined): SessionW * only then the session record's own fields. `null` means "not indexed yet", * never "no directory" — callers must fall back rather than treat it as empty. */ +/** + * The directory that owns a session, from the two server-backed signals. + * + * The session's own record wins. Holding a session in a child store proves + * containment, not ownership: a project's session list legitimately includes + * the sessions of its worktrees so the sidebar can group them, so the parent + * repository holds worktree sessions too. Reading ownership from store + * membership therefore reports the parent for a session that lives in a + * worktree, and every fetch is then addressed to a directory that does not own + * it. Store membership remains the fallback for a session whose record carries + * no directory. + */ const getAuthoritativeSessionDirectory = (sessionId: string): string | null => { - const owningDirectory = getSyncSessionDirectory(sessionId) - if (owningDirectory) return normalizePath(owningDirectory) const target = getAllSyncSessions().find((s) => s.id === sessionId) - return target ? resolveDirectoryKey(target) : null + const recordDirectory = target ? resolveDirectoryKey(target) : null + if (recordDirectory) return normalizePath(recordDirectory) + const owningDirectory = getSyncSessionDirectory(sessionId) + return owningDirectory ? normalizePath(owningDirectory) : null } /**