diff --git a/apps/electron/src/renderer/components/app-shell/LeftSidebar.tsx b/apps/electron/src/renderer/components/app-shell/LeftSidebar.tsx index d63a7e926..d9e85df71 100644 --- a/apps/electron/src/renderer/components/app-shell/LeftSidebar.tsx +++ b/apps/electron/src/renderer/components/app-shell/LeftSidebar.tsx @@ -96,6 +96,8 @@ import { detectIsMac } from '@/lib/platform' import { getActiveAccelerator, getAcceleratorDisplay } from '@/lib/shortcut-registry' import { collectAgentSessionTreeIds, + countSettledDelegatedChildren, + getDelegatedChildSessionStatus, isAgentSessionVisibleInTrees, replaceAgentSessionInFreshnessOrder, sortAgentSessionsByUpdatedAtDesc, @@ -506,9 +508,7 @@ function getDelegatedChildStatus( session: AgentSessionMeta, agentIndicatorMap: Map, ): SessionIndicatorStatus { - const status = agentIndicatorMap.get(session.id) - if (status) return status - return session.delegationStatus === 'running' ? 'running' : 'idle' + return getDelegatedChildSessionStatus(session, agentIndicatorMap) } function getSessionTreeStatus( @@ -526,10 +526,6 @@ function getSessionTreeStatus( return 'idle' } -function countCompletedDelegatedChildren(childSessions: AgentSessionMeta[]): number { - return childSessions.filter((session) => session.delegationStatus === 'completed').length -} - function treeContainsSessionId(item: AgentSessionTreeItem, sessionId: string | null): boolean { if (!sessionId) return false return item.session.id === sessionId || item.childSessions.some((session) => session.id === sessionId) @@ -2744,7 +2740,7 @@ export function LeftSidebar({ width, noTransition }: LeftSidebarProps): React.Re delegationSummary={childCount > 0 ? { total: childCount, - completed: countCompletedDelegatedChildren(item.childSessions), + settled: countSettledDelegatedChildren(item.childSessions, agentIndicatorMap), expanded: expandedChildren, onToggle: () => handleToggleDelegationParent(item.session.id, expandedChildren), } @@ -2945,7 +2941,7 @@ export function LeftSidebar({ width, noTransition }: LeftSidebarProps): React.Re delegationSummary={childCount > 0 ? { total: childCount, - completed: countCompletedDelegatedChildren(item.childSessions), + settled: countSettledDelegatedChildren(item.childSessions, agentIndicatorMap), expanded: expandedChildren, onToggle: () => handleToggleDelegationParent(item.session.id, expandedChildren), } @@ -3535,7 +3531,7 @@ interface AgentSessionItemProps { showPinIcon?: boolean delegationSummary?: { total: number - completed: number + settled: number expanded: boolean onToggle: () => void } @@ -3774,7 +3770,7 @@ const AgentSessionItem = React.memo(function AgentSessionItem({ )} {delegationSummary && ( - {delegationSummary.completed}/{delegationSummary.total} + {delegationSummary.settled}/{delegationSummary.total} )} @@ -4270,7 +4266,7 @@ const AgentProjectGroupItem = React.memo(function AgentProjectGroupItem({ delegationSummary={childCount > 0 ? { total: childCount, - completed: countCompletedDelegatedChildren(item.childSessions), + settled: countSettledDelegatedChildren(item.childSessions, agentIndicatorMap), expanded: expandedChildren, onToggle: () => onToggleDelegationParent(item.session.id, expandedChildren), } diff --git a/apps/electron/src/renderer/lib/agent-session-list.test.ts b/apps/electron/src/renderer/lib/agent-session-list.test.ts index 53216684f..86ec184cd 100644 --- a/apps/electron/src/renderer/lib/agent-session-list.test.ts +++ b/apps/electron/src/renderer/lib/agent-session-list.test.ts @@ -5,6 +5,7 @@ import { replaceAgentSessionInFreshnessOrder, upsertAgentSession, mergeFetchedAgentSessions, + countSettledDelegatedChildren, } from './agent-session-list' function makeSession( @@ -124,3 +125,36 @@ describe('mergeFetchedAgentSessions', () => { expect(result).toHaveLength(2) }) }) + +describe('countSettledDelegatedChildren', () => { + test('取消后的子会话被直接重跑时,实时 running 状态优先于历史 cancelled', () => { + const child = makeSession('child', 1, { + parentSessionId: 'parent', + sourceDelegationId: 'delegation', + delegationStatus: 'cancelled', + }) + + expect(countSettledDelegatedChildren([child], new Map([['child', 'running' as const]]))).toBe(0) + expect(countSettledDelegatedChildren([child], new Map())).toBe(1) + }) + + test('运行中或阻塞中的子会话不计入已停止数量', () => { + const running = makeSession('running', 1, { delegationStatus: 'running' }) + const blocked = makeSession('blocked', 1, { delegationStatus: 'completed' }) + const finished = makeSession('finished', 1, { delegationStatus: 'cancelled' }) + + const statusMap = new Map([ + ['running', 'running' as const], + ['blocked', 'blocked' as const], + ['finished', 'completed' as const], + ]) + + expect(countSettledDelegatedChildren([running, blocked, finished], statusMap)).toBe(1) + }) + + test('应用重启后没有实时流状态时,持久化 running 仍不计入已停止数量', () => { + const child = makeSession('child', 1, { delegationStatus: 'running' }) + + expect(countSettledDelegatedChildren([child], new Map())).toBe(0) + }) +}) diff --git a/apps/electron/src/renderer/lib/agent-session-list.ts b/apps/electron/src/renderer/lib/agent-session-list.ts index 92e59512e..5f30de3e6 100644 --- a/apps/electron/src/renderer/lib/agent-session-list.ts +++ b/apps/electron/src/renderer/lib/agent-session-list.ts @@ -1,4 +1,5 @@ import type { AgentSessionMeta } from '@proma/shared' +import type { SessionIndicatorStatus } from '@/atoms/agent-atoms' interface AgentSessionTreeLike { session: Pick @@ -100,3 +101,35 @@ export function isAgentSessionVisibleInTrees( if (!sessionId) return false return collectAgentSessionTreeIds(items).has(sessionId) } + +/** + * 以侧栏指示器为准解析协作子会话的当前状态。 + * + * 实时流状态优先于持久化的 delegationStatus。后者只在应用重启后、流式 + * 状态尚未恢复时保留“原委派仍在运行”的信息;历史 cancelled 不应覆盖子会话 + * 后续被直接重跑时的真实运行状态。 + */ +export function getDelegatedChildSessionStatus( + session: AgentSessionMeta, + agentIndicatorMap: ReadonlyMap, +): SessionIndicatorStatus { + const status = agentIndicatorMap.get(session.id) + if (status) return status + return session.delegationStatus === 'running' ? 'running' : 'idle' +} + +/** + * 父会话进度的分子:当前已停止的直属子会话数量。 + * + * 这里刻意不读取历史 delegationStatus 是否为 completed。用户可在原委派 + * 被取消后直接重跑子会话,父行进度应与该子会话在左侧栏的实时状态保持一致。 + */ +export function countSettledDelegatedChildren( + childSessions: readonly AgentSessionMeta[], + agentIndicatorMap: ReadonlyMap, +): number { + return childSessions.filter((session) => { + const status = getDelegatedChildSessionStatus(session, agentIndicatorMap) + return status !== 'running' && status !== 'blocked' + }).length +}