From 1f80043928665b55460328f45f437a584f28df92 Mon Sep 17 00:00:00 2001 From: cepvor Date: Thu, 14 May 2026 15:40:28 +0800 Subject: [PATCH 1/5] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E5=AD=90=E4=BB=A3?= =?UTF-8?q?=E7=90=86=20token=20=E6=B6=88=E8=80=97=E5=9C=A8=E4=B8=BB=20spin?= =?UTF-8?q?ner=20=E4=B8=AD=E5=A7=8B=E7=BB=88=E6=98=BE=E7=A4=BA=E4=B8=BA=20?= =?UTF-8?q?0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Spinner.tsx 的 token 聚合循环仅统计 in_process_teammate 类型任务, 漏掉了 local_agent(后台代理/verification agent)类型。当后台代理 运行时,主界面 spinner 一直显示 "↓ 0 tokens",因为 background agent 的 token 消耗未被纳入 teammateTokens 聚合。 同时在 inProcessRunner.ts 中,进程内队友完成时计算并设置 result (含 totalTokens/totalToolUseCount/content/usage),使详情弹窗可以 正确展示累计 token 消耗,不再仅依赖 progress.tokenCount 间歇更新。 Co-Authored-By: deepseek-v4-pro[1m] --- src/components/Spinner.tsx | 3 +- src/utils/swarm/inProcessRunner.ts | 50 +++++++++++++++++++++++++++++- 2 files changed, 51 insertions(+), 2 deletions(-) diff --git a/src/components/Spinner.tsx b/src/components/Spinner.tsx index 0ef3ab4d5..5d8e15622 100644 --- a/src/components/Spinner.tsx +++ b/src/components/Spinner.tsx @@ -23,6 +23,7 @@ import { getDefaultCharacters, type SpinnerMode } from './Spinner/index.js'; import { SpinnerAnimationRow } from './Spinner/SpinnerAnimationRow.js'; import { useSettings } from '../hooks/useSettings.js'; import { isInProcessTeammateTask } from '../tasks/InProcessTeammateTask/types.js'; +import { isLocalAgentTask } from '../tasks/LocalAgentTask/LocalAgentTask.js'; import { isBackgroundTask } from '../tasks/types.js'; import { getAllInProcessTeammateTasks } from '../tasks/InProcessTeammateTask/InProcessTeammateTask.js'; import { getEffortSuffix } from '../utils/effort.js'; @@ -214,7 +215,7 @@ function SpinnerWithVerbInner({ let teammateTokens = 0; if (!showSpinnerTree) { for (const task of Object.values(tasks)) { - if (isInProcessTeammateTask(task) && task.status === 'running') { + if (task.status === 'running' && (isInProcessTeammateTask(task) || isLocalAgentTask(task))) { if (task.progress?.tokenCount) { teammateTokens += task.progress.tokenCount; } diff --git a/src/utils/swarm/inProcessRunner.ts b/src/utils/swarm/inProcessRunner.ts index bf2020248..4d2c7e605 100644 --- a/src/utils/swarm/inProcessRunner.ts +++ b/src/utils/swarm/inProcessRunner.ts @@ -47,6 +47,7 @@ import { import type { CustomAgentDefinition } from '@claude-code-best/builtin-tools/tools/AgentTool/loadAgentsDir.js' import { runAgent } from '@claude-code-best/builtin-tools/tools/AgentTool/runAgent.js' import { awaitClassifierAutoApproval } from '@claude-code-best/builtin-tools/tools/BashTool/bashPermissions.js' +import type { AgentToolResult } from '@claude-code-best/builtin-tools/tools/AgentTool/agentToolUtils.js' import { BASH_TOOL_NAME } from '@claude-code-best/builtin-tools/tools/BashTool/toolName.js' import { SEND_MESSAGE_TOOL_NAME } from '@claude-code-best/builtin-tools/tools/SendMessageTool/constants.js' import { TASK_CREATE_TOOL_NAME } from '@claude-code-best/builtin-tools/tools/TaskCreateTool/constants.js' @@ -63,7 +64,10 @@ import { } from '../../utils/messages.js' import { evictTaskOutput } from '../../utils/task/diskOutput.js' import { evictTerminalTask } from '../../utils/task/framework.js' -import { tokenCountWithEstimation } from '../../utils/tokens.js' +import { + tokenCountWithEstimation, + getTokenCountFromUsage, +} from '../../utils/tokens.js' import { createAbortController } from '../abortController.js' import { type AgentContext, runWithAgentContext } from '../agentContext.js' import { @@ -915,6 +919,7 @@ export async function runInProcessTeammate( invokingRequestId, } = config const { setAppState } = toolUseContext + const startTime = Date.now() logForDebugging( `[inProcessRunner] Starting agent loop for ${identity.agentId}`, @@ -1463,6 +1468,48 @@ export async function runInProcessTeammate( // Mark as completed when exiting the loop let alreadyTerminal = false let toolUseId: string | undefined + + // Compute result so the detail dialog can show token usage. + // Walk backwards for the last API usage (cumulative input_tokens from the + // Anthropic API already includes all prior context). + let completionTokens = 0 + let completionToolUseCount = 0 + let lastAssistantContent: AgentToolResult['content'] = [] + let lastUsage: AgentToolResult['usage'] | undefined + for (let i = allMessages.length - 1; i >= 0; i--) { + const m = allMessages[i]! + if (m.type === 'assistant') { + const blocks = (m.message?.content ?? []) as any[] + for (const b of blocks) { + if (b?.type === 'tool_use') completionToolUseCount++ + } + const textBlocks = blocks.filter((b: any) => b?.type === 'text') + if (textBlocks.length > 0 && lastAssistantContent.length === 0) { + lastAssistantContent = textBlocks.map((b: any) => ({ + type: 'text' as const, + text: b.text, + })) + } + if (!lastUsage && m.message?.usage) { + lastUsage = m.message.usage as AgentToolResult['usage'] + completionTokens = getTokenCountFromUsage( + m.message.usage as Parameters[0], + ) + } + if (completionTokens > 0 && lastAssistantContent.length > 0) break + } + } + + const teammateResult: AgentToolResult = { + agentId: identity.agentId, + agentType: 'teammate', + content: lastAssistantContent, + totalToolUseCount: completionToolUseCount, + totalDurationMs: Date.now() - startTime, + totalTokens: completionTokens, + usage: lastUsage as AgentToolResult['usage'], + } as unknown as AgentToolResult + updateTaskState( taskId, task => { @@ -1481,6 +1528,7 @@ export async function runInProcessTeammate( status: 'completed' as const, notified: true, endTime: Date.now(), + result: teammateResult, messages: task.messages?.length ? [task.messages.at(-1)!] : undefined, pendingUserMessages: [], inProgressToolUseIDs: undefined, From b3d28bcdf1beeea470c7771b9f07d54f12f05093 Mon Sep 17 00:00:00 2001 From: cepvor Date: Thu, 14 May 2026 16:05:16 +0800 Subject: [PATCH 2/5] =?UTF-8?q?fix:=20=E4=B8=BA=20cacheWarningStateBySourc?= =?UTF-8?q?e=20Map=20=E8=AE=BE=E7=BD=AE=E4=B8=8A=E9=99=90=E9=98=B2?= =?UTF-8?q?=E6=AD=A2=E5=86=85=E5=AD=98=E6=B3=84=E6=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Map 以 querySource 为 key 存储每个来源的缓存命中率历史状态, 但 querySource 类型为 `any`,长时间会话中可能产生大量唯一值, Map 持续增长永不清理。 新增 MAX_SOURCE_ENTRIES = 50 上限,新增条目时若达到上限则 逐出最早插入的条目(Map 按插入顺序迭代)。 同时也新增 _resetCacheWarningStateForTest() 用于测试隔离。 Co-Authored-By: deepseek-v4-pro[1m] --- src/utils/cacheWarning.ts | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/utils/cacheWarning.ts b/src/utils/cacheWarning.ts index 39ba5a599..2e193f9a4 100644 --- a/src/utils/cacheWarning.ts +++ b/src/utils/cacheWarning.ts @@ -24,6 +24,12 @@ interface CacheWarningState { // 模块级状态,每个 querySource 独立跟踪 const cacheWarningStateBySource = new Map() +// Limit the number of tracked sources to prevent unbounded Map growth. +// querySource strings are effectively unbounded (typed as `any`), so a +// long-running session that spawns many subagents could leak memory. +// Evict the oldest entry (by insertion order) when the limit is exceeded. +const MAX_SOURCE_ENTRIES = 50 + const DEFAULT_CACHE_THRESHOLD = 80 /** @@ -81,6 +87,13 @@ export function shouldShowCacheWarning( let state = cacheWarningStateBySource.get(querySource) if (!state) { state = { lastHitRate: null, lastTimestamp: null } + // Evict oldest entry when at capacity so the Map stays bounded + if (cacheWarningStateBySource.size >= MAX_SOURCE_ENTRIES) { + const oldestKey = cacheWarningStateBySource.keys().next().value + if (oldestKey !== undefined) { + cacheWarningStateBySource.delete(oldestKey) + } + } cacheWarningStateBySource.set(querySource, state) } @@ -132,3 +145,10 @@ export function createCacheWarningMessage(info: CacheHitRateInfo): Message { isMeta: false, } as Message } + +/** + * Reset the per-source tracking state — only used in tests. + */ +export function _resetCacheWarningStateForTest(): void { + cacheWarningStateBySource.clear() +} From 78d46aa2339a62b6a1aad39bcd2322eeb9530b46 Mon Sep 17 00:00:00 2001 From: claude-code-best Date: Thu, 14 May 2026 17:44:47 +0800 Subject: [PATCH 3/5] =?UTF-8?q?fix:=20=E6=9B=BF=E6=8D=A2=20extractMemories?= =?UTF-8?q?=20=E7=9A=84=20require()=20=E4=B8=BA=E5=8A=A8=E6=80=81=20import?= =?UTF-8?q?()=20=E4=BF=AE=E5=A4=8D=20Vite=20=E6=9E=84=E5=BB=BA=E5=B4=A9?= =?UTF-8?q?=E6=BA=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Vite/Rollup 构建时将 require() 通过 __toCommonJS() 包装 ESM 导出, 导致命名导出被包裹在 { default: namespace } 中,访问 extractMemoriesModule.initExtractMemories 为 undefined 触发 React error boundary 崩溃。改用标准 ESM 动态 import() 绕过 CJS interop。 Co-Authored-By: glm-5-turbo --- src/cli/print.ts | 12 ++++++++---- src/query/stopHooks.ts | 19 ++++++++++--------- src/utils/backgroundHousekeeping.ts | 11 +++++++---- 3 files changed, 25 insertions(+), 17 deletions(-) diff --git a/src/cli/print.ts b/src/cli/print.ts index 7a9ca405c..9873c7a21 100644 --- a/src/cli/print.ts +++ b/src/cli/print.ts @@ -377,9 +377,6 @@ const cronJitterConfigModule = require('../utils/cronJitterConfig.js') as typeof import('../utils/cronJitterConfig.js') const cronGate = require('@claude-code-best/builtin-tools/tools/ScheduleCronTool/prompt.js') as typeof import('@claude-code-best/builtin-tools/tools/ScheduleCronTool/prompt.js') -const extractMemoriesModule = feature('EXTRACT_MEMORIES') - ? (require('../services/extractMemories/extractMemories.js') as typeof import('../services/extractMemories/extractMemories.js')) - : null /* eslint-enable @typescript-eslint/no-require-imports */ const SHUTDOWN_TEAM_PROMPT = ` @@ -985,7 +982,14 @@ export async function runHeadless( // the forked agent mid-flight. Gated by isExtractModeActive so the // tengu_slate_thimble flag controls non-interactive extraction end-to-end. if (feature('EXTRACT_MEMORIES') && isExtractModeActive()) { - await extractMemoriesModule!.drainPendingExtraction() + try { + const { drainPendingExtraction } = await import( + '../services/extractMemories/extractMemories.js' + ) + await drainPendingExtraction() + } catch { + // Module load failure — non-critical at shutdown + } } gracefulShutdownSync( diff --git a/src/query/stopHooks.ts b/src/query/stopHooks.ts index a3b354c9a..f63f1ee0d 100644 --- a/src/query/stopHooks.ts +++ b/src/query/stopHooks.ts @@ -39,9 +39,6 @@ import { getTaskListId, listTasks } from '../utils/tasks.js' import { getAgentName, getTeamName, isTeammate } from '../utils/teammate.js' /* eslint-disable @typescript-eslint/no-require-imports */ -const extractMemoriesModule = feature('EXTRACT_MEMORIES') - ? (require('../services/extractMemories/extractMemories.js') as typeof import('../services/extractMemories/extractMemories.js')) - : null const jobClassifierModule = feature('TEMPLATES') ? (require('../jobs/classifier.js') as typeof import('../jobs/classifier.js')) : null @@ -154,12 +151,16 @@ export async function* handleStopHooks( // Fire-and-forget in both interactive and non-interactive. For -p/SDK, // print.ts drains the in-flight promise after flushing the response // but before gracefulShutdownSync (see drainPendingExtraction). - void extractMemoriesModule!.executeExtractMemories( - stopHookContext, - toolUseContext.appendSystemMessage as - | ((msg: import('../types/message.js').SystemMessage) => void) - | undefined, - ) + void import('../services/extractMemories/extractMemories.js') + .then(({ executeExtractMemories }) => + executeExtractMemories( + stopHookContext, + toolUseContext.appendSystemMessage as + | ((msg: import('../types/message.js').SystemMessage) => void) + | undefined, + ), + ) + .catch(() => {}) } if (!toolUseContext.agentId && !poorMode) { void executeAutoDream(stopHookContext, toolUseContext.appendSystemMessage) diff --git a/src/utils/backgroundHousekeeping.ts b/src/utils/backgroundHousekeeping.ts index fa5672132..6312dd62b 100644 --- a/src/utils/backgroundHousekeeping.ts +++ b/src/utils/backgroundHousekeeping.ts @@ -4,9 +4,6 @@ import { initMagicDocs } from '../services/MagicDocs/magicDocs.js' import { initSkillImprovement } from './hooks/skillImprovement.js' /* eslint-disable @typescript-eslint/no-require-imports */ -const extractMemoriesModule = feature('EXTRACT_MEMORIES') - ? (require('../services/extractMemories/extractMemories.js') as typeof import('../services/extractMemories/extractMemories.js')) - : null const registerProtocolModule = feature('LODESTONE') ? (require('./deepLink/registerProtocol.js') as typeof import('./deepLink/registerProtocol.js')) : null @@ -32,7 +29,13 @@ export function startBackgroundHousekeeping(): void { void initMagicDocs() void initSkillImprovement() if (feature('EXTRACT_MEMORIES')) { - extractMemoriesModule!.initExtractMemories() + void import('../services/extractMemories/extractMemories.js') + .then(({ initExtractMemories }) => { + initExtractMemories() + }) + .catch(() => { + // Module load failure — non-critical, memory extraction just won't run + }) } initAutoDream() void autoUpdateMarketplacesAndPluginsInBackground() From 833181e02554f682dfd0146c683733d387f40297 Mon Sep 17 00:00:00 2001 From: cepvor Date: Thu, 14 May 2026 16:47:12 +0800 Subject: [PATCH 4/5] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=20MiMo=20?= =?UTF-8?q?=E6=A8=A1=E5=9E=8B=20thinking=20mode=20=E8=87=AA=E5=8A=A8?= =?UTF-8?q?=E6=A3=80=E6=B5=8B=E4=B8=8E=E5=85=BC=E5=AE=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit isOpenAIThinkingEnabled() 现在自动检测模型名包含 "mimo" 的模型 (与 DeepSeek 并列),因为 MiMo 同样使用 reasoning_content 字段 且支持 thinking mode。 buildOpenAIRequestBody() 在 chat_template_kwargs 中同时发送 thinking: true 和 enable_thinking: true,兼容 DeepSeek 自托管和 MiMo 的 thinking 启用格式。 已有 reasoning_content 回传逻辑(openaiConvertMessages.ts)和流 解析逻辑(openaiStreamAdapter.ts)无需修改,MiMo 与 DeepSeek 共用 相同的 reasoning_content 字段协议。 Co-Authored-By: deepseek-v4-pro[1m] --- .../api/openai/__tests__/thinking.test.ts | 21 ++++++++++++++- src/services/api/openai/requestBody.ts | 27 ++++++++++--------- 2 files changed, 34 insertions(+), 14 deletions(-) diff --git a/src/services/api/openai/__tests__/thinking.test.ts b/src/services/api/openai/__tests__/thinking.test.ts index a1f477a18..c147a8dc1 100644 --- a/src/services/api/openai/__tests__/thinking.test.ts +++ b/src/services/api/openai/__tests__/thinking.test.ts @@ -147,6 +147,22 @@ describe('isOpenAIThinkingEnabled', () => { expect(isOpenAIThinkingEnabled('deepseek-coder')).toBe(true) }) + test('returns true when model name is "mimo-v2-flash"', () => { + expect(isOpenAIThinkingEnabled('mimo-v2-flash')).toBe(true) + }) + + test('returns true when model name is "mimo-v2-pro"', () => { + expect(isOpenAIThinkingEnabled('mimo-v2-pro')).toBe(true) + }) + + test('returns true when model name is "mimo-v2.5-pro"', () => { + expect(isOpenAIThinkingEnabled('mimo-v2.5-pro')).toBe(true) + }) + + test('returns true when model name contains "mimo"', () => { + expect(isOpenAIThinkingEnabled('MiMo-V2-Omni')).toBe(true) + }) + test('returns false when model name is "gpt-4o"', () => { expect(isOpenAIThinkingEnabled('gpt-4o')).toBe(false) }) @@ -197,7 +213,10 @@ describe('buildOpenAIRequestBody — thinking params', () => { test('includes vLLM/self-hosted thinking format when enabled', () => { const body = buildOpenAIRequestBody({ ...baseParams, enableThinking: true }) expect(body.enable_thinking).toBe(true) - expect(body.chat_template_kwargs).toEqual({ thinking: true }) + expect(body.chat_template_kwargs).toEqual({ + thinking: true, + enable_thinking: true, + }) }) test('includes both formats simultaneously when enabled', () => { diff --git a/src/services/api/openai/requestBody.ts b/src/services/api/openai/requestBody.ts index 9de76752d..d47ccdfc8 100644 --- a/src/services/api/openai/requestBody.ts +++ b/src/services/api/openai/requestBody.ts @@ -7,11 +7,11 @@ import type { ChatCompletionCreateParamsStreaming } from 'openai/resources/chat/ import { isEnvTruthy, isEnvDefinedFalsy } from '../../../utils/envUtils.js' /** - * Detect whether DeepSeek-style thinking mode should be enabled. + * Detect whether thinking mode should be enabled for this model. * * Enabled when: * 1. OPENAI_ENABLE_THINKING=1 is set (explicit enable), OR - * 2. Model name contains "deepseek-reasoner" OR "DeepSeek-V3.2" (auto-detect, case-insensitive) + * 2. Model name contains "deepseek" or "mimo" (auto-detect, case-insensitive) * * Disabled when: * - OPENAI_ENABLE_THINKING=0/false/no/off is explicitly set (overrides model detection) @@ -23,9 +23,9 @@ export function isOpenAIThinkingEnabled(model: string): boolean { if (isEnvDefinedFalsy(process.env.OPENAI_ENABLE_THINKING)) return false // Explicit enable if (isEnvTruthy(process.env.OPENAI_ENABLE_THINKING)) return true - // Auto-detect from model name (all DeepSeek models support thinking mode) + // Auto-detect from model name (DeepSeek and MiMo models support thinking mode) const modelLower = model.toLowerCase() - return modelLower.includes('deepseek') + return modelLower.includes('deepseek') || modelLower.includes('mimo') } /** @@ -58,12 +58,12 @@ export function resolveOpenAIMaxTokens( * Build the request body for OpenAI chat.completions.create(). * Extracted for testability — the thinking mode params are injected here. * - * DeepSeek thinking mode: inject thinking params via request body. - * Two formats are added simultaneously to support different deployments: - * - Official DeepSeek API: `thinking: { type: 'enabled' }` - * - Self-hosted DeepSeek-V3.2: `enable_thinking: true` + `chat_template_kwargs: { thinking: true }` + * Three thinking-mode formats are sent simultaneously; each endpoint uses the + * format it recognizes and ignores the others: + * - Official DeepSeek API: `thinking: { type: 'enabled' }` + * - Self-hosted DeepSeek: `enable_thinking: true` + `chat_template_kwargs: { thinking: true }` + * - MiMo (Xiaomi): `chat_template_kwargs: { enable_thinking: true }` * OpenAI SDK passes unknown keys through to the HTTP body. - * Each endpoint will use the format it recognizes and ignore the others. */ export function buildOpenAIRequestBody(params: { model: string @@ -76,7 +76,7 @@ export function buildOpenAIRequestBody(params: { }): ChatCompletionCreateParamsStreaming & { thinking?: { type: string } enable_thinking?: boolean - chat_template_kwargs?: { thinking: boolean } + chat_template_kwargs?: { thinking: boolean; enable_thinking: boolean } } { const { model, @@ -97,14 +97,15 @@ export function buildOpenAIRequestBody(params: { }), stream: true, stream_options: { include_usage: true }, - // DeepSeek thinking mode: enable chain-of-thought output. - // When active, temperature/top_p/presence_penalty/frequency_penalty are ignored by DeepSeek. + // Enable chain-of-thought output for DeepSeek and MiMo models. + // When active, temperature/top_p/presence_penalty/frequency_penalty are ignored. ...(enableThinking && { // Official DeepSeek API format thinking: { type: 'enabled' }, // Self-hosted DeepSeek-V3.2 format enable_thinking: true, - chat_template_kwargs: { thinking: true }, + // Both DeepSeek self-hosted and MiMo formats in chat_template_kwargs + chat_template_kwargs: { thinking: true, enable_thinking: true }, }), // Only send temperature when thinking mode is off (DeepSeek ignores it anyway, // but other providers may respect it) From e7070e072f2781148688264ab0dc4b8b7674a38d Mon Sep 17 00:00:00 2001 From: cepvor Date: Thu, 14 May 2026 20:45:24 +0800 Subject: [PATCH 5/5] =?UTF-8?q?fix:=20showSpinnerTree=20=E6=A8=A1=E5=BC=8F?= =?UTF-8?q?=E4=B8=8B=E4=BF=9D=E7=95=99=20local-agent=20token=20=E6=98=BE?= =?UTF-8?q?=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PR #1226 的 CodeRabbit 审查指出:当 spinner-tree 模式开启时, local-agent(后台代理)的 token 消耗完全不可见,因为它们没有 在树中有独立行,但被 showSpinnerTree 的 guard 排除了。 修复:将 guard 从循环外移到循环内,仅对 in_process_teammate 任务在 tree 模式下跳过(它们有独立树行),local-agent 任务 始终计入 teammateTokens。 Closes: review comment from PR #1226 (originally belongs to PR #1225) Co-Authored-By: deepseek-v4-pro[1m] --- src/components/Spinner.tsx | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/src/components/Spinner.tsx b/src/components/Spinner.tsx index 5d8e15622..33a82a460 100644 --- a/src/components/Spinner.tsx +++ b/src/components/Spinner.tsx @@ -210,15 +210,22 @@ function SpinnerWithVerbInner({ const hasRunningTeammates = runningTeammates.length > 0; const allIdle = hasRunningTeammates && runningTeammates.every(t => t.isIdle); - // Gather aggregate token stats from all running swarm teammates - // In spinner-tree mode, skip aggregation (teammates have their own lines in the tree) + // Gather aggregate token stats from all running agents. + // In spinner-tree mode, skip in-process teammates (they have their own + // per-teammate lines in the tree) but still count local-agent tasks + // (background agents) which have no dedicated tree rows. let teammateTokens = 0; - if (!showSpinnerTree) { - for (const task of Object.values(tasks)) { - if (task.status === 'running' && (isInProcessTeammateTask(task) || isLocalAgentTask(task))) { - if (task.progress?.tokenCount) { - teammateTokens += task.progress.tokenCount; - } + for (const task of Object.values(tasks)) { + if (task.status !== 'running') continue; + if (isInProcessTeammateTask(task)) { + if (!showSpinnerTree && task.progress?.tokenCount) { + teammateTokens += task.progress.tokenCount; + } + continue; + } + if (isLocalAgentTask(task)) { + if (task.progress?.tokenCount) { + teammateTokens += task.progress.tokenCount; } } }