Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 8 additions & 12 deletions apps/electron/src/renderer/components/app-shell/LeftSidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,8 @@ import { detectIsMac } from '@/lib/platform'
import { getActiveAccelerator, getAcceleratorDisplay } from '@/lib/shortcut-registry'
import {
collectAgentSessionTreeIds,
countSettledDelegatedChildren,
getDelegatedChildSessionStatus,
isAgentSessionVisibleInTrees,
replaceAgentSessionInFreshnessOrder,
sortAgentSessionsByUpdatedAtDesc,
Expand Down Expand Up @@ -506,9 +508,7 @@ function getDelegatedChildStatus(
session: AgentSessionMeta,
agentIndicatorMap: Map<string, SessionIndicatorStatus>,
): SessionIndicatorStatus {
const status = agentIndicatorMap.get(session.id)
if (status) return status
return session.delegationStatus === 'running' ? 'running' : 'idle'
return getDelegatedChildSessionStatus(session, agentIndicatorMap)
}

function getSessionTreeStatus(
Expand All @@ -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)
Expand Down Expand Up @@ -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),
}
Expand Down Expand Up @@ -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),
}
Expand Down Expand Up @@ -3535,7 +3531,7 @@ interface AgentSessionItemProps {
showPinIcon?: boolean
delegationSummary?: {
total: number
completed: number
settled: number
expanded: boolean
onToggle: () => void
}
Expand Down Expand Up @@ -3774,7 +3770,7 @@ const AgentSessionItem = React.memo(function AgentSessionItem({
)}
{delegationSummary && (
<span className="flex-shrink-0 text-[11px] leading-4 text-foreground/45">
{delegationSummary.completed}/{delegationSummary.total}
{delegationSummary.settled}/{delegationSummary.total}
</span>
)}
</div>
Expand Down Expand Up @@ -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),
}
Expand Down
34 changes: 34 additions & 0 deletions apps/electron/src/renderer/lib/agent-session-list.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
replaceAgentSessionInFreshnessOrder,
upsertAgentSession,
mergeFetchedAgentSessions,
countSettledDelegatedChildren,
} from './agent-session-list'

function makeSession(
Expand Down Expand Up @@ -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)
})
})
33 changes: 33 additions & 0 deletions apps/electron/src/renderer/lib/agent-session-list.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { AgentSessionMeta } from '@proma/shared'
import type { SessionIndicatorStatus } from '@/atoms/agent-atoms'

interface AgentSessionTreeLike {
session: Pick<AgentSessionMeta, 'id'>
Expand Down Expand Up @@ -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<string, SessionIndicatorStatus>,
): 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<string, SessionIndicatorStatus>,
): number {
return childSessions.filter((session) => {
const status = getDelegatedChildSessionStatus(session, agentIndicatorMap)
return status !== 'running' && status !== 'blocked'
}).length
}