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 {