fix: stabilize subagent command UX and suppress nested command rows

This commit is contained in:
Saoud Rizwan
2026-02-09 17:31:34 -08:00
parent aa9448bf75
commit ba7ee64abc
15 changed files with 355 additions and 151 deletions
+78 -43
View File
@@ -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"
}
+6 -1
View File
@@ -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<void>,
private sayAndCreateMissingParamError: (toolName: ClineDefaultTool, paramName: string, relPath?: string) => Promise<any>,
private removeLastPartialMessageIfExistsWithType: (type: "ask" | "say", askOrSay: ClineAsk | ClineSay) => Promise<void>,
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<boolean>,
private doesLatestTaskCompletionHaveNewChanges: () => Promise<boolean>,
private updateFCListFromToolResponse: (taskProgress: string | undefined) => Promise<void>,
+7 -2
View File
@@ -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)
}
/**
@@ -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<void> {
@@ -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<void> = Promise.resolve()
const queueStatusUpdate = (status: ClineSaySubagentStatus["status"], partial: boolean): Promise<void> => {
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}`
@@ -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,
},
}
})
+27 -1
View File
@@ -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)
}
+6 -1
View File
@@ -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<void>
executeCommandTool: (command: string, timeoutSeconds: number | undefined) => Promise<[boolean, any]>
executeCommandTool: (
command: string,
timeoutSeconds: number | undefined,
options?: CommandExecutionOptions,
) => Promise<[boolean, any]>
cancelRunningCommandTool?: () => Promise<boolean>
doesLatestTaskCompletionHaveNewChanges: () => Promise<boolean>
+8 -2
View File
@@ -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
@@ -61,8 +61,35 @@ export async function orchestrateCommandExecution(
showShellIntegrationSuggestion,
onProceedWhileRunning,
terminalType = "vscode",
suppressUserInteraction = false,
} = options
const say = async (
type: Parameters<CommandExecutorCallbacks["say"]>[0],
text?: Parameters<CommandExecutorCallbacks["say"]>[1],
images?: Parameters<CommandExecutorCallbacks["say"]>[2],
files?: Parameters<CommandExecutorCallbacks["say"]>[3],
partial?: Parameters<CommandExecutorCallbacks["say"]>[4],
): Promise<Awaited<ReturnType<CommandExecutorCallbacks["say"]>>> => {
if (suppressUserInteraction) {
return undefined
}
return callbacks.say(type, text, images, files, partial)
}
const ask = async (
type: Parameters<CommandExecutorCallbacks["ask"]>[0],
text?: Parameters<CommandExecutorCallbacks["ask"]>[1],
partial?: Parameters<CommandExecutorCallbacks["ask"]>[2],
): Promise<Awaited<ReturnType<CommandExecutorCallbacks["ask"]>> | 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) {
+1
View File
@@ -36,6 +36,7 @@ export type {
// Command Executor types
ActiveBackgroundCommand,
AskResponse,
CommandExecutionOptions,
CommandExecutorCallbacks,
CommandExecutorConfig,
FullCommandExecutorConfig,
+21
View File
@@ -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
}
/**
+6 -1
View File
@@ -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[]
}
+12 -35
View File
@@ -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",
@@ -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 <LoaderCircleIcon className="size-2 mr-2 animate-spin text-link" />
return <LoaderCircleIcon className="size-2 animate-spin text-link shrink-0 mt-[1px]" />
case "completed":
return <CheckIcon className="size-2 mr-2 text-success" />
return <CheckIcon className="size-2 text-success shrink-0 mt-[1px]" />
case "failed":
return <CircleXIcon className="size-2 mr-2 text-error" />
return <CircleXIcon className="size-2 text-error shrink-0 mt-[1px]" />
case "cancelled":
return <CircleSlashIcon className="size-2 text-foreground shrink-0 mt-[1px]" />
default:
return <LoaderCircleIcon className="size-2 mr-2 animate-spin text-link" />
return <BotIcon className="size-2 text-foreground/70 shrink-0 mt-[1px]" />
}
}
@@ -45,6 +59,7 @@ const formatCount = (value: number | undefined): string => {
}
export default function SubagentStatusRow({ message, isLast, lastModifiedMessage }: SubagentStatusRowProps) {
const [expandedItems, setExpandedItems] = useState<Record<number, boolean>>({})
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 (
<div className="bg-code border border-editor-group-border rounded-sm py-2.5 px-3">
<div className="flex items-center">
{wasCancelled ? <CircleSlashIcon className="size-2 mr-2" /> : aggregateIcon(displayStatus)}
<span className="font-semibold text-foreground">Subagent batch</span>
{wasCancelled && <span className="ml-2 text-xs opacity-80">Cancelled</span>}
<div className="ml-2 text-xs opacity-80">
{data.completed}/{data.total} complete, {data.successes} succeeded, {data.failures} failed
</div>
</div>
<div className="mt-2 text-xs opacity-75">
{formatCount(data.toolCalls)} tool calls, {formatCount(data.inputTokens)} input tokens,{" "}
{formatCount(data.outputTokens)} output tokens
</div>
<div className="text-sm text-foreground">{title}</div>
<div className="mt-2 space-y-2">
{data.items.map((entry) => (
<div
className="rounded-xs border border-editor-group-border bg-vscode-editor-background px-2 py-1.5"
key={entry.index}>
<div className="flex items-center justify-between gap-2">
<div className="text-xs font-medium text-foreground">
[{entry.index}] {statusLabel(entry.status)}
</div>
<div className="text-[11px] opacity-70">
{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 (
<div
className="rounded-xs border border-editor-group-border bg-vscode-editor-background px-2 py-1.5"
key={entry.index}>
<div className="flex items-start gap-2">
{statusIcon(displayStatus)}
<div className="min-w-0 flex-1">
<div className="text-xs font-medium text-foreground whitespace-pre-wrap break-words">
Subagent: "{entry.prompt}"
</div>
<div className="mt-1 text-[11px] opacity-70">
#{entry.index} | {statusLabel(displayStatus)} | {formatCount(entry.toolCalls)} tools
called | {formatCount(entry.contextTokens)} tokens used
</div>
{hasDetails && (
<button
aria-label={isExpanded ? "Collapse subagent output" : "Expand subagent output"}
className="mt-1 text-[11px] opacity-80 flex items-center gap-1.5 bg-transparent border-0 p-0 cursor-pointer text-left text-foreground"
onClick={() => toggleItem(entry.index)}
type="button">
{isExpanded ? (
<ChevronDownIcon className="size-2" />
) : (
<ChevronRightIcon className="size-2" />
)}
<span>{isExpanded ? "Hide output" : "Show output"}</span>
</button>
)}
</div>
</div>
{isExpanded && entry.result && entry.status === "completed" && (
<div className="mt-2 text-xs opacity-80 whitespace-pre-wrap break-words">{entry.result}</div>
)}
{isExpanded && entry.error && entry.status === "failed" && (
<div className="mt-2 text-xs text-error whitespace-pre-wrap break-words">{entry.error}</div>
)}
</div>
<div className="mt-1 text-xs font-editor whitespace-pre-wrap break-words">{entry.prompt}</div>
{entry.result && entry.status === "completed" && (
<div className="mt-1 text-xs opacity-80 whitespace-pre-wrap break-words">{entry.result}</div>
)}
{entry.error && entry.status === "failed" && (
<div className="mt-1 text-xs text-error whitespace-pre-wrap break-words">{entry.error}</div>
)}
</div>
))}
)
})}
</div>
</div>
)
@@ -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
})