From ba7ee64abc1ef50a07f9bc81a6fb2a44a523859a Mon Sep 17 00:00:00 2001 From: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com> Date: Sun, 8 Feb 2026 17:49:28 -0800 Subject: [PATCH] fix: stabilize subagent command UX and suppress nested command rows --- package-lock.json | 121 +++++++++++------- src/core/task/ToolExecutor.ts | 7 +- src/core/task/index.ts | 9 +- .../tools/handlers/SubagentToolHandler.ts | 40 ++++-- .../__tests__/SubagentToolHandler.test.ts | 27 +++- .../task/tools/subagent/SubagentRunner.ts | 28 +++- src/core/task/tools/types/TaskConfig.ts | 7 +- src/integrations/terminal/CommandExecutor.ts | 10 +- .../terminal/CommandOrchestrator.ts | 53 ++++++-- src/integrations/terminal/index.ts | 1 + src/integrations/terminal/types.ts | 21 +++ src/shared/ExtensionMessage.ts | 7 +- webview-ui/package-lock.json | 47 ++----- .../src/components/chat/SubagentStatusRow.tsx | 118 +++++++++++------ .../chat/chat-view/utils/messageUtils.ts | 10 ++ 15 files changed, 355 insertions(+), 151 deletions(-) diff --git a/package-lock.json b/package-lock.json index f033e14d97..30cb7c6edd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3128,7 +3128,6 @@ "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.14.3.tgz", "integrity": "sha512-Iq8QQQ/7X3Sac15oB6p0FmUg/klxQvXLeileoqrTRGJYLV+/9tubbr9ipz0GKHjmXVsgFPo/+W+2cA8eNcR+XA==", "license": "Apache-2.0", - "peer": true, "dependencies": { "@grpc/proto-loader": "^0.8.0", "@js-sdsl/ordered-map": "^4.4.2" @@ -4031,7 +4030,6 @@ "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.26.0.tgz", "integrity": "sha512-Y5RmPncpiDtTXDbLKswIJzTqu2hyBKxTNsgKqKclDbhIgg1wgtf1fRuvxgTnRfcnxtvvgbIEcqUOzZrJ6iSReg==", "license": "MIT", - "peer": true, "dependencies": { "@hono/node-server": "^1.19.9", "ajv": "^8.17.1", @@ -4107,7 +4105,6 @@ "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz", "integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==", "license": "Apache-2.0", - "peer": true, "engines": { "node": ">=8.0.0" } @@ -5798,7 +5795,8 @@ "optional": true, "os": [ "android" - ] + ], + "peer": true }, "node_modules/@rollup/rollup-android-arm64": { "version": "4.57.1", @@ -5811,7 +5809,8 @@ "optional": true, "os": [ "android" - ] + ], + "peer": true }, "node_modules/@rollup/rollup-darwin-arm64": { "version": "4.57.1", @@ -5824,7 +5823,8 @@ "optional": true, "os": [ "darwin" - ] + ], + "peer": true }, "node_modules/@rollup/rollup-darwin-x64": { "version": "4.57.1", @@ -5837,7 +5837,8 @@ "optional": true, "os": [ "darwin" - ] + ], + "peer": true }, "node_modules/@rollup/rollup-freebsd-arm64": { "version": "4.57.1", @@ -5850,7 +5851,8 @@ "optional": true, "os": [ "freebsd" - ] + ], + "peer": true }, "node_modules/@rollup/rollup-freebsd-x64": { "version": "4.57.1", @@ -5863,7 +5865,8 @@ "optional": true, "os": [ "freebsd" - ] + ], + "peer": true }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { "version": "4.57.1", @@ -5876,7 +5879,8 @@ "optional": true, "os": [ "linux" - ] + ], + "peer": true }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { "version": "4.57.1", @@ -5889,7 +5893,8 @@ "optional": true, "os": [ "linux" - ] + ], + "peer": true }, "node_modules/@rollup/rollup-linux-arm64-gnu": { "version": "4.57.1", @@ -5902,7 +5907,8 @@ "optional": true, "os": [ "linux" - ] + ], + "peer": true }, "node_modules/@rollup/rollup-linux-arm64-musl": { "version": "4.57.1", @@ -5915,7 +5921,8 @@ "optional": true, "os": [ "linux" - ] + ], + "peer": true }, "node_modules/@rollup/rollup-linux-loong64-gnu": { "version": "4.57.1", @@ -5928,7 +5935,8 @@ "optional": true, "os": [ "linux" - ] + ], + "peer": true }, "node_modules/@rollup/rollup-linux-loong64-musl": { "version": "4.57.1", @@ -5941,7 +5949,8 @@ "optional": true, "os": [ "linux" - ] + ], + "peer": true }, "node_modules/@rollup/rollup-linux-ppc64-gnu": { "version": "4.57.1", @@ -5954,7 +5963,8 @@ "optional": true, "os": [ "linux" - ] + ], + "peer": true }, "node_modules/@rollup/rollup-linux-ppc64-musl": { "version": "4.57.1", @@ -5967,7 +5977,8 @@ "optional": true, "os": [ "linux" - ] + ], + "peer": true }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { "version": "4.57.1", @@ -5980,7 +5991,8 @@ "optional": true, "os": [ "linux" - ] + ], + "peer": true }, "node_modules/@rollup/rollup-linux-riscv64-musl": { "version": "4.57.1", @@ -5993,7 +6005,8 @@ "optional": true, "os": [ "linux" - ] + ], + "peer": true }, "node_modules/@rollup/rollup-linux-s390x-gnu": { "version": "4.57.1", @@ -6006,7 +6019,8 @@ "optional": true, "os": [ "linux" - ] + ], + "peer": true }, "node_modules/@rollup/rollup-linux-x64-gnu": { "version": "4.57.1", @@ -6019,7 +6033,8 @@ "optional": true, "os": [ "linux" - ] + ], + "peer": true }, "node_modules/@rollup/rollup-linux-x64-musl": { "version": "4.57.1", @@ -6032,7 +6047,8 @@ "optional": true, "os": [ "linux" - ] + ], + "peer": true }, "node_modules/@rollup/rollup-openbsd-x64": { "version": "4.57.1", @@ -6045,7 +6061,8 @@ "optional": true, "os": [ "openbsd" - ] + ], + "peer": true }, "node_modules/@rollup/rollup-openharmony-arm64": { "version": "4.57.1", @@ -6058,7 +6075,8 @@ "optional": true, "os": [ "openharmony" - ] + ], + "peer": true }, "node_modules/@rollup/rollup-win32-arm64-msvc": { "version": "4.57.1", @@ -6071,7 +6089,8 @@ "optional": true, "os": [ "win32" - ] + ], + "peer": true }, "node_modules/@rollup/rollup-win32-ia32-msvc": { "version": "4.57.1", @@ -6084,7 +6103,8 @@ "optional": true, "os": [ "win32" - ] + ], + "peer": true }, "node_modules/@rollup/rollup-win32-x64-gnu": { "version": "4.57.1", @@ -6097,7 +6117,8 @@ "optional": true, "os": [ "win32" - ] + ], + "peer": true }, "node_modules/@rollup/rollup-win32-x64-msvc": { "version": "4.57.1", @@ -6110,7 +6131,8 @@ "optional": true, "os": [ "win32" - ] + ], + "peer": true }, "node_modules/@sap-ai-sdk/ai-api": { "version": "2.6.0", @@ -7782,7 +7804,6 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.32.tgz", "integrity": "sha512-Ez8QE4DMfhjjTsES9K2dwfV258qBui7qxUsoaixZDiTzbde4U12e1pXGNu/ECsUIOi5/zoCxAQxIhQnaUQ2VvA==", "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~6.21.0" } @@ -7845,7 +7866,6 @@ "integrity": "sha512-KkiJeU6VbYbUOp5ITMIc7kBfqlYkKA5KhEHVrGMmUUMt7NeaZg65ojdPk+FtNrBAOXNVM5QM72jnADjM+XVRAQ==", "devOptional": true, "license": "MIT", - "peer": true, "dependencies": { "csstype": "^3.2.2" } @@ -8780,7 +8800,6 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -9745,7 +9764,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -11144,8 +11162,7 @@ "version": "0.0.1367902", "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1367902.tgz", "integrity": "sha512-XxtPuC3PGakY6PD7dG66/o8KwJ/LkH2/EKe19Dcw58w53dv4/vSQEkn/SzuyhHE2q4zPgCkxQBxus3VV4ql+Pg==", - "license": "BSD-3-Clause", - "peer": true + "license": "BSD-3-Clause" }, "node_modules/diff": { "version": "5.2.2", @@ -12162,7 +12179,6 @@ "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", "license": "MIT", - "peer": true, "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", @@ -13470,7 +13486,6 @@ "resolved": "https://registry.npmjs.org/hono/-/hono-4.11.7.tgz", "integrity": "sha512-l7qMiNee7t82bH3SeyUCt9UF15EVmaBvsppY2zQtrbIhl/yzBTny+YUxsVjSjQ6gaqaeVtZmGocom8TzBlA4Yw==", "license": "MIT", - "peer": true, "engines": { "node": ">=16.9.0" } @@ -13749,7 +13764,6 @@ "resolved": "https://registry.npmjs.org/@jrichman/ink/-/ink-6.4.7.tgz", "integrity": "sha512-QHyxhNF5VonF5cRmdAJD/UPucB9nRx3FozWMjQrDGfBxfAL9lpyu72/MlFPgloS1TMTGsOt7YN6dTPPA6mh0Aw==", "license": "MIT", - "peer": true, "dependencies": { "@alcalzone/ansi-tokenize": "^0.2.1", "ansi-escapes": "^7.0.0", @@ -15003,7 +15017,6 @@ "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", "license": "MIT", - "peer": true, "bin": { "jiti": "lib/jiti-cli.mjs" } @@ -15353,7 +15366,6 @@ "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.2.tgz", "integrity": "sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==", "license": "MPL-2.0", - "peer": true, "dependencies": { "detect-libc": "^2.0.3" }, @@ -19090,7 +19102,6 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -21798,7 +21809,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -22120,7 +22130,6 @@ "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz", "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", @@ -22202,6 +22211,7 @@ "os": [ "aix" ], + "peer": true, "engines": { "node": ">=18" } @@ -22218,6 +22228,7 @@ "os": [ "android" ], + "peer": true, "engines": { "node": ">=18" } @@ -22234,6 +22245,7 @@ "os": [ "android" ], + "peer": true, "engines": { "node": ">=18" } @@ -22250,6 +22262,7 @@ "os": [ "android" ], + "peer": true, "engines": { "node": ">=18" } @@ -22266,6 +22279,7 @@ "os": [ "darwin" ], + "peer": true, "engines": { "node": ">=18" } @@ -22282,6 +22296,7 @@ "os": [ "darwin" ], + "peer": true, "engines": { "node": ">=18" } @@ -22298,6 +22313,7 @@ "os": [ "freebsd" ], + "peer": true, "engines": { "node": ">=18" } @@ -22314,6 +22330,7 @@ "os": [ "freebsd" ], + "peer": true, "engines": { "node": ">=18" } @@ -22330,6 +22347,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">=18" } @@ -22346,6 +22364,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">=18" } @@ -22362,6 +22381,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">=18" } @@ -22378,6 +22398,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">=18" } @@ -22394,6 +22415,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">=18" } @@ -22410,6 +22432,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">=18" } @@ -22426,6 +22449,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">=18" } @@ -22442,6 +22466,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">=18" } @@ -22458,6 +22483,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">=18" } @@ -22474,6 +22500,7 @@ "os": [ "netbsd" ], + "peer": true, "engines": { "node": ">=18" } @@ -22490,6 +22517,7 @@ "os": [ "netbsd" ], + "peer": true, "engines": { "node": ">=18" } @@ -22506,6 +22534,7 @@ "os": [ "openbsd" ], + "peer": true, "engines": { "node": ">=18" } @@ -22522,6 +22551,7 @@ "os": [ "openbsd" ], + "peer": true, "engines": { "node": ">=18" } @@ -22538,6 +22568,7 @@ "os": [ "openharmony" ], + "peer": true, "engines": { "node": ">=18" } @@ -22554,6 +22585,7 @@ "os": [ "sunos" ], + "peer": true, "engines": { "node": ">=18" } @@ -22570,6 +22602,7 @@ "os": [ "win32" ], + "peer": true, "engines": { "node": ">=18" } @@ -22586,6 +22619,7 @@ "os": [ "win32" ], + "peer": true, "engines": { "node": ">=18" } @@ -22602,6 +22636,7 @@ "os": [ "win32" ], + "peer": true, "engines": { "node": ">=18" } @@ -22657,6 +22692,7 @@ "os": [ "darwin" ], + "peer": true, "engines": { "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } @@ -23591,7 +23627,6 @@ "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", "license": "MIT", - "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/src/core/task/ToolExecutor.ts b/src/core/task/ToolExecutor.ts index 17d8bea801..0883c1d275 100644 --- a/src/core/task/ToolExecutor.ts +++ b/src/core/task/ToolExecutor.ts @@ -4,6 +4,7 @@ import { getHooksEnabledSafe } from "@core/hooks/hooks-utils" import { ClineIgnoreController } from "@core/ignore/ClineIgnoreController" import { CommandPermissionController } from "@core/permissions" import { DiffViewProvider } from "@integrations/editor/DiffViewProvider" +import type { CommandExecutionOptions } from "@integrations/terminal" import { BrowserSession } from "@services/browser/BrowserSession" import { UrlContentFetcher } from "@services/browser/UrlContentFetcher" import { McpHub } from "@services/mcp/McpHub" @@ -117,7 +118,11 @@ export class ToolExecutor { private saveCheckpoint: (isAttemptCompletionMessage?: boolean, completionMessageTs?: number) => Promise, private sayAndCreateMissingParamError: (toolName: ClineDefaultTool, paramName: string, relPath?: string) => Promise, private removeLastPartialMessageIfExistsWithType: (type: "ask" | "say", askOrSay: ClineAsk | ClineSay) => Promise, - private executeCommandTool: (command: string, timeoutSeconds: number | undefined) => Promise<[boolean, any]>, + private executeCommandTool: ( + command: string, + timeoutSeconds: number | undefined, + options?: CommandExecutionOptions, + ) => Promise<[boolean, any]>, private cancelRunningCommandTool: () => Promise, private doesLatestTaskCompletionHaveNewChanges: () => Promise, private updateFCListFromToolResponse: (taskProgress: string | undefined) => Promise, diff --git a/src/core/task/index.ts b/src/core/task/index.ts index b59a243418..e3e4e1de67 100644 --- a/src/core/task/index.ts +++ b/src/core/task/index.ts @@ -76,6 +76,7 @@ import { getSystemPrompt } from "@/core/prompts/system-prompt" import { HostProvider } from "@/hosts/host-provider" import { FileEditProvider } from "@/integrations/editor/FileEditProvider" import { + type CommandExecutionOptions, CommandExecutor, CommandExecutorCallbacks, FullCommandExecutorConfig, @@ -1549,8 +1550,12 @@ export class Task { } // Tools - async executeCommandTool(command: string, timeoutSeconds: number | undefined): Promise<[boolean, ClineToolResponseContent]> { - return this.commandExecutor.execute(command, timeoutSeconds) + async executeCommandTool( + command: string, + timeoutSeconds: number | undefined, + options?: CommandExecutionOptions, + ): Promise<[boolean, ClineToolResponseContent]> { + return this.commandExecutor.execute(command, timeoutSeconds, options) } /** diff --git a/src/core/task/tools/handlers/SubagentToolHandler.ts b/src/core/task/tools/handlers/SubagentToolHandler.ts index 16c1baa0b0..bd45d5560d 100644 --- a/src/core/task/tools/handlers/SubagentToolHandler.ts +++ b/src/core/task/tools/handlers/SubagentToolHandler.ts @@ -31,7 +31,7 @@ export class UseSubagentsToolHandler implements IFullyManagedTool { readonly name = ClineDefaultTool.USE_SUBAGENTS getDescription(_block: ToolUse): string { - return "[subagent batch]" + return "[subagents]" } async handlePartialBlock(_block: ToolUse, _uiHelpers: StronglyTypedUIHelpers): Promise { @@ -64,7 +64,6 @@ export class UseSubagentsToolHandler implements IFullyManagedTool { const didAutoApprove = !!autoApproveSafe if (didAutoApprove) { - await config.callbacks.say("use_subagents", approvalBody, undefined, undefined, false) telemetryService.captureToolUsage( config.ulid, this.name, @@ -115,6 +114,9 @@ export class UseSubagentsToolHandler implements IFullyManagedTool { toolCalls: 0, inputTokens: 0, outputTokens: 0, + contextTokens: 0, + contextWindow: 0, + contextUsagePercentage: 0, })) const emitStatus = async (status: ClineSaySubagentStatus["status"], partial: boolean) => { @@ -124,6 +126,9 @@ export class UseSubagentsToolHandler implements IFullyManagedTool { const toolCalls = entries.reduce((acc, entry) => acc + (entry.toolCalls || 0), 0) const inputTokens = entries.reduce((acc, entry) => acc + (entry.inputTokens || 0), 0) const outputTokens = entries.reduce((acc, entry) => acc + (entry.outputTokens || 0), 0) + const contextWindow = entries.reduce((acc, entry) => Math.max(acc, entry.contextWindow || 0), 0) + const maxContextTokens = entries.reduce((acc, entry) => Math.max(acc, entry.contextTokens || 0), 0) + const maxContextUsagePercentage = entries.reduce((acc, entry) => Math.max(acc, entry.contextUsagePercentage || 0), 0) const payload: ClineSaySubagentStatus = { status, @@ -134,14 +139,23 @@ export class UseSubagentsToolHandler implements IFullyManagedTool { toolCalls, inputTokens, outputTokens, + contextWindow, + maxContextTokens, + maxContextUsagePercentage, items: entries, } await config.callbacks.say("subagent", JSON.stringify(payload), undefined, undefined, partial) } + let statusUpdateQueue: Promise = Promise.resolve() + const queueStatusUpdate = (status: ClineSaySubagentStatus["status"], partial: boolean): Promise => { + statusUpdateQueue = statusUpdateQueue.catch(() => undefined).then(() => emitStatus(status, partial)) + return statusUpdateQueue + } + await config.callbacks.removeLastPartialMessageIfExistsWithType("say", "subagent") - await emitStatus("running", true) + await queueStatusUpdate("running", true) const runners = prompts.map(() => new SubagentRunner(config)) const abortPollInterval = setInterval(() => { @@ -174,8 +188,11 @@ export class UseSubagentsToolHandler implements IFullyManagedTool { current.toolCalls = update.stats.toolCalls || 0 current.inputTokens = update.stats.inputTokens || 0 current.outputTokens = update.stats.outputTokens || 0 + current.contextTokens = update.stats.contextTokens || 0 + current.contextWindow = update.stats.contextWindow || 0 + current.contextUsagePercentage = update.stats.contextUsagePercentage || 0 } - await emitStatus("running", true) + await queueStatusUpdate("running", true) }), ) @@ -193,24 +210,27 @@ export class UseSubagentsToolHandler implements IFullyManagedTool { entries[index].toolCalls = result.value.stats.toolCalls || 0 entries[index].inputTokens = result.value.stats.inputTokens || 0 entries[index].outputTokens = result.value.stats.outputTokens || 0 + entries[index].contextTokens = result.value.stats.contextTokens || 0 + entries[index].contextWindow = result.value.stats.contextWindow || 0 + entries[index].contextUsagePercentage = result.value.stats.contextUsagePercentage || 0 }) const failures = entries.filter((entry) => entry.status === "failed").length - await emitStatus(failures > 0 ? "failed" : "completed", false) + await queueStatusUpdate(failures > 0 ? "failed" : "completed", false) const successCount = entries.length - failures const totalToolCalls = entries.reduce((acc, entry) => acc + (entry.toolCalls || 0), 0) - const totalInputTokens = entries.reduce((acc, entry) => acc + (entry.inputTokens || 0), 0) - const totalOutputTokens = entries.reduce((acc, entry) => acc + (entry.outputTokens || 0), 0) + const maxContextUsagePercentage = entries.reduce((acc, entry) => Math.max(acc, entry.contextUsagePercentage || 0), 0) + const maxContextTokens = entries.reduce((acc, entry) => Math.max(acc, entry.contextTokens || 0), 0) + const contextWindow = entries.reduce((acc, entry) => Math.max(acc, entry.contextWindow || 0), 0) const summary = [ - `Subagent batch complete.`, + "Subagent results:", `Total: ${entries.length}`, `Succeeded: ${successCount}`, `Failed: ${failures}`, `Tool calls: ${totalToolCalls}`, - `Input tokens: ${totalInputTokens}`, - `Output tokens: ${totalOutputTokens}`, + `Peak context usage: ${maxContextTokens.toLocaleString()} / ${contextWindow.toLocaleString()} (${maxContextUsagePercentage.toFixed(1)}%)`, "", ...entries.map((entry) => { const header = `[${entry.index}] ${entry.status.toUpperCase()} - ${entry.prompt}` diff --git a/src/core/task/tools/handlers/__tests__/SubagentToolHandler.test.ts b/src/core/task/tools/handlers/__tests__/SubagentToolHandler.test.ts index 3d23f6afbe..a5755c7eea 100644 --- a/src/core/task/tools/handlers/__tests__/SubagentToolHandler.test.ts +++ b/src/core/task/tools/handlers/__tests__/SubagentToolHandler.test.ts @@ -147,6 +147,9 @@ describe("SubagentToolHandler", () => { outputTokens: 3, cacheWriteTokens: 0, cacheReadTokens: 0, + contextTokens: 5, + contextWindow: 200000, + contextUsagePercentage: 0.0025, }, }) @@ -161,8 +164,8 @@ describe("SubagentToolHandler", () => { }) sinon.assert.notCalled(callbacks.ask) - const approvalSayCalls = callbacks.say.getCalls().filter((call: any) => call.args[0] === "use_subagents") - assert.ok(approvalSayCalls.length >= 1) + const subagentStatusCalls = callbacks.say.getCalls().filter((call: any) => call.args[0] === "subagent") + assert.ok(subagentStatusCalls.length >= 1) }) it("fans out prompts in parallel and emits aggregated status", async () => { @@ -175,7 +178,16 @@ describe("SubagentToolHandler", () => { maxActiveRuns = Math.max(maxActiveRuns, activeRuns) onProgress({ status: "running", - stats: { toolCalls: 0, inputTokens: 0, outputTokens: 0, cacheWriteTokens: 0, cacheReadTokens: 0 }, + stats: { + toolCalls: 0, + inputTokens: 0, + outputTokens: 0, + cacheWriteTokens: 0, + cacheReadTokens: 0, + contextTokens: 0, + contextWindow: 200000, + contextUsagePercentage: 0, + }, }) await delay(10) activeRuns-- @@ -188,6 +200,9 @@ describe("SubagentToolHandler", () => { outputTokens: 3, cacheWriteTokens: 0, cacheReadTokens: 0, + contextTokens: 5, + contextWindow: 200000, + contextUsagePercentage: 0.0025, }, } }) @@ -228,6 +243,9 @@ describe("SubagentToolHandler", () => { outputTokens: 0, cacheWriteTokens: 0, cacheReadTokens: 0, + contextTokens: 0, + contextWindow: 200000, + contextUsagePercentage: 0, }, } } @@ -240,6 +258,9 @@ describe("SubagentToolHandler", () => { outputTokens: 0, cacheWriteTokens: 0, cacheReadTokens: 0, + contextTokens: 0, + contextWindow: 200000, + contextUsagePercentage: 0, }, } }) diff --git a/src/core/task/tools/subagent/SubagentRunner.ts b/src/core/task/tools/subagent/SubagentRunner.ts index 320087cba4..6f5343652d 100644 --- a/src/core/task/tools/subagent/SubagentRunner.ts +++ b/src/core/task/tools/subagent/SubagentRunner.ts @@ -46,6 +46,9 @@ interface SubagentRunStats { outputTokens: number cacheWriteTokens: number cacheReadTokens: number + contextTokens: number + contextWindow: number + contextUsagePercentage: number } interface SubagentToolCall { @@ -172,6 +175,9 @@ export class SubagentRunner { outputTokens: 0, cacheWriteTokens: 0, cacheReadTokens: 0, + contextTokens: 0, + contextWindow: 0, + contextUsagePercentage: 0, } onProgress({ status: "running", stats }) @@ -191,6 +197,7 @@ export class SubagentRunner { mode, customPrompt: this.baseConfig.services.stateManager.getGlobalSettingsKey("customPrompt"), } + stats.contextWindow = providerInfo.model.info.contextWindow || 0 const useNativeToolCalls = providerInfo.model.info.apiFormat === ApiFormat.OPENAI_RESPONSES || !!this.baseConfig.services.stateManager.getGlobalStateKey("nativeToolCallEnabled") @@ -244,6 +251,10 @@ export class SubagentRunner { while (true) { const streamHandler = new StreamResponseHandler() const { toolUseHandler } = streamHandler.getHandlers() + let requestInputTokens = 0 + let requestOutputTokens = 0 + let requestCacheWriteTokens = 0 + let requestCacheReadTokens = 0 let assistantText = "" let assistantTextSignature: string | undefined @@ -259,6 +270,14 @@ export class SubagentRunner { stats.outputTokens += chunk.outputTokens || 0 stats.cacheWriteTokens += chunk.cacheWriteTokens || 0 stats.cacheReadTokens += chunk.cacheReadTokens || 0 + requestInputTokens += chunk.inputTokens || 0 + requestOutputTokens += chunk.outputTokens || 0 + requestCacheWriteTokens += chunk.cacheWriteTokens || 0 + requestCacheReadTokens += chunk.cacheReadTokens || 0 + stats.contextTokens = + requestInputTokens + requestOutputTokens + requestCacheWriteTokens + requestCacheReadTokens + stats.contextUsagePercentage = + stats.contextWindow > 0 ? (stats.contextTokens / stats.contextWindow) * 100 : 0 onProgress({ stats: { ...stats } }) break case "text": @@ -438,12 +457,19 @@ export class SubagentRunner { ...this.baseConfig, taskState: state, isSubagentExecution: true, + vscodeTerminalExecutionMode: "backgroundExec", callbacks: { ...baseCallbacks, + say: async () => undefined, + sayAndCreateMissingParamError: async (_toolName, paramName) => + formatResponse.toolError(formatResponse.missingToolParameterError(paramName)), executeCommandTool: async (command: string, timeoutSeconds: number | undefined) => { this.activeCommandExecutions += 1 try { - return await baseCallbacks.executeCommandTool(command, timeoutSeconds) + return await baseCallbacks.executeCommandTool(command, timeoutSeconds, { + useBackgroundExecution: true, + suppressUserInteraction: true, + }) } finally { this.activeCommandExecutions = Math.max(0, this.activeCommandExecutions - 1) } diff --git a/src/core/task/tools/types/TaskConfig.ts b/src/core/task/tools/types/TaskConfig.ts index 7ac264b43e..a1d08daa6b 100644 --- a/src/core/task/tools/types/TaskConfig.ts +++ b/src/core/task/tools/types/TaskConfig.ts @@ -3,6 +3,7 @@ import type { FileContextTracker } from "@core/context/context-tracking/FileCont import type { ClineIgnoreController } from "@core/ignore/ClineIgnoreController" import type { CommandPermissionController } from "@core/permissions" import type { DiffViewProvider } from "@integrations/editor/DiffViewProvider" +import type { CommandExecutionOptions } from "@integrations/terminal" import type { BrowserSession } from "@services/browser/BrowserSession" import type { UrlContentFetcher } from "@services/browser/UrlContentFetcher" import type { McpHub } from "@services/mcp/McpHub" @@ -105,7 +106,11 @@ export interface TaskCallbacks { removeLastPartialMessageIfExistsWithType: (type: "ask" | "say", askOrSay: ClineAsk | ClineSay) => Promise - executeCommandTool: (command: string, timeoutSeconds: number | undefined) => Promise<[boolean, any]> + executeCommandTool: ( + command: string, + timeoutSeconds: number | undefined, + options?: CommandExecutionOptions, + ) => Promise<[boolean, any]> cancelRunningCommandTool?: () => Promise doesLatestTaskCompletionHaveNewChanges: () => Promise diff --git a/src/integrations/terminal/CommandExecutor.ts b/src/integrations/terminal/CommandExecutor.ts index 33a1fd7729..90135976d2 100644 --- a/src/integrations/terminal/CommandExecutor.ts +++ b/src/integrations/terminal/CommandExecutor.ts @@ -19,6 +19,7 @@ import { Logger } from "@/shared/services/Logger" import { orchestrateCommandExecution } from "./CommandOrchestrator" import { StandaloneTerminalManager } from "./standalone/StandaloneTerminalManager" import type { + CommandExecutionOptions, CommandExecutorCallbacks, CommandExecutorConfig, ITerminalManager, @@ -94,7 +95,11 @@ export class CommandExecutor { * @param timeoutSeconds Optional timeout in seconds * @returns [userRejected, result] tuple */ - async execute(command: string, timeoutSeconds: number | undefined): Promise<[boolean, ClineToolResponseContent]> { + async execute( + command: string, + timeoutSeconds: number | undefined, + options?: CommandExecutionOptions, + ): Promise<[boolean, ClineToolResponseContent]> { // Strip leading `cd` to workspace from command const workspaceCdPrefix = `cd ${this.cwd} && ` if (command.startsWith(workspaceCdPrefix)) { @@ -102,7 +107,7 @@ export class CommandExecutor { } // Select the appropriate terminal manager - const useStandalone = this.terminalExecutionMode === "backgroundExec" + const useStandalone = options?.useBackgroundExecution || this.terminalExecutionMode === "backgroundExec" const manager = useStandalone ? this.standaloneManager : this.terminalManager Logger.info(`Executing command in ${useStandalone ? "standalone" : "VSCode"} terminal: ${command}`) @@ -125,6 +130,7 @@ export class CommandExecutor { const result = await orchestrateCommandExecution(process, manager, this.callbacks, { command, timeoutSeconds, + suppressUserInteraction: options?.suppressUserInteraction, // When "Proceed While Running" is triggered, track the command in the manager // Returns the log file path so the orchestrator can send it to the UI // existingOutput contains all output lines captured so far diff --git a/src/integrations/terminal/CommandOrchestrator.ts b/src/integrations/terminal/CommandOrchestrator.ts index 6bc859eda2..d1095c4ea3 100644 --- a/src/integrations/terminal/CommandOrchestrator.ts +++ b/src/integrations/terminal/CommandOrchestrator.ts @@ -61,8 +61,35 @@ export async function orchestrateCommandExecution( showShellIntegrationSuggestion, onProceedWhileRunning, terminalType = "vscode", + suppressUserInteraction = false, } = options + const say = async ( + type: Parameters[0], + text?: Parameters[1], + images?: Parameters[2], + files?: Parameters[3], + partial?: Parameters[4], + ): Promise>> => { + if (suppressUserInteraction) { + return undefined + } + + return callbacks.say(type, text, images, files, partial) + } + + const ask = async ( + type: Parameters[0], + text?: Parameters[1], + partial?: Parameters[2], + ): Promise> | undefined> => { + if (suppressUserInteraction) { + return undefined + } + + return callbacks.ask(type, text, partial) + } + // Track command execution state callbacks.updateBackgroundCommandState(true) @@ -121,7 +148,11 @@ export async function orchestrateCommandExecution( try { // Use ask() to present output and wait for user response // This enables "Proceed While Running" button functionality - const { response, text, images, files } = await callbacks.ask("command_output", chunk) + const interaction = await ask("command_output", chunk) + if (!interaction) { + return + } + const { response, text, images, files } = interaction if (response === "yesButtonClicked") { // Track when user clicks "Proceed While Running" @@ -167,7 +198,7 @@ export async function orchestrateCommandExecution( // Send log file message to UI BEFORE resuming the process // This ensures the message appears before any new output lines if (trackingResult?.logFilePath) { - await callbacks.say("command_output", `\nšŸ“‹ Output is being logged to: ${trackingResult.logFilePath}`) + await say("command_output", `\nšŸ“‹ Output is being logged to: ${trackingResult.logFilePath}`) } // Now resume the process - any new lines will be handled by the background tracker @@ -186,7 +217,7 @@ export async function orchestrateCommandExecution( outputBufferSize = 0 // Send cancellation message BEFORE resuming the process // This ensures the message appears before any new output lines - await callbacks.say("command_output", "Command cancelled") + await say("command_output", "Command cancelled") // Now resume the process process.continue() } else { @@ -209,7 +240,7 @@ export async function orchestrateCommandExecution( } } else { // After "Proceed While Running": stream output directly to UI - await callbacks.say("command_output", chunk) + await say("command_output", chunk) } } @@ -245,7 +276,7 @@ export async function orchestrateCommandExecution( outputBufferSize = 0 if (!didContinue) { // Use say() instead of ask() since we're transitioning to file mode - await callbacks.say("command_output", chunk) + await say("command_output", chunk) } } @@ -271,7 +302,7 @@ export async function orchestrateCommandExecution( lastLines = outputLines.slice(-SUMMARY_LINES_TO_KEEP) // FINALLY: Notify user (now this will appear at the end after all buffered output) - await callbacks.say( + await say( "command_output", `\nšŸ“‹ Output is large (${outputLines.length} lines, ${Math.round(totalOutputBytes / 1024)}KB). Writing to: ${largeOutputLogPath}`, ) @@ -346,7 +377,7 @@ export async function orchestrateCommandExecution( // After "Proceed While Running" (without background tracking): stream output directly to UI // But throttle if we're in file mode to avoid flooding UI if (!isWritingToFile) { - await callbacks.say("command_output", line) + await say("command_output", line) } } }) @@ -383,9 +414,9 @@ export async function orchestrateCommandExecution( process.once("no_shell_integration", async () => { if (showShellIntegrationSuggestion) { - await callbacks.say("shell_integration_warning_with_suggestion") + await say("shell_integration_warning_with_suggestion") } else { - await callbacks.say("shell_integration_warning") + await say("shell_integration_warning") } }) @@ -435,7 +466,7 @@ export async function orchestrateCommandExecution( // Send log file message to UI BEFORE resuming the process if (trackingResult?.logFilePath) { - await callbacks.say( + await say( "command_output", `\nā±ļø Command timed out. Output is being logged to: ${trackingResult.logFilePath}`, ) @@ -523,7 +554,7 @@ export async function orchestrateCommandExecution( } if (userFeedback) { - await callbacks.say("user_feedback", userFeedback.text, userFeedback.images, userFeedback.files) + await say("user_feedback", userFeedback.text, userFeedback.images, userFeedback.files) let fileContentString = "" if (userFeedback.files && userFeedback.files.length > 0) { diff --git a/src/integrations/terminal/index.ts b/src/integrations/terminal/index.ts index 61f6476fd4..86f9aaa375 100644 --- a/src/integrations/terminal/index.ts +++ b/src/integrations/terminal/index.ts @@ -36,6 +36,7 @@ export type { // Command Executor types ActiveBackgroundCommand, AskResponse, + CommandExecutionOptions, CommandExecutorCallbacks, CommandExecutorConfig, FullCommandExecutorConfig, diff --git a/src/integrations/terminal/types.ts b/src/integrations/terminal/types.ts index 7aaaf524c7..b31ef68d47 100644 --- a/src/integrations/terminal/types.ts +++ b/src/integrations/terminal/types.ts @@ -341,6 +341,22 @@ export interface CommandExecutorCallbacks { addToUserMessageContent: (content: { type: string; text: string }) => void } +/** + * Optional per-command execution behavior overrides. + */ +export interface CommandExecutionOptions { + /** + * Force command execution in standalone/background terminal mode for this command. + * This is useful for subagent runs and headless-style execution flows. + */ + useBackgroundExecution?: boolean + /** + * Suppress command interaction/output UI messages (ask/say) for this command execution. + * Command output is still captured and returned as the tool result. + */ + suppressUserInteraction?: boolean +} + /** * Configuration for CommandExecutor */ @@ -388,6 +404,11 @@ export interface OrchestrationOptions { * Defaults to "vscode" for backward compatibility. */ terminalType?: "vscode" | "standalone" + /** + * If true, suppresses command-output ask/say UI interactions. + * Output is still collected and included in the final result. + */ + suppressUserInteraction?: boolean } /** diff --git a/src/shared/ExtensionMessage.ts b/src/shared/ExtensionMessage.ts index c78a123c47..c5fae57c4f 100644 --- a/src/shared/ExtensionMessage.ts +++ b/src/shared/ExtensionMessage.ts @@ -37,7 +37,6 @@ export type Platform = "aix" | "darwin" | "freebsd" | "linux" | "openbsd" | "sun export const DEFAULT_PLATFORM = "unknown" export const COMMAND_CANCEL_TOKEN = "__cline_command_cancel__" - export interface ExtensionState { isNewUser: boolean welcomeViewCompleted: boolean @@ -279,6 +278,9 @@ export interface SubagentStatusItem { toolCalls: number inputTokens: number outputTokens: number + contextTokens: number + contextWindow: number + contextUsagePercentage: number result?: string error?: string } @@ -292,6 +294,9 @@ export interface ClineSaySubagentStatus { toolCalls: number inputTokens: number outputTokens: number + contextWindow: number + maxContextTokens: number + maxContextUsagePercentage: number items: SubagentStatusItem[] } diff --git a/webview-ui/package-lock.json b/webview-ui/package-lock.json index 625b23d4ba..f181507c28 100644 --- a/webview-ui/package-lock.json +++ b/webview-ui/package-lock.json @@ -160,7 +160,6 @@ "version": "7.28.4", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.3", @@ -501,7 +500,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=18" }, @@ -523,7 +521,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=18" } @@ -531,7 +528,6 @@ "node_modules/@emotion/is-prop-valid": { "version": "1.2.2", "license": "MIT", - "peer": true, "dependencies": { "@emotion/memoize": "^0.8.1" } @@ -1037,7 +1033,6 @@ "node_modules/@firebase/app": { "version": "0.13.2", "license": "Apache-2.0", - "peer": true, "dependencies": { "@firebase/component": "0.6.18", "@firebase/logger": "0.4.4", @@ -1094,7 +1089,6 @@ "node_modules/@firebase/app-compat": { "version": "0.4.2", "license": "Apache-2.0", - "peer": true, "dependencies": { "@firebase/app": "0.13.2", "@firebase/component": "0.6.18", @@ -1108,8 +1102,7 @@ }, "node_modules/@firebase/app-types": { "version": "0.9.3", - "license": "Apache-2.0", - "peer": true + "license": "Apache-2.0" }, "node_modules/@firebase/auth-compat": { "version": "0.5.28", @@ -1496,7 +1489,6 @@ "version": "1.12.1", "hasInstallScript": true, "license": "Apache-2.0", - "peer": true, "dependencies": { "tslib": "^2.1.0" }, @@ -2593,7 +2585,6 @@ "node_modules/@heroui/system": { "version": "2.4.22", "license": "MIT", - "peer": true, "dependencies": { "@heroui/react-utils": "2.1.13", "@heroui/system-rsc": "2.3.19", @@ -2678,7 +2669,6 @@ "node_modules/@heroui/theme": { "version": "2.4.22", "license": "MIT", - "peer": true, "dependencies": { "@heroui/shared-utils": "2.1.11", "clsx": "^1.2.1", @@ -6361,7 +6351,8 @@ "node_modules/@types/aria-query": { "version": "5.0.4", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@types/babel__core": { "version": "7.20.5", @@ -6738,7 +6729,6 @@ "node_modules/@types/react": { "version": "18.3.18", "license": "MIT", - "peer": true, "dependencies": { "@types/prop-types": "*", "csstype": "^3.0.2" @@ -6748,7 +6738,6 @@ "version": "18.3.7", "devOptional": true, "license": "MIT", - "peer": true, "peerDependencies": { "@types/react": "^18.0.0" } @@ -7140,7 +7129,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.8.3", "caniuse-lite": "^1.0.30001741", @@ -7270,7 +7258,6 @@ "node_modules/chevrotain": { "version": "11.0.3", "license": "Apache-2.0", - "peer": true, "dependencies": { "@chevrotain/cst-dts-gen": "11.0.3", "@chevrotain/gast": "11.0.3", @@ -7541,7 +7528,6 @@ "node_modules/cytoscape": { "version": "3.33.1", "license": "MIT", - "peer": true, "engines": { "node": ">=0.10" } @@ -7874,7 +7860,6 @@ "node_modules/d3-selection": { "version": "3.0.0", "license": "ISC", - "peer": true, "engines": { "node": ">=12" } @@ -8101,7 +8086,8 @@ "node_modules/dom-accessibility-api": { "version": "0.5.16", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/dompurify": { "version": "3.2.7", @@ -8164,7 +8150,6 @@ "dev": true, "hasInstallScript": true, "license": "MIT", - "peer": true, "bin": { "esbuild": "bin/esbuild" }, @@ -8445,7 +8430,6 @@ "node_modules/framer-motion": { "version": "12.23.18", "license": "MIT", - "peer": true, "dependencies": { "motion-dom": "^12.23.18", "motion-utils": "^12.23.6", @@ -9388,7 +9372,6 @@ "version": "26.1.0", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "cssstyle": "^4.2.1", "data-urls": "^5.0.0", @@ -9859,6 +9842,7 @@ "version": "1.5.0", "dev": true, "license": "MIT", + "peer": true, "bin": { "lz-string": "bin/bin.js" } @@ -11761,6 +11745,7 @@ "version": "27.5.1", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "ansi-regex": "^5.0.1", "ansi-styles": "^5.0.0", @@ -11774,6 +11759,7 @@ "version": "5.2.0", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=10" }, @@ -11836,7 +11822,6 @@ "node_modules/react": { "version": "18.3.1", "license": "MIT", - "peer": true, "dependencies": { "loose-envify": "^1.1.0" }, @@ -11875,7 +11860,6 @@ "node_modules/react-dom": { "version": "18.3.1", "license": "MIT", - "peer": true, "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" @@ -11887,7 +11871,8 @@ "node_modules/react-is": { "version": "17.0.2", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/react-markdown": { "version": "10.1.0", @@ -12551,7 +12536,6 @@ "version": "4.52.1", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@types/estree": "1.0.8" }, @@ -12875,7 +12859,6 @@ "version": "9.1.17", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@storybook/global": "^5.0.0", "@testing-library/jest-dom": "^6.6.3", @@ -13141,7 +13124,6 @@ "node_modules/tailwind-merge": { "version": "3.3.1", "license": "MIT", - "peer": true, "funding": { "type": "github", "url": "https://github.com/sponsors/dcastil" @@ -13166,8 +13148,7 @@ }, "node_modules/tailwindcss": { "version": "4.1.13", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/tailwindcss-animate": { "version": "1.0.7", @@ -13379,14 +13360,12 @@ }, "node_modules/tslib": { "version": "2.8.1", - "license": "0BSD", - "peer": true + "license": "0BSD" }, "node_modules/typescript": { "version": "5.9.2", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -13739,7 +13718,6 @@ "version": "7.2.2", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", @@ -13861,7 +13839,6 @@ "version": "3.2.4", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@types/chai": "^5.2.2", "@vitest/expect": "3.2.4", diff --git a/webview-ui/src/components/chat/SubagentStatusRow.tsx b/webview-ui/src/components/chat/SubagentStatusRow.tsx index 4b744f4a52..9ad6c33852 100644 --- a/webview-ui/src/components/chat/SubagentStatusRow.tsx +++ b/webview-ui/src/components/chat/SubagentStatusRow.tsx @@ -1,6 +1,14 @@ import { ClineMessage, ClineSaySubagentStatus, SubagentExecutionStatus } from "@shared/ExtensionMessage" -import { CheckIcon, CircleSlashIcon, CircleXIcon, LoaderCircleIcon } from "lucide-react" -import { useMemo } from "react" +import { + BotIcon, + CheckIcon, + ChevronDownIcon, + ChevronRightIcon, + CircleSlashIcon, + CircleXIcon, + LoaderCircleIcon, +} from "lucide-react" +import { useMemo, useState } from "react" interface SubagentStatusRowProps { message: ClineMessage @@ -8,7 +16,9 @@ interface SubagentStatusRowProps { lastModifiedMessage?: ClineMessage } -const statusLabel = (status: SubagentExecutionStatus): string => { +type DisplayStatus = SubagentExecutionStatus | "cancelled" + +const statusLabel = (status: DisplayStatus): string => { switch (status) { case "pending": return "Pending" @@ -18,21 +28,25 @@ const statusLabel = (status: SubagentExecutionStatus): string => { return "Completed" case "failed": return "Failed" + case "cancelled": + return "Cancelled" default: return "Unknown" } } -const aggregateIcon = (status: ClineSaySubagentStatus["status"]) => { +const statusIcon = (status: DisplayStatus) => { switch (status) { case "running": - return + return case "completed": - return + return case "failed": - return + return + case "cancelled": + return default: - return + return } } @@ -45,6 +59,7 @@ const formatCount = (value: number | undefined): string => { } export default function SubagentStatusRow({ message, isLast, lastModifiedMessage }: SubagentStatusRowProps) { + const [expandedItems, setExpandedItems] = useState>({}) const data = useMemo(() => { try { if (!message.text) { @@ -63,45 +78,66 @@ export default function SubagentStatusRow({ message, isLast, lastModifiedMessage const wasCancelled = data.status === "running" && (!isLast || lastModifiedMessage?.ask === "resume_task" || lastModifiedMessage?.ask === "resume_completed_task") - const displayStatus: ClineSaySubagentStatus["status"] = wasCancelled ? "failed" : data.status + + const singular = data.items.length === 1 + const title = singular ? "Cline wants to use a subagent:" : "Cline wants to use subagents:" + const toggleItem = (index: number) => { + setExpandedItems((prev) => ({ + ...prev, + [index]: !prev[index], + })) + } return (
-
- {wasCancelled ? : aggregateIcon(displayStatus)} - Subagent batch - {wasCancelled && Cancelled} -
- {data.completed}/{data.total} complete, {data.successes} succeeded, {data.failures} failed -
-
-
- {formatCount(data.toolCalls)} tool calls, {formatCount(data.inputTokens)} input tokens,{" "} - {formatCount(data.outputTokens)} output tokens -
+
{title}
- {data.items.map((entry) => ( -
-
-
- [{entry.index}] {statusLabel(entry.status)} -
-
- {formatCount(entry.toolCalls || 0)} tools, {formatCount(entry.inputTokens || 0)} in,{" "} - {formatCount(entry.outputTokens || 0)} out + {data.items.map((entry) => { + const displayStatus: DisplayStatus = + wasCancelled && (entry.status === "running" || entry.status === "pending") ? "cancelled" : entry.status + const hasDetails = Boolean( + (entry.result && entry.status === "completed") || (entry.error && entry.status === "failed"), + ) + const isExpanded = expandedItems[entry.index] === true + return ( +
+
+ {statusIcon(displayStatus)} +
+
+ Subagent: "{entry.prompt}" +
+
+ #{entry.index} | {statusLabel(displayStatus)} | {formatCount(entry.toolCalls)} tools + called | {formatCount(entry.contextTokens)} tokens used +
+ {hasDetails && ( + + )} +
+ {isExpanded && entry.result && entry.status === "completed" && ( +
{entry.result}
+ )} + {isExpanded && entry.error && entry.status === "failed" && ( +
{entry.error}
+ )}
-
{entry.prompt}
- {entry.result && entry.status === "completed" && ( -
{entry.result}
- )} - {entry.error && entry.status === "failed" && ( -
{entry.error}
- )} -
- ))} + ) + })}
) diff --git a/webview-ui/src/components/chat/chat-view/utils/messageUtils.ts b/webview-ui/src/components/chat/chat-view/utils/messageUtils.ts index 1b9ce3824a..fac206813b 100644 --- a/webview-ui/src/components/chat/chat-view/utils/messageUtils.ts +++ b/webview-ui/src/components/chat/chat-view/utils/messageUtils.ts @@ -63,6 +63,11 @@ export function filterVisibleMessages(messages: ClineMessage[]): ClineMessage[] case "resume_task": case "resume_completed_task": return false + case "use_subagents": + if (arr.slice(index + 1).some((candidate) => candidate.type === "say" && candidate.say === "subagent")) { + return false + } + break } switch (message.say) { case "api_req_finished": // combineApiRequests removes this from modifiedMessages anyways @@ -93,6 +98,11 @@ export function filterVisibleMessages(messages: ClineMessage[]): ClineMessage[] break case "mcp_server_request_started": return false + case "use_subagents": + if (arr.slice(index + 1).some((candidate) => candidate.type === "say" && candidate.say === "subagent")) { + return false + } + break } return true })