diff --git a/apps/electron/src/renderer/atoms/agent-atoms.ts b/apps/electron/src/renderer/atoms/agent-atoms.ts
index f7cec51..eadfc4a 100644
--- a/apps/electron/src/renderer/atoms/agent-atoms.ts
+++ b/apps/electron/src/renderer/atoms/agent-atoms.ts
@@ -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 }
diff --git a/apps/electron/src/renderer/components/agent/AgentView.tsx b/apps/electron/src/renderer/components/agent/AgentView.tsx
index d205ca9..8ea2efb 100644
--- a/apps/electron/src/renderer/components/agent/AgentView.tsx
+++ b/apps/electron/src/renderer/components/agent/AgentView.tsx
@@ -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
})
diff --git a/apps/electron/src/renderer/components/agent/ContentBlock.tsx b/apps/electron/src/renderer/components/agent/ContentBlock.tsx
index 4c5b818..12ea5e5 100644
--- a/apps/electron/src/renderer/components/agent/ContentBlock.tsx
+++ b/apps/electron/src/renderer/components/agent/ContentBlock.tsx
@@ -204,6 +204,8 @@ export interface ContentBlockProps {
dimmed?: boolean
/** 子代理的内容块(Agent/Task 工具调用的嵌套子块) */
childBlocks?: SDKContentBlock[]
+ /** 是否正在流式输出中(仅流式中的未完成工具调用才显示 spinner) */
+ isStreaming?: boolean
}
// ===== 提示词折叠行 =====
@@ -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'
@@ -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'
@@ -315,8 +319,8 @@ function ToolUseBlock({ block, allMessages, animate = false, index = 0, dimmed =
)}
/>
- {/* 状态指示 */}
- {!isCompleted ? (
+ {/* 状态指示:仅流式中的未完成工具才显示 spinner */}
+ {!isCompleted && isStreaming ? (
) : isError ? (
@@ -353,6 +357,7 @@ function ToolUseBlock({ block, allMessages, animate = false, index = 0, dimmed =
animate
index={ci}
dimmed
+ isStreaming={isStreaming}
/>
))}
@@ -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 ? (
) : isError ? (
@@ -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
@@ -532,6 +537,7 @@ export function ContentBlock({ block, allMessages, basePath, animate = false, in
dimmed={dimmed}
childBlocks={childBlocks}
basePath={basePath}
+ isStreaming={isStreaming}
/>
)
}
diff --git a/apps/electron/src/renderer/components/agent/SDKMessageRenderer.tsx b/apps/electron/src/renderer/components/agent/SDKMessageRenderer.tsx
index 57632e9..66b5337 100644
--- a/apps/electron/src/renderer/components/agent/SDKMessageRenderer.tsx
+++ b/apps/electron/src/renderer/components/agent/SDKMessageRenderer.tsx
@@ -412,6 +412,7 @@ export function AssistantTurnRenderer({ turn, allMessages, basePath, onFork, isS
index={i}
dimmed={hasTextContent && block.type !== 'text'}
childBlocks={childBlocks}
+ isStreaming={isStreaming}
/>
)
})}
diff --git a/apps/electron/src/renderer/hooks/useGlobalAgentListeners.ts b/apps/electron/src/renderer/hooks/useGlobalAgentListeners.ts
index fe9bd61..f71c214 100644
--- a/apps/electron/src/renderer/hooks/useGlobalAgentListeners.ts
+++ b/apps/electron/src/renderer/hooks/useGlobalAgentListeners.ts
@@ -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
})