diff --git a/src/renderer/src/components/sidebar/use-visible-workspace-kanban-worktree-ids.test.tsx b/src/renderer/src/components/sidebar/use-visible-workspace-kanban-worktree-ids.test.tsx index 44b758721e9..3175a35f43c 100644 --- a/src/renderer/src/components/sidebar/use-visible-workspace-kanban-worktree-ids.test.tsx +++ b/src/renderer/src/components/sidebar/use-visible-workspace-kanban-worktree-ids.test.tsx @@ -3,6 +3,7 @@ import { afterEach, beforeEach, describe, expect, it } from 'vitest' import { cleanup, renderHook } from '@testing-library/react' import { useAppStore } from '@/store' +import type { Tab } from '../../../../shared/tab-types' import { getWorktreeHostIdentity } from '../../../../shared/worktree/host-qualified-identity' import { makeRepo, makeWorktree } from '../worktree-jump-palette-test-fixtures' import { useVisibleWorkspaceKanbanWorktreeIds } from './use-visible-workspace-kanban-worktree-ids' @@ -38,4 +39,36 @@ describe('useVisibleWorkspaceKanbanWorktreeIds', () => { expect(result.current).toEqual(new Set([getWorktreeHostIdentity(local)])) }) + + it('keeps a structured-chat workspace visible when sleeping workspaces are hidden', () => { + const worktree = makeWorktree('chat', 'Chat workspace') + const repo = makeRepo() + const structuredTab: Tab = { + id: 'chat-tab', + entityId: 'chat-session', + groupId: 'chat-group', + worktreeId: worktree.id, + contentType: 'agent-session', + agentSessionAgent: 'codex', + label: 'Chat', + customLabel: null, + color: null, + sortOrder: 0, + createdAt: 0 + } + useAppStore.setState({ + worktreesByRepo: { [repo.id]: [worktree] }, + unifiedTabsByWorktree: { [worktree.id]: [structuredTab] }, + showSleepingWorkspaces: false + }) + + const { result } = renderHook(() => + useVisibleWorkspaceKanbanWorktreeIds({ + allWorktrees: [worktree], + repoMap: new Map([[repo.id, repo]]) + }) + ) + + expect(result.current).toEqual(new Set([getWorktreeHostIdentity(worktree)])) + }) }) diff --git a/src/renderer/src/components/sidebar/use-visible-workspace-kanban-worktree-ids.ts b/src/renderer/src/components/sidebar/use-visible-workspace-kanban-worktree-ids.ts index 41f9bab9dad..531b0b6d45e 100644 --- a/src/renderer/src/components/sidebar/use-visible-workspace-kanban-worktree-ids.ts +++ b/src/renderer/src/components/sidebar/use-visible-workspace-kanban-worktree-ids.ts @@ -11,6 +11,10 @@ import { EMPTY_PAIRED_DEVICE_IDS_BY_ENVIRONMENT, getPairedDeviceIdsByEnvironment } from './workspace-creator-visibility' +import { + EMPTY_STRUCTURED_CHAT_WORKTREE_IDS, + getWorktreeIdsWithStructuredChat +} from './visible-worktree-activity-inputs' import { getWorktreeHostIdentity } from '../../../../shared/worktree/host-qualified-identity' type UseVisibleWorkspaceKanbanWorktreeIdsParams = { @@ -51,6 +55,11 @@ export function useVisibleWorkspaceKanbanWorktreeIds({ const browserTabsByWorktree = useAppStore((s) => !showSleepingWorkspaces ? s.browserTabsByWorktree : null ) + const worktreeIdsWithStructuredChat = useAppStore((s) => + showSleepingWorkspaces + ? EMPTY_STRUCTURED_CHAT_WORKTREE_IDS + : getWorktreeIdsWithStructuredChat(s.unifiedTabsByWorktree) + ) const agentStatusEpoch = useAppStore((s) => (!showSleepingWorkspaces ? s.agentStatusEpoch : 0)) // Why: skip the clock entirely when the epoch is the opt-out sentinel, so a // sleeping-workspaces board cannot evict the sample the live boards share. @@ -82,6 +91,7 @@ export function useVisibleWorkspaceKanbanWorktreeIds({ ptyIdsByTabId, browserTabsByWorktree, worktreeIdsWithLiveAgent, + worktreeIdsWithStructuredChat, hideDefaultBranchWorkspace, hideAutomationGeneratedWorkspaces, hideCliCreatedWorkspaces, @@ -121,6 +131,7 @@ export function useVisibleWorkspaceKanbanWorktreeIds({ showSleepingWorkspaces, tabsByWorktree, worktreeIdsWithLiveAgent, + worktreeIdsWithStructuredChat, worktreesByRepo ]) } diff --git a/src/renderer/src/components/sidebar/visible-worktree-activity-inputs.ts b/src/renderer/src/components/sidebar/visible-worktree-activity-inputs.ts index 802f9779353..8998e87d6a7 100644 --- a/src/renderer/src/components/sidebar/visible-worktree-activity-inputs.ts +++ b/src/renderer/src/components/sidebar/visible-worktree-activity-inputs.ts @@ -36,7 +36,7 @@ export function getVisibleWorktreeBrowserActivityTabs( return browserProjection.project(browserTabsByWorktree) } -const EMPTY_WORKTREE_IDS: ReadonlySet = new Set() +export const EMPTY_STRUCTURED_CHAT_WORKTREE_IDS: ReadonlySet = new Set() const structuredChatWorktreeIds = new WeakMap, ReadonlySet>() /** @@ -51,7 +51,7 @@ export function getWorktreeIdsWithStructuredChat( unifiedTabsByWorktree: Record | null | undefined ): ReadonlySet { if (!unifiedTabsByWorktree) { - return EMPTY_WORKTREE_IDS + return EMPTY_STRUCTURED_CHAT_WORKTREE_IDS } // Keyed on the snapshot, like the tab projection it reads: zustand re-runs every mounted card's // selector on each store write, and this is a whole-store scan. diff --git a/src/renderer/src/components/sidebar/visible-worktrees.ts b/src/renderer/src/components/sidebar/visible-worktrees.ts index 2bbdb07c7f5..194c363568e 100644 --- a/src/renderer/src/components/sidebar/visible-worktrees.ts +++ b/src/renderer/src/components/sidebar/visible-worktrees.ts @@ -24,7 +24,10 @@ import { import type { Worktree } from '../../../../shared/worktree/types' import { buildWorktreeComparator, sortWorktreesSmart } from './smart-sort' import { isInactiveWorkspace } from '@/lib/worktree-activity-state' -export { getWorktreeIdsWithStructuredChat } from './visible-worktree-activity-inputs' +export { + EMPTY_STRUCTURED_CHAT_WORKTREE_IDS, + getWorktreeIdsWithStructuredChat +} from './visible-worktree-activity-inputs' // Runtime edge only one way: the builder imports VisibleWorktreeOptions as a type, which erases. import { buildVisibleWorktreeOptionsFromState } from './visible-worktree-options-from-state' import { useAppStore } from '@/store' diff --git a/src/renderer/src/components/sidebar/worktree-list/listing/use-visible-worktrees.test.tsx b/src/renderer/src/components/sidebar/worktree-list/listing/use-visible-worktrees.test.tsx index 422a14b3693..17f1d4d0756 100644 --- a/src/renderer/src/components/sidebar/worktree-list/listing/use-visible-worktrees.test.tsx +++ b/src/renderer/src/components/sidebar/worktree-list/listing/use-visible-worktrees.test.tsx @@ -1,7 +1,7 @@ // @vitest-environment happy-dom import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import { cleanup, renderHook } from '@testing-library/react' +import { act, cleanup, renderHook } from '@testing-library/react' import { useAppStore } from '@/store' import { LOCAL_EXECUTION_HOST_ID } from '../../../../../../shared/execution-host' import { getWorktreeHostIdentity } from '../../../../../../shared/worktree/host-qualified-identity' @@ -159,4 +159,39 @@ describe('useVisibleSidebarWorktrees', () => { rerender(Object.assign({}, withSettings(nextSettings), { defaultHostId: 'runtime:other' })) expect(computeVisibleWorktreesCalls.count).toBe(callsAfterFirstRender + 1) }) + + it('does not rescan visible worktrees for chat-tab writes when sleeping workspaces are shown', () => { + const repo = makeRepo() + const worktree = makeWorktree('alpha', 'Alpha workspace') + useAppStore.setState({ worktreesByRepo: { [repo.id]: [worktree] } }) + computeVisibleWorktreesCalls.count = 0 + + renderHook(() => + useVisibleSidebarWorktrees({ + filterState: { + showSleepingWorkspaces: true, + filterRepoIds: [], + hideDefaultBranchWorkspace: false, + hideAutomationGeneratedWorkspaces: false, + hideCliCreatedWorkspaces: false, + hideDetachedHeadWorkspaces: false, + hideWorkspacesFromOtherDevices: false, + alwaysShowDefaultBranchWorkspace: true, + visibleWorkspaceHostIds: null, + workspaceHostScope: 'all' + }, + sortBy: 'recent', + sortedIds: [worktree.id], + repoMap: new Map([[repo.id, repo]]), + worktreeLineageById: {}, + defaultHostId: LOCAL_EXECUTION_HOST_ID, + agentSendTargetWorktreeId: null + }) + ) + const callsAfterFirstRender = computeVisibleWorktreesCalls.count + + act(() => useAppStore.setState({ unifiedTabsByWorktree: { [worktree.id]: [] } })) + + expect(computeVisibleWorktreesCalls.count).toBe(callsAfterFirstRender) + }) }) diff --git a/src/renderer/src/components/sidebar/worktree-list/listing/use-visible-worktrees.ts b/src/renderer/src/components/sidebar/worktree-list/listing/use-visible-worktrees.ts index f48ea818444..5a73dcbf2f4 100644 --- a/src/renderer/src/components/sidebar/worktree-list/listing/use-visible-worktrees.ts +++ b/src/renderer/src/components/sidebar/worktree-list/listing/use-visible-worktrees.ts @@ -11,6 +11,7 @@ import { getPairedDeviceIdsByEnvironment } from '../../workspace-creator-visibility' import { + EMPTY_STRUCTURED_CHAT_WORKTREE_IDS, getVisibleWorktreeBrowserActivityTabs, getVisibleWorktreeTerminalActivityTabs, getWorktreeIdsWithStructuredChat @@ -72,7 +73,9 @@ export function useVisibleSidebarWorktrees(args: { !showSleepingWorkspaces ? getVisibleWorktreeBrowserActivityTabs(s.browserTabsByWorktree) : null ) const worktreeIdsWithStructuredChat = useAppStore((s) => - getWorktreeIdsWithStructuredChat(s.unifiedTabsByWorktree) + showSleepingWorkspaces + ? EMPTY_STRUCTURED_CHAT_WORKTREE_IDS + : getWorktreeIdsWithStructuredChat(s.unifiedTabsByWorktree) ) const recomputedVisibleWorktrees = useMemo(() => { diff --git a/src/renderer/src/components/use-worktree-jump-palette-worktrees.ts b/src/renderer/src/components/use-worktree-jump-palette-worktrees.ts index 4faad5451ec..4081566fb7c 100644 --- a/src/renderer/src/components/use-worktree-jump-palette-worktrees.ts +++ b/src/renderer/src/components/use-worktree-jump-palette-worktrees.ts @@ -4,6 +4,7 @@ import { isCliCreatedWorkspace, isDetachedHeadWorkspace, isSleepingSweepExemptWorkspace, + EMPTY_STRUCTURED_CHAT_WORKTREE_IDS, getWorktreeIdsWithStructuredChat } from '@/components/sidebar/visible-worktrees' import { isDefaultBranchWorkspace } from '@/components/sidebar/default-branch-workspace' @@ -96,6 +97,9 @@ export function useWorktreeJumpPaletteWorktrees({ : EMPTY_PAIRED_DEVICE_IDS_BY_ENVIRONMENT, [hideWorkspacesFromOtherDevices, runtimeEnvironments, runtimeStatusByEnvironmentId] ) + const worktreeIdsWithStructuredChat = showSleepingWorkspaces + ? EMPTY_STRUCTURED_CHAT_WORKTREE_IDS + : getWorktreeIdsWithStructuredChat(unifiedTabsByWorktree) const emptyQueryVisibleWorktrees = useMemo( () => allWorktrees.filter((worktree) => { @@ -132,7 +136,7 @@ export function useWorktreeJumpPaletteWorktrees({ ptyIdsByTabId, browserTabsByWorktree, worktreeIdsWithLiveAgent, - getWorktreeIdsWithStructuredChat(unifiedTabsByWorktree) + worktreeIdsWithStructuredChat ) ) { return false @@ -154,7 +158,7 @@ export function useWorktreeJumpPaletteWorktrees({ showSleepingWorkspaces, tabsByWorktree, worktreeIdsWithLiveAgent, - unifiedTabsByWorktree + worktreeIdsWithStructuredChat ] ) const { visibleWorktreesForState, switchableWorktreesForRows } = useMemo(