diff --git a/src/renderer/src/components/dashboard/build-dashboard-snapshot-folder-workspace.test.ts b/src/renderer/src/components/dashboard/build-dashboard-snapshot-folder-workspace.test.ts index 890259d35ba..6c670934cb5 100644 --- a/src/renderer/src/components/dashboard/build-dashboard-snapshot-folder-workspace.test.ts +++ b/src/renderer/src/components/dashboard/build-dashboard-snapshot-folder-workspace.test.ts @@ -3,6 +3,7 @@ import { makePaneKey } from '../../../../shared/stable-pane-id' import { folderWorkspaceKey } from '../../../../shared/workspace-scope' import type { FolderWorkspace } from '../../../../shared/folder-workspace-types' import type { ProjectGroup } from '../../../../shared/project-group-types' +import type { Tab } from '../../../../shared/tab-types' import type { TerminalTab } from '../../../../shared/terminal-tab-types' import type { AgentStatusEntry } from '../../../../shared/agent-status-types' import { buildDashboardSnapshot, type DashboardSnapshotState } from './build-dashboard-snapshot' @@ -205,6 +206,46 @@ describe('buildDashboardSnapshot folder workspaces', () => { expect(buildDashboardBucketCounts(mixedState, NOW)).toEqual(expected) }) + it('keeps done structured sessions visible when their tab exists only in unified tabs', () => { + const structuredState = state() + structuredState.tabsByWorktree = { [WORKSPACE_ID]: [] } + structuredState.unifiedTabsByWorktree = { + [WORKSPACE_ID]: [ + { + id: TAB_ID, + entityId: 'session-1', + groupId: 'group-1', + worktreeId: WORKSPACE_ID, + contentType: 'agent-session', + label: 'Codex Chat', + customLabel: null, + color: null, + sortOrder: 0, + createdAt: NOW, + isPinned: false, + agentSessionAgent: 'codex' + } satisfies Tab + ] + } + structuredState.agentStatusByPaneKey = { + [PANE_KEY]: { + ...entry(), + state: 'done', + sessionBoundary: true + } + } + + const snapshot = buildDashboardSnapshot(structuredState, NOW) + const expected = { attention: 0, working: 0, done: 0, idle: 0 } + for (const card of snapshot.cards) { + expected[card.bucket] += 1 + } + + expect(snapshot.cards).toHaveLength(1) + expect(snapshot.cards[0]).toMatchObject({ paneKey: PANE_KEY, bucket: 'done' }) + expect(buildDashboardBucketCounts(structuredState, NOW)).toEqual(expected) + }) + it('places folder-workspace agents in their real project group without git assumptions', () => { const sshState = state() sshState.sshTargetLabels = new Map([['ssh-1', 'openclaw']]) diff --git a/src/renderer/src/components/dashboard/build-dashboard-snapshot.ts b/src/renderer/src/components/dashboard/build-dashboard-snapshot.ts index e0091066972..4dd93173696 100644 --- a/src/renderer/src/components/dashboard/build-dashboard-snapshot.ts +++ b/src/renderer/src/components/dashboard/build-dashboard-snapshot.ts @@ -78,7 +78,7 @@ export type DashboardSnapshotState = Pick< Partial< DashboardCardTerminalInputState & DashboardLaunchDetectionState & - Pick + Pick > /** diff --git a/src/renderer/src/components/dashboard/useAgentBucketCounts.test.tsx b/src/renderer/src/components/dashboard/useAgentBucketCounts.test.tsx index 5bf5d4a45e0..7e4c90fdb8e 100644 --- a/src/renderer/src/components/dashboard/useAgentBucketCounts.test.tsx +++ b/src/renderer/src/components/dashboard/useAgentBucketCounts.test.tsx @@ -8,6 +8,7 @@ const mocks = vi.hoisted(() => ({ repos: [], worktreesByRepo: {}, tabsByWorktree: {}, + unifiedTabsByWorktree: {}, agentStatusByPaneKey: {}, retainedAgentsByPaneKey: {}, migrationUnsupportedByPtyId: {}, diff --git a/src/renderer/src/components/dashboard/useAgentBucketCounts.ts b/src/renderer/src/components/dashboard/useAgentBucketCounts.ts index d8cf2fb2fbe..210c7a969df 100644 --- a/src/renderer/src/components/dashboard/useAgentBucketCounts.ts +++ b/src/renderer/src/components/dashboard/useAgentBucketCounts.ts @@ -16,6 +16,7 @@ export function useAgentBucketCounts(): AgentBucketCounts { repos, worktreesByRepo, tabsByWorktree, + unifiedTabsByWorktree, agentStatusByPaneKey, retainedAgentsByPaneKey, migrationUnsupportedByPtyId, @@ -31,6 +32,7 @@ export function useAgentBucketCounts(): AgentBucketCounts { repos: s.repos, worktreesByRepo: s.worktreesByRepo, tabsByWorktree: s.tabsByWorktree, + unifiedTabsByWorktree: s.unifiedTabsByWorktree, agentStatusByPaneKey: s.agentStatusByPaneKey, retainedAgentsByPaneKey: s.retainedAgentsByPaneKey, migrationUnsupportedByPtyId: s.migrationUnsupportedByPtyId, @@ -50,6 +52,7 @@ export function useAgentBucketCounts(): AgentBucketCounts { repos, worktreesByRepo, tabsByWorktree, + unifiedTabsByWorktree, agentStatusByPaneKey, retainedAgentsByPaneKey, migrationUnsupportedByPtyId, @@ -72,6 +75,7 @@ export function useAgentBucketCounts(): AgentBucketCounts { repos, worktreesByRepo, tabsByWorktree, + unifiedTabsByWorktree, agentStatusByPaneKey, retainedAgentsByPaneKey, migrationUnsupportedByPtyId, diff --git a/src/renderer/src/components/dashboard/useDashboardPopoutBridge.test.tsx b/src/renderer/src/components/dashboard/useDashboardPopoutBridge.test.tsx index 84ea917f372..aa427ed55c5 100644 --- a/src/renderer/src/components/dashboard/useDashboardPopoutBridge.test.tsx +++ b/src/renderer/src/components/dashboard/useDashboardPopoutBridge.test.tsx @@ -59,6 +59,7 @@ function makeSnapshotWatchState(): DashboardSnapshotWatchState { repos: [], worktreesByRepo: {}, tabsByWorktree: {}, + unifiedTabsByWorktree: {}, agentStatusByPaneKey: {}, retainedAgentsByPaneKey: {}, migrationUnsupportedByPtyId: {}, @@ -182,6 +183,7 @@ describe('useDashboardPopoutBridge', () => { 'repos', 'worktreesByRepo', 'tabsByWorktree', + 'unifiedTabsByWorktree', 'retainedAgentsByPaneKey', 'migrationUnsupportedByPtyId', 'runtimeAgentOrchestrationByPaneKey', @@ -241,6 +243,19 @@ describe('useDashboardPopoutBridge', () => { expect(republished).toEqual(profileInputs.map((next) => Object.keys(next)[0])) }) + it('republishes when the unified agent-session tab projection changes', () => { + const previousState = makeSnapshotWatchState() + expect( + dashboardSnapshotInputsChanged( + { + ...previousState, + unifiedTabsByWorktree: { 'worktree-1': [] } + }, + previousState + ) + ).toBe(true) + }) + it('releases every dashboard listener when the experiment is disabled', async () => { await act(async () => root.render()) diff --git a/src/renderer/src/components/dashboard/useDashboardPopoutBridge.ts b/src/renderer/src/components/dashboard/useDashboardPopoutBridge.ts index 3d58b9100c8..c2446f4e11f 100644 --- a/src/renderer/src/components/dashboard/useDashboardPopoutBridge.ts +++ b/src/renderer/src/components/dashboard/useDashboardPopoutBridge.ts @@ -46,6 +46,7 @@ export function dashboardSnapshotInputsChanged( state.repos !== previousState.repos || state.worktreesByRepo !== previousState.worktreesByRepo || state.tabsByWorktree !== previousState.tabsByWorktree || + state.unifiedTabsByWorktree !== previousState.unifiedTabsByWorktree || state.retainedAgentsByPaneKey !== previousState.retainedAgentsByPaneKey || state.migrationUnsupportedByPtyId !== previousState.migrationUnsupportedByPtyId || state.runtimeAgentOrchestrationByPaneKey !== previousState.runtimeAgentOrchestrationByPaneKey || diff --git a/src/renderer/src/components/dashboard/useLiveDashboardSnapshot.ts b/src/renderer/src/components/dashboard/useLiveDashboardSnapshot.ts index d3848178509..07317083d7e 100644 --- a/src/renderer/src/components/dashboard/useLiveDashboardSnapshot.ts +++ b/src/renderer/src/components/dashboard/useLiveDashboardSnapshot.ts @@ -13,6 +13,7 @@ export function useLiveDashboardSnapshot(): DashboardSnapshot { const repos = useAppStore((s) => s.repos) const worktreesByRepo = useAppStore((s) => s.worktreesByRepo) const tabsByWorktree = useAppStore((s) => s.tabsByWorktree) + const unifiedTabsByWorktree = useAppStore((s) => s.unifiedTabsByWorktree) const agentStatusByPaneKey = useAppStore((s) => s.agentStatusByPaneKey) const retainedAgentsByPaneKey = useAppStore((s) => s.retainedAgentsByPaneKey) const migrationUnsupportedByPtyId = useAppStore((s) => s.migrationUnsupportedByPtyId) @@ -68,6 +69,7 @@ export function useLiveDashboardSnapshot(): DashboardSnapshot { repos, worktreesByRepo, tabsByWorktree, + unifiedTabsByWorktree, agentStatusByPaneKey, retainedAgentsByPaneKey, migrationUnsupportedByPtyId, @@ -107,6 +109,7 @@ export function useLiveDashboardSnapshot(): DashboardSnapshot { repos, worktreesByRepo, tabsByWorktree, + unifiedTabsByWorktree, agentStatusByPaneKey, retainedAgentsByPaneKey, migrationUnsupportedByPtyId,