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
5 changes: 4 additions & 1 deletion apps/electron/src/renderer/atoms/agent-atoms.ts
Original file line number Diff line number Diff line change
Expand Up @@ -603,10 +603,13 @@ export function applyAgentEvent(
// 成功完成 — 清除 retrying,但保持 running: true
// 等待 STREAM_COMPLETE IPC 回调通过删除流式状态来控制 UI 就绪状态
// 这避免了用户在后端尚未完成清理时就能发送新消息的竞态条件
// 同时将仍 running 的 teammates 标记为 stopped(兜底)
// 同时将仍 running 的 teammates 标记为 stopped、未完成的工具活动标记为 done(兜底)
return {
...prev,
retrying: undefined,
toolActivities: prev.toolActivities.map((ta) =>
ta.done ? ta : { ...ta, done: true }
),
teammates: prev.teammates.map((tm) =>
tm.status === 'running'
? { ...tm, status: 'stopped' as const, endedAt: Date.now(), currentToolName: undefined, currentToolElapsedSeconds: undefined, currentToolUseId: undefined }
Expand Down
15 changes: 14 additions & 1 deletion apps/electron/src/renderer/components/agent/AgentView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -906,7 +906,20 @@ export function AgentView({ sessionId }: { sessionId: string }): React.ReactElem
const current = prev.get(sessionId)
if (!current) return prev
const map = new Map(prev)
map.set(sessionId, { ...current, running: false })
map.set(sessionId, {
...current,
running: false,
// 将所有未完成的工具活动标记为已完成,防止 spinner 继续转动
toolActivities: current.toolActivities.map((ta) =>
ta.done ? ta : { ...ta, done: true }
),
// 将所有 running 的 teammates 标记为 stopped
teammates: current.teammates.map((tm) =>
tm.status === 'running'
? { ...tm, status: 'stopped' as const, endedAt: Date.now(), currentToolName: undefined, currentToolElapsedSeconds: undefined, currentToolUseId: undefined }
: tm
),
})
return map
})

Expand Down
20 changes: 13 additions & 7 deletions apps/electron/src/renderer/components/agent/ContentBlock.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,8 @@ export interface ContentBlockProps {
dimmed?: boolean
/** 子代理的内容块(Agent/Task 工具调用的嵌套子块) */
childBlocks?: SDKContentBlock[]
/** 是否正在流式输出中(仅流式中的未完成工具调用才显示 spinner) */
isStreaming?: boolean
}

// ===== 提示词折叠行 =====
Expand Down Expand Up @@ -262,9 +264,11 @@ interface ToolUseBlockProps {
dimmed?: boolean
childBlocks?: SDKContentBlock[]
basePath?: string
/** 是否正在流式输出中 */
isStreaming?: boolean
}

function ToolUseBlock({ block, allMessages, animate = false, index = 0, dimmed = false, childBlocks, basePath }: ToolUseBlockProps): React.ReactElement {
function ToolUseBlock({ block, allMessages, animate = false, index = 0, dimmed = false, childBlocks, basePath, isStreaming }: ToolUseBlockProps): React.ReactElement {
const [expanded, setExpanded] = React.useState(false)
const toolResult = useToolResult(block.id, allMessages)
const isAgentTool = block.name === 'Agent' || block.name === 'Task'
Expand All @@ -280,8 +284,8 @@ function ToolUseBlock({ block, allMessages, animate = false, index = 0, dimmed =
const isCompleted = toolResult !== null
const isError = toolResult?.isError === true

// 运行中显示进行时短语,完成后显示完成态短语
const displayLabel = isCompleted ? phrase.label : phrase.loadingLabel
// 运行中显示进行时短语,完成或非流式(已终止)显示完成态短语
const displayLabel = (isCompleted || !isStreaming) ? phrase.label : phrase.loadingLabel

const delay = animate && index < 10 ? `${index * 30}ms` : '0ms'

Expand Down Expand Up @@ -315,8 +319,8 @@ function ToolUseBlock({ block, allMessages, animate = false, index = 0, dimmed =
)}
/>

{/* 状态指示 */}
{!isCompleted ? (
{/* 状态指示:仅流式中的未完成工具才显示 spinner */}
{!isCompleted && isStreaming ? (
<Loader2 className="size-3.5 animate-spin text-primary/50 shrink-0" />
) : isError ? (
<XCircle className="size-3.5 text-destructive/70 shrink-0" />
Expand Down Expand Up @@ -353,6 +357,7 @@ function ToolUseBlock({ block, allMessages, animate = false, index = 0, dimmed =
animate
index={ci}
dimmed
isStreaming={isStreaming}
/>
))}

Expand Down Expand Up @@ -382,7 +387,7 @@ function ToolUseBlock({ block, allMessages, animate = false, index = 0, dimmed =
className="flex items-center gap-2 py-0.5 text-left hover:opacity-70 transition-opacity group"
onClick={() => setExpanded(!expanded)}
>
{!isCompleted ? (
{!isCompleted && isStreaming ? (
<Loader2 className="size-3.5 animate-spin text-primary/50 shrink-0" />
) : isError ? (
<XCircle className="size-3.5 text-destructive/70 shrink-0" />
Expand Down Expand Up @@ -510,7 +515,7 @@ function ThinkingBlock({ block, dimmed = false }: ThinkingBlockProps): React.Rea

// ===== ContentBlock 主组件 =====

export function ContentBlock({ block, allMessages, basePath, animate = false, index = 0, dimmed = false, childBlocks }: ContentBlockProps): React.ReactElement | null {
export function ContentBlock({ block, allMessages, basePath, animate = false, index = 0, dimmed = false, childBlocks, isStreaming }: ContentBlockProps): React.ReactElement | null {
// text 块 — 主要内容,不受 dimmed 影响
if (block.type === 'text') {
const textBlock = block as SDKTextBlock
Expand All @@ -532,6 +537,7 @@ export function ContentBlock({ block, allMessages, basePath, animate = false, in
dimmed={dimmed}
childBlocks={childBlocks}
basePath={basePath}
isStreaming={isStreaming}
/>
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -412,6 +412,7 @@ export function AssistantTurnRenderer({ turn, allMessages, basePath, onFork, isS
index={i}
dimmed={hasTextContent && block.type !== 'text'}
childBlocks={childBlocks}
isStreaming={isStreaming}
/>
)
})}
Expand Down
14 changes: 13 additions & 1 deletion apps/electron/src/renderer/hooks/useGlobalAgentListeners.ts
Original file line number Diff line number Diff line change
Expand Up @@ -462,12 +462,24 @@ export function useGlobalAgentListeners(): void {
)

// STREAM_COMPLETE 表示后端已完全结束 — 立即标记 running: false
// 同时将所有未完成的工具活动标记为已完成,防止 subagent spinner 继续转动
// (complete 事件只清除 retrying,保持 running: true 以防竞态)
store.set(agentStreamingStatesAtom, (prev) => {
const current = prev.get(data.sessionId)
if (!current || !current.running) return prev
const map = new Map(prev)
map.set(data.sessionId, { ...current, running: false })
map.set(data.sessionId, {
...current,
running: false,
toolActivities: current.toolActivities.map((ta) =>
ta.done ? ta : { ...ta, done: true }
),
teammates: current.teammates.map((tm) =>
tm.status === 'running'
? { ...tm, status: 'stopped' as const, endedAt: Date.now(), currentToolName: undefined, currentToolElapsedSeconds: undefined, currentToolUseId: undefined }
: tm
),
})
return map
})

Expand Down