Compare commits

...
Author SHA1 Message Date
Saoud Rizwan c1e275030f fix: remove duplicate subagentsEnabled declaration after rebase 2026-02-09 17:33:44 -08:00
Saoud Rizwan 7a63ff1348 fix(webview): stream subagent stats per prompt while constructing prompts 2026-02-09 17:32:35 -08:00
Saoud Rizwan 741ead7da1 feat: add subagents settings toggle wiring across webview and cli 2026-02-09 17:32:35 -08:00
Saoud Rizwan b3a4f77d37 fix: ensure unique act mode hint keys in CLI chat 2026-02-09 17:31:35 -08:00
Saoud Rizwan d0e41fe779 fix: refine subagent streaming rows in cli and webview 2026-02-09 17:31:35 -08:00
Saoud Rizwan 7c993284dc fix: refine cli subagent tree alignment and wrapping 2026-02-09 17:31:35 -08:00
Saoud Rizwan ace7bec246 feat: surface subagent costs in task metrics and status rows 2026-02-09 17:31:34 -08:00
Saoud Rizwan 2fbaf35b64 refactor: unify subagent chat row rendering 2026-02-09 17:31:34 -08:00
Saoud Rizwan a335cb5aee fix: stabilize subagent cline requests with ulid and initial workspace metadata 2026-02-09 17:31:34 -08:00
Saoud Rizwan 8bb4ede283 feat: enhance SubagentRunner with tool use ID resolution and fallback handling 2026-02-09 17:31:34 -08:00
Saoud Rizwan 18ce14cff8 feat: implement subagent message rendering for approval prompts and progress updates 2026-02-09 17:31:34 -08:00
Saoud Rizwan 453196ac56 fix: keep cancelled subagent state during immediate resume 2026-02-09 17:31:34 -08:00
Saoud Rizwan 9ee428926f fix: align subagent row spacing with chat row conventions 2026-02-09 17:31:34 -08:00
Saoud Rizwan a197b0d06e chore: tune subagent prompt guidance for context-heavy exploration 2026-02-09 17:31:34 -08:00
Saoud Rizwan ba7ee64abc fix: stabilize subagent command UX and suppress nested command rows 2026-02-09 17:31:34 -08:00
Saoud Rizwan aa9448bf75 fix: expose use_subagents when native tool calling is disabled 2026-02-09 17:31:34 -08:00
Saoud Rizwan 8d30992ea0 feat: support subagent tool execution without native tool calls 2026-02-09 17:31:34 -08:00
Saoud Rizwan 7a725934d3 feat: checkpoint subagent tool workflow and approval UX 2026-02-09 17:31:34 -08:00
119 changed files with 3511 additions and 921 deletions
+11
View File
@@ -147,6 +147,17 @@ Required steps:
Common mistake: Adding only the return value without the `context.globalState.get()` call. This compiles but the value is always `undefined` on load.
Settings plumbing gotcha: if a key is user-toggleable from settings, wire both controller update paths:
- `src/core/controller/state/updateSettings.ts` for webview `updateSetting(...)`
- `src/core/controller/state/updateSettingsCli.ts` for CLI/ACP settings updates
Missing one path causes a toggle to appear to change in one surface while the backend state stays unchanged.
Webview toggle gotcha: settings changes must also round-trip back in state payloads.
- Add the field to `UpdateSettingsRequest` in `proto/cline/state.proto` (for webview update requests), then run `npm run protos`
- Include the key in `Controller.getStateToPostToWebview()` (`src/core/controller/index.ts`)
- Ensure `ExtensionState` and webview defaults include the key (`src/shared/ExtensionMessage.ts`, `webview-ui/src/context/ExtensionStateContext.tsx`)
If this round-trip wiring is missing, the backend value can update but the toggle in webview appears stuck or reverts.
## StateManager Cache vs Direct globalState Access
StateManager uses an in-memory cache populated during `StateManager.initialize(context)` in `common.ts`. For most state, use `controller.stateManager.setGlobalState()`/`getGlobalStateKey()`.
+10 -28
View File
@@ -12,11 +12,7 @@
import type * as acp from "@agentclientprotocol/sdk"
import type { TerminalHandle } from "@agentclientprotocol/sdk"
import {
DEFAULT_SUBAGENT_TERMINAL_OUTPUT_LINE_LIMIT,
DEFAULT_TERMINAL_OUTPUT_LINE_LIMIT,
PROCESS_HOT_TIMEOUT_NORMAL,
} from "@integrations/terminal/constants"
import { DEFAULT_TERMINAL_OUTPUT_LINE_LIMIT, PROCESS_HOT_TIMEOUT_NORMAL } from "@integrations/terminal/constants"
import type {
ITerminal,
ITerminalManager,
@@ -142,12 +138,12 @@ export interface ManagedTerminal {
* Wraps ACP terminal operations and emits events compatible with ITerminalProcess.
*/
class AcpTerminalProcess extends EventEmitter<TerminalProcessEvents> implements ITerminalProcess {
isHot: boolean = false
waitForShellIntegration: boolean = false
isHot = false
waitForShellIntegration = false
private _unretrievedOutput: string = ""
private _continued: boolean = false
private _completed: boolean = false
private _unretrievedOutput = ""
private _continued = false
private _completed = false
private _hotTimeout: NodeJS.Timeout | null = null
private _exitWaitTimeout: NodeJS.Timeout | null = null
private readonly manager: AcpTerminalManager
@@ -397,7 +393,7 @@ export class AcpTerminalManager implements ITerminalManager {
private readonly numericIdToStringId: Map<number, string> = new Map()
/** Next numeric ID to assign */
private nextNumericId: number = 1
private nextNumericId = 1
/** Active processes indexed by numeric terminal ID */
private readonly processes: Map<number, AcpTerminalProcess> = new Map()
@@ -406,9 +402,8 @@ export class AcpTerminalManager implements ITerminalManager {
private readonly terminalInfos: Map<number, TerminalInfo> = new Map()
// Configuration options for ITerminalManager
private terminalReuseEnabled: boolean = true
private terminalReuseEnabled = true
private terminalOutputLineLimit: number = DEFAULT_TERMINAL_OUTPUT_LINE_LIMIT
private subagentTerminalOutputLineLimit: number = DEFAULT_SUBAGENT_TERMINAL_OUTPUT_LINE_LIMIT
/**
* Creates a new AcpTerminalManager.
@@ -667,14 +662,6 @@ export class AcpTerminalManager implements ITerminalManager {
this.terminalOutputLineLimit = limit
}
/**
* Set the maximum number of output lines for subagent commands.
* @param limit Maximum number of lines
*/
setSubagentTerminalOutputLineLimit(limit: number): void {
this.subagentTerminalOutputLineLimit = limit
}
/**
* Set the default terminal profile.
* @param profile The profile identifier
@@ -687,15 +674,10 @@ export class AcpTerminalManager implements ITerminalManager {
* Process output lines, potentially truncating if over limit.
* @param outputLines Array of output lines
* @param overrideLimit Optional limit override
* @param isSubagentCommand Whether this is a subagent command
* @returns Processed output string
*/
processOutput(outputLines: string[], overrideLimit?: number, isSubagentCommand?: boolean): string {
const limit = isSubagentCommand
? overrideLimit !== undefined
? overrideLimit
: this.subagentTerminalOutputLineLimit
: this.terminalOutputLineLimit
processOutput(outputLines: string[], overrideLimit?: number): string {
const limit = overrideLimit !== undefined ? overrideLimit : this.terminalOutputLineLimit
if (outputLines.length > limit) {
const halfLimit = Math.floor(limit / 2)
+4
View File
@@ -312,6 +312,10 @@ function translateSayMessage(
// API request finished - no specific update needed
break
case "subagent_usage":
// Hidden aggregate metrics event used for task-level accounting.
break
case "task":
// Task started - don't echo the user's prompt back to them
// The ACP client already knows what they typed
+106
View File
@@ -0,0 +1,106 @@
import type { ClineMessage } from "@shared/ExtensionMessage"
import { render } from "ink-testing-library"
import React from "react"
import { describe, expect, it, vi } from "vitest"
import { ChatMessage } from "./ChatMessage"
vi.mock("../hooks/useTerminalSize", () => ({
useTerminalSize: () => ({
columns: 120,
rows: 40,
resizeKey: 0,
}),
}))
describe("ChatMessage subagent rendering", () => {
it("renders subagent approval prompts as a tree", () => {
const message: ClineMessage = {
ts: Date.now(),
type: "ask",
ask: "use_subagents",
text: JSON.stringify({
prompts: [
"Find codebase stats and size",
"Find funny comments and easter eggs",
"Find unusual patterns and history",
],
}),
}
const { lastFrame } = render(React.createElement(ChatMessage, { message, mode: "act" }))
const frame = lastFrame() || ""
expect(frame).toContain("Cline wants to run subagents")
expect(frame).toContain("├─ Find codebase stats and size")
expect(frame).toContain("├─ Find funny comments and easter eggs")
expect(frame).toContain("└─ Find unusual patterns and history")
})
it("renders subagent progress rows with compact token stats and completion checks", () => {
const message: ClineMessage = {
ts: Date.now(),
type: "say",
say: "subagent",
text: JSON.stringify({
status: "running",
total: 3,
completed: 1,
successes: 1,
failures: 0,
toolCalls: 21,
inputTokens: 0,
outputTokens: 0,
contextWindow: 0,
maxContextTokens: 0,
maxContextUsagePercentage: 0,
items: [
{
index: 1,
prompt: "Find codebase stats and size",
status: "completed",
toolCalls: 5,
inputTokens: 0,
outputTokens: 0,
totalCost: 0.034,
contextTokens: 24400,
contextWindow: 200000,
contextUsagePercentage: 12.2,
},
{
index: 2,
prompt: "Find funny comments and easter eggs",
status: "running",
toolCalls: 11,
inputTokens: 0,
outputTokens: 0,
totalCost: 0.056,
contextTokens: 31600,
contextWindow: 200000,
contextUsagePercentage: 15.8,
},
{
index: 3,
prompt: "Find unusual patterns and history",
status: "pending",
toolCalls: 5,
inputTokens: 0,
outputTokens: 0,
totalCost: 0,
contextTokens: 28900,
contextWindow: 200000,
contextUsagePercentage: 14.4,
},
],
}),
}
const { lastFrame } = render(React.createElement(ChatMessage, { isStreaming: true, message, mode: "act" }))
const frame = lastFrame() || ""
expect(frame).toContain("Cline is running subagents")
expect(frame).toContain("✓ Find codebase stats and size")
expect(frame).toContain("5 tool uses · 24.4k tokens · $0.03")
expect(frame).toContain("11 tool uses · 31.6k tokens · $0.06")
expect(frame).toContain("5 tool uses · 28.9k tokens · $0.00")
})
})
+16 -7
View File
@@ -17,6 +17,7 @@ import { useTerminalSize } from "../hooks/useTerminalSize"
import { jsonParseSafe } from "../utils/parser"
import { getToolDescription, isFileEditTool, parseToolFromMessage } from "../utils/tools"
import { DiffView } from "./DiffView"
import { SubagentMessage } from "./SubagentMessage"
/**
* Add "(Tab)" hint after "Act mode" mentions.
@@ -24,7 +25,7 @@ import { DiffView } from "./DiffView"
* Matches just "Act mode" without requiring "to " prefix because markdown
* processing may split "toggle to **Act mode**" into separate text chunks.
*/
function addActModeHint(text: string): React.ReactNode[] {
function addActModeHint(text: string, keyPrefix: string): React.ReactNode[] {
// Match "Act mode" in various capitalizations, but not if already followed by (Tab)
const actModeRegex = /\bact\s+mode\b(?!\s*\(tab\))/gi
const parts = text.split(actModeRegex)
@@ -41,7 +42,7 @@ function addActModeHint(text: string): React.ReactNode[] {
}
if (matches[i]) {
nodes.push(
<React.Fragment key={`act-mode-${i}`}>
<React.Fragment key={`${keyPrefix}-act-mode-${i}`}>
{matches[i]}
<Text color="gray"> (Tab)</Text>
</React.Fragment>,
@@ -59,6 +60,8 @@ function addActModeHint(text: string): React.ReactNode[] {
*/
function renderInlineMarkdown(text: string): React.ReactNode[] {
const nodes: React.ReactNode[] = []
let hintCallIndex = 0
const addHintedText = (value: string) => addActModeHint(value, `hint-${hintCallIndex++}`)
// Match **bold**, *italic*, or `code` - order matters (** before *)
const regex = /(\*\*[^*]+\*\*|\*[^*]+\*|`[^`]+`)/g
let lastIndex = 0
@@ -68,7 +71,7 @@ function renderInlineMarkdown(text: string): React.ReactNode[] {
// Add text before match (with Act Mode hint processing)
if (match.index > lastIndex) {
const beforeText = text.slice(lastIndex, match.index)
nodes.push(...addActModeHint(beforeText))
nodes.push(...addHintedText(beforeText))
}
const fullMatch = match[0]
@@ -77,7 +80,7 @@ function renderInlineMarkdown(text: string): React.ReactNode[] {
if (fullMatch.startsWith("**") && fullMatch.endsWith("**")) {
// Bold - also process for Act Mode hints inside bold text
const boldContent = fullMatch.slice(2, -2)
const hintedContent = addActModeHint(boldContent)
const hintedContent = addHintedText(boldContent)
nodes.push(
<Text bold key={key}>
{hintedContent}
@@ -100,10 +103,10 @@ function renderInlineMarkdown(text: string): React.ReactNode[] {
// Add remaining text (with Act Mode hint processing)
if (lastIndex < text.length) {
nodes.push(...addActModeHint(text.slice(lastIndex)))
nodes.push(...addHintedText(text.slice(lastIndex)))
}
return nodes.length > 0 ? nodes : addActModeHint(text)
return nodes.length > 0 ? nodes : addHintedText(text)
}
/**
@@ -224,7 +227,7 @@ function truncate(text: string, maxLength: number): string {
/**
* Format tool result for display
*/
function formatToolResult(result: string, maxLines: number = 5): string[] {
function formatToolResult(result: string, maxLines = 5): string[] {
const lines = result.split("\n")
if (lines.length <= maxLines) {
return lines
@@ -446,6 +449,10 @@ export const ChatMessage: React.FC<ChatMessageProps> = ({ message, mode, isStrea
)
}
if ((type === "ask" && ask === "use_subagents") || say === "use_subagents" || say === "subagent") {
return <SubagentMessage isStreaming={isStreaming} message={message} mode={mode} />
}
// MCP response
if (say === "mcp_server_response" && text) {
const lines = formatToolResult(text, 8)
@@ -800,6 +807,8 @@ export const ChatMessageList: React.FC<ChatMessageListProps> = ({ messages, maxM
const displayMessages = messages.filter((m) => {
// Skip api_req_finished, they're just markers
if (m.say === "api_req_finished") return false
// Skip hidden aggregated usage messages
if (m.say === "subagent_usage") return false
// Skip empty text messages
if (m.say === "text" && !m.text?.trim()) return false
// Skip checkpoint messages
+2 -9
View File
@@ -46,14 +46,7 @@ export interface SkillInfo {
enabled: boolean
}
export const EXCLUDED_KEYS = new Set([
"taskHistory",
"primaryRootIndex",
"subagentsEnabled",
"subagentTerminalOutputLineLimit",
"welcomeViewCompleted",
"isNewUser",
])
export const EXCLUDED_KEYS = new Set(["taskHistory", "primaryRootIndex", "welcomeViewCompleted", "isNewUser"])
export const EDITABLE_TYPES: Set<ValueType> = new Set(["string", "number", "boolean"])
export const MAX_VISIBLE = 12
@@ -135,7 +128,7 @@ export function parseValue(input: string, type: ValueType): unknown {
return input.toLowerCase() === "true" || input === "1"
}
if (type === "number") {
const num = parseFloat(input)
const num = Number.parseFloat(input)
return Number.isNaN(num) ? 0 : num
}
if (type === "object") {
@@ -82,6 +82,12 @@ const TABS: PanelTab[] = [
// Settings configuration for simple boolean toggles
const FEATURE_SETTINGS = {
subagents: {
stateKey: "subagentsEnabled",
default: false,
label: "Subagents",
description: "Let Cline run focused subagents in parallel to explore the codebase for you",
},
autoCondense: {
stateKey: "useAutoCondense",
default: false,
+343
View File
@@ -0,0 +1,343 @@
import type { ClineAskUseSubagents, ClineMessage, ClineSaySubagentStatus } from "@shared/ExtensionMessage"
import { Box, Text } from "ink"
import Spinner from "ink-spinner"
import React from "react"
import { COLORS } from "../constants/colors"
import { useTerminalSize } from "../hooks/useTerminalSize"
import { jsonParseSafe } from "../utils/parser"
interface SubagentMessageProps {
message: ClineMessage
isStreaming?: boolean
mode?: "act" | "plan"
}
const TREE_PREFIX_WIDTH = 5
const MIN_PROMPT_WIDTH = 20
const DotRow: React.FC<{ children: React.ReactNode; color?: string; flashing?: boolean }> = ({
children,
color,
flashing = false,
}) => (
<Box flexDirection="row">
<Box width={2}>
{flashing ? (
<Text color={color}>
<Spinner type="toggle8" />
</Text>
) : (
<Text color={color}></Text>
)}
</Box>
<Box flexGrow={1}>{children}</Box>
</Box>
)
function formatCompactTokens(tokens: number | undefined): string {
const value = Number.isFinite(tokens) ? Math.max(0, tokens || 0) : 0
return new Intl.NumberFormat("en-US", {
notation: "compact",
maximumFractionDigits: 1,
})
.format(value)
.toLowerCase()
}
function formatCompactCost(cost: number | undefined): string {
const value = Number.isFinite(cost) ? Math.max(0, cost || 0) : 0
const maximumFractionDigits = value >= 0.01 ? 2 : 4
return new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
minimumFractionDigits: 2,
maximumFractionDigits,
}).format(value)
}
function formatSubagentStatsValues(
toolCalls: number | undefined,
contextTokens: number | undefined,
totalCost: number | undefined,
) {
const safeToolCalls = Number.isFinite(toolCalls) ? Math.max(0, toolCalls || 0) : 0
const toolUses = safeToolCalls === 1 ? "tool use" : "tool uses"
const tokensUsed = formatCompactTokens(contextTokens || 0)
const formattedCost = formatCompactCost(totalCost || 0)
return `${safeToolCalls} ${toolUses} · ${tokensUsed} tokens · ${formattedCost}`
}
function wrapPrompt(text: string, width: number): string[] {
if (!text) {
return [""]
}
const normalizedWidth = Math.max(1, width)
const wrappedLines: string[] = []
const paragraphs = text.split("\n")
for (const paragraph of paragraphs) {
const words = paragraph.trim().split(/\s+/).filter(Boolean)
if (words.length === 0) {
wrappedLines.push("")
continue
}
let line = ""
for (const word of words) {
if (!line) {
if (word.length <= normalizedWidth) {
line = word
continue
}
let remaining = word
while (remaining.length > normalizedWidth) {
wrappedLines.push(remaining.slice(0, normalizedWidth))
remaining = remaining.slice(normalizedWidth)
}
line = remaining
continue
}
if (line.length + 1 + word.length <= normalizedWidth) {
line = `${line} ${word}`
continue
}
wrappedLines.push(line)
if (word.length <= normalizedWidth) {
line = word
continue
}
let remaining = word
while (remaining.length > normalizedWidth) {
wrappedLines.push(remaining.slice(0, normalizedWidth))
remaining = remaining.slice(normalizedWidth)
}
line = remaining
}
if (line) {
wrappedLines.push(line)
}
}
return wrappedLines.length > 0 ? wrappedLines : [text]
}
const TreePromptRow: React.FC<{
prefix: React.ReactNode
continuationPrefix: string
prompt: string
promptWidth: number
color?: string
}> = ({ prefix, continuationPrefix, prompt, promptWidth, color }) => {
const lines = wrapPrompt(prompt, promptWidth)
return (
<Box flexDirection="column" width="100%">
{lines.map((line, index) => (
<Box flexDirection="row" key={`${line}-${index}`} width="100%">
<Box flexShrink={0} width={TREE_PREFIX_WIDTH}>
{index === 0 ? prefix : <Text color="gray">{continuationPrefix}</Text>}
</Box>
<Box flexGrow={1}>
<Text color={color}>{line}</Text>
</Box>
</Box>
))}
</Box>
)
}
const TreeStatsRow: React.FC<{ prefix: string; stats: string }> = ({ prefix, stats }) => (
<Box flexDirection="row" width="100%">
<Box flexShrink={0} width={TREE_PREFIX_WIDTH}>
<Text color="gray">{prefix}</Text>
</Box>
<Box flexGrow={1}>
<Text color="gray"> {stats}</Text>
</Box>
</Box>
)
export const SubagentMessage: React.FC<SubagentMessageProps> = ({ message, mode, isStreaming }) => {
const { type, ask, say, text, partial } = message
const toolColor = mode === "plan" ? "yellow" : COLORS.primaryBlue
const { columns } = useTerminalSize()
const promptWidth = Math.max(MIN_PROMPT_WIDTH, columns - 2 - TREE_PREFIX_WIDTH)
if ((type === "ask" && ask === "use_subagents") || say === "use_subagents") {
const parsed = text
? jsonParseSafe<ClineAskUseSubagents>(text, {
prompts: [],
})
: { prompts: [] }
const prompts = (parsed.prompts || []).map((prompt) => prompt?.trim()).filter(Boolean)
if (prompts.length === 0) {
return (
<Box flexDirection="column" marginBottom={1} width="100%">
<DotRow color={toolColor}>
<Text color={toolColor}>Cline wants to run subagents:</Text>
</DotRow>
</Box>
)
}
const singular = prompts.length === 1
return (
<Box flexDirection="column" marginBottom={1} width="100%">
<DotRow color={toolColor} flashing={partial === true && isStreaming}>
<Text color={toolColor}>{singular ? "Cline wants to run a subagent:" : "Cline wants to run subagents:"}</Text>
</DotRow>
<Box flexDirection="column" marginLeft={2} width="100%">
{prompts.map((prompt, index) => {
const isLastPrompt = index === prompts.length - 1
const branch = isLastPrompt ? "└─" : "├─"
const continuationPrefix = isLastPrompt ? " " : "│ "
const shouldShowPromptStats = partial !== true || !isLastPrompt
return (
<Box flexDirection="column" key={`${prompt}-${index}`}>
<TreePromptRow
color={toolColor}
continuationPrefix={continuationPrefix}
prefix={<Text color={toolColor}>{`${branch} `}</Text>}
prompt={prompt}
promptWidth={promptWidth}
/>
{shouldShowPromptStats && (
<TreeStatsRow
prefix={continuationPrefix}
stats={formatSubagentStatsValues(undefined, undefined, undefined)}
/>
)}
</Box>
)
})}
</Box>
</Box>
)
}
if (say === "subagent" && text) {
const parsed = jsonParseSafe<ClineSaySubagentStatus>(text, {
status: "running",
total: 0,
completed: 0,
successes: 0,
failures: 0,
toolCalls: 0,
inputTokens: 0,
outputTokens: 0,
contextWindow: 0,
maxContextTokens: 0,
maxContextUsagePercentage: 0,
items: [],
})
const items = parsed.items || []
if (items.length === 0) {
return null
}
return (
<Box flexDirection="column" marginBottom={1} width="100%">
<DotRow color={toolColor} flashing={partial === true && isStreaming}>
<Text color={toolColor}>
{items.length === 1 ? "Cline is running a subagent:" : "Cline is running subagents:"}
</Text>
</DotRow>
<Box flexDirection="column" marginLeft={2} width="100%">
{items.map((entry, index) => {
const isLastEntry = index === items.length - 1
const branch = isLastEntry ? "└─" : "├─"
const continuationPrefix = isLastEntry ? " " : "│ "
const key = `${entry.index}-${index}`
const shouldShowStats = true
if (entry.status === "completed") {
return (
<Box flexDirection="column" key={key}>
<TreePromptRow
color="green"
continuationPrefix={continuationPrefix}
prefix={
<Box flexDirection="row">
<Text color="gray">{`${branch} `}</Text>
<Text color="green"></Text>
</Box>
}
prompt={entry.prompt}
promptWidth={promptWidth}
/>
<TreeStatsRow
prefix={continuationPrefix}
stats={formatSubagentStatsValues(entry.toolCalls, entry.contextTokens, entry.totalCost)}
/>
</Box>
)
}
if (entry.status === "failed") {
return (
<Box flexDirection="column" key={key}>
<TreePromptRow
color="red"
continuationPrefix={continuationPrefix}
prefix={
<Box flexDirection="row">
<Text color="gray">{`${branch} `}</Text>
<Text color="red"></Text>
</Box>
}
prompt={entry.prompt}
promptWidth={promptWidth}
/>
<TreeStatsRow
prefix={continuationPrefix}
stats={formatSubagentStatsValues(entry.toolCalls, entry.contextTokens, entry.totalCost)}
/>
</Box>
)
}
return (
<Box flexDirection="column" key={key}>
<TreePromptRow
color={toolColor}
continuationPrefix={continuationPrefix}
prefix={
<Box flexDirection="row">
<Text color="gray">{branch} </Text>
{entry.status === "running" ? (
<Text color={toolColor}>
<Spinner type="dots" />
</Text>
) : (
<Text color={toolColor}></Text>
)}
</Box>
}
prompt={entry.prompt}
promptWidth={promptWidth}
/>
{shouldShowStats && (
<TreeStatsRow
prefix={continuationPrefix}
stats={formatSubagentStatsValues(entry.toolCalls, entry.contextTokens, entry.totalCost)}
/>
)}
</Box>
)
})}
</Box>
</Box>
)
}
return null
}
+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"
}
+4
View File
@@ -33,6 +33,7 @@ enum ClineAsk {
REPORT_BUG = 14;
SUMMARIZE_TASK = 15;
ACT_MODE_RESPOND = 16;
USE_SUBAGENTS = 17;
}
// Enum for ClineSay types
@@ -71,6 +72,9 @@ enum ClineSay {
HOOK_OUTPUT_STREAM = 31;
COMMAND_PERMISSION_DENIED = 32;
CONDITIONAL_RULES_APPLIED = 33;
SUBAGENT_STATUS = 34;
USE_SUBAGENTS_SAY = 35;
SUBAGENT_USAGE = 36;
}
// Enum for ClineSayTool tool types
+5
View File
@@ -49,6 +49,11 @@ export const toolParamNames = [
"from_ref",
"to_ref",
"skill_name",
"prompt_1",
"prompt_2",
"prompt_3",
"prompt_4",
"prompt_5",
] as const
export type ToolParamName = (typeof toolParamNames)[number]
+4 -2
View File
@@ -241,8 +241,10 @@ function extractTaskInformation(clineMessages: ClineMessage[], metadata: any): T
let cacheReads = 0
let totalCost = 0
// Look for api_req_started messages with token info
const apiReqMessages = clineMessages.filter((msg) => msg.type === "say" && msg.say === "api_req_started" && msg.text)
// Look for usage-carrying messages with token info
const apiReqMessages = clineMessages.filter(
(msg) => msg.type === "say" && (msg.say === "api_req_started" || msg.say === "subagent_usage") && msg.text,
)
for (const msg of apiReqMessages) {
try {
+2 -6
View File
@@ -250,7 +250,6 @@ export class Controller {
const terminalReuseEnabled = this.stateManager.getGlobalStateKey("terminalReuseEnabled")
const vscodeTerminalExecutionMode = this.stateManager.getGlobalStateKey("vscodeTerminalExecutionMode")
const terminalOutputLineLimit = this.stateManager.getGlobalSettingsKey("terminalOutputLineLimit")
const subagentTerminalOutputLineLimit = this.stateManager.getGlobalSettingsKey("subagentTerminalOutputLineLimit")
const defaultTerminalProfile = this.stateManager.getGlobalSettingsKey("defaultTerminalProfile")
const isNewUser = this.stateManager.getGlobalStateKey("isNewUser")
const taskHistory = this.stateManager.getGlobalStateKey("taskHistory")
@@ -314,7 +313,6 @@ export class Controller {
shellIntegrationTimeout,
terminalReuseEnabled: terminalReuseEnabled ?? true,
terminalOutputLineLimit: terminalOutputLineLimit ?? 500,
subagentTerminalOutputLineLimit: subagentTerminalOutputLineLimit ?? 2000,
defaultTerminalProfile: defaultTerminalProfile ?? "default",
vscodeTerminalExecutionMode,
cwd,
@@ -859,6 +857,7 @@ export class Controller {
const strictPlanModeEnabled = this.stateManager.getGlobalSettingsKey("strictPlanModeEnabled")
const yoloModeToggled = this.stateManager.getGlobalSettingsKey("yoloModeToggled")
const useAutoCondense = this.stateManager.getGlobalSettingsKey("useAutoCondense")
const subagentsEnabled = this.stateManager.getGlobalSettingsKey("subagentsEnabled")
const userInfo = this.stateManager.getGlobalStateKey("userInfo")
const mcpMarketplaceEnabled = this.stateManager.getGlobalStateKey("mcpMarketplaceEnabled")
const mcpDisplayMode = this.stateManager.getGlobalStateKey("mcpDisplayMode")
@@ -883,13 +882,11 @@ export class Controller {
const mcpResponsesCollapsed = this.stateManager.getGlobalStateKey("mcpResponsesCollapsed")
const terminalOutputLineLimit = this.stateManager.getGlobalSettingsKey("terminalOutputLineLimit")
const maxConsecutiveMistakes = this.stateManager.getGlobalSettingsKey("maxConsecutiveMistakes")
const subagentTerminalOutputLineLimit = this.stateManager.getGlobalSettingsKey("subagentTerminalOutputLineLimit")
const favoritedModelIds = this.stateManager.getGlobalStateKey("favoritedModelIds")
const lastDismissedInfoBannerVersion = this.stateManager.getGlobalStateKey("lastDismissedInfoBannerVersion") || 0
const lastDismissedModelBannerVersion = this.stateManager.getGlobalStateKey("lastDismissedModelBannerVersion") || 0
const lastDismissedCliBannerVersion = this.stateManager.getGlobalStateKey("lastDismissedCliBannerVersion") || 0
const dismissedBanners = this.stateManager.getGlobalStateKey("dismissedBanners")
const subagentsEnabled = this.stateManager.getGlobalSettingsKey("subagentsEnabled")
const doubleCheckCompletionEnabled = this.stateManager.getGlobalSettingsKey("doubleCheckCompletionEnabled")
const localClineRulesToggles = this.stateManager.getWorkspaceStateKey("localClineRulesToggles")
@@ -944,6 +941,7 @@ export class Controller {
strictPlanModeEnabled,
yoloModeToggled,
useAutoCondense,
subagentsEnabled,
userInfo,
mcpMarketplaceEnabled,
mcpDisplayMode,
@@ -974,7 +972,6 @@ export class Controller {
mcpResponsesCollapsed,
terminalOutputLineLimit,
maxConsecutiveMistakes,
subagentTerminalOutputLineLimit,
customPrompt,
taskHistory: processedTaskHistory,
shouldShowAnnouncement,
@@ -1004,7 +1001,6 @@ export class Controller {
remoteConfigSettings: this.stateManager.getRemoteConfigSettings(),
lastDismissedCliBannerVersion,
dismissedBanners,
subagentsEnabled,
nativeToolCallSetting: this.stateManager.getGlobalStateKey("nativeToolCallEnabled"),
enableParallelToolCalling: this.stateManager.getGlobalSettingsKey("enableParallelToolCalling"),
backgroundEditEnabled: this.stateManager.getGlobalSettingsKey("backgroundEditEnabled"),
@@ -1,165 +0,0 @@
import { UpdateSettingsRequest } from "@shared/proto/cline/state"
import * as assert from "assert"
import * as sinon from "sinon"
import { Controller } from ".."
import { updateSettings } from "./updateSettings"
// Mock telemetryService
const telemetryServiceMock = {
captureSubagentToggle: sinon.stub(),
}
describe("updateSettings platform validation", () => {
let mockController: Controller
let originalPlatform: NodeJS.Platform
beforeEach(() => {
// Store original platform
originalPlatform = process.platform
// Create mock controller
mockController = {
stateManager: {
getGlobalSettingsKey: sinon.stub(),
setGlobalState: sinon.stub(),
setApiConfiguration: sinon.stub(),
},
postStateToWebview: sinon.stub().resolves({}),
task: undefined,
updateTelemetrySetting: sinon.stub(),
} as unknown as Controller
// Clear telemetry service mock
telemetryServiceMock.captureSubagentToggle.reset()
})
afterEach(() => {
// Restore original platform
Object.defineProperty(process, "platform", {
value: originalPlatform,
writable: true,
configurable: true,
})
sinon.restore()
})
it("should allow enabling subagents on macOS (darwin)", async () => {
// Set platform to macOS
Object.defineProperty(process, "platform", { value: "darwin" })
;(mockController.stateManager.getGlobalSettingsKey as sinon.SinonStub).returns(false)
const request = UpdateSettingsRequest.create({
subagentsEnabled: true,
})
// Should not throw
await updateSettings(mockController, request)
assert.ok(
(mockController.stateManager.setGlobalState as sinon.SinonStub).calledWith("subagentsEnabled", true),
"Should enable subagents on macOS",
)
})
it("should allow enabling subagents on Linux", async () => {
// Set platform to Linux
Object.defineProperty(process, "platform", { value: "linux" })
;(mockController.stateManager.getGlobalSettingsKey as sinon.SinonStub).returns(false)
const request = UpdateSettingsRequest.create({
subagentsEnabled: true,
})
// Should not throw
await updateSettings(mockController, request)
assert.ok(
(mockController.stateManager.setGlobalState as sinon.SinonStub).calledWith("subagentsEnabled", true),
"Should enable subagents on Linux",
)
})
it("should throw error when trying to enable subagents on Windows", async () => {
// Set platform to Windows
Object.defineProperty(process, "platform", { value: "win32" })
;(mockController.stateManager.getGlobalSettingsKey as sinon.SinonStub).returns(false)
const request = UpdateSettingsRequest.create({
subagentsEnabled: true,
})
try {
await updateSettings(mockController, request)
assert.fail("Should have thrown an error")
} catch (error) {
assert.strictEqual(
(error as Error).message,
"CLI subagents are only supported on macOS and Linux platforms",
"Should throw platform restriction error",
)
}
assert.ok(
!(mockController.stateManager.setGlobalState as sinon.SinonStub).called,
"Should not call setGlobalState when platform validation fails",
)
})
it("should allow disabling subagents on any platform", async () => {
// Test on Windows
Object.defineProperty(process, "platform", { value: "win32" })
;(mockController.stateManager.getGlobalSettingsKey as sinon.SinonStub).returns(true)
const request = UpdateSettingsRequest.create({
subagentsEnabled: false,
})
// Should not throw
await updateSettings(mockController, request)
assert.ok(
(mockController.stateManager.setGlobalState as sinon.SinonStub).calledWith("subagentsEnabled", false),
"Should allow disabling subagents on any platform",
)
})
it("should allow keeping subagents disabled on non-macOS platforms", async () => {
// Test on Windows with subagents already disabled
Object.defineProperty(process, "platform", { value: "win32" })
;(mockController.stateManager.getGlobalSettingsKey as sinon.SinonStub).returns(false)
const request = UpdateSettingsRequest.create({
subagentsEnabled: false,
})
// Should not throw
await updateSettings(mockController, request)
assert.ok(
(mockController.stateManager.setGlobalState as sinon.SinonStub).calledWith("subagentsEnabled", false),
"Should allow keeping subagents disabled on non-macOS platforms",
)
})
it("should not perform platform validation when subagentsEnabled is undefined", async () => {
// Test on Windows but don't try to change subagents setting
Object.defineProperty(process, "platform", { value: "win32" })
const request = UpdateSettingsRequest.create({
strictPlanModeEnabled: true, // Some other setting
})
// Should not throw error since subagentsEnabled is not being changed
await updateSettings(mockController, request)
assert.ok(
(mockController.postStateToWebview as sinon.SinonStub).called,
"Should complete successfully when subagentsEnabled is not being changed",
)
})
})
+12 -35
View File
@@ -128,22 +128,6 @@ export async function updateSettings(controller: Controller, request: UpdateSett
)
}
// Update subagent terminal output line limit
if (request.subagentTerminalOutputLineLimit !== undefined) {
controller.stateManager.setGlobalState(
"subagentTerminalOutputLineLimit",
Number(request.subagentTerminalOutputLineLimit),
)
}
// Update subagent terminal output line limit
if (request.subagentTerminalOutputLineLimit !== undefined) {
controller.stateManager.setGlobalState(
"subagentTerminalOutputLineLimit",
Number(request.subagentTerminalOutputLineLimit),
)
}
// Update max consecutive mistakes
if (request.maxConsecutiveMistakes !== undefined) {
controller.stateManager.setGlobalState("maxConsecutiveMistakes", Number(request.maxConsecutiveMistakes))
@@ -174,6 +158,18 @@ export async function updateSettings(controller: Controller, request: UpdateSett
controller.stateManager.setGlobalState("worktreesEnabled", request.worktreesEnabled)
}
// Update subagents setting
if (request.subagentsEnabled !== undefined) {
const wasEnabled = controller.stateManager.getGlobalSettingsKey("subagentsEnabled") ?? false
const isEnabled = !!request.subagentsEnabled
controller.stateManager.setGlobalState("subagentsEnabled", isEnabled)
// Capture telemetry when setting changes
if (wasEnabled !== isEnabled) {
telemetryService.captureSubagentToggle(isEnabled)
}
}
if (request.dictationSettings !== undefined) {
// Convert from protobuf format (snake_case) to TypeScript format (camelCase)
const dictationSettings = {
@@ -314,25 +310,6 @@ export async function updateSettings(controller: Controller, request: UpdateSett
controller.stateManager.setGlobalState("multiRootEnabled", !!request.multiRootEnabled)
}
if (request.subagentsEnabled !== undefined) {
const currentSettings = controller.stateManager.getGlobalSettingsKey("subagentsEnabled")
const wasEnabled = currentSettings ?? false
const isEnabled = !!request.subagentsEnabled
// Platform validation: Only allow enabling subagents on macOS and Linux
if (isEnabled && process.platform !== "darwin" && process.platform !== "linux") {
throw new Error("CLI subagents are only supported on macOS and Linux platforms")
}
controller.stateManager.setGlobalState("subagentsEnabled", isEnabled)
// Capture telemetry when setting changes
if (wasEnabled !== isEnabled) {
telemetryService.captureSubagentToggle(isEnabled)
}
controller.stateManager.setGlobalState("subagentsEnabled", !!request.subagentsEnabled)
}
if (request.nativeToolCallEnabled !== undefined) {
controller.stateManager.setGlobalState("nativeToolCallEnabled", !!request.nativeToolCallEnabled)
if (controller.task) {
@@ -50,6 +50,7 @@ export async function updateSettingsCli(controller: Controller, request: UpdateS
useAutoCondense,
clineWebToolsEnabled,
worktreesEnabled,
subagentsEnabled,
focusChainSettings,
browserSettings,
defaultTerminalProfile,
@@ -162,6 +163,17 @@ export async function updateSettingsCli(controller: Controller, request: UpdateS
controller.stateManager.setGlobalState("worktreesEnabled", worktreesEnabled)
}
// Update subagents setting (requires telemetry on state change)
if (subagentsEnabled !== undefined) {
const wasEnabled = controller.stateManager.getGlobalSettingsKey("subagentsEnabled") ?? false
const isEnabled = !!subagentsEnabled
controller.stateManager.setGlobalState("subagentsEnabled", isEnabled)
if (wasEnabled !== isEnabled) {
telemetryService.captureSubagentToggle(isEnabled)
}
}
// Update focus chain settings (requires telemetry on state change)
if (focusChainSettings !== undefined) {
const currentSettings = controller.stateManager.getGlobalSettingsKey("focusChainSettings")
-9
View File
@@ -226,15 +226,6 @@ Below is the user's input when they indicated that they wanted to submit a Githu
</explicit_instructions>\n
`
export const subagentToolResponse = () =>
`<explicit_instructions type="subagent">
The user has requested to invoke a Cline CLI subagent with the context below. You should execute a subagent command to handle this request using the CLI subagents feature.
Transform the user's request into a subagent command by executing:
cline "<prompt>"
</explicit_instructions>\n
`
export const explainChangesToolResponse = () =>
`<explicit_instructions type="explain_changes">
The user has asked you to explain code changes. You have access to a tool called **generate_explanation** that opens a multi-file diff view with AI-generated inline comments explaining code changes between two git references.
@@ -266,6 +266,23 @@ Usage:
<to_ref>HEAD</to_ref>
</generate_explanation>
## use_subagents
Description: Run up to five focused in-process subagents in parallel. Each subagent gets its own prompt and returns a comprehensive research result with tool and token stats. Use this for broad exploration when reading many files would consume the main agent's context window.
Parameters:
- prompt_1: (required) First subagent prompt.
- prompt_2: (optional) Optional second subagent prompt.
- prompt_3: (optional) Optional third subagent prompt.
- prompt_4: (optional) Optional fourth subagent prompt.
- prompt_5: (optional) Optional fifth subagent prompt.
Usage:
<use_subagents>
<prompt_1></prompt_1>
<prompt_2></prompt_2>
<prompt_3></prompt_3>
<prompt_4></prompt_4>
<prompt_5></prompt_5>
</use_subagents>
# Tool Use Examples
## Example 1: Requesting to execute a command
@@ -232,6 +232,23 @@ Usage:
<to_ref>HEAD</to_ref>
</generate_explanation>
## use_subagents
Description: Run up to five focused in-process subagents in parallel. Each subagent gets its own prompt and returns a comprehensive research result with tool and token stats. Use this for broad exploration when reading many files would consume the main agent's context window.
Parameters:
- prompt_1: (required) First subagent prompt.
- prompt_2: (optional) Optional second subagent prompt.
- prompt_3: (optional) Optional third subagent prompt.
- prompt_4: (optional) Optional fourth subagent prompt.
- prompt_5: (optional) Optional fifth subagent prompt.
Usage:
<use_subagents>
<prompt_1></prompt_1>
<prompt_2></prompt_2>
<prompt_3></prompt_3>
<prompt_4></prompt_4>
<prompt_5></prompt_5>
</use_subagents>
# Tool Use Examples
## Example 1: Requesting to execute a command
@@ -240,6 +240,23 @@ Usage:
<to_ref>HEAD</to_ref>
</generate_explanation>
## use_subagents
Description: Run up to five focused in-process subagents in parallel. Each subagent gets its own prompt and returns a comprehensive research result with tool and token stats. Use this for broad exploration when reading many files would consume the main agent's context window.
Parameters:
- prompt_1: (required) First subagent prompt.
- prompt_2: (optional) Optional second subagent prompt.
- prompt_3: (optional) Optional third subagent prompt.
- prompt_4: (optional) Optional fourth subagent prompt.
- prompt_5: (optional) Optional fifth subagent prompt.
Usage:
<use_subagents>
<prompt_1></prompt_1>
<prompt_2></prompt_2>
<prompt_3></prompt_3>
<prompt_4></prompt_4>
<prompt_5></prompt_5>
</use_subagents>
# Tool Use Examples
## Example 1: Requesting to execute a command
@@ -266,6 +266,23 @@ Usage:
<to_ref>HEAD</to_ref>
</generate_explanation>
## use_subagents
Description: Run up to five focused in-process subagents in parallel. Each subagent gets its own prompt and returns a comprehensive research result with tool and token stats. Use this for broad exploration when reading many files would consume the main agent's context window.
Parameters:
- prompt_1: (required) First subagent prompt.
- prompt_2: (optional) Optional second subagent prompt.
- prompt_3: (optional) Optional third subagent prompt.
- prompt_4: (optional) Optional fourth subagent prompt.
- prompt_5: (optional) Optional fifth subagent prompt.
Usage:
<use_subagents>
<prompt_1></prompt_1>
<prompt_2></prompt_2>
<prompt_3></prompt_3>
<prompt_4></prompt_4>
<prompt_5></prompt_5>
</use_subagents>
# Tool Use Examples
## Example 1: Requesting to execute a command
@@ -297,6 +297,23 @@ Usage:
<load_mcp_documentation>
</load_mcp_documentation>
## use_subagents
Description: Run up to five focused in-process subagents in parallel. Each subagent gets its own prompt and returns a comprehensive research result with tool and token stats. Use this for broad exploration when reading many files would consume the main agent's context window.
Parameters:
- prompt_1: (required) First subagent prompt.
- prompt_2: (optional) Optional second subagent prompt.
- prompt_3: (optional) Optional third subagent prompt.
- prompt_4: (optional) Optional fourth subagent prompt.
- prompt_5: (optional) Optional fifth subagent prompt.
Usage:
<use_subagents>
<prompt_1></prompt_1>
<prompt_2></prompt_2>
<prompt_3></prompt_3>
<prompt_4></prompt_4>
<prompt_5></prompt_5>
</use_subagents>
# Tool Use Examples
## Example 1: Requesting to execute a command
@@ -263,6 +263,23 @@ Usage:
<load_mcp_documentation>
</load_mcp_documentation>
## use_subagents
Description: Run up to five focused in-process subagents in parallel. Each subagent gets its own prompt and returns a comprehensive research result with tool and token stats. Use this for broad exploration when reading many files would consume the main agent's context window.
Parameters:
- prompt_1: (required) First subagent prompt.
- prompt_2: (optional) Optional second subagent prompt.
- prompt_3: (optional) Optional third subagent prompt.
- prompt_4: (optional) Optional fourth subagent prompt.
- prompt_5: (optional) Optional fifth subagent prompt.
Usage:
<use_subagents>
<prompt_1></prompt_1>
<prompt_2></prompt_2>
<prompt_3></prompt_3>
<prompt_4></prompt_4>
<prompt_5></prompt_5>
</use_subagents>
# Tool Use Examples
## Example 1: Requesting to execute a command
@@ -267,6 +267,23 @@ Usage:
<load_mcp_documentation>
</load_mcp_documentation>
## use_subagents
Description: Run up to five focused in-process subagents in parallel. Each subagent gets its own prompt and returns a comprehensive research result with tool and token stats. Use this for broad exploration when reading many files would consume the main agent's context window.
Parameters:
- prompt_1: (required) First subagent prompt.
- prompt_2: (optional) Optional second subagent prompt.
- prompt_3: (optional) Optional third subagent prompt.
- prompt_4: (optional) Optional fourth subagent prompt.
- prompt_5: (optional) Optional fifth subagent prompt.
Usage:
<use_subagents>
<prompt_1></prompt_1>
<prompt_2></prompt_2>
<prompt_3></prompt_3>
<prompt_4></prompt_4>
<prompt_5></prompt_5>
</use_subagents>
# Tool Use Examples
## Example 1: Requesting to execute a command
@@ -297,6 +297,23 @@ Usage:
<load_mcp_documentation>
</load_mcp_documentation>
## use_subagents
Description: Run up to five focused in-process subagents in parallel. Each subagent gets its own prompt and returns a comprehensive research result with tool and token stats. Use this for broad exploration when reading many files would consume the main agent's context window.
Parameters:
- prompt_1: (required) First subagent prompt.
- prompt_2: (optional) Optional second subagent prompt.
- prompt_3: (optional) Optional third subagent prompt.
- prompt_4: (optional) Optional fourth subagent prompt.
- prompt_5: (optional) Optional fifth subagent prompt.
Usage:
<use_subagents>
<prompt_1></prompt_1>
<prompt_2></prompt_2>
<prompt_3></prompt_3>
<prompt_4></prompt_4>
<prompt_5></prompt_5>
</use_subagents>
# Tool Use Examples
## Example 1: Requesting to execute a command
@@ -466,6 +466,43 @@
}
}
},
{
"type": "function",
"function": {
"name": "use_subagents",
"description": "Run up to five focused in-process subagents in parallel. Each subagent gets its own prompt and returns a comprehensive research result with tool and token stats. Use this for broad exploration when reading many files would consume the main agent's context window.",
"strict": false,
"parameters": {
"type": "object",
"properties": {
"prompt_1": {
"type": "string",
"description": "First subagent prompt."
},
"prompt_2": {
"type": "string",
"description": "Optional second subagent prompt."
},
"prompt_3": {
"type": "string",
"description": "Optional third subagent prompt."
},
"prompt_4": {
"type": "string",
"description": "Optional fourth subagent prompt."
},
"prompt_5": {
"type": "string",
"description": "Optional fifth subagent prompt."
}
},
"required": [
"prompt_1"
],
"additionalProperties": false
}
}
},
{
"type": "function",
"function": {
@@ -266,6 +266,23 @@ Usage:
<to_ref>HEAD</to_ref>
</generate_explanation>
## use_subagents
Description: Run up to five focused in-process subagents in parallel. Each subagent gets its own prompt and returns a comprehensive research result with tool and token stats. Use this for broad exploration when reading many files would consume the main agent's context window.
Parameters:
- prompt_1: (required) First subagent prompt.
- prompt_2: (optional) Optional second subagent prompt.
- prompt_3: (optional) Optional third subagent prompt.
- prompt_4: (optional) Optional fourth subagent prompt.
- prompt_5: (optional) Optional fifth subagent prompt.
Usage:
<use_subagents>
<prompt_1></prompt_1>
<prompt_2></prompt_2>
<prompt_3></prompt_3>
<prompt_4></prompt_4>
<prompt_5></prompt_5>
</use_subagents>
# Tool Use Examples
## Example 1: Requesting to execute a command
@@ -232,6 +232,23 @@ Usage:
<to_ref>HEAD</to_ref>
</generate_explanation>
## use_subagents
Description: Run up to five focused in-process subagents in parallel. Each subagent gets its own prompt and returns a comprehensive research result with tool and token stats. Use this for broad exploration when reading many files would consume the main agent's context window.
Parameters:
- prompt_1: (required) First subagent prompt.
- prompt_2: (optional) Optional second subagent prompt.
- prompt_3: (optional) Optional third subagent prompt.
- prompt_4: (optional) Optional fourth subagent prompt.
- prompt_5: (optional) Optional fifth subagent prompt.
Usage:
<use_subagents>
<prompt_1></prompt_1>
<prompt_2></prompt_2>
<prompt_3></prompt_3>
<prompt_4></prompt_4>
<prompt_5></prompt_5>
</use_subagents>
# Tool Use Examples
## Example 1: Requesting to execute a command
@@ -240,6 +240,23 @@ Usage:
<to_ref>HEAD</to_ref>
</generate_explanation>
## use_subagents
Description: Run up to five focused in-process subagents in parallel. Each subagent gets its own prompt and returns a comprehensive research result with tool and token stats. Use this for broad exploration when reading many files would consume the main agent's context window.
Parameters:
- prompt_1: (required) First subagent prompt.
- prompt_2: (optional) Optional second subagent prompt.
- prompt_3: (optional) Optional third subagent prompt.
- prompt_4: (optional) Optional fourth subagent prompt.
- prompt_5: (optional) Optional fifth subagent prompt.
Usage:
<use_subagents>
<prompt_1></prompt_1>
<prompt_2></prompt_2>
<prompt_3></prompt_3>
<prompt_4></prompt_4>
<prompt_5></prompt_5>
</use_subagents>
# Tool Use Examples
## Example 1: Requesting to execute a command
@@ -266,6 +266,23 @@ Usage:
<to_ref>HEAD</to_ref>
</generate_explanation>
## use_subagents
Description: Run up to five focused in-process subagents in parallel. Each subagent gets its own prompt and returns a comprehensive research result with tool and token stats. Use this for broad exploration when reading many files would consume the main agent's context window.
Parameters:
- prompt_1: (required) First subagent prompt.
- prompt_2: (optional) Optional second subagent prompt.
- prompt_3: (optional) Optional third subagent prompt.
- prompt_4: (optional) Optional fourth subagent prompt.
- prompt_5: (optional) Optional fifth subagent prompt.
Usage:
<use_subagents>
<prompt_1></prompt_1>
<prompt_2></prompt_2>
<prompt_3></prompt_3>
<prompt_4></prompt_4>
<prompt_5></prompt_5>
</use_subagents>
# Tool Use Examples
## Example 1: Requesting to execute a command
@@ -266,6 +266,23 @@ Usage:
<to_ref>HEAD</to_ref>
</generate_explanation>
## use_subagents
Description: Run up to five focused in-process subagents in parallel. Each subagent gets its own prompt and returns a comprehensive research result with tool and token stats. Use this for broad exploration when reading many files would consume the main agent's context window.
Parameters:
- prompt_1: (required) First subagent prompt.
- prompt_2: (optional) Optional second subagent prompt.
- prompt_3: (optional) Optional third subagent prompt.
- prompt_4: (optional) Optional fourth subagent prompt.
- prompt_5: (optional) Optional fifth subagent prompt.
Usage:
<use_subagents>
<prompt_1></prompt_1>
<prompt_2></prompt_2>
<prompt_3></prompt_3>
<prompt_4></prompt_4>
<prompt_5></prompt_5>
</use_subagents>
# Tool Use Examples
## Example 1: Requesting to execute a command
@@ -232,6 +232,23 @@ Usage:
<to_ref>HEAD</to_ref>
</generate_explanation>
## use_subagents
Description: Run up to five focused in-process subagents in parallel. Each subagent gets its own prompt and returns a comprehensive research result with tool and token stats. Use this for broad exploration when reading many files would consume the main agent's context window.
Parameters:
- prompt_1: (required) First subagent prompt.
- prompt_2: (optional) Optional second subagent prompt.
- prompt_3: (optional) Optional third subagent prompt.
- prompt_4: (optional) Optional fourth subagent prompt.
- prompt_5: (optional) Optional fifth subagent prompt.
Usage:
<use_subagents>
<prompt_1></prompt_1>
<prompt_2></prompt_2>
<prompt_3></prompt_3>
<prompt_4></prompt_4>
<prompt_5></prompt_5>
</use_subagents>
# Tool Use Examples
## Example 1: Requesting to execute a command
@@ -240,6 +240,23 @@ Usage:
<to_ref>HEAD</to_ref>
</generate_explanation>
## use_subagents
Description: Run up to five focused in-process subagents in parallel. Each subagent gets its own prompt and returns a comprehensive research result with tool and token stats. Use this for broad exploration when reading many files would consume the main agent's context window.
Parameters:
- prompt_1: (required) First subagent prompt.
- prompt_2: (optional) Optional second subagent prompt.
- prompt_3: (optional) Optional third subagent prompt.
- prompt_4: (optional) Optional fourth subagent prompt.
- prompt_5: (optional) Optional fifth subagent prompt.
Usage:
<use_subagents>
<prompt_1></prompt_1>
<prompt_2></prompt_2>
<prompt_3></prompt_3>
<prompt_4></prompt_4>
<prompt_5></prompt_5>
</use_subagents>
# Tool Use Examples
## Example 1: Requesting to execute a command
@@ -266,6 +266,23 @@ Usage:
<to_ref>HEAD</to_ref>
</generate_explanation>
## use_subagents
Description: Run up to five focused in-process subagents in parallel. Each subagent gets its own prompt and returns a comprehensive research result with tool and token stats. Use this for broad exploration when reading many files would consume the main agent's context window.
Parameters:
- prompt_1: (required) First subagent prompt.
- prompt_2: (optional) Optional second subagent prompt.
- prompt_3: (optional) Optional third subagent prompt.
- prompt_4: (optional) Optional fourth subagent prompt.
- prompt_5: (optional) Optional fifth subagent prompt.
Usage:
<use_subagents>
<prompt_1></prompt_1>
<prompt_2></prompt_2>
<prompt_3></prompt_3>
<prompt_4></prompt_4>
<prompt_5></prompt_5>
</use_subagents>
# Tool Use Examples
## Example 1: Requesting to execute a command
@@ -417,6 +417,43 @@
}
}
},
{
"type": "function",
"function": {
"name": "use_subagents",
"description": "Run up to five focused in-process subagents in parallel. Each subagent gets its own prompt and returns a comprehensive research result with tool and token stats. Use this for broad exploration when reading many files would consume the main agent's context window.",
"strict": false,
"parameters": {
"type": "object",
"properties": {
"prompt_1": {
"type": "string",
"description": "First subagent prompt."
},
"prompt_2": {
"type": "string",
"description": "Optional second subagent prompt."
},
"prompt_3": {
"type": "string",
"description": "Optional third subagent prompt."
},
"prompt_4": {
"type": "string",
"description": "Optional fourth subagent prompt."
},
"prompt_5": {
"type": "string",
"description": "Optional fifth subagent prompt."
}
},
"required": [
"prompt_1"
],
"additionalProperties": false
}
}
},
{
"type": "function",
"function": {
@@ -368,6 +368,43 @@
}
}
},
{
"type": "function",
"function": {
"name": "use_subagents",
"description": "Run up to five focused in-process subagents in parallel. Each subagent gets its own prompt and returns a comprehensive research result with tool and token stats. Use this for broad exploration when reading many files would consume the main agent's context window.",
"strict": false,
"parameters": {
"type": "object",
"properties": {
"prompt_1": {
"type": "string",
"description": "First subagent prompt."
},
"prompt_2": {
"type": "string",
"description": "Optional second subagent prompt."
},
"prompt_3": {
"type": "string",
"description": "Optional third subagent prompt."
},
"prompt_4": {
"type": "string",
"description": "Optional fourth subagent prompt."
},
"prompt_5": {
"type": "string",
"description": "Optional fifth subagent prompt."
}
},
"required": [
"prompt_1"
],
"additionalProperties": false
}
}
},
{
"type": "function",
"function": {
@@ -275,6 +275,23 @@ Usage:
<to_ref>HEAD</to_ref>
</generate_explanation>
## use_subagents
Description: Run up to five focused in-process subagents in parallel. Each subagent gets its own prompt and returns a comprehensive research result with tool and token stats. Use this for broad exploration when reading many files would consume the main agent's context window.
Parameters:
- prompt_1: (required) First subagent prompt.
- prompt_2: (optional) Optional second subagent prompt.
- prompt_3: (optional) Optional third subagent prompt.
- prompt_4: (optional) Optional fourth subagent prompt.
- prompt_5: (optional) Optional fifth subagent prompt.
Usage:
<use_subagents>
<prompt_1></prompt_1>
<prompt_2></prompt_2>
<prompt_3></prompt_3>
<prompt_4></prompt_4>
<prompt_5></prompt_5>
</use_subagents>
# Tool Use Examples
## Example 1: Requesting to execute a command
@@ -241,6 +241,23 @@ Usage:
<to_ref>HEAD</to_ref>
</generate_explanation>
## use_subagents
Description: Run up to five focused in-process subagents in parallel. Each subagent gets its own prompt and returns a comprehensive research result with tool and token stats. Use this for broad exploration when reading many files would consume the main agent's context window.
Parameters:
- prompt_1: (required) First subagent prompt.
- prompt_2: (optional) Optional second subagent prompt.
- prompt_3: (optional) Optional third subagent prompt.
- prompt_4: (optional) Optional fourth subagent prompt.
- prompt_5: (optional) Optional fifth subagent prompt.
Usage:
<use_subagents>
<prompt_1></prompt_1>
<prompt_2></prompt_2>
<prompt_3></prompt_3>
<prompt_4></prompt_4>
<prompt_5></prompt_5>
</use_subagents>
# Tool Use Examples
## Example 1: Requesting to execute a command
@@ -249,6 +249,23 @@ Usage:
<to_ref>HEAD</to_ref>
</generate_explanation>
## use_subagents
Description: Run up to five focused in-process subagents in parallel. Each subagent gets its own prompt and returns a comprehensive research result with tool and token stats. Use this for broad exploration when reading many files would consume the main agent's context window.
Parameters:
- prompt_1: (required) First subagent prompt.
- prompt_2: (optional) Optional second subagent prompt.
- prompt_3: (optional) Optional third subagent prompt.
- prompt_4: (optional) Optional fourth subagent prompt.
- prompt_5: (optional) Optional fifth subagent prompt.
Usage:
<use_subagents>
<prompt_1></prompt_1>
<prompt_2></prompt_2>
<prompt_3></prompt_3>
<prompt_4></prompt_4>
<prompt_5></prompt_5>
</use_subagents>
# Tool Use Examples
## Example 1: Requesting to execute a command
@@ -275,6 +275,23 @@ Usage:
<to_ref>HEAD</to_ref>
</generate_explanation>
## use_subagents
Description: Run up to five focused in-process subagents in parallel. Each subagent gets its own prompt and returns a comprehensive research result with tool and token stats. Use this for broad exploration when reading many files would consume the main agent's context window.
Parameters:
- prompt_1: (required) First subagent prompt.
- prompt_2: (optional) Optional second subagent prompt.
- prompt_3: (optional) Optional third subagent prompt.
- prompt_4: (optional) Optional fourth subagent prompt.
- prompt_5: (optional) Optional fifth subagent prompt.
Usage:
<use_subagents>
<prompt_1></prompt_1>
<prompt_2></prompt_2>
<prompt_3></prompt_3>
<prompt_4></prompt_4>
<prompt_5></prompt_5>
</use_subagents>
# Tool Use Examples
## Example 1: Requesting to execute a command
@@ -352,6 +352,33 @@
]
}
},
{
"name": "use_subagents",
"description": "Run up to five focused in-process subagents in parallel. Each subagent gets its own prompt and returns a comprehensive research result with tool and token stats. Use this for broad exploration when reading many files would consume the main agent's context window.",
"parameters": {
"type": "OBJECT",
"properties": {
"prompt_1": {
"type": "STRING"
},
"prompt_2": {
"type": "STRING"
},
"prompt_3": {
"type": "STRING"
},
"prompt_4": {
"type": "STRING"
},
"prompt_5": {
"type": "STRING"
}
},
"required": [
"prompt_1"
]
}
},
{
"name": "12345670mcp0test_tool",
"description": "test-server: A test tool",
@@ -117,7 +117,7 @@ export const mockProviderInfo = {
mode: "act" as const,
}
const makeProviderInfo = (modelId: string, providerId: string = "test") => ({
const makeProviderInfo = (modelId: string, providerId = "test") => ({
providerId: modelId.includes("ollama") ? "ollama" : providerId,
model: { ...mockProviderInfo.model, id: modelId },
mode: "act" as const,
@@ -129,6 +129,7 @@ const baseContext: SystemPromptContext = {
ide: "TestIde",
supportsBrowserUse: true,
clineWebToolsEnabled: true,
subagentsEnabled: true,
mcpHub: {
getServers: () => [
{
@@ -1,60 +0,0 @@
import { SystemPromptSection } from "../templates/placeholders"
import { TemplateEngine } from "../templates/TemplateEngine"
import type { PromptVariant, SystemPromptContext } from "../types"
const getCliSubagentsTemplateText = (_context: SystemPromptContext) => `USING THE CLINE CLI TOOL
The Cline CLI tool can be used to assign Cline AI agents with focused tasks. This can be used to keep you focused by delegating information-gathering and exploration to separate Cline instances. Use the Cline CLI tool to research large codebases, explore file structures, gather information from multiple files, analyze dependencies, or summarize code sections when the complete context may be too large or overwhelming.
## Creating Cline AI agents
Cline AI agents may be referred to as agents, subagents, or subtasks. Requests may not specifically invoke agents, but you may invoke them directly if warranted. Unless you are specifically asked to use this tool, only create agents when it seems likely you may be exploring across 10 or more files. If users specifically ask that you use this tool, you then must use this tool. Do not use subagents for editing code or executing commands- they should only be used for reading and research to help you better answer questions or build useful context for future coding tasks. If you are performing a search via search_files or the terminal (grep etc.), and the results are long and overwhleming, it is reccomended that you switch to use Cline CLI agents to perform this task. You may perform code edits directly using the write_to_file and replace_in_file tools, and commands using the execute_command tool.
## Command Syntax
You must use the following command syntax for creating Cline AI agents:
\`\`\`bash
cline "your prompt here"
\`\`\`
## Examples of how you might use this tool
\`\`\`bash
# Find specific patterns
cline "find all React components that use the useState hook and list their names"
# Analyze code structure
cline "analyze the authentication flow. Reverse trace through all relevant functions and methods, and provide a summary of how it works. Include file/class references in your summary."
# Gather targeted information
cline "list all API endpoints and their HTTP methods"
# Summarize directories
cline "summarize the purpose of all files in the src/services directory"
# Research implementations
cline "find how error handling is implemented across the application"
\`\`\`
## Tips
- Request brief, technically dense summaries over full file dumps.
- Be specific with your instructions to get focused results.
- Request summaries rather than full file contents. Encourage the agent to be brief, but specific and technically dense with their response.
- If files you want to read are large or complicated, use Cline CLI agents for exploration before instead of reading these files.`
export async function getCliSubagentsSection(variant: PromptVariant, context: SystemPromptContext): Promise<string | undefined> {
// If this is a CLI subagent, don't include CLI subagent instructions to prevent nesting/allignment concerns
if (context.isCliSubagent) {
return undefined
}
// Only include this section if CLI is installed and subagents are enabled
if (!context.isSubagentsEnabledAndCliInstalled) {
return undefined
}
const template = variant.componentOverrides?.[SystemPromptSection.CLI_SUBAGENTS]?.template || getCliSubagentsTemplateText
return new TemplateEngine().resolve(template, context, {})
}
@@ -2,7 +2,6 @@ import { SystemPromptSection } from "../templates/placeholders"
import { getActVsPlanModeSection } from "./act_vs_plan_mode"
import { getAgentRoleSection } from "./agent_role"
import { getCapabilitiesSection } from "./capabilities"
import { getCliSubagentsSection } from "./cli_subagents"
import { getEditingFilesSection } from "./editing_files"
import { getFeedbackSection } from "./feedback"
import { getMcp } from "./mcp"
@@ -44,10 +43,6 @@ export function getSystemPromptComponents() {
id: SystemPromptSection.ACT_VS_PLAN,
fn: getActVsPlanModeSection,
},
{
id: SystemPromptSection.CLI_SUBAGENTS,
fn: getCliSubagentsSection,
},
{
id: SystemPromptSection.FEEDBACK,
fn: getFeedbackSection,
@@ -5,7 +5,6 @@ export enum SystemPromptSection {
MCP = "MCP_SECTION",
EDITING_FILES = "EDITING_FILES_SECTION",
ACT_VS_PLAN = "ACT_VS_PLAN_SECTION",
CLI_SUBAGENTS = "CLI_SUBAGENTS_SECTION",
TODO = "TODO_SECTION",
CAPABILITIES = "CAPABILITIES_SECTION",
SKILLS = "SKILLS_SECTION",
@@ -15,6 +15,7 @@ export * from "./plan_mode_respond"
export * from "./read_file"
export * from "./replace_in_file"
export * from "./search_files"
export * from "./subagent"
export * from "./use_mcp_tool"
export * from "./use_skill"
export * from "./web_fetch"
@@ -17,6 +17,7 @@ import { plan_mode_respond_variants } from "./plan_mode_respond"
import { read_file_variants } from "./read_file"
import { replace_in_file_variants } from "./replace_in_file"
import { search_files_variants } from "./search_files"
import { subagent_variants } from "./subagent"
import { use_mcp_tool_variants } from "./use_mcp_tool"
import { use_skill_variants } from "./use_skill"
import { web_fetch_variants } from "./web_fetch"
@@ -47,6 +48,7 @@ export function registerClineToolSets(): void {
...read_file_variants,
...replace_in_file_variants,
...search_files_variants,
...subagent_variants,
...use_mcp_tool_variants,
...use_skill_variants,
...web_fetch_variants,
@@ -0,0 +1,43 @@
import { ModelFamily } from "@/shared/prompts"
import { ClineDefaultTool } from "@/shared/tools"
import type { ClineToolSpec } from "../spec"
const id = ClineDefaultTool.USE_SUBAGENTS
const generic: ClineToolSpec = {
variant: ModelFamily.GENERIC,
id,
name: "use_subagents",
description:
"Run up to five focused in-process subagents in parallel. Each subagent gets its own prompt and returns a comprehensive research result with tool and token stats. Use this for broad exploration when reading many files would consume the main agent's context window.",
contextRequirements: (context) => context.subagentsEnabled === true && !context.isSubagentRun,
parameters: [
{
name: "prompt_1",
required: true,
instruction: "First subagent prompt.",
},
{
name: "prompt_2",
required: false,
instruction: "Optional second subagent prompt.",
},
{
name: "prompt_3",
required: false,
instruction: "Optional third subagent prompt.",
},
{
name: "prompt_4",
required: false,
instruction: "Optional fourth subagent prompt.",
},
{
name: "prompt_5",
required: false,
instruction: "Optional fifth subagent prompt.",
},
],
}
export const subagent_variants = [generic]
+2
View File
@@ -115,11 +115,13 @@ export interface SystemPromptContext {
readonly isTesting?: boolean
readonly runtimePlaceholders?: Readonly<Record<string, unknown>>
readonly yoloModeToggled?: boolean
readonly subagentsEnabled?: boolean
readonly clineWebToolsEnabled?: boolean
readonly isMultiRootEnabled?: boolean
readonly workspaceRoots?: Array<{ path: string; name: string; vcs?: string }>
readonly isSubagentsEnabledAndCliInstalled?: boolean
readonly isCliSubagent?: boolean
readonly isSubagentRun?: boolean
readonly isCliEnvironment?: boolean
readonly enableNativeToolCalls?: boolean
readonly enableParallelToolCalling?: boolean
@@ -37,7 +37,6 @@ export const config: Omit<PromptVariant, "id"> = createVariant(ModelFamily.GENER
SystemPromptSection.MCP,
SystemPromptSection.EDITING_FILES,
SystemPromptSection.ACT_VS_PLAN,
SystemPromptSection.CLI_SUBAGENTS,
SystemPromptSection.TODO,
SystemPromptSection.CAPABILITIES,
SystemPromptSection.RULES,
@@ -124,7 +123,6 @@ export const createAdvancedVariant = (family: ModelFamily) =>
SystemPromptSection.MCP,
SystemPromptSection.EDITING_FILES,
SystemPromptSection.ACT_VS_PLAN,
SystemPromptSection.CLI_SUBAGENTS,
SystemPromptSection.TODO,
SystemPromptSection.CAPABILITIES,
SystemPromptSection.FEEDBACK,
@@ -151,4 +149,5 @@ export const createAdvancedVariant = (family: ModelFamily) =>
ClineDefaultTool.PLAN_MODE,
ClineDefaultTool.MCP_DOCS,
ClineDefaultTool.TODO,
ClineDefaultTool.USE_SUBAGENTS,
)
@@ -27,7 +27,6 @@ export const config = createVariant(ModelFamily.DEVSTRAL)
SystemPromptSection.MCP,
SystemPromptSection.EDITING_FILES,
SystemPromptSection.ACT_VS_PLAN,
SystemPromptSection.CLI_SUBAGENTS,
SystemPromptSection.CAPABILITIES,
SystemPromptSection.RULES,
SystemPromptSection.SYSTEM_INFO,
@@ -54,6 +53,7 @@ export const config = createVariant(ModelFamily.DEVSTRAL)
ClineDefaultTool.MCP_DOCS,
ClineDefaultTool.TODO,
ClineDefaultTool.USE_SKILL,
ClineDefaultTool.USE_SUBAGENTS,
)
.placeholders({
MODEL_FAMILY: "devstral",
@@ -22,7 +22,6 @@ export const baseTemplate = `{{${SystemPromptSection.AGENT_ROLE}}}
====
{{${SystemPromptSection.CLI_SUBAGENTS}}}
====
@@ -34,7 +34,6 @@ export const config = createVariant(ModelFamily.GEMINI_3)
SystemPromptSection.TOOL_USE,
SystemPromptSection.RULES,
SystemPromptSection.ACT_VS_PLAN,
SystemPromptSection.CLI_SUBAGENTS,
SystemPromptSection.CAPABILITIES,
SystemPromptSection.EDITING_FILES,
SystemPromptSection.FEEDBACK,
@@ -66,6 +65,7 @@ export const config = createVariant(ModelFamily.GEMINI_3)
ClineDefaultTool.TODO,
ClineDefaultTool.GENERATE_EXPLANATION,
ClineDefaultTool.USE_SKILL,
ClineDefaultTool.USE_SUBAGENTS,
)
.placeholders({
MODEL_FAMILY: ModelFamily.GEMINI_3,
@@ -10,7 +10,6 @@ export const baseTemplate = `{{${SystemPromptSection.AGENT_ROLE}}}
====
{{${SystemPromptSection.CLI_SUBAGENTS}}}
====
@@ -48,7 +48,6 @@ export const config = createVariant(ModelFamily.GENERIC)
SystemPromptSection.MCP,
SystemPromptSection.EDITING_FILES,
SystemPromptSection.ACT_VS_PLAN,
SystemPromptSection.CLI_SUBAGENTS,
SystemPromptSection.CAPABILITIES,
SystemPromptSection.RULES,
SystemPromptSection.SYSTEM_INFO,
@@ -74,6 +73,7 @@ export const config = createVariant(ModelFamily.GENERIC)
ClineDefaultTool.TODO,
ClineDefaultTool.GENERATE_EXPLANATION,
ClineDefaultTool.USE_SKILL,
ClineDefaultTool.USE_SUBAGENTS,
)
.placeholders({
MODEL_FAMILY: "generic",
@@ -22,7 +22,6 @@ export const baseTemplate = `{{${SystemPromptSection.AGENT_ROLE}}}
====
{{${SystemPromptSection.CLI_SUBAGENTS}}}
====
@@ -26,7 +26,6 @@ export const config = createVariant(ModelFamily.GLM)
SystemPromptSection.TASK_PROGRESS,
SystemPromptSection.RULES,
SystemPromptSection.ACT_VS_PLAN,
SystemPromptSection.CLI_SUBAGENTS,
SystemPromptSection.CAPABILITIES,
SystemPromptSection.EDITING_FILES,
SystemPromptSection.TODO,
@@ -54,6 +53,7 @@ export const config = createVariant(ModelFamily.GLM)
ClineDefaultTool.TODO,
ClineDefaultTool.GENERATE_EXPLANATION,
ClineDefaultTool.USE_SKILL,
ClineDefaultTool.USE_SUBAGENTS,
)
.placeholders({
MODEL_FAMILY: ModelFamily.GLM,
@@ -10,7 +10,6 @@ export const baseTemplate = `{{${SystemPromptSection.AGENT_ROLE}}}
## {{${SystemPromptSection.ACT_VS_PLAN}}}
## {{${SystemPromptSection.CLI_SUBAGENTS}}}
## {{${SystemPromptSection.CAPABILITIES}}}
@@ -36,7 +36,6 @@ export const config = createVariant(ModelFamily.GPT_5)
SystemPromptSection.MCP,
SystemPromptSection.EDITING_FILES,
SystemPromptSection.ACT_VS_PLAN,
SystemPromptSection.CLI_SUBAGENTS,
SystemPromptSection.CAPABILITIES,
SystemPromptSection.FEEDBACK,
SystemPromptSection.RULES,
@@ -65,6 +64,7 @@ export const config = createVariant(ModelFamily.GPT_5)
ClineDefaultTool.TODO,
ClineDefaultTool.GENERATE_EXPLANATION,
ClineDefaultTool.USE_SKILL,
ClineDefaultTool.USE_SUBAGENTS,
)
.placeholders({
MODEL_FAMILY: ModelFamily.GPT_5,
@@ -27,7 +27,6 @@ export const BASE = `{{${SystemPromptSection.AGENT_ROLE}}}
====
{{${SystemPromptSection.CLI_SUBAGENTS}}}
====
@@ -68,7 +67,7 @@ const RULES = (context: SystemPromptContext) => `RULES
- Be sure to consider the type of project (e.g. Python, JavaScript, web application) when determining the appropriate structure and files to include. Also consider what files may be most relevant to accomplishing the task, for example looking at a project's manifest file would help you understand the project's dependencies, which you could incorporate into any code you write.
- When making changes to code, always consider the context in which the code is being used. Ensure that your changes are compatible with the existing codebase and that they follow the project's coding standards and best practices.
- When you want to modify a file, use the replace_in_file or write_to_file tool directly with the desired changes. You do not need to display the changes before using the tool.
- Use Markdown **only where semantically correct** (e.g., \`inline code\`, \`\`\`code fences\`\`\`, lists, tables). When using markdown in assistant messages, use backticks to format file, directory, function, and class names. Use \( and \) for inline math, \[ and \] for block math.
- Use Markdown **only where semantically correct** (e.g., \`inline code\`, \`\`\`code fences\`\`\`, lists, tables). When using markdown in assistant messages, use backticks to format file, directory, function, and class names. Use ( and ) for inline math, [ and ] for block math.
- Do not ask for more information than necessary. Use the tools provided to accomplish the user's request efficiently and effectively. When you've completed your task, you must use the attempt_completion tool to present the result to the user. The user may provide feedback, which you can use to make improvements and try again.
- ${context.yoloModeToggled !== true ? "You are only allowed to ask the user questions using the ask_followup_question tool. Use this tool only when you need additional details to complete a task, and be sure to use a clear and concise question that will help you move forward with the task. However if you can use the available tools to avoid having to ask the user questions, you should do so" : "Use your available tools and apply your best judgment to accomplish the task without asking the user any followup questions, making reasonable assumptions from the provided context"}. For example, if the user mentions a file that may be in an outside directory like the Desktop, you should use the list_files tool to list the files in the Desktop and check if the file they are talking about is there, rather than asking the user to provide the file path themselves.${context.yoloModeToggled !== true ? "\n- When the user is being vague, you should be proactive about asking clarifying questions using the ask_followup_question tool to ensure you understand their request. However, if you can infer the user's intent based on the context and available tools, you should proceed without asking unnecessary questions" : ""}
- When executing commands, if you don't see the expected output, assume the terminal executed the command successfully and proceed with the task. The user's terminal may be unable to stream the output back properly.${context.yoloModeToggled !== true ? " If you absolutely need to see the actual terminal output, use the ask_followup_question tool to request the user to copy and paste it back to you." : ""}
@@ -26,7 +26,6 @@ export const config = createVariant(ModelFamily.HERMES)
SystemPromptSection.TOOL_USE,
SystemPromptSection.RULES,
SystemPromptSection.ACT_VS_PLAN,
SystemPromptSection.CLI_SUBAGENTS,
SystemPromptSection.CAPABILITIES,
SystemPromptSection.EDITING_FILES,
SystemPromptSection.TODO,
@@ -56,6 +55,7 @@ export const config = createVariant(ModelFamily.HERMES)
ClineDefaultTool.TODO,
ClineDefaultTool.GENERATE_EXPLANATION,
ClineDefaultTool.USE_SKILL,
ClineDefaultTool.USE_SUBAGENTS,
)
.placeholders({
MODEL_FAMILY: "hermes",
@@ -8,7 +8,6 @@ export const baseTemplate = `{{${SystemPromptSection.AGENT_ROLE}}}
## {{${SystemPromptSection.ACT_VS_PLAN}}}
## {{${SystemPromptSection.CLI_SUBAGENTS}}}
## {{${SystemPromptSection.CAPABILITIES}}}
@@ -42,7 +42,6 @@ export const config = createVariant(ModelFamily.NATIVE_GPT_5_1)
SystemPromptSection.TOOL_USE,
SystemPromptSection.TASK_PROGRESS,
SystemPromptSection.ACT_VS_PLAN,
SystemPromptSection.CLI_SUBAGENTS,
SystemPromptSection.CAPABILITIES,
SystemPromptSection.FEEDBACK,
SystemPromptSection.RULES,
@@ -72,6 +71,7 @@ export const config = createVariant(ModelFamily.NATIVE_GPT_5_1)
ClineDefaultTool.TODO,
ClineDefaultTool.GENERATE_EXPLANATION,
ClineDefaultTool.USE_SKILL,
ClineDefaultTool.USE_SUBAGENTS,
)
.placeholders({
MODEL_FAMILY: ModelFamily.NATIVE_GPT_5_1,
@@ -17,7 +17,6 @@ export const BASE = `{{${SystemPromptSection.AGENT_ROLE}}}
====
{{${SystemPromptSection.CLI_SUBAGENTS}}}
====
@@ -48,7 +48,6 @@ export const config = createVariant(ModelFamily.NATIVE_GPT_5)
SystemPromptSection.TOOL_USE,
SystemPromptSection.TASK_PROGRESS,
SystemPromptSection.ACT_VS_PLAN,
SystemPromptSection.CLI_SUBAGENTS,
SystemPromptSection.CAPABILITIES,
SystemPromptSection.FEEDBACK,
SystemPromptSection.RULES,
@@ -78,6 +77,7 @@ export const config = createVariant(ModelFamily.NATIVE_GPT_5)
ClineDefaultTool.TODO,
ClineDefaultTool.GENERATE_EXPLANATION,
ClineDefaultTool.USE_SKILL,
ClineDefaultTool.USE_SUBAGENTS,
)
.placeholders({
MODEL_FAMILY: ModelFamily.NATIVE_GPT_5,
@@ -17,7 +17,6 @@ export const BASE = `{{${SystemPromptSection.AGENT_ROLE}}}
{{${SystemPromptSection.ACT_VS_PLAN}}}
====
{{${SystemPromptSection.CLI_SUBAGENTS}}}
====
@@ -63,6 +63,7 @@ export const config = createVariant(ModelFamily.NATIVE_NEXT_GEN)
ClineDefaultTool.TODO,
ClineDefaultTool.GENERATE_EXPLANATION,
ClineDefaultTool.USE_SKILL,
ClineDefaultTool.USE_SUBAGENTS,
)
.placeholders({
MODEL_FAMILY: ModelFamily.NATIVE_NEXT_GEN,
@@ -39,7 +39,6 @@ export const config = createVariant(ModelFamily.NEXT_GEN)
SystemPromptSection.MCP,
SystemPromptSection.EDITING_FILES,
SystemPromptSection.ACT_VS_PLAN,
SystemPromptSection.CLI_SUBAGENTS,
SystemPromptSection.CAPABILITIES,
SystemPromptSection.FEEDBACK,
SystemPromptSection.RULES,
@@ -68,6 +67,7 @@ export const config = createVariant(ModelFamily.NEXT_GEN)
ClineDefaultTool.TODO,
ClineDefaultTool.GENERATE_EXPLANATION,
ClineDefaultTool.USE_SKILL,
ClineDefaultTool.USE_SUBAGENTS,
)
.placeholders({
MODEL_FAMILY: ModelFamily.NEXT_GEN,
@@ -23,7 +23,6 @@ export const baseTemplate = `{{${SystemPromptSection.AGENT_ROLE}}}
====
{{${SystemPromptSection.CLI_SUBAGENTS}}}
====
@@ -29,7 +29,6 @@ export const config = createVariant(ModelFamily.TRINITY)
SystemPromptSection.MCP,
SystemPromptSection.EDITING_FILES,
SystemPromptSection.ACT_VS_PLAN,
SystemPromptSection.CLI_SUBAGENTS,
SystemPromptSection.CAPABILITIES,
SystemPromptSection.RULES,
SystemPromptSection.SYSTEM_INFO,
@@ -55,6 +54,7 @@ export const config = createVariant(ModelFamily.TRINITY)
ClineDefaultTool.TODO,
ClineDefaultTool.GENERATE_EXPLANATION,
ClineDefaultTool.USE_SKILL,
ClineDefaultTool.USE_SUBAGENTS,
)
.placeholders({
MODEL_FAMILY: ModelFamily.TRINITY,
@@ -22,7 +22,6 @@ export const baseTemplate = `{{${SystemPromptSection.AGENT_ROLE}}}
====
{{${SystemPromptSection.CLI_SUBAGENTS}}}
====
@@ -33,7 +33,6 @@ export const config = createVariant(ModelFamily.XS)
SystemPromptSection.TOOL_USE,
SystemPromptSection.RULES,
SystemPromptSection.ACT_VS_PLAN,
SystemPromptSection.CLI_SUBAGENTS,
SystemPromptSection.CAPABILITIES,
SystemPromptSection.EDITING_FILES,
SystemPromptSection.OBJECTIVE,
@@ -50,6 +49,7 @@ export const config = createVariant(ModelFamily.XS)
ClineDefaultTool.ASK,
ClineDefaultTool.ATTEMPT,
ClineDefaultTool.PLAN_MODE,
ClineDefaultTool.USE_SUBAGENTS,
)
.placeholders({
MODEL_FAMILY: ModelFamily.XS,
@@ -63,9 +63,6 @@ export const config = createVariant(ModelFamily.XS)
.overrideComponent(SystemPromptSection.RULES, {
template: xsComponentOverrides.RULES,
})
.overrideComponent(SystemPromptSection.CLI_SUBAGENTS, {
template: xsComponentOverrides.CLI_SUBAGENTS,
})
.overrideComponent(SystemPromptSection.ACT_VS_PLAN, {
template: xsComponentOverrides.ACT_VS_PLAN,
})
@@ -39,21 +39,6 @@ const XS_OBJECTIVES = `EXECUTION FLOW
- Prefer replace_in_file; respect final formatted state.
- When all steps succeed and are confirmed, call attempt_completion (optional demo command).`
const XS_CLI_SUBAGENTS = (context: SystemPromptContext) =>
context.enableNativeToolCalls
? ""
: `USING THE CLINE CLI TOOL
The Cline CLI tool is installed and available for you to use to handle focused tasks without polluting your main context window. This can be done using
\`\`\`bash
cline t o "your prompt here"
This must only be used for searching and exploring code. It cannot be used to edit files or execute commands.
Example:
# Find specific patterns
cline t o "find all React components that use the useState hook and list their names"
\`\`\``
const XS_TOOLS_OVERRIDE = (context: SystemPromptContext) =>
context.enableNativeToolCalls
? `TOOLS
@@ -118,7 +103,6 @@ export const xsComponentOverrides = {
AGENT_ROLE:
"You are Cline, a senior software engineer + precise task runner. Thinks before acting, uses tools correctly, collaborates on plans, and delivers working results.",
RULES: XS_RULES,
CLI_SUBAGENTS: XS_CLI_SUBAGENTS,
ACT_VS_PLAN: XS_ACT_PLAN_MODE,
CAPABILITIES: XS_CAPABILITIES,
OBJECTIVE: XS_OBJECTIVES,
@@ -6,7 +6,6 @@ export const baseTemplate = `{{${SystemPromptSection.AGENT_ROLE}}}
## {{${SystemPromptSection.ACT_VS_PLAN}}}
## {{${SystemPromptSection.CLI_SUBAGENTS}}}
## {{${SystemPromptSection.CAPABILITIES}}}
+3 -15
View File
@@ -12,7 +12,6 @@ import {
newRuleToolResponse,
newTaskToolResponse,
reportBugToolResponse,
subagentToolResponse,
} from "../prompts/commands"
import { StateManager } from "../storage/StateManager"
@@ -50,16 +49,7 @@ export async function parseSlashCommands(
providerInfo?: ApiProviderInfo,
mcpPromptFetcher?: McpPromptFetcher,
): Promise<{ processedText: string; needsClinerulesFileCheck: boolean }> {
const SUPPORTED_DEFAULT_COMMANDS = [
"newtask",
"smol",
"compact",
"newrule",
"reportbug",
"deep-planning",
"subagent",
"explain-changes",
]
const SUPPORTED_DEFAULT_COMMANDS = ["newtask", "smol", "compact", "newrule", "reportbug", "deep-planning", "explain-changes"]
// Determine if the current provider/model/setting actually uses native tool calling
const willUseNativeTools = isNativeToolCallingConfig(providerInfo!, enableNativeToolCalls || false)
@@ -71,7 +61,6 @@ export async function parseSlashCommands(
newrule: newRuleToolResponse(),
reportbug: reportBugToolResponse(),
"deep-planning": deepPlanningToolResponse(focusChainSettings, providerInfo, willUseNativeTools),
subagent: subagentToolResponse(),
"explain-changes": explainChangesToolResponse(),
}
@@ -175,10 +164,9 @@ export async function parseSlashCommands(
telemetryService.captureSlashCommandUsed(ulid, commandName, "mcp_prompt")
return { processedText, needsClinerulesFileCheck: false }
} else {
// Prompt not found - log for debugging and fall through to workflow checking
Logger.debug(`MCP prompt not found: ${commandName} (server: ${serverName}, prompt: ${promptName})`)
}
// Prompt not found - log for debugging and fall through to workflow checking
Logger.debug(`MCP prompt not found: ${commandName} (server: ${serverName}, prompt: ${promptName})`)
} catch (error) {
Logger.error(`Error fetching MCP prompt ${commandName}: ${error}`)
}
+11 -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"
@@ -39,6 +40,7 @@ import { PlanModeRespondHandler } from "./tools/handlers/PlanModeRespondHandler"
import { ReadFileToolHandler } from "./tools/handlers/ReadFileToolHandler"
import { ReportBugHandler } from "./tools/handlers/ReportBugHandler"
import { SearchFilesToolHandler } from "./tools/handlers/SearchFilesToolHandler"
import { UseSubagentsToolHandler } from "./tools/handlers/SubagentToolHandler"
import { SummarizeTaskHandler } from "./tools/handlers/SummarizeTaskHandler"
import { UseMcpToolHandler } from "./tools/handlers/UseMcpToolHandler"
import { UseSkillToolHandler } from "./tools/handlers/UseSkillToolHandler"
@@ -116,7 +118,12 @@ 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>,
private switchToActMode: () => Promise<boolean>,
@@ -151,6 +158,7 @@ export class ToolExecutor {
doubleCheckCompletionEnabled: this.stateManager.getGlobalSettingsKey("doubleCheckCompletionEnabled"),
vscodeTerminalExecutionMode: this.vscodeTerminalExecutionMode,
enableParallelToolCalling: this.isParallelToolCallingEnabled(),
isSubagentExecution: false,
cwd: this.cwd,
workspaceManager: this.workspaceManager,
isMultiRootEnabled: this.isMultiRootEnabled,
@@ -181,6 +189,7 @@ export class ToolExecutor {
cancelTask: this.cancelTask,
updateTaskHistory: async (_: any) => [],
executeCommandTool: this.executeCommandTool,
cancelRunningCommandTool: this.cancelRunningCommandTool,
doesLatestTaskCompletionHaveNewChanges: this.doesLatestTaskCompletionHaveNewChanges,
updateFCListFromToolResponse: this.updateFCListFromToolResponse,
sayAndCreateMissingParamError: this.sayAndCreateMissingParamError,
@@ -238,6 +247,7 @@ export class ToolExecutor {
this.coordinator.register(new ReportBugHandler())
this.coordinator.register(new ApplyPatchHandler(validator))
this.coordinator.register(new GenerateExplanationToolHandler())
this.coordinator.register(new UseSubagentsToolHandler())
}
/**
+10 -22
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,
@@ -98,7 +99,6 @@ import { ApiFormat } from "@/shared/proto/cline/models"
import { ShowMessageType } from "@/shared/proto/index.host"
import { Logger } from "@/shared/services/Logger"
import { Session } from "@/shared/services/Session"
import { isClineCliInstalled, isCliSubagentContext } from "@/utils/cli-detector"
import { RuleContextBuilder } from "../context/instructions/user-instructions/RuleContextBuilder"
import { ensureLocalClineDirExists } from "../context/instructions/user-instructions/rule-helpers"
import { discoverSkills, getAvailableSkills } from "../context/instructions/user-instructions/skills"
@@ -126,7 +126,6 @@ type TaskParams = {
shellIntegrationTimeout: number
terminalReuseEnabled: boolean
terminalOutputLineLimit: number
subagentTerminalOutputLineLimit: number
defaultTerminalProfile: string
vscodeTerminalExecutionMode: "vscodeTerminal" | "backgroundExec"
cwd: string
@@ -261,7 +260,6 @@ export class Task {
shellIntegrationTimeout,
terminalReuseEnabled,
terminalOutputLineLimit,
subagentTerminalOutputLineLimit,
defaultTerminalProfile,
vscodeTerminalExecutionMode,
cwd,
@@ -303,7 +301,6 @@ export class Task {
this.terminalManager.setShellIntegrationTimeout(shellIntegrationTimeout)
this.terminalManager.setTerminalReuseEnabled(terminalReuseEnabled ?? true)
this.terminalManager.setTerminalOutputLineLimit(terminalOutputLineLimit)
this.terminalManager.setSubagentTerminalOutputLineLimit(subagentTerminalOutputLineLimit)
this.terminalManager.setDefaultTerminalProfile(defaultTerminalProfile)
this.urlContentFetcher = new UrlContentFetcher(controller.context)
@@ -552,6 +549,7 @@ export class Task {
this.sayAndCreateMissingParamError.bind(this),
this.removeLastPartialMessageIfExistsWithType.bind(this),
this.executeCommandTool.bind(this),
this.cancelBackgroundCommand.bind(this),
() => this.checkpointManager?.doesLatestTaskCompletionHaveNewChanges() ?? Promise.resolve(false),
this.FocusChainManager?.updateFCListFromToolResponse.bind(this.FocusChainManager) || (async () => {}),
this.switchToActModeCallback.bind(this),
@@ -1552,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)
}
/**
@@ -1757,14 +1759,6 @@ export class Task {
? `# Preferred Language\n\nSpeak in ${preferredLanguage}.`
: ""
// Check CLI installation status only if subagents are enabled
const subagentsEnabled = this.stateManager.getGlobalSettingsKey("subagentsEnabled")
let isSubagentsEnabledAndCliInstalled = false
if (subagentsEnabled) {
const clineCliInstalled = await isClineCliInstalled()
isSubagentsEnabledAndCliInstalled = subagentsEnabled && clineCliInstalled
}
const { globalToggles, localToggles } = await refreshClineRulesToggles(this.controller, this.cwd)
const { windsurfLocalToggles, cursorLocalToggles, agentsLocalToggles } = await refreshExternalRulesToggles(
this.controller,
@@ -1808,12 +1802,6 @@ export class Task {
}))
}
// Detect if this is a CLI subagent to prevent nested subagent creation
const isCliSubagent = isCliSubagentContext({
yoloModeToggled: this.stateManager.getGlobalSettingsKey("yoloModeToggled"),
maxConsecutiveMistakes: this.stateManager.getGlobalSettingsKey("maxConsecutiveMistakes"),
})
// Discover and filter available skills
const allSkills = await discoverSkills(this.cwd)
const resolvedSkills = getAvailableSkills(allSkills)
@@ -1856,12 +1844,12 @@ export class Task {
preferredLanguageInstructions,
browserSettings: this.stateManager.getGlobalSettingsKey("browserSettings"),
yoloModeToggled: this.stateManager.getGlobalSettingsKey("yoloModeToggled"),
subagentsEnabled: this.stateManager.getGlobalSettingsKey("subagentsEnabled"),
clineWebToolsEnabled:
this.stateManager.getGlobalSettingsKey("clineWebToolsEnabled") && featureFlagsService.getWebtoolsEnabled(),
isMultiRootEnabled: multiRootEnabled,
workspaceRoots,
isSubagentsEnabledAndCliInstalled,
isCliSubagent,
isSubagentRun: false,
isCliEnvironment,
enableNativeToolCalls:
providerInfo.model.info.apiFormat === ApiFormat.OPENAI_RESPONSES ||
+5 -3
View File
@@ -51,6 +51,7 @@ export class AutoApprove {
case ClineDefaultTool.FILE_EDIT:
case ClineDefaultTool.APPLY_PATCH:
case ClineDefaultTool.BASH:
case ClineDefaultTool.USE_SUBAGENTS:
return [true, true]
case ClineDefaultTool.BROWSER:
@@ -73,6 +74,7 @@ export class AutoApprove {
case ClineDefaultTool.FILE_EDIT:
case ClineDefaultTool.APPLY_PATCH:
case ClineDefaultTool.BASH:
case ClineDefaultTool.USE_SUBAGENTS:
return [true, true]
case ClineDefaultTool.BROWSER:
case ClineDefaultTool.WEB_FETCH:
@@ -90,6 +92,7 @@ export class AutoApprove {
case ClineDefaultTool.LIST_FILES:
case ClineDefaultTool.LIST_CODE_DEF:
case ClineDefaultTool.SEARCH:
case ClineDefaultTool.USE_SUBAGENTS:
return [autoApprovalSettings.actions.readFiles, autoApprovalSettings.actions.readFilesExternally ?? false]
case ClineDefaultTool.NEW_RULE:
case ClineDefaultTool.FILE_NEW:
@@ -127,7 +130,7 @@ export class AutoApprove {
return true
}
let isLocalRead: boolean = false
let isLocalRead = false
if (autoApproveActionpath) {
// Use cached workspace info instead of fetching every time
const { isMultiRootScenario } = await this.getWorkspaceInfo()
@@ -159,8 +162,7 @@ export class AutoApprove {
if ((isLocalRead && autoApproveLocal) || (!isLocalRead && autoApproveLocal && autoApproveExternal)) {
return true
} else {
return false
}
return false
}
}
@@ -67,6 +67,9 @@ export class ExecuteCommandToolHandler implements IFullyManagedTool {
async handlePartialBlock(block: ToolUse, uiHelpers: StronglyTypedUIHelpers): Promise<void> {
const command = block.params.command
if (uiHelpers.getConfig().isSubagentExecution) {
return
}
// Check if this should be auto-approved to determine UI flow
const shouldAutoApprove = uiHelpers.shouldAutoApproveTool(this.name)
@@ -168,14 +171,18 @@ export class ExecuteCommandToolHandler implements IFullyManagedTool {
`Command "${actualCommand}" was denied by CLINE_COMMAND_PERMISSIONS. ` +
`Reason: ${permissionResult.reason}${matchedPattern}`
}
await config.callbacks.say("command_permission_denied", errorMessage)
if (!config.isSubagentExecution) {
await config.callbacks.say("command_permission_denied", errorMessage)
}
return formatResponse.toolError(formatResponse.permissionDeniedError(errorMessage))
}
// Check clineignore validation for command
const ignoredFileAttemptedToAccess = config.services.clineIgnoreController.validateCommand(actualCommand)
if (ignoredFileAttemptedToAccess) {
await config.callbacks.say("clineignore_error", ignoredFileAttemptedToAccess)
if (!config.isSubagentExecution) {
await config.callbacks.say("clineignore_error", ignoredFileAttemptedToAccess)
}
return formatResponse.toolError(formatResponse.clineIgnoreError(ignoredFileAttemptedToAccess))
}
@@ -210,10 +217,16 @@ export class ExecuteCommandToolHandler implements IFullyManagedTool {
)
}
if ((!requiresApprovalPerLLM && autoApproveSafe) || (requiresApprovalPerLLM && autoApproveSafe && autoApproveAll)) {
if (
config.isSubagentExecution ||
(!requiresApprovalPerLLM && autoApproveSafe) ||
(requiresApprovalPerLLM && autoApproveSafe && autoApproveAll)
) {
// Auto-approve flow
await config.callbacks.removeLastPartialMessageIfExistsWithType("ask", "command")
await config.callbacks.say("command", actualCommand, undefined, undefined, false)
if (!config.isSubagentExecution) {
await config.callbacks.removeLastPartialMessageIfExistsWithType("ask", "command")
await config.callbacks.say("command", actualCommand, undefined, undefined, false)
}
didAutoApprove = true
telemetryService.captureToolUsage(
config.ulid,
@@ -276,7 +289,7 @@ export class ExecuteCommandToolHandler implements IFullyManagedTool {
// Setup timeout notification for long-running auto-approved commands
let timeoutId: NodeJS.Timeout | undefined
if (didAutoApprove && config.autoApprovalSettings.enableNotifications) {
if (didAutoApprove && config.autoApprovalSettings.enableNotifications && !config.isSubagentExecution) {
// if the command was auto-approved, and it's long running we need to notify the user after some time has passed without proceeding
timeoutId = setTimeout(() => {
showSystemNotification({
@@ -26,6 +26,9 @@ export class ListCodeDefinitionNamesToolHandler implements IFullyManagedTool {
const relPath = block.params.path
const config = uiHelpers.getConfig()
if (config.isSubagentExecution) {
return
}
// Create and show partial UI message
const sharedMessageProps = {
@@ -82,10 +85,14 @@ export class ListCodeDefinitionNamesToolHandler implements IFullyManagedTool {
const completeMessage = JSON.stringify(sharedMessageProps)
if (await config.callbacks.shouldAutoApproveToolWithPath(block.name, relDirPath)) {
const shouldAutoApprove =
config.isSubagentExecution || (await config.callbacks.shouldAutoApproveToolWithPath(block.name, relDirPath))
if (shouldAutoApprove) {
// Auto-approval flow
await config.callbacks.removeLastPartialMessageIfExistsWithType("ask", "tool")
await config.callbacks.say("tool", completeMessage, undefined, undefined, false)
if (!config.isSubagentExecution) {
await config.callbacks.removeLastPartialMessageIfExistsWithType("ask", "tool")
await config.callbacks.say("tool", completeMessage, undefined, undefined, false)
}
// Capture telemetry
telemetryService.captureToolUsage(
@@ -120,18 +127,17 @@ export class ListCodeDefinitionNamesToolHandler implements IFullyManagedTool {
block.isNativeToolCall,
)
return formatResponse.toolDenied()
} else {
telemetryService.captureToolUsage(
config.ulid,
block.name,
config.api.getModel().id,
provider,
false,
true,
undefined,
block.isNativeToolCall,
)
}
telemetryService.captureToolUsage(
config.ulid,
block.name,
config.api.getModel().id,
provider,
false,
true,
undefined,
block.isNativeToolCall,
)
}
// Run PreToolUse hook after approval but before execution
@@ -28,6 +28,9 @@ export class ListFilesToolHandler implements IFullyManagedTool {
// Get config access for services
const config = uiHelpers.getConfig()
if (config.isSubagentExecution) {
return
}
// Create and show partial UI message
const recursiveRaw = block.params.recursive
@@ -87,7 +90,9 @@ export class ListFilesToolHandler implements IFullyManagedTool {
// Check clineignore access
const accessValidation = this.validator.checkClineIgnorePath(relDirPath!)
if (!accessValidation.ok) {
await config.callbacks.say("clineignore_error", relDirPath)
if (!config.isSubagentExecution) {
await config.callbacks.say("clineignore_error", relDirPath)
}
return formatResponse.toolError(formatResponse.clineIgnoreError(relDirPath!))
}
@@ -106,10 +111,14 @@ export class ListFilesToolHandler implements IFullyManagedTool {
const completeMessage = JSON.stringify(sharedMessageProps)
if (await config.callbacks.shouldAutoApproveToolWithPath(block.name, relDirPath)) {
const shouldAutoApprove =
config.isSubagentExecution || (await config.callbacks.shouldAutoApproveToolWithPath(block.name, relDirPath))
if (shouldAutoApprove) {
// Auto-approval flow
await config.callbacks.removeLastPartialMessageIfExistsWithType("ask", "tool")
await config.callbacks.say("tool", completeMessage, undefined, undefined, false)
if (!config.isSubagentExecution) {
await config.callbacks.removeLastPartialMessageIfExistsWithType("ask", "tool")
await config.callbacks.say("tool", completeMessage, undefined, undefined, false)
}
// Capture telemetry
telemetryService.captureToolUsage(
@@ -144,18 +153,17 @@ export class ListFilesToolHandler implements IFullyManagedTool {
block.isNativeToolCall,
)
return formatResponse.toolDenied()
} else {
telemetryService.captureToolUsage(
config.ulid,
block.name,
config.api.getModel().id,
provider,
false,
true,
workspaceContext,
block.isNativeToolCall,
)
}
telemetryService.captureToolUsage(
config.ulid,
block.name,
config.api.getModel().id,
provider,
false,
true,
workspaceContext,
block.isNativeToolCall,
)
}
// Run PreToolUse hook after approval but before execution
@@ -28,6 +28,9 @@ export class ReadFileToolHandler implements IFullyManagedTool {
const relPath = block.params.path
const config = uiHelpers.getConfig()
if (config.isSubagentExecution) {
return
}
// Create and show partial UI message
const sharedMessageProps = {
@@ -67,7 +70,9 @@ export class ReadFileToolHandler implements IFullyManagedTool {
// Check clineignore access
const accessValidation = this.validator.checkClineIgnorePath(relPath!)
if (!accessValidation.ok) {
await config.callbacks.say("clineignore_error", relPath)
if (!config.isSubagentExecution) {
await config.callbacks.say("clineignore_error", relPath)
}
return formatResponse.toolError(formatResponse.clineIgnoreError(relPath!))
}
@@ -97,10 +102,14 @@ export class ReadFileToolHandler implements IFullyManagedTool {
const completeMessage = JSON.stringify(sharedMessageProps)
if (await config.callbacks.shouldAutoApproveToolWithPath(block.name, relPath)) {
const shouldAutoApprove =
config.isSubagentExecution || (await config.callbacks.shouldAutoApproveToolWithPath(block.name, relPath))
if (shouldAutoApprove) {
// Auto-approval flow
await config.callbacks.removeLastPartialMessageIfExistsWithType("ask", "tool")
await config.callbacks.say("tool", completeMessage, undefined, undefined, false)
if (!config.isSubagentExecution) {
await config.callbacks.removeLastPartialMessageIfExistsWithType("ask", "tool")
await config.callbacks.say("tool", completeMessage, undefined, undefined, false)
}
// Capture telemetry
telemetryService.captureToolUsage(
@@ -135,18 +144,17 @@ export class ReadFileToolHandler implements IFullyManagedTool {
block.isNativeToolCall,
)
return formatResponse.toolDenied()
} else {
telemetryService.captureToolUsage(
config.ulid,
block.name,
config.api.getModel().id,
provider,
false,
true,
workspaceContext,
block.isNativeToolCall,
)
}
telemetryService.captureToolUsage(
config.ulid,
block.name,
config.api.getModel().id,
provider,
false,
true,
workspaceContext,
block.isNativeToolCall,
)
}
// Run PreToolUse hook after approval but before execution
@@ -51,23 +51,21 @@ export class SearchFilesToolHandler implements IFullyManagedTool {
const workspaceRoots = adapter.getWorkspaceRoots()
const root = workspaceRoots.find((r) => r.name === workspaceHint)
return [{ absolutePath, workspaceName: workspaceHint, workspaceRoot: root?.path }]
} else {
// As a fallback, perform the search across all available workspaces.
// Typically, models should provide explicit hints to target specific workspaces for searching.
const allPaths = adapter.getAllPossiblePaths(parsedPath)
const workspaceRoots = adapter.getWorkspaceRoots()
return allPaths.map((absPath, index) => ({
absolutePath: absPath,
workspaceName: workspaceRoots[index]?.name || path.basename(workspaceRoots[index]?.path || absPath),
workspaceRoot: workspaceRoots[index]?.path,
}))
}
} else {
// Single-workspace mode (backward compatible)
const pathResult = resolveWorkspacePath(config, originalPath, "SearchFilesTool.execute")
const absolutePath = typeof pathResult === "string" ? pathResult : pathResult.absolutePath
return [{ absolutePath, workspaceRoot: config.cwd }]
// As a fallback, perform the search across all available workspaces.
// Typically, models should provide explicit hints to target specific workspaces for searching.
const allPaths = adapter.getAllPossiblePaths(parsedPath)
const workspaceRoots = adapter.getWorkspaceRoots()
return allPaths.map((absPath, index) => ({
absolutePath: absPath,
workspaceName: workspaceRoots[index]?.name || path.basename(workspaceRoots[index]?.path || absPath),
workspaceRoot: workspaceRoots[index]?.path,
}))
}
// Single-workspace mode (backward compatible)
const pathResult = resolveWorkspacePath(config, originalPath, "SearchFilesTool.execute")
const absolutePath = typeof pathResult === "string" ? pathResult : pathResult.absolutePath
return [{ absolutePath, workspaceRoot: config.cwd }]
}
/**
@@ -96,7 +94,7 @@ export class SearchFilesToolHandler implements IFullyManagedTool {
// Parse the result count from the first line
const firstLine = workspaceResults.split("\n")[0]
const resultMatch = firstLine.match(/Found (\d+) result/)
const resultCount = resultMatch ? parseInt(resultMatch[1], 10) : 0
const resultCount = resultMatch ? Number.parseInt(resultMatch[1], 10) : 0
return {
workspaceName,
@@ -164,13 +162,11 @@ export class SearchFilesToolHandler implements IFullyManagedTool {
// Multi-workspace search result
if (totalResultCount === 0) {
return "Found 0 results."
} else {
return `Found ${totalResultCount === 1 ? "1 result" : `${totalResultCount.toLocaleString()} results`} across ${searchPaths.length} workspace${searchPaths.length > 1 ? "s" : ""}.\n\n${allResults.join("\n\n")}`
}
} else {
// Single workspace result
return allResults[0] || "Found 0 results."
return `Found ${totalResultCount === 1 ? "1 result" : `${totalResultCount.toLocaleString()} results`} across ${searchPaths.length} workspace${searchPaths.length > 1 ? "s" : ""}.\n\n${allResults.join("\n\n")}`
}
// Single workspace result
return allResults[0] || "Found 0 results."
}
async handlePartialBlock(block: ToolUse, uiHelpers: StronglyTypedUIHelpers): Promise<void> {
@@ -178,6 +174,9 @@ export class SearchFilesToolHandler implements IFullyManagedTool {
const regex = block.params.regex
const config = uiHelpers.getConfig()
if (config.isSubagentExecution) {
return
}
// Create and show partial UI message
const filePattern = block.params.file_pattern
@@ -306,10 +305,14 @@ export class SearchFilesToolHandler implements IFullyManagedTool {
const completeMessage = JSON.stringify(sharedMessageProps)
if (await config.callbacks.shouldAutoApproveToolWithPath(block.name, relDirPath)) {
const shouldAutoApprove =
config.isSubagentExecution || (await config.callbacks.shouldAutoApproveToolWithPath(block.name, relDirPath))
if (shouldAutoApprove) {
// Auto-approval flow
await config.callbacks.removeLastPartialMessageIfExistsWithType("ask", "tool")
await config.callbacks.say("tool", completeMessage, undefined, undefined, false)
if (!config.isSubagentExecution) {
await config.callbacks.removeLastPartialMessageIfExistsWithType("ask", "tool")
await config.callbacks.say("tool", completeMessage, undefined, undefined, false)
}
// Capture telemetry
telemetryService.captureToolUsage(
@@ -344,18 +347,17 @@ export class SearchFilesToolHandler implements IFullyManagedTool {
block.isNativeToolCall,
)
return formatResponse.toolDenied()
} else {
telemetryService.captureToolUsage(
config.ulid,
block.name,
config.api.getModel().id,
provider,
false,
true,
workspaceContext,
block.isNativeToolCall,
)
}
telemetryService.captureToolUsage(
config.ulid,
block.name,
config.api.getModel().id,
provider,
false,
true,
workspaceContext,
block.isNativeToolCall,
)
}
// Run PreToolUse hook after approval but before execution
@@ -0,0 +1,296 @@
import type { ToolUse } from "@core/assistant-message"
import { formatResponse } from "@core/prompts/responses"
import {
ClineAskUseSubagents,
ClineSaySubagentStatus,
ClineSubagentUsageInfo,
SubagentStatusItem,
} from "@shared/ExtensionMessage"
import { telemetryService } from "@/services/telemetry"
import { ClineDefaultTool } from "@/shared/tools"
import type { ToolResponse } from "../../index"
import { showNotificationForApproval } from "../../utils"
import { SubagentRunner } from "../subagent/SubagentRunner"
import type { IFullyManagedTool } from "../ToolExecutorCoordinator"
import type { TaskConfig } from "../types/TaskConfig"
import type { StronglyTypedUIHelpers } from "../types/UIHelpers"
import { ToolResultUtils } from "../utils/ToolResultUtils"
const MAX_SUBAGENT_PROMPTS = 5
const PROMPT_KEYS = ["prompt_1", "prompt_2", "prompt_3", "prompt_4", "prompt_5"] as const
function excerpt(text: string | undefined, maxChars = 1200): string {
if (!text) {
return ""
}
const trimmed = text.trim()
if (trimmed.length <= maxChars) {
return trimmed
}
return `${trimmed.slice(0, maxChars)}...`
}
export class UseSubagentsToolHandler implements IFullyManagedTool {
readonly name = ClineDefaultTool.USE_SUBAGENTS
getDescription(_block: ToolUse): string {
return "[subagents]"
}
async handlePartialBlock(block: ToolUse, uiHelpers: StronglyTypedUIHelpers): Promise<void> {
const prompts = PROMPT_KEYS.map((key) => uiHelpers.removeClosingTag(block, key, block.params[key]?.trim()))
.map((prompt) => prompt?.trim())
.filter((prompt): prompt is string => !!prompt)
if (prompts.length === 0) {
return
}
const partialMessage = JSON.stringify({ prompts } satisfies ClineAskUseSubagents)
const autoApproveResult = uiHelpers.shouldAutoApproveTool(this.name)
const [shouldAutoApprove] = Array.isArray(autoApproveResult) ? autoApproveResult : [autoApproveResult, false]
if (shouldAutoApprove) {
await uiHelpers.removeLastPartialMessageIfExistsWithType("ask", "use_subagents")
await uiHelpers.say("use_subagents", partialMessage, undefined, undefined, block.partial)
} else {
await uiHelpers.removeLastPartialMessageIfExistsWithType("say", "use_subagents")
await uiHelpers.ask("use_subagents", partialMessage, block.partial).catch(() => {})
}
}
async execute(config: TaskConfig, block: ToolUse): Promise<ToolResponse> {
const subagentsEnabled = config.services.stateManager.getGlobalSettingsKey("subagentsEnabled")
if (!subagentsEnabled) {
return formatResponse.toolError("Subagents are disabled. Enable them in Settings > Features to use this tool.")
}
const prompts = PROMPT_KEYS.map((key) => block.params[key]?.trim()).filter((prompt): prompt is string => !!prompt)
if (prompts.length === 0) {
config.taskState.consecutiveMistakeCount++
return await config.callbacks.sayAndCreateMissingParamError(this.name, "prompt_1")
}
if (prompts.length > MAX_SUBAGENT_PROMPTS) {
config.taskState.consecutiveMistakeCount++
return formatResponse.toolError(
`Too many subagent prompts provided (${prompts.length}). Maximum is ${MAX_SUBAGENT_PROMPTS}.`,
)
}
const apiConfig = config.services.stateManager.getApiConfiguration()
const currentMode = config.services.stateManager.getGlobalSettingsKey("mode")
const provider = (currentMode === "plan" ? apiConfig.planModeApiProvider : apiConfig.actModeApiProvider) as string
const approvalPayload: ClineAskUseSubagents = { prompts }
const approvalBody = JSON.stringify(approvalPayload)
const autoApproveResult = config.autoApprover?.shouldAutoApproveTool(this.name)
const [autoApproveSafe] = Array.isArray(autoApproveResult) ? autoApproveResult : [autoApproveResult, false]
const didAutoApprove = !!autoApproveSafe
if (didAutoApprove) {
telemetryService.captureToolUsage(
config.ulid,
this.name,
config.api.getModel().id,
provider,
true,
true,
undefined,
block.isNativeToolCall,
)
} else {
showNotificationForApproval(
prompts.length === 1 ? "Cline wants to use a subagent" : `Cline wants to use ${prompts.length} subagents`,
config.autoApprovalSettings.enableNotifications,
)
const didApprove = await ToolResultUtils.askApprovalAndPushFeedback("use_subagents", approvalBody, config)
if (!didApprove) {
telemetryService.captureToolUsage(
config.ulid,
this.name,
config.api.getModel().id,
provider,
false,
false,
undefined,
block.isNativeToolCall,
)
return formatResponse.toolDenied()
}
telemetryService.captureToolUsage(
config.ulid,
this.name,
config.api.getModel().id,
provider,
false,
true,
undefined,
block.isNativeToolCall,
)
}
config.taskState.consecutiveMistakeCount = 0
const entries: SubagentStatusItem[] = prompts.map((prompt, index) => ({
index: index + 1,
prompt,
status: "pending",
toolCalls: 0,
inputTokens: 0,
outputTokens: 0,
totalCost: 0,
contextTokens: 0,
contextWindow: 0,
contextUsagePercentage: 0,
}))
const emitStatus = async (status: ClineSaySubagentStatus["status"], partial: boolean) => {
const completed = entries.filter((entry) => entry.status === "completed" || entry.status === "failed").length
const successes = entries.filter((entry) => entry.status === "completed").length
const failures = entries.filter((entry) => entry.status === "failed").length
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,
total: entries.length,
completed,
successes,
failures,
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 queueStatusUpdate("running", true)
const runners = prompts.map(() => new SubagentRunner(config))
const abortPollInterval = setInterval(() => {
if (!config.taskState.abort) {
return
}
clearInterval(abortPollInterval)
void Promise.allSettled(runners.map((runner) => runner.abort()))
}, 100)
const execution = prompts.map((prompt, index) =>
runners[index].run(prompt, async (update) => {
const current = entries[index]
if (update.status === "running") {
current.status = "running"
}
if (update.status === "completed") {
current.status = "completed"
}
if (update.status === "failed") {
current.status = "failed"
}
if (update.result !== undefined) {
current.result = update.result
}
if (update.error !== undefined) {
current.error = update.error
}
if (update.stats) {
current.toolCalls = update.stats.toolCalls || 0
current.inputTokens = update.stats.inputTokens || 0
current.outputTokens = update.stats.outputTokens || 0
current.totalCost = update.stats.totalCost || 0
current.contextTokens = update.stats.contextTokens || 0
current.contextWindow = update.stats.contextWindow || 0
current.contextUsagePercentage = update.stats.contextUsagePercentage || 0
}
await queueStatusUpdate("running", true)
}),
)
const settled = await Promise.allSettled(execution)
clearInterval(abortPollInterval)
let usageTokensIn = 0
let usageTokensOut = 0
let usageCacheWrites = 0
let usageCacheReads = 0
let usageCost = 0
settled.forEach((result, index) => {
if (result.status === "rejected") {
entries[index].status = "failed"
entries[index].error = (result.reason as Error)?.message || "Subagent execution failed"
return
}
entries[index].status = result.value.status
entries[index].result = result.value.result
entries[index].error = result.value.error
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].totalCost = result.value.stats.totalCost || 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
usageTokensIn += result.value.stats.inputTokens || 0
usageTokensOut += result.value.stats.outputTokens || 0
usageCacheWrites += result.value.stats.cacheWriteTokens || 0
usageCacheReads += result.value.stats.cacheReadTokens || 0
usageCost += result.value.stats.totalCost || 0
})
const failures = entries.filter((entry) => entry.status === "failed").length
await queueStatusUpdate(failures > 0 ? "failed" : "completed", false)
const subagentUsagePayload: ClineSubagentUsageInfo = {
source: "subagents",
tokensIn: usageTokensIn,
tokensOut: usageTokensOut,
cacheWrites: usageCacheWrites,
cacheReads: usageCacheReads,
cost: usageCost,
}
await config.callbacks.say("subagent_usage", JSON.stringify(subagentUsagePayload))
const successCount = entries.length - failures
const totalToolCalls = entries.reduce((acc, entry) => acc + (entry.toolCalls || 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 results:",
`Total: ${entries.length}`,
`Succeeded: ${successCount}`,
`Failed: ${failures}`,
`Tool calls: ${totalToolCalls}`,
`Peak context usage: ${maxContextTokens.toLocaleString()} / ${contextWindow.toLocaleString()} (${maxContextUsagePercentage.toFixed(1)}%)`,
"",
...entries.map((entry) => {
const header = `[${entry.index}] ${entry.status.toUpperCase()} - ${entry.prompt}`
const detail = entry.status === "completed" ? excerpt(entry.result) : excerpt(entry.error)
return detail ? `${header}\n${detail}` : header
}),
].join("\n")
return formatResponse.toolResult(summary)
}
}
@@ -20,6 +20,9 @@ export class UseSkillToolHandler implements IToolHandler, IPartialBlockHandler {
async handlePartialBlock(block: ToolUse, uiHelpers: StronglyTypedUIHelpers): Promise<void> {
const skillName = block.params.skill_name
if (uiHelpers.getConfig().isSubagentExecution) {
return
}
const message = JSON.stringify({ tool: "useSkill", path: skillName || "" })
await uiHelpers.say("tool", message, undefined, undefined, true)
}
@@ -58,7 +61,9 @@ export class UseSkillToolHandler implements IToolHandler, IPartialBlockHandler {
// Show tool message
const message = JSON.stringify({ tool: "useSkill", path: skillName })
await config.callbacks.say("tool", message, undefined, undefined, false)
if (!config.isSubagentExecution) {
await config.callbacks.say("tool", message, undefined, undefined, false)
}
config.taskState.consecutiveMistakeCount = 0
@@ -0,0 +1,383 @@
import { strict as assert } from "node:assert"
import { setTimeout as delay } from "node:timers/promises"
import { ClineSubagentUsageInfo } from "@shared/ExtensionMessage"
import { ClineDefaultTool } from "@shared/tools"
import { afterEach, describe, it } from "mocha"
import sinon from "sinon"
import { TaskState } from "../../../TaskState"
import { SubagentRunner } from "../../subagent/SubagentRunner"
import type { TaskConfig } from "../../types/TaskConfig"
import { createUIHelpers } from "../../types/UIHelpers"
import { UseSubagentsToolHandler } from "../SubagentToolHandler"
function createConfig(options?: {
autoApproveSafe?: boolean
autoApproveAll?: boolean
taskAskResponse?: "yesButtonClicked" | "noButtonClicked"
subagentsEnabled?: boolean
}) {
const taskState = new TaskState()
const askResponse = options?.taskAskResponse ?? "yesButtonClicked"
const subagentsEnabled = options?.subagentsEnabled ?? true
const callbacks = {
say: sinon.stub().resolves(undefined),
ask: sinon.stub().resolves({ response: askResponse }),
saveCheckpoint: sinon.stub().resolves(),
sayAndCreateMissingParamError: sinon.stub().resolves("missing"),
removeLastPartialMessageIfExistsWithType: sinon.stub().resolves(),
executeCommandTool: sinon.stub().resolves([false, "ok"]),
cancelRunningCommandTool: sinon.stub().resolves(false),
doesLatestTaskCompletionHaveNewChanges: sinon.stub().resolves(false),
updateFCListFromToolResponse: sinon.stub().resolves(),
shouldAutoApproveTool: sinon.stub().returns([options?.autoApproveSafe ?? false, options?.autoApproveAll ?? false]),
shouldAutoApproveToolWithPath: sinon.stub().resolves(false),
postStateToWebview: sinon.stub().resolves(),
reinitExistingTaskFromId: sinon.stub().resolves(),
cancelTask: sinon.stub().resolves(),
updateTaskHistory: sinon.stub().resolves([]),
applyLatestBrowserSettings: sinon.stub().resolves(undefined),
switchToActMode: sinon.stub().resolves(false),
setActiveHookExecution: sinon.stub().resolves(),
clearActiveHookExecution: sinon.stub().resolves(),
getActiveHookExecution: sinon.stub().resolves(undefined),
runUserPromptSubmitHook: sinon.stub().resolves({}),
}
const config = {
taskId: "task-1",
ulid: "ulid-1",
cwd: "/tmp",
mode: "act",
strictPlanModeEnabled: false,
yoloModeToggled: false,
vscodeTerminalExecutionMode: "backgroundExec",
enableParallelToolCalling: true,
context: {},
taskState,
messageState: {},
api: {
getModel: () => ({ id: "openai/gpt-5", info: {} }),
},
autoApprovalSettings: {
enableNotifications: false,
actions: {
executeSafeCommands: false,
executeAllCommands: false,
},
},
autoApprover: {
shouldAutoApproveTool: sinon.stub().returns([options?.autoApproveSafe ?? false, options?.autoApproveAll ?? false]),
},
browserSettings: {},
focusChainSettings: {},
services: {
stateManager: {
getGlobalStateKey: (key: string) => (key === "nativeToolCallEnabled" ? true : undefined),
getGlobalSettingsKey: (key: string) => {
if (key === "mode") {
return "act"
}
if (key === "customPrompt") {
return undefined
}
if (key === "subagentsEnabled") {
return subagentsEnabled
}
return undefined
},
getApiConfiguration: () => ({
planModeApiProvider: "openai",
actModeApiProvider: "openai",
}),
},
mcpHub: {},
},
callbacks,
coordinator: {
getHandler: sinon.stub(),
},
} as unknown as TaskConfig
return { config, callbacks, taskState }
}
describe("SubagentToolHandler", () => {
afterEach(() => {
sinon.restore()
})
it("returns missing parameter error when no prompts are provided", async () => {
const { config, callbacks, taskState } = createConfig()
const handler = new UseSubagentsToolHandler()
const result = await handler.execute(config, {
type: "tool_use",
name: ClineDefaultTool.USE_SUBAGENTS,
params: {},
partial: false,
})
assert.equal(result, "missing")
assert.equal(taskState.consecutiveMistakeCount, 1)
sinon.assert.calledOnce(callbacks.sayAndCreateMissingParamError)
})
it("returns an error when subagents are disabled", async () => {
const { config } = createConfig({ subagentsEnabled: false })
const handler = new UseSubagentsToolHandler()
const result = await handler.execute(config, {
type: "tool_use",
name: ClineDefaultTool.USE_SUBAGENTS,
params: {
prompt_1: "first prompt",
},
partial: false,
})
assert.equal(
result,
"The tool execution failed with the following error:\n<error>\nSubagents are disabled. Enable them in Settings > Features to use this tool.\n</error>",
)
})
it("streams partial use_subagents approval as ask when not auto-approved", async () => {
const { config, callbacks } = createConfig({ autoApproveSafe: false, autoApproveAll: false })
const handler = new UseSubagentsToolHandler()
const uiHelpers = createUIHelpers(config)
await handler.handlePartialBlock(
{
type: "tool_use",
name: ClineDefaultTool.USE_SUBAGENTS,
params: {
prompt_1: "first prompt",
prompt_2: "second prompt",
},
partial: true,
},
uiHelpers,
)
sinon.assert.calledOnce(callbacks.removeLastPartialMessageIfExistsWithType)
sinon.assert.calledWithExactly(callbacks.removeLastPartialMessageIfExistsWithType, "say", "use_subagents")
sinon.assert.calledOnce(callbacks.ask)
sinon.assert.calledWithMatch(callbacks.ask, "use_subagents", sinon.match.string, true)
const payload = JSON.parse(callbacks.ask.firstCall.args[1])
assert.deepEqual(payload.prompts, ["first prompt", "second prompt"])
sinon.assert.notCalled(callbacks.say)
})
it("streams partial use_subagents approval as say when auto-approved", async () => {
const { config, callbacks } = createConfig({ autoApproveSafe: true, autoApproveAll: false })
const handler = new UseSubagentsToolHandler()
const uiHelpers = createUIHelpers(config)
await handler.handlePartialBlock(
{
type: "tool_use",
name: ClineDefaultTool.USE_SUBAGENTS,
params: {
prompt_1: "first prompt",
prompt_2: "second prompt",
},
partial: true,
},
uiHelpers,
)
sinon.assert.calledOnce(callbacks.removeLastPartialMessageIfExistsWithType)
sinon.assert.calledWithExactly(callbacks.removeLastPartialMessageIfExistsWithType, "ask", "use_subagents")
sinon.assert.calledOnce(callbacks.say)
sinon.assert.calledWithMatch(callbacks.say, "use_subagents", sinon.match.string, undefined, undefined, true)
const payload = JSON.parse(callbacks.say.firstCall.args[1])
assert.deepEqual(payload.prompts, ["first prompt", "second prompt"])
sinon.assert.notCalled(callbacks.ask)
})
it("uses one approval for the full batch and stops on denial", async () => {
const { config, callbacks, taskState } = createConfig({ taskAskResponse: "noButtonClicked" })
const runStub = sinon.stub(SubagentRunner.prototype, "run")
const handler = new UseSubagentsToolHandler()
const result = await handler.execute(config, {
type: "tool_use",
name: ClineDefaultTool.USE_SUBAGENTS,
params: {
prompt_1: "one",
prompt_2: "two",
},
partial: false,
})
assert.equal(result, "The user denied this operation.")
assert.equal(taskState.didRejectTool, true)
sinon.assert.calledOnce(callbacks.ask)
assert.equal(callbacks.ask.firstCall.args[0], "use_subagents")
sinon.assert.notCalled(runStub)
})
it("uses read-file auto-approve level (safe only) for approval bypass", async () => {
const { config, callbacks } = createConfig({ autoApproveSafe: true, autoApproveAll: false })
sinon.stub(SubagentRunner.prototype, "run").resolves({
status: "completed",
result: "done",
stats: {
toolCalls: 1,
inputTokens: 2,
outputTokens: 3,
cacheWriteTokens: 0,
cacheReadTokens: 0,
totalCost: 0.25,
contextTokens: 5,
contextWindow: 200000,
contextUsagePercentage: 0.0025,
},
})
const handler = new UseSubagentsToolHandler()
await handler.execute(config, {
type: "tool_use",
name: ClineDefaultTool.USE_SUBAGENTS,
params: {
prompt_1: "one",
},
partial: false,
})
sinon.assert.notCalled(callbacks.ask)
const subagentStatusCalls = callbacks.say.getCalls().filter((call) => call.args[0] === "subagent")
assert.ok(subagentStatusCalls.length >= 1)
})
it("fans out prompts in parallel and emits aggregated status", async () => {
const { config, callbacks } = createConfig({ autoApproveSafe: true, autoApproveAll: true })
let activeRuns = 0
let maxActiveRuns = 0
sinon.stub(SubagentRunner.prototype, "run").callsFake(async (_prompt: string, onProgress) => {
activeRuns++
maxActiveRuns = Math.max(maxActiveRuns, activeRuns)
onProgress({
status: "running",
stats: {
toolCalls: 0,
inputTokens: 0,
outputTokens: 0,
cacheWriteTokens: 0,
cacheReadTokens: 0,
totalCost: 0,
contextTokens: 0,
contextWindow: 200000,
contextUsagePercentage: 0,
},
})
await delay(10)
activeRuns--
return {
status: "completed",
result: "done",
stats: {
toolCalls: 1,
inputTokens: 2,
outputTokens: 3,
cacheWriteTokens: 0,
cacheReadTokens: 0,
totalCost: 0.25,
contextTokens: 5,
contextWindow: 200000,
contextUsagePercentage: 0.0025,
},
}
})
const handler = new UseSubagentsToolHandler()
const result = await handler.execute(config, {
type: "tool_use",
name: ClineDefaultTool.USE_SUBAGENTS,
params: {
prompt_1: "one",
prompt_2: "two",
prompt_3: "three",
},
partial: false,
})
assert.equal(typeof result, "string")
assert.ok((result as string).includes("Total: 3"))
assert.ok(maxActiveRuns > 1)
const subagentStatusCalls = callbacks.say.getCalls().filter((call) => call.args[0] === "subagent")
assert.ok(subagentStatusCalls.length >= 2)
const finalCall = subagentStatusCalls[subagentStatusCalls.length - 1]
assert.equal(finalCall.args[4], false)
const usageCalls = callbacks.say.getCalls().filter((call) => call.args[0] === "subagent_usage")
assert.equal(usageCalls.length, 1)
const usagePayload = JSON.parse(usageCalls[0].args[1]) as ClineSubagentUsageInfo
assert.equal(usagePayload.source, "subagents")
assert.equal(usagePayload.tokensIn, 6)
assert.equal(usagePayload.tokensOut, 9)
assert.equal(usagePayload.cacheWrites, 0)
assert.equal(usagePayload.cacheReads, 0)
assert.equal(usagePayload.cost, 0.75)
})
it("continues after per-subagent failures and reports both outcomes", async () => {
const { config } = createConfig({ autoApproveSafe: true, autoApproveAll: true })
sinon.stub(SubagentRunner.prototype, "run").callsFake(async (prompt: string) => {
if (prompt.includes("fail")) {
return {
status: "failed",
error: "boom",
stats: {
toolCalls: 1,
inputTokens: 0,
outputTokens: 0,
cacheWriteTokens: 0,
cacheReadTokens: 0,
totalCost: 0,
contextTokens: 0,
contextWindow: 200000,
contextUsagePercentage: 0,
},
}
}
return {
status: "completed",
result: "ok",
stats: {
toolCalls: 2,
inputTokens: 0,
outputTokens: 0,
cacheWriteTokens: 0,
cacheReadTokens: 0,
totalCost: 0,
contextTokens: 0,
contextWindow: 200000,
contextUsagePercentage: 0,
},
}
})
const handler = new UseSubagentsToolHandler()
const result = await handler.execute(config, {
type: "tool_use",
name: ClineDefaultTool.USE_SUBAGENTS,
params: {
prompt_1: "succeed",
prompt_2: "fail",
},
partial: false,
})
assert.equal(typeof result, "string")
assert.ok((result as string).includes("Succeeded: 1"))
assert.ok((result as string).includes("Failed: 1"))
assert.ok((result as string).includes("boom"))
})
})
@@ -0,0 +1,592 @@
import { setTimeout as delay } from "node:timers/promises"
import { buildApiHandler } from "@core/api"
import { parseAssistantMessageV2, ToolUse } from "@core/assistant-message"
import { discoverSkills, getAvailableSkills } from "@core/context/instructions/user-instructions/skills"
import { formatResponse } from "@core/prompts/responses"
import { PromptRegistry } from "@core/prompts/system-prompt"
import { ClineToolSet } from "@core/prompts/system-prompt/registry/ClineToolSet"
import type { SystemPromptContext } from "@core/prompts/system-prompt/types"
import { StreamResponseHandler } from "@core/task/StreamResponseHandler"
import { ClineAssistantToolUseBlock, ClineStorageMessage, ClineTextContentBlock } from "@shared/messages"
import { Logger } from "@shared/services/Logger"
import { ClineDefaultTool } from "@shared/tools"
import * as path from "path"
import { HostProvider } from "@/hosts/host-provider"
import { ApiFormat } from "@/shared/proto/cline/models"
import { calculateApiCostAnthropic } from "@/utils/cost"
import { TaskState } from "../../TaskState"
import type { TaskConfig } from "../types/TaskConfig"
const SUBAGENT_ALLOWED_TOOLS: ClineDefaultTool[] = [
ClineDefaultTool.FILE_READ,
ClineDefaultTool.LIST_FILES,
ClineDefaultTool.SEARCH,
ClineDefaultTool.LIST_CODE_DEF,
ClineDefaultTool.BASH,
ClineDefaultTool.USE_SKILL,
]
export type SubagentRunStatus = "completed" | "failed"
export interface SubagentRunResult {
status: SubagentRunStatus
result?: string
error?: string
stats: SubagentRunStats
}
interface SubagentProgressUpdate {
stats?: SubagentRunStats
status?: "running" | "completed" | "failed"
result?: string
error?: string
}
interface SubagentRunStats {
toolCalls: number
inputTokens: number
outputTokens: number
cacheWriteTokens: number
cacheReadTokens: number
totalCost: number
contextTokens: number
contextWindow: number
contextUsagePercentage: number
}
interface SubagentToolCall {
toolUseId: string
id?: string
call_id?: string
signature?: string
name: string
input: unknown
isNativeToolCall: boolean
}
const SUBAGENT_SYSTEM_SUFFIX = `\n\n# Subagent Execution Mode
You are running as a research subagent. Your job is to thoroughly explore the codebase and gather comprehensive information to answer the question.
Explore broadly, read related files, trace through call chains, and build a complete picture before reporting back.
You can read files, list directories, search for patterns, list code definitions, and run commands.
Only use execute_command for readonly operations like ls, grep, git log, git diff, gh, etc.
Do not run commands that modify files or system state.
When you have a comprehensive answer, respond with your findings including file paths and line numbers.
Also include a section titled "Recommended files for main agent" with a concise list of the highest-value files the main agent should read next, and a one-line reason for each file.`
function serializeToolResult(result: unknown): string {
if (typeof result === "string") {
return result
}
if (Array.isArray(result)) {
return result
.map((item) => {
if (!item || typeof item !== "object") {
return String(item)
}
const maybeText = (item as { text?: string }).text
if (typeof maybeText === "string") {
return maybeText
}
return JSON.stringify(item)
})
.join("\n")
}
return JSON.stringify(result, null, 2)
}
function toToolUseParams(input: unknown): Partial<Record<string, string>> {
if (!input || typeof input !== "object") {
return {}
}
const params: Record<string, string> = {}
for (const [key, value] of Object.entries(input)) {
params[key] = typeof value === "string" ? value : JSON.stringify(value)
}
return params
}
function normalizeToolCallArguments(argumentsPayload: unknown): string {
if (typeof argumentsPayload === "string") {
return argumentsPayload
}
try {
return JSON.stringify(argumentsPayload ?? {})
} catch {
return "{}"
}
}
function resolveToolUseId(call: { id?: string; call_id?: string; name?: string }, index: number): string {
const id = call.id?.trim()
if (id) {
return id
}
const callId = call.call_id?.trim()
if (callId) {
return callId
}
const fallbackId = `subagent_tool_${Date.now()}_${index + 1}`
Logger.warn(`[SubagentRunner] Missing tool call id for '${call.name || "unknown"}'; using fallback '${fallbackId}'`)
return fallbackId
}
function toAssistantToolUseBlock(call: SubagentToolCall): ClineAssistantToolUseBlock {
return {
type: "tool_use",
id: call.toolUseId,
name: call.name,
input: call.input,
call_id: call.call_id,
signature: call.signature,
}
}
function parseNonNativeToolCalls(assistantText: string): SubagentToolCall[] {
const parsedBlocks = parseAssistantMessageV2(assistantText)
return parsedBlocks
.filter((block): block is ToolUse => block.type === "tool_use")
.filter((block) => !block.partial)
.map((block, index) => ({
toolUseId: resolveToolUseId({ call_id: block.call_id, name: block.name }, index),
name: block.name,
input: block.params,
call_id: block.call_id,
signature: block.signature,
isNativeToolCall: false,
}))
}
export class SubagentRunner {
private activeApiAbort: (() => void) | undefined
private abortRequested = false
private activeCommandExecutions = 0
private abortingCommands = false
constructor(private baseConfig: TaskConfig) {}
async abort(): Promise<void> {
this.abortRequested = true
try {
this.activeApiAbort?.()
} catch (error) {
Logger.error("[SubagentRunner] failed to abort active API stream", error)
}
if (this.activeCommandExecutions > 0 && !this.abortingCommands && this.baseConfig.callbacks.cancelRunningCommandTool) {
this.abortingCommands = true
try {
await this.baseConfig.callbacks.cancelRunningCommandTool()
} catch (error) {
Logger.error("[SubagentRunner] failed to cancel running command execution", error)
} finally {
this.abortingCommands = false
}
}
}
private shouldAbort(): boolean {
return this.abortRequested || this.baseConfig.taskState.abort
}
private async getWorkspaceMetadataEnvironmentBlock(): Promise<string | null> {
try {
const workspacesJson =
(await this.baseConfig.workspaceManager?.buildWorkspacesJson()) ??
JSON.stringify(
{
workspaces: {
[this.baseConfig.cwd]: {
hint: path.basename(this.baseConfig.cwd) || this.baseConfig.cwd,
},
},
},
null,
2,
)
return `<environment_details>\n# Workspace Configuration\n${workspacesJson}\n</environment_details>`
} catch (error) {
Logger.warn("[SubagentRunner] Failed to build workspace metadata block", error)
return null
}
}
async run(prompt: string, onProgress: (update: SubagentProgressUpdate) => void): Promise<SubagentRunResult> {
this.abortRequested = false
const state = new TaskState()
const stats: SubagentRunStats = {
toolCalls: 0,
inputTokens: 0,
outputTokens: 0,
cacheWriteTokens: 0,
cacheReadTokens: 0,
totalCost: 0,
contextTokens: 0,
contextWindow: 0,
contextUsagePercentage: 0,
}
onProgress({ status: "running", stats })
try {
const mode = this.baseConfig.services.stateManager.getGlobalSettingsKey("mode")
const apiConfiguration = this.baseConfig.services.stateManager.getApiConfiguration()
const effectiveApiConfiguration = {
...apiConfiguration,
ulid: this.baseConfig.ulid,
}
const api = buildApiHandler(effectiveApiConfiguration, mode)
this.activeApiAbort = api.abort?.bind(api)
const providerId = (
mode === "plan" ? apiConfiguration.planModeApiProvider : apiConfiguration.actModeApiProvider
) as string
const providerInfo = {
providerId,
model: api.getModel(),
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")
const host = await HostProvider.env.getHostVersion({})
const discoveredSkills = await discoverSkills(this.baseConfig.cwd)
const skills = getAvailableSkills(discoveredSkills)
const context: SystemPromptContext = {
providerInfo,
cwd: this.baseConfig.cwd,
ide: host?.platform || "Unknown",
skills,
focusChainSettings: this.baseConfig.focusChainSettings,
browserSettings: this.baseConfig.browserSettings,
yoloModeToggled: false,
enableNativeToolCalls: useNativeToolCalls,
enableParallelToolCalling: false,
isSubagentRun: true,
}
const promptRegistry = PromptRegistry.getInstance()
const systemPrompt = (await promptRegistry.get(context)) + SUBAGENT_SYSTEM_SUFFIX
const nativeTools = useNativeToolCalls ? this.buildNativeTools(context) : undefined
const workspaceMetadataEnvironmentBlock = await this.getWorkspaceMetadataEnvironmentBlock()
if (useNativeToolCalls && (!nativeTools || nativeTools.length === 0)) {
const error = "Subagent tool requires native tool calling support."
onProgress({ status: "failed", error, stats })
return { status: "failed", error, stats }
}
if (this.shouldAbort()) {
await this.abort()
const error = "Subagent run cancelled."
onProgress({ status: "failed", error, stats: { ...stats } })
return { status: "failed", error, stats }
}
const conversation: ClineStorageMessage[] = [
{
role: "user",
content: [
{
type: "text",
text: prompt,
} as ClineTextContentBlock,
// Server-side task loop checks require workspace metadata to be present in the
// initial user message of subagent runs.
...(workspaceMetadataEnvironmentBlock
? [
{
type: "text",
text: workspaceMetadataEnvironmentBlock,
} as ClineTextContentBlock,
]
: []),
],
},
]
while (true) {
const streamHandler = new StreamResponseHandler()
const { toolUseHandler } = streamHandler.getHandlers()
let requestInputTokens = 0
let requestOutputTokens = 0
let requestCacheWriteTokens = 0
let requestCacheReadTokens = 0
let requestTotalCost: number | undefined
let assistantText = ""
let assistantTextSignature: string | undefined
let requestId: string | undefined
const stream = api.createMessage(systemPrompt, conversation, nativeTools)
for await (const chunk of stream) {
switch (chunk.type) {
case "usage":
requestId = requestId ?? chunk.id
stats.inputTokens += chunk.inputTokens || 0
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
requestTotalCost = chunk.totalCost ?? requestTotalCost
stats.contextTokens =
requestInputTokens + requestOutputTokens + requestCacheWriteTokens + requestCacheReadTokens
stats.contextUsagePercentage =
stats.contextWindow > 0 ? (stats.contextTokens / stats.contextWindow) * 100 : 0
onProgress({ stats: { ...stats } })
break
case "text":
requestId = requestId ?? chunk.id
assistantText += chunk.text || ""
assistantTextSignature = chunk.signature || assistantTextSignature
break
case "tool_calls":
requestId = requestId ?? chunk.id
toolUseHandler.processToolUseDelta(
{
id: chunk.tool_call.function?.id,
type: "tool_use",
name: chunk.tool_call.function?.name,
input: normalizeToolCallArguments(chunk.tool_call.function?.arguments),
signature: chunk.signature,
},
chunk.tool_call.call_id,
)
break
case "reasoning":
requestId = requestId ?? chunk.id
break
}
if (this.shouldAbort()) {
await this.abort()
const error = "Subagent run cancelled."
onProgress({ status: "failed", error, stats: { ...stats } })
return { status: "failed", error, stats }
}
}
const calculatedRequestCost =
requestTotalCost ??
calculateApiCostAnthropic(
providerInfo.model.info,
requestInputTokens,
requestOutputTokens,
requestCacheWriteTokens,
requestCacheReadTokens,
)
stats.totalCost += calculatedRequestCost || 0
const nativeFinalizedToolCalls = toolUseHandler.getAllFinalizedToolUses().map((toolCall, index) => ({
toolUseId: resolveToolUseId(toolCall, index),
id: toolCall.id,
call_id: toolCall.call_id,
signature: toolCall.signature,
name: toolCall.name,
input: toolCall.input,
isNativeToolCall: true,
}))
const parsedNonNativeToolCalls = parseNonNativeToolCalls(assistantText)
const fallbackNonNativeToolCalls = nativeFinalizedToolCalls.map((toolCall) => ({
...toolCall,
isNativeToolCall: false,
}))
let finalizedToolCalls: SubagentToolCall[] = []
if (useNativeToolCalls) {
finalizedToolCalls = nativeFinalizedToolCalls
} else if (parsedNonNativeToolCalls.length > 0) {
finalizedToolCalls = parsedNonNativeToolCalls
} else if (fallbackNonNativeToolCalls.length > 0) {
// Defensive fallback: if non-native mode receives structured tool call chunks,
// execute them but serialize results as plain text to avoid tool_result pairing mismatches.
Logger.warn(
"[SubagentRunner] Received structured tool_calls while native tool calling is disabled; falling back to non-native result serialization.",
)
finalizedToolCalls = fallbackNonNativeToolCalls
}
const assistantContent = [] as any[]
if (assistantText.trim().length > 0) {
assistantContent.push({
type: "text",
text: assistantText,
signature: assistantTextSignature,
})
}
if (useNativeToolCalls) {
assistantContent.push(...finalizedToolCalls.map(toAssistantToolUseBlock))
}
if (assistantContent.length > 0) {
conversation.push({
role: "assistant",
content: assistantContent,
id: requestId,
})
}
if (finalizedToolCalls.length === 0) {
if (assistantText.trim().length > 0) {
onProgress({ status: "completed", result: assistantText.trim(), stats: { ...stats } })
return { status: "completed", result: assistantText.trim(), stats }
}
const error = "Subagent ended without a final text response."
onProgress({ status: "failed", error, stats: { ...stats } })
return { status: "failed", error, stats }
}
const toolResultBlocks = [] as any[]
for (const call of finalizedToolCalls) {
const toolName = call.name as ClineDefaultTool
if (!SUBAGENT_ALLOWED_TOOLS.includes(toolName)) {
const deniedResult = formatResponse.toolError(`Tool '${toolName}' is not available inside subagent runs.`)
if (call.isNativeToolCall) {
toolResultBlocks.push({
type: "tool_result",
tool_use_id: call.toolUseId,
call_id: call.call_id,
content: deniedResult,
})
} else {
toolResultBlocks.push({
type: "text",
text: `${toolName} Result:\n${deniedResult}`,
})
}
continue
}
const toolCallParams = toToolUseParams(call.input)
const toolCallBlock: ToolUse = {
type: "tool_use",
name: toolName,
params: toolCallParams,
partial: false,
isNativeToolCall: call.isNativeToolCall,
call_id: call.call_id || call.toolUseId,
signature: call.signature,
}
if (call.call_id) {
state.toolUseIdMap.set(call.call_id, call.toolUseId)
}
const subagentConfig = this.createSubagentTaskConfig(state)
const handler = this.baseConfig.coordinator.getHandler(toolName)
let toolResult: unknown
if (!handler) {
toolResult = formatResponse.toolError(`No handler registered for tool '${toolName}'.`)
} else {
try {
toolResult = await handler.execute(subagentConfig, toolCallBlock)
} catch (error) {
toolResult = formatResponse.toolError((error as Error).message)
}
}
stats.toolCalls += 1
onProgress({ stats: { ...stats } })
const serializedToolResult = serializeToolResult(toolResult)
if (call.isNativeToolCall) {
toolResultBlocks.push({
type: "tool_result",
tool_use_id: call.toolUseId,
call_id: call.call_id,
content: serializedToolResult,
})
} else {
const toolDescription = handler?.getDescription(toolCallBlock) || `[${toolName}]`
toolResultBlocks.push({
type: "text",
text: `${toolDescription} Result:\n${serializedToolResult}`,
})
}
}
conversation.push({
role: "user",
content: toolResultBlocks,
})
await delay(0)
}
} catch (error) {
if (this.shouldAbort()) {
const cancelledError = "Subagent run cancelled."
onProgress({ status: "failed", error: cancelledError, stats: { ...stats } })
return { status: "failed", error: cancelledError, stats }
}
const errorText = (error as Error).message || "Subagent execution failed."
Logger.error("[SubagentRunner] run failed", error)
onProgress({ status: "failed", error: errorText, stats: { ...stats } })
return { status: "failed", error: errorText, stats }
} finally {
this.activeApiAbort = undefined
}
}
private createSubagentTaskConfig(state: TaskState): TaskConfig {
const baseCallbacks = this.baseConfig.callbacks
return {
...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, {
useBackgroundExecution: true,
suppressUserInteraction: true,
})
} finally {
this.activeCommandExecutions = Math.max(0, this.activeCommandExecutions - 1)
}
},
},
}
}
private buildNativeTools(context: SystemPromptContext) {
const family = PromptRegistry.getInstance().getModelFamily(context)
const toolSets = ClineToolSet.getToolsForVariantWithFallback(family, SUBAGENT_ALLOWED_TOOLS)
const filteredToolSpecs = toolSets
.map((toolSet) => toolSet.config)
.filter((toolSpec) => !toolSpec.contextRequirements || toolSpec.contextRequirements(context))
const converter = ClineToolSet.getNativeConverter(context.providerInfo.providerId, context.providerInfo.model.id)
return filteredToolSpecs.map((tool) => converter(tool, context))
}
}
@@ -0,0 +1,373 @@
import { strict as assert } from "node:assert"
import * as coreApi from "@core/api"
import * as skills from "@core/context/instructions/user-instructions/skills"
import { PromptRegistry } from "@core/prompts/system-prompt"
import type { TaskConfig } from "@core/task/tools/types/TaskConfig"
import { afterEach, describe, it } from "mocha"
import sinon from "sinon"
import { HostProvider } from "@/hosts/host-provider"
import { ApiFormat } from "@/shared/proto/cline/models"
import { ClineDefaultTool } from "@/shared/tools"
import { TaskState } from "../../../TaskState"
import { SubagentRunner } from "../SubagentRunner"
function initializeHostProvider() {
HostProvider.reset()
HostProvider.initialize(
() => ({}) as never,
() => ({}) as never,
() => ({}) as never,
() => ({}) as never,
{
workspaceClient: {},
envClient: {
getHostVersion: async () => ({ platform: "test" }),
},
windowClient: {},
diffClient: {},
} as never,
() => undefined,
async () => "",
async () => "",
"",
"",
)
}
function createTaskConfig(nativeToolCallEnabled: boolean): TaskConfig {
return {
taskId: "task-1",
ulid: "ulid-1",
cwd: "/tmp",
mode: "act",
strictPlanModeEnabled: false,
yoloModeToggled: false,
vscodeTerminalExecutionMode: "backgroundExec",
enableParallelToolCalling: false,
isSubagentExecution: false,
context: {},
taskState: new TaskState(),
messageState: {},
api: {
getModel: () => ({
id: "anthropic/claude-sonnet-4.5",
info: {
contextWindow: 200_000,
apiFormat: ApiFormat.ANTHROPIC_CHAT,
supportsPromptCache: true,
},
}),
},
services: {
stateManager: {
getGlobalSettingsKey: (key: string) => {
if (key === "mode") {
return "act"
}
if (key === "customPrompt") {
return undefined
}
return undefined
},
getGlobalStateKey: (key: string) => (key === "nativeToolCallEnabled" ? nativeToolCallEnabled : undefined),
getApiConfiguration: () => ({
actModeApiProvider: "anthropic",
planModeApiProvider: "anthropic",
}),
},
},
browserSettings: {},
focusChainSettings: {},
autoApprovalSettings: {
enableNotifications: false,
actions: { executeSafeCommands: false, executeAllCommands: false },
},
autoApprover: { shouldAutoApproveTool: sinon.stub().returns([false, false]) },
callbacks: {
say: sinon.stub().resolves(undefined),
ask: sinon.stub().resolves({ response: "yesButtonClicked" }),
saveCheckpoint: sinon.stub().resolves(),
sayAndCreateMissingParamError: sinon.stub().resolves("missing"),
removeLastPartialMessageIfExistsWithType: sinon.stub().resolves(),
executeCommandTool: sinon.stub().resolves([false, "ok"]),
cancelRunningCommandTool: sinon.stub().resolves(false),
doesLatestTaskCompletionHaveNewChanges: sinon.stub().resolves(false),
updateFCListFromToolResponse: sinon.stub().resolves(),
shouldAutoApproveTool: sinon.stub().returns([true, true]),
shouldAutoApproveToolWithPath: sinon.stub().resolves(false),
postStateToWebview: sinon.stub().resolves(),
reinitExistingTaskFromId: sinon.stub().resolves(),
cancelTask: sinon.stub().resolves(),
updateTaskHistory: sinon.stub().resolves([]),
applyLatestBrowserSettings: sinon.stub().resolves(undefined),
switchToActMode: sinon.stub().resolves(false),
setActiveHookExecution: sinon.stub().resolves(),
clearActiveHookExecution: sinon.stub().resolves(),
getActiveHookExecution: sinon.stub().resolves(undefined),
runUserPromptSubmitHook: sinon.stub().resolves({}),
},
coordinator: {
getHandler: sinon.stub().callsFake((toolName: ClineDefaultTool) => {
if (toolName === ClineDefaultTool.LIST_FILES) {
return {
execute: sinon.stub().resolves("ok"),
getDescription: sinon.stub().returns("list_files"),
}
}
return undefined
}),
},
} as unknown as TaskConfig
}
describe("SubagentRunner", () => {
afterEach(() => {
sinon.restore()
HostProvider.reset()
})
it("emits native tool_use blocks with matching tool_result tool_use_id across turns", async () => {
const createMessage = sinon.stub()
createMessage.onFirstCall().callsFake(async function* () {
yield {
type: "tool_calls",
tool_call: {
function: {
id: "toolu_subagent_1",
name: ClineDefaultTool.LIST_FILES,
arguments: JSON.stringify({ path: ".", recursive: false }),
},
},
}
})
createMessage.onSecondCall().callsFake(async function* (_systemPrompt: string, conversation: unknown[]) {
const assistantMessage = conversation[1] as {
role: string
content: Array<{ type?: string; [key: string]: unknown }>
}
assert.equal(assistantMessage.role, "assistant")
assert.ok(Array.isArray(assistantMessage.content))
const toolUse = assistantMessage.content.find((block) => block.type === "tool_use")
assert.ok(toolUse, "assistant message should include tool_use block")
assert.equal(toolUse.id, "toolu_subagent_1")
assert.equal(toolUse.name, ClineDefaultTool.LIST_FILES)
const userMessage = conversation[2] as { role: string; content: Array<{ type?: string; [key: string]: unknown }> }
assert.equal(userMessage.role, "user")
assert.ok(Array.isArray(userMessage.content))
const toolResult = userMessage.content.find((block) => block.type === "tool_result")
assert.ok(toolResult, "user message should include tool_result block")
assert.equal(toolResult.tool_use_id, "toolu_subagent_1")
yield {
type: "text",
text: "done",
}
})
sinon.stub(PromptRegistry.getInstance(), "get").resolves("system prompt")
sinon.stub(coreApi, "buildApiHandler").returns({
abort: sinon.stub(),
getModel: () => ({
id: "anthropic/claude-sonnet-4.5",
info: {
contextWindow: 200_000,
apiFormat: ApiFormat.ANTHROPIC_CHAT,
supportsPromptCache: true,
},
}),
createMessage,
})
sinon.stub(skills, "discoverSkills").resolves([])
sinon.stub(skills, "getAvailableSkills").returns([])
initializeHostProvider()
const config = createTaskConfig(true)
const runner = new SubagentRunner(config)
sinon
.stub(runner as unknown as { buildNativeTools: () => unknown[] }, "buildNativeTools")
.returns([{ name: "list_files" }])
const result = await runner.run("List files", () => {})
assert.equal(result.status, "completed")
assert.equal(result.result, "done")
assert.equal(createMessage.callCount, 2)
})
it("falls back to non-native result blocks if structured tool calls appear while native mode is disabled", async () => {
const createMessage = sinon.stub()
createMessage.onFirstCall().callsFake(async function* () {
yield {
type: "tool_calls",
tool_call: {
function: {
id: "toolu_subagent_2",
name: ClineDefaultTool.LIST_FILES,
arguments: JSON.stringify({ path: ".", recursive: false }),
},
},
}
})
createMessage.onSecondCall().callsFake(async function* (_systemPrompt: string, conversation: unknown[]) {
const lastMessage = conversation[conversation.length - 1] as {
role: string
content: Array<{ type?: string; [key: string]: unknown }>
}
assert.equal(lastMessage.role, "user")
assert.ok(Array.isArray(lastMessage.content))
assert.ok(lastMessage.content.every((block) => block.type === "text"))
assert.equal(
lastMessage.content.some((block) => block.type === "tool_result"),
false,
)
yield {
type: "text",
text: "done",
}
})
sinon.stub(PromptRegistry.getInstance(), "get").resolves("system prompt")
sinon.stub(coreApi, "buildApiHandler").returns({
abort: sinon.stub(),
getModel: () => ({
id: "anthropic/claude-sonnet-4.5",
info: {
contextWindow: 200_000,
apiFormat: ApiFormat.ANTHROPIC_CHAT,
supportsPromptCache: true,
},
}),
createMessage,
})
sinon.stub(skills, "discoverSkills").resolves([])
sinon.stub(skills, "getAvailableSkills").returns([])
initializeHostProvider()
const config = createTaskConfig(false)
const runner = new SubagentRunner(config)
const result = await runner.run("List files", () => {})
assert.equal(result.status, "completed")
assert.equal(result.result, "done")
assert.equal(createMessage.callCount, 2)
})
it("builds subagent api handler with the parent task ulid", async () => {
const createMessage = sinon.stub().callsFake(async function* () {
yield {
type: "text",
text: "done",
}
})
sinon.stub(PromptRegistry.getInstance(), "get").resolves("system prompt")
const buildApiHandlerStub = sinon.stub(coreApi, "buildApiHandler").returns({
abort: sinon.stub(),
getModel: () => ({
id: "anthropic/claude-sonnet-4.5",
info: {
contextWindow: 200_000,
apiFormat: ApiFormat.ANTHROPIC_CHAT,
supportsPromptCache: true,
},
}),
createMessage,
})
sinon.stub(skills, "discoverSkills").resolves([])
sinon.stub(skills, "getAvailableSkills").returns([])
initializeHostProvider()
const config = createTaskConfig(true)
const runner = new SubagentRunner(config)
sinon
.stub(runner as unknown as { buildNativeTools: () => unknown[] }, "buildNativeTools")
.returns([{ name: "list_files" }])
const result = await runner.run("List files", () => {})
assert.equal(result.status, "completed")
assert.equal(buildApiHandlerStub.called, true)
sinon.assert.calledWithMatch(buildApiHandlerStub, sinon.match({ ulid: "ulid-1" }), "act")
})
it("includes workspace metadata only in the initial user message", async () => {
const createMessage = sinon.stub()
createMessage.onFirstCall().callsFake(async function* (_systemPrompt: string, conversation: unknown[]) {
const initialUser = conversation[0] as {
role: string
content: Array<{ type?: string; text?: string }>
}
assert.equal(initialUser.role, "user")
const initialTexts = initialUser.content
.filter((block) => block.type === "text")
.map((block) => block.text || "")
.join("\n")
assert.match(initialTexts, /# Workspace Configuration/)
yield {
type: "tool_calls",
tool_call: {
function: {
id: "toolu_subagent_workspace_1",
name: ClineDefaultTool.LIST_FILES,
arguments: JSON.stringify({ path: ".", recursive: false }),
},
},
}
})
createMessage.onSecondCall().callsFake(async function* (_systemPrompt: string, conversation: unknown[]) {
const followUpUser = conversation[2] as {
role: string
content: Array<{ type?: string; text?: string }>
}
assert.equal(followUpUser.role, "user")
const followUpTexts = followUpUser.content
.filter((block) => block.type === "text")
.map((block) => block.text || "")
.join("\n")
assert.equal(followUpTexts.includes("# Workspace Configuration"), false)
yield {
type: "text",
text: "done",
}
})
sinon.stub(PromptRegistry.getInstance(), "get").resolves("system prompt")
sinon.stub(coreApi, "buildApiHandler").returns({
abort: sinon.stub(),
getModel: () => ({
id: "anthropic/claude-sonnet-4.5",
info: {
contextWindow: 200_000,
apiFormat: ApiFormat.ANTHROPIC_CHAT,
supportsPromptCache: true,
},
}),
createMessage,
})
sinon.stub(skills, "discoverSkills").resolves([])
sinon.stub(skills, "getAvailableSkills").returns([])
initializeHostProvider()
const config = createTaskConfig(true)
const runner = new SubagentRunner(config)
sinon
.stub(runner as unknown as { buildNativeTools: () => unknown[] }, "buildNativeTools")
.returns([{ name: "list_files" }])
const result = await runner.run("List files", () => {})
assert.equal(result.status, "completed")
assert.equal(result.result, "done")
assert.equal(createMessage.callCount, 2)
})
})
+8 -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"
@@ -39,6 +40,7 @@ export interface TaskConfig {
doubleCheckCompletionEnabled: boolean
vscodeTerminalExecutionMode: "vscodeTerminal" | "backgroundExec"
enableParallelToolCalling: boolean
isSubagentExecution: boolean
context: vscode.ExtensionContext
// Multi-workspace support (optional for backward compatibility)
@@ -104,7 +106,12 @@ 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>
@@ -19,6 +19,7 @@ export const TASK_CONFIG_KEYS = [
"doubleCheckCompletionEnabled",
"vscodeTerminalExecutionMode",
"enableParallelToolCalling",
"isSubagentExecution",
"context",
"taskState",
"messageState",
+6 -3
View File
@@ -123,6 +123,10 @@ export class ToolResultUtils {
* Handles tool approval flow and processes any user feedback
*/
static async askApprovalAndPushFeedback(type: ClineAsk, completeMessage: string, config: TaskConfig) {
if (config.isSubagentExecution) {
return true
}
const { response, text, images, files } = await config.callbacks.ask(type, completeMessage, false)
if (text || (images && images.length > 0) || (files && files.length > 0)) {
@@ -139,9 +143,8 @@ export class ToolResultUtils {
// User pressed reject button or responded with a message, which we treat as a rejection
config.taskState.didRejectTool = true // Prevent further tool uses in this message
return false
} else {
// User hit the approve button, and may have provided feedback
return true
}
// User hit the approve button, and may have provided feedback
return true
}
}
@@ -100,11 +100,10 @@ export class VscodeTerminalManager implements ITerminalManager {
private terminalIds: Set<number> = new Set()
private processes: Map<number, VscodeTerminalProcess> = new Map()
private disposables: vscode.Disposable[] = []
private shellIntegrationTimeout: number = 4000
private terminalReuseEnabled: boolean = true
private terminalOutputLineLimit: number = 500
private subagentTerminalOutputLineLimit: number = 2000
private defaultTerminalProfile: string = "default"
private shellIntegrationTimeout = 4000
private terminalReuseEnabled = true
private terminalOutputLineLimit = 500
private defaultTerminalProfile = "default"
constructor() {
let disposable: vscode.Disposable | undefined
@@ -364,16 +363,8 @@ export class VscodeTerminalManager implements ITerminalManager {
this.terminalOutputLineLimit = limit
}
setSubagentTerminalOutputLineLimit(limit: number): void {
this.subagentTerminalOutputLineLimit = limit
}
public processOutput(outputLines: string[], overrideLimit?: number, isSubagentCommand?: boolean): string {
const limit = isSubagentCommand
? overrideLimit !== undefined
? overrideLimit
: this.subagentTerminalOutputLineLimit
: this.terminalOutputLineLimit
public processOutput(outputLines: string[], overrideLimit?: number): string {
const limit = overrideLimit !== undefined ? overrideLimit : this.terminalOutputLineLimit
if (outputLines.length > limit) {
const halfLimit = Math.floor(limit / 2)
const start = outputLines.slice(0, halfLimit)
@@ -425,7 +416,7 @@ export class VscodeTerminalManager implements ITerminalManager {
* @param force If true, closes even busy terminals (with warning)
* @returns Number of terminals closed
*/
closeTerminals(filterFn: (terminal: TerminalInfo) => boolean, force: boolean = false): number {
closeTerminals(filterFn: (terminal: TerminalInfo) => boolean, force = false): number {
const terminalsToClose = this.filterTerminals(filterFn)
let closedCount = 0
@@ -1,78 +0,0 @@
/**
* Pattern to match simplified Cline CLI syntax: cline "prompt" or cline 'prompt'
* with optional additional flags after the closing quote
*/
const CLINE_COMMAND_PATTERN = /^cline\s+(['"])(.+?)\1(\s+.*)?$/
/**
* Detects if a command is a Cline CLI subagent command.
*
* Matches the simplified syntax: cline "prompt" or cline 'prompt'
* This allows the system to apply subagent-specific settings like autonomous execution.
*
* @param command - The command string to check
* @returns True if the command is a Cline CLI subagent command, false otherwise
*/
export function isSubagentCommand(command: string): boolean {
// Match simplified syntaxes
// cline "prompt"
// cline 'prompt'
return CLINE_COMMAND_PATTERN.test(command)
}
/**
* Transforms simplified Cline CLI command syntax with subagent settings.
*
* Converts: cline "prompt" or cline 'prompt'
* To: cline "prompt" --json -y
*
* Preserves additional flags like --cwd:
* cline "prompt" --cwd ./path cline "prompt" --json -y --cwd ./path
*
* This enables autonomous subagent execution with proper CLI flags for automation.
*
* @param command - The command string to potentially transform
* @returns The transformed command if it matches the pattern, otherwise the original command
*/
export function transformClineCommand(command: string): string {
if (!isSubagentCommand(command)) {
return command
}
// Inject subagent-specific command structure and settings
const commandWithSettings = injectSubagentSettings(command)
return commandWithSettings
}
/**
* Injects subagent-specific command structure and settings into Cline CLI commands.
*
* @param command - The Cline CLI command (simplified or full syntax)
* @returns The command with injected flags and settings
*/
function injectSubagentSettings(command: string): string {
// No pre-prompt flags needed - use standard "cline 'prompt'" syntax
const prePromptFlags: string[] = []
// Flags/settings to insert after the prompt
const postPromptFlags = ["--json", "-y"]
const match = command.match(CLINE_COMMAND_PATTERN)
if (match) {
const quote = match[1]
const prompt = match[2]
const additionalFlags = match[3] || ""
const prePromptPart = prePromptFlags.length > 0 ? prePromptFlags.join(" ") + " " : ""
return `cline ${prePromptPart}${quote}${prompt}${quote} ${postPromptFlags.join(" ")}${additionalFlags}`
}
// Already full format: just inject settings after prompt
const parts = command.split(" ")
const promptEndIndex = parts.findIndex((p) => p.endsWith('"') || p.endsWith("'"))
if (promptEndIndex !== -1) {
parts.splice(promptEndIndex + 1, 0, ...postPromptFlags)
}
return parts.join(" ")
}
+14 -30
View File
@@ -9,20 +9,17 @@
* - VscodeTerminalManager VscodeTerminalProcess (shell integration)
* - StandaloneTerminalManager StandaloneTerminalProcess (child_process)
*
* IMPORTANT: Subagent commands (cline CLI) are ALWAYS routed to use
* StandaloneTerminalManager regardless of the configured mode. This ensures
* subagents run in hidden/background terminals rather than cluttering the
* user's visible VSCode terminal.
* IMPORTANT: Background execution mode uses StandaloneTerminalManager to run
* commands in hidden terminals without cluttering the visible terminal.
*/
import { isSubagentCommand, transformClineCommand } from "@integrations/cli-subagents/subagent_command"
import { telemetryService } from "@services/telemetry"
import { findLastIndex } from "@shared/array"
import { ClineToolResponseContent } from "@shared/messages"
import { Logger } from "@/shared/services/Logger"
import { orchestrateCommandExecution } from "./CommandOrchestrator"
import { StandaloneTerminalManager } from "./standalone/StandaloneTerminalManager"
import type {
CommandExecutionOptions,
CommandExecutorCallbacks,
CommandExecutorConfig,
ITerminalManager,
@@ -73,10 +70,9 @@ export class CommandExecutor {
this.standaloneManager = config.terminalManager
Logger.info(`[CommandExecutor] Reusing Task's StandaloneTerminalManager for backgroundExec mode`)
} else {
// Create new StandaloneTerminalManager for subagents (even in VSCode mode)
// This ensures subagents run in hidden terminals, not cluttering the user's VSCode terminal
// Create a standalone manager for background execution support.
this.standaloneManager = new StandaloneTerminalManager()
Logger.info(`[CommandExecutor] Created new StandaloneTerminalManager for subagents`)
Logger.info(`[CommandExecutor] Created new StandaloneTerminalManager`)
// Copy settings from the provided terminalManager to ensure consistency
if ("shellIntegrationTimeout" in config.terminalManager) {
@@ -84,7 +80,6 @@ export class CommandExecutor {
this.standaloneManager.setShellIntegrationTimeout(tm.shellIntegrationTimeout || 4000)
this.standaloneManager.setTerminalReuseEnabled(tm.terminalReuseEnabled ?? true)
this.standaloneManager.setTerminalOutputLineLimit(tm.terminalOutputLineLimit || 500)
this.standaloneManager.setSubagentTerminalOutputLineLimit(tm.subagentTerminalOutputLineLimit || 2000)
}
}
}
@@ -93,32 +88,26 @@ export class CommandExecutor {
* Execute a command in the terminal.
*
* Routing logic:
* 1. Subagent commands (cline CLI) Always use StandaloneTerminalManager
* This ensures subagents run in hidden terminals, not cluttering the user's VSCode terminal
* 2. Regular commands Use the configured terminal manager based on terminalExecutionMode
* 1. Background mode commands use StandaloneTerminalManager
* 2. Regular commands use the configured terminal manager
*
* @param command The command to execute
* @param timeoutSeconds Optional timeout in seconds
* @returns [userRejected, result] tuple
*/
async execute(command: string, timeoutSeconds: number | undefined): Promise<[boolean, ClineToolResponseContent]> {
// Transform subagent commands to ensure flags are correct
const isSubagent = isSubagentCommand(command)
if (isSubagent) {
command = transformClineCommand(command)
}
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)) {
command = command.substring(workspaceCdPrefix.length)
}
const subAgentStartTime = isSubagent ? performance.now() : 0
// Select the appropriate terminal manager
// Subagents always use standalone manager (hidden terminal)
const useStandalone = isSubagent || 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}`)
@@ -141,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
@@ -154,12 +144,6 @@ export class CommandExecutor {
terminalType: useStandalone ? "standalone" : "vscode",
})
// Capture subagent telemetry
if (isSubagent && subAgentStartTime > 0) {
const durationMs = Math.round(performance.now() - subAgentStartTime)
telemetryService.captureSubagentExecution(this.ulid, durationMs, result.outputLines.length, result.completed)
}
// If the command was cancelled externally (via cancel button), return a clear cancellation message
// This ensures the AI agent knows the command was cancelled by the user
if (this.wasCancelledExternally) {
@@ -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) {
-3
View File
@@ -68,9 +68,6 @@ export const TRUNCATE_KEEP_LINES = 100
/** Default max lines for command output */
export const DEFAULT_TERMINAL_OUTPUT_LINE_LIMIT = 500
/** Max lines for subagent commands (more context needed) */
export const DEFAULT_SUBAGENT_TERMINAL_OUTPUT_LINE_LIMIT = 2000
// =============================================================================
// Background Command Tracking
// =============================================================================
+1
View File
@@ -36,6 +36,7 @@ export type {
// Command Executor types
ActiveBackgroundCommand,
AskResponse,
CommandExecutionOptions,
CommandExecutorCallbacks,
CommandExecutorConfig,
FullCommandExecutorConfig,
@@ -14,11 +14,7 @@
import { ClineTempManager } from "@services/temp"
import * as fs from "fs"
import {
BACKGROUND_COMMAND_TIMEOUT_MS,
DEFAULT_SUBAGENT_TERMINAL_OUTPUT_LINE_LIMIT,
DEFAULT_TERMINAL_OUTPUT_LINE_LIMIT,
} from "../constants"
import { BACKGROUND_COMMAND_TIMEOUT_MS, DEFAULT_TERMINAL_OUTPUT_LINE_LIMIT } from "../constants"
import type { BackgroundCommand, ITerminalManager, TerminalInfo, TerminalProcessResultPromise } from "../types"
import { StandaloneTerminalProcess } from "./StandaloneTerminalProcess"
import { StandaloneTerminalRegistry } from "./StandaloneTerminalRegistry"
@@ -81,9 +77,6 @@ export class StandaloneTerminalManager implements ITerminalManager {
/** Maximum output lines to keep */
private terminalOutputLineLimit: number = DEFAULT_TERMINAL_OUTPUT_LINE_LIMIT
/** Maximum output lines for subagent commands */
private subagentTerminalOutputLineLimit: number = DEFAULT_SUBAGENT_TERMINAL_OUTPUT_LINE_LIMIT
/** Default terminal profile */
private defaultTerminalProfile = "default"
@@ -227,15 +220,10 @@ export class StandaloneTerminalManager implements ITerminalManager {
* Process output lines, potentially truncating if over limit.
* @param outputLines Array of output lines
* @param overrideLimit Optional limit override
* @param isSubagentCommand Whether this is a subagent command
* @returns Processed output string
*/
processOutput(outputLines: string[], overrideLimit?: number, isSubagentCommand?: boolean): string {
const limit = isSubagentCommand
? overrideLimit !== undefined
? overrideLimit
: this.subagentTerminalOutputLineLimit
: this.terminalOutputLineLimit
processOutput(outputLines: string[], overrideLimit?: number): string {
const limit = overrideLimit !== undefined ? overrideLimit : this.terminalOutputLineLimit
if (outputLines.length > limit) {
const halfLimit = Math.floor(limit / 2)
const start = outputLines.slice(0, halfLimit)
@@ -295,14 +283,6 @@ export class StandaloneTerminalManager implements ITerminalManager {
this.terminalOutputLineLimit = limit
}
/**
* Set the maximum number of output lines for subagent commands.
* @param limit Maximum number of lines
*/
setSubagentTerminalOutputLineLimit(limit: number): void {
this.subagentTerminalOutputLineLimit = limit
}
/**
* Set the default terminal profile.
* @param profile The profile identifier
+22 -8
View File
@@ -223,12 +223,6 @@ export interface ITerminalManager {
*/
setTerminalOutputLineLimit(limit: number): void
/**
* Set the maximum number of output lines for subagent commands.
* @param limit Maximum number of lines
*/
setSubagentTerminalOutputLineLimit(limit: number): void
/**
* Set the default terminal profile.
* @param profile The profile identifier
@@ -239,10 +233,9 @@ export interface ITerminalManager {
* Process output lines, potentially truncating if over limit.
* @param outputLines Array of output lines
* @param overrideLimit Optional limit override
* @param isSubagentCommand Whether this is a subagent command
* @returns Processed output string
*/
processOutput(outputLines: string[], overrideLimit?: number, isSubagentCommand?: boolean): string
processOutput(outputLines: string[], overrideLimit?: number): string
}
/**
@@ -348,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
*/
@@ -395,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
}
/**
+50 -3
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
@@ -65,7 +64,6 @@ export interface ExtensionState {
terminalReuseEnabled?: boolean
terminalOutputLineLimit: number
maxConsecutiveMistakes: number
subagentTerminalOutputLineLimit: number
defaultTerminalProfile?: string
vscodeTerminalExecutionMode: string
backgroundCommandRunning?: boolean
@@ -87,6 +85,7 @@ export interface ExtensionState {
strictPlanModeEnabled?: boolean
yoloModeToggled?: boolean
useAutoCondense?: boolean
subagentsEnabled?: boolean
clineWebToolsEnabled?: ClineFeatureSetting
worktreesEnabled?: ClineFeatureSetting
focusChainSettings: FocusChainSettings
@@ -105,7 +104,6 @@ export interface ExtensionState {
dismissedBanners?: Array<{ bannerId: string; dismissedAt: number }>
hooksEnabled?: boolean
remoteConfigSettings?: Partial<RemoteConfigFields>
subagentsEnabled?: boolean
globalSkillsToggles?: Record<string, boolean>
localSkillsToggles?: Record<string, boolean>
nativeToolCallSetting?: boolean
@@ -154,6 +152,7 @@ export type ClineAsk =
| "condense"
| "summarize_task"
| "report_bug"
| "use_subagents"
export type ClineSay =
| "task"
@@ -190,6 +189,9 @@ export type ClineSay =
| "task_progress"
| "hook_status"
| "hook_output_stream"
| "subagent"
| "use_subagents"
| "subagent_usage"
| "conditional_rules_applied"
export interface ClineSayTool {
@@ -269,6 +271,38 @@ export interface ClineSayGenerateExplanation {
error?: string
}
export type SubagentExecutionStatus = "pending" | "running" | "completed" | "failed"
export interface SubagentStatusItem {
index: number
prompt: string
status: SubagentExecutionStatus
toolCalls: number
inputTokens: number
outputTokens: number
totalCost: number
contextTokens: number
contextWindow: number
contextUsagePercentage: number
result?: string
error?: string
}
export interface ClineSaySubagentStatus {
status: "running" | "completed" | "failed"
total: number
completed: number
successes: number
failures: number
toolCalls: number
inputTokens: number
outputTokens: number
contextWindow: number
maxContextTokens: number
maxContextUsagePercentage: number
items: SubagentStatusItem[]
}
export type BrowserActionResult = {
screenshot?: string
logs?: string
@@ -284,6 +318,10 @@ export interface ClineAskUseMcpServer {
uri?: string
}
export interface ClineAskUseSubagents {
prompts: string[]
}
export interface ClinePlanModeResponse {
response: string
options?: string[]
@@ -317,6 +355,15 @@ export interface ClineApiReqInfo {
}
}
export interface ClineSubagentUsageInfo {
source: "subagents"
tokensIn: number
tokensOut: number
cacheWrites: number
cacheReads: number
cost: number
}
export type ClineApiReqCancelReason = "streaming_failed" | "user_cancelled" | "retries_exhausted"
export const COMPLETION_RESULT_CHANGES_FLAG = "HAS_CHANGES"

Some files were not shown because too many files have changed in this diff Show More