From c75d68caa290e749d542b9acd28f375e7590d636 Mon Sep 17 00:00:00 2001 From: kev1n77 Date: Sun, 20 Sep 2026 19:14:44 +0800 Subject: [PATCH] fix(web-ui): attribute sessions to their execution workspace only A worktree session carries both its own workspace ID and the main project's ID. The nav membership predicate OR-matched both, so the same session rendered under its own worktree group and the main project group, and switching it activated whichever group matched first. Match membership by the owning execution workspace ID and keep the project ID only as the legacy fallback for records created before execution-workspace stamping. --- .../flow_chat/services/FlowChatManager.test.ts | 4 ++++ .../src/flow_chat/utils/sessionOrdering.test.ts | 16 ++++++++++++++++ .../src/flow_chat/utils/sessionOrdering.ts | 11 +++++++++-- 3 files changed, 29 insertions(+), 2 deletions(-) diff --git a/src/web-ui/src/flow_chat/services/FlowChatManager.test.ts b/src/web-ui/src/flow_chat/services/FlowChatManager.test.ts index 554ee37c6d..2c49ae98d4 100644 --- a/src/web-ui/src/flow_chat/services/FlowChatManager.test.ts +++ b/src/web-ui/src/flow_chat/services/FlowChatManager.test.ts @@ -333,6 +333,10 @@ describe('FlowChatManager initialization', () => { projectWorkspaceId: workspaceFixture('/project', 'ssh-user@server', 'server').id, workspaceHostname: 'server', }); + // Legacy record: it predates execution-workspace stamping, so it has no + // session workspace ID and is attributed through its project scope only. + session.workspaceId = undefined; + delete (session as { config?: Record }).config.workspaceId; storeMocks.store = { registerPersistUnreadCompletionCallback: vi.fn(), getSurfaceGeneration: vi.fn(() => 0), diff --git a/src/web-ui/src/flow_chat/utils/sessionOrdering.test.ts b/src/web-ui/src/flow_chat/utils/sessionOrdering.test.ts index 9a2c2987b4..08935646ed 100644 --- a/src/web-ui/src/flow_chat/utils/sessionOrdering.test.ts +++ b/src/web-ui/src/flow_chat/utils/sessionOrdering.test.ts @@ -104,6 +104,22 @@ describe('sessionOrdering', () => { expect(sessionBelongsToWorkspaceNavRow({ workspaceId: 'host-b-project' }, 'host-a-project')).toBe(false); }); + it('does not show a worktree session under its main project group', () => { + const worktreeSession = { + workspaceId: 'worktree-cli', + projectWorkspaceId: 'main-project', + }; + expect(sessionBelongsToWorkspaceNavRow(worktreeSession, 'worktree-cli')).toBe(true); + expect(sessionBelongsToWorkspaceNavRow(worktreeSession, 'main-project')).toBe(false); + expect(sessionBelongsToWorkspaceNavRow(worktreeSession, 'sibling-worktree')).toBe(false); + }); + + it('still attributes a legacy record that only carries the project ID', () => { + const legacySession = { projectWorkspaceId: 'main-project' }; + expect(sessionBelongsToWorkspaceNavRow(legacySession, 'main-project')).toBe(true); + expect(sessionBelongsToWorkspaceNavRow(legacySession, 'other-project')).toBe(false); + }); + it('does not guess membership from a missing or stale ID', () => { expect(sessionBelongsToWorkspaceNavRow({}, 'known')).toBe(false); expect(sessionBelongsToWorkspaceNavRow({ workspaceId: 'stale' }, 'known')).toBe(false); diff --git a/src/web-ui/src/flow_chat/utils/sessionOrdering.ts b/src/web-ui/src/flow_chat/utils/sessionOrdering.ts index 1451ced407..be667adc2f 100644 --- a/src/web-ui/src/flow_chat/utils/sessionOrdering.ts +++ b/src/web-ui/src/flow_chat/utils/sessionOrdering.ts @@ -1,11 +1,18 @@ import type { Session } from '../types/flow-chat'; import type { SessionMetadata } from '@/shared/types/session-history'; -/** Session list membership is the owning host's workspace ID, never a path. */ +/** + * Session list membership is the owning execution workspace ID (`workspaceId`), + * never a path. `projectWorkspaceId` is only the persistence owner (the main + * project for linked worktrees) and a legacy fallback for records created + * before execution-workspace stamping; it must never widen membership to + * sibling worktrees of the same project. + */ export function sessionBelongsToWorkspaceNavRow( session: Pick, workspaceId?: string, ): boolean { - return Boolean(workspaceId && (session.workspaceId === workspaceId || session.projectWorkspaceId === workspaceId)); + if (!workspaceId) return false; + return (session.workspaceId ?? session.projectWorkspaceId) === workspaceId; } export function getSessionSortTimestamp(session: Pick): number {