feat: checkpoint subagent tool workflow and approval UX

This commit is contained in:
Saoud Rizwan
2026-02-09 17:31:34 -08:00
parent 6e253dfa9a
commit 7a725934d3
84 changed files with 1530 additions and 819 deletions
+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)
+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") {
+3 -7
View File
@@ -101,15 +101,13 @@ message Secrets {
optional string oca_refresh_token = 42;
optional string mcp_o_auth_secrets = 43;
optional string cline_api_key = 44;
optional string openai_codex_oauth_credentials = 46;
optional string openai_codex_oauth_credentials = 47;
}
// NOTE: Add new fields under API_HANDLER_SETTINGS_FIELDS or USER_SETTINGS_FIELDS
// in src/shared/storage/state-keys.ts and use the scripts/generate-state-proto.mjs
// script to regenerate this list.
message Settings {
reserved 146; // was openai_reasoning_effort (moved to mode-scoped reasoning effort)
optional string lite_llm_base_url = 1;
optional bool lite_llm_use_prompt_cache = 2;
optional string anthropic_base_url = 4;
@@ -248,7 +246,6 @@ message Settings {
optional string default_terminal_profile = 137;
optional int32 terminal_output_line_limit = 138;
optional int32 max_consecutive_mistakes = 139;
optional int32 subagent_terminal_output_line_limit = 140;
optional bool strict_plan_mode_enabled = 141;
optional bool yolo_mode_toggled = 142;
optional bool use_auto_condense = 143;
@@ -259,7 +256,6 @@ message Settings {
optional FocusChainSettings focus_chain_settings = 149;
optional string custom_prompt = 150;
optional double auto_condense_threshold = 151;
optional bool subagents_enabled = 153;
optional bool enable_parallel_tool_calling = 154;
optional bool background_edit_enabled = 155;
optional bool opt_out_of_remote_config = 157;
@@ -390,6 +386,8 @@ message UpdateSettingsRequest {
reserved 15; // was openai_reasoning_effort (moved to mode-scoped reasoning effort)
reserved 26; // was hooks_enabled (removed - now always enabled on macOS/Linux)
reserved 38; // was skills_enabled (removed - now always enabled)
reserved 29; // was subagents_enabled
reserved 30; // was subagent_terminal_output_line_limit
Metadata metadata = 1;
optional ModelsApiConfiguration api_configuration = 2;
@@ -416,8 +414,6 @@ message UpdateSettingsRequest {
optional bool multi_root_enabled = 25;
optional string vscode_terminal_execution_mode = 27;
optional int32 max_consecutive_mistakes = 28;
optional bool subagents_enabled = 29;
optional int32 subagent_terminal_output_line_limit = 30;
optional string cline_env = 31;
optional bool native_tool_call_enabled = 32;
optional OnboardingModelGroup onboarding_models = 33;
+3
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,8 @@ enum ClineSay {
HOOK_OUTPUT_STREAM = 31;
COMMAND_PERMISSION_DENIED = 32;
CONDITIONAL_RULES_APPLIED = 33;
SUBAGENT_STATUS = 34;
USE_SUBAGENTS_SAY = 35;
}
// 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]
-5
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,
@@ -883,7 +881,6 @@ 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
@@ -974,7 +971,6 @@ export class Controller {
mcpResponsesCollapsed,
terminalOutputLineLimit,
maxConsecutiveMistakes,
subagentTerminalOutputLineLimit,
customPrompt,
taskHistory: processedTaskHistory,
shouldShowAnnouncement,
@@ -1004,7 +1000,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",
)
})
})
@@ -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))
@@ -314,25 +298,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) {
-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.
@@ -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.",
"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": {
@@ -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.",
"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.",
"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": {
@@ -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.",
"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",
@@ -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.",
contextRequirements: (context) => Boolean(context.enableNativeToolCalls) && !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]
+1
View File
@@ -120,6 +120,7 @@ export interface SystemPromptContext {
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}`)
}
+5
View File
@@ -39,6 +39,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"
@@ -117,6 +118,7 @@ export class ToolExecutor {
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 cancelRunningCommandTool: () => Promise<boolean>,
private doesLatestTaskCompletionHaveNewChanges: () => Promise<boolean>,
private updateFCListFromToolResponse: (taskProgress: string | undefined) => Promise<void>,
private switchToActMode: () => Promise<boolean>,
@@ -151,6 +153,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 +184,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 +242,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())
}
/**
+2 -20
View File
@@ -98,7 +98,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 +125,6 @@ type TaskParams = {
shellIntegrationTimeout: number
terminalReuseEnabled: boolean
terminalOutputLineLimit: number
subagentTerminalOutputLineLimit: number
defaultTerminalProfile: string
vscodeTerminalExecutionMode: "vscodeTerminal" | "backgroundExec"
cwd: string
@@ -261,7 +259,6 @@ export class Task {
shellIntegrationTimeout,
terminalReuseEnabled,
terminalOutputLineLimit,
subagentTerminalOutputLineLimit,
defaultTerminalProfile,
vscodeTerminalExecutionMode,
cwd,
@@ -303,7 +300,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 +548,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),
@@ -1757,14 +1754,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 +1797,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)
@@ -1860,8 +1843,7 @@ export class Task {
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,224 @@
import type { ToolUse } from "@core/assistant-message"
import { formatResponse } from "@core/prompts/responses"
import { ClineAskUseSubagents, ClineSaySubagentStatus, 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 "[subagent batch]"
}
async handlePartialBlock(_block: ToolUse, _uiHelpers: StronglyTypedUIHelpers): Promise<void> {
return
}
async execute(config: TaskConfig, block: ToolUse): Promise<ToolResponse> {
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) {
await config.callbacks.say("use_subagents", approvalBody, undefined, undefined, false)
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,
}))
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 payload: ClineSaySubagentStatus = {
status,
total: entries.length,
completed,
successes,
failures,
toolCalls,
inputTokens,
outputTokens,
items: entries,
}
await config.callbacks.say("subagent", JSON.stringify(payload), undefined, undefined, partial)
}
await config.callbacks.removeLastPartialMessageIfExistsWithType("say", "subagent")
await emitStatus("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
}
await emitStatus("running", true)
}),
)
const settled = await Promise.allSettled(execution)
clearInterval(abortPollInterval)
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
})
const failures = entries.filter((entry) => entry.status === "failed").length
await emitStatus(failures > 0 ? "failed" : "completed", false)
const successCount = entries.length - failures
const totalToolCalls = entries.reduce((acc, entry) => acc + (entry.toolCalls || 0), 0)
const totalInputTokens = entries.reduce((acc, entry) => acc + (entry.inputTokens || 0), 0)
const totalOutputTokens = entries.reduce((acc, entry) => acc + (entry.outputTokens || 0), 0)
const summary = [
`Subagent batch complete.`,
`Total: ${entries.length}`,
`Succeeded: ${successCount}`,
`Failed: ${failures}`,
`Tool calls: ${totalToolCalls}`,
`Input tokens: ${totalInputTokens}`,
`Output tokens: ${totalOutputTokens}`,
"",
...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,263 @@
import { strict as assert } from "node:assert"
import { setTimeout as delay } from "node:timers/promises"
import { afterEach, describe, it } from "mocha"
import sinon from "sinon"
import { TaskState } from "../../../TaskState"
import { SubagentRunner } from "../../subagent/SubagentRunner"
import { UseSubagentsToolHandler } from "../SubagentToolHandler"
function createConfig(options?: {
autoApproveSafe?: boolean
autoApproveAll?: boolean
taskAskResponse?: "yesButtonClicked" | "noButtonClicked"
}) {
const taskState = new TaskState()
const askResponse = options?.taskAskResponse ?? "yesButtonClicked"
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: any = {
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
}
return undefined
},
getApiConfiguration: () => ({
planModeApiProvider: "openai",
actModeApiProvider: "openai",
}),
},
mcpHub: {},
},
callbacks,
coordinator: {
getHandler: sinon.stub(),
},
}
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: "use_subagents" as any,
params: {},
partial: false,
})
assert.equal(result, "missing")
assert.equal(taskState.consecutiveMistakeCount, 1)
sinon.assert.calledOnce(callbacks.sayAndCreateMissingParamError)
})
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: "use_subagents" as any,
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,
},
})
const handler = new UseSubagentsToolHandler()
await handler.execute(config, {
type: "tool_use",
name: "use_subagents" as any,
params: {
prompt_1: "one",
},
partial: false,
})
sinon.assert.notCalled(callbacks.ask)
const approvalSayCalls = callbacks.say.getCalls().filter((call: any) => call.args[0] === "use_subagents")
assert.ok(approvalSayCalls.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: any) => {
activeRuns++
maxActiveRuns = Math.max(maxActiveRuns, activeRuns)
onProgress({
status: "running",
stats: { toolCalls: 0, inputTokens: 0, outputTokens: 0, cacheWriteTokens: 0, cacheReadTokens: 0 },
})
await delay(10)
activeRuns--
return {
status: "completed",
result: "done",
stats: {
toolCalls: 1,
inputTokens: 2,
outputTokens: 3,
cacheWriteTokens: 0,
cacheReadTokens: 0,
},
}
})
const handler = new UseSubagentsToolHandler()
const result = await handler.execute(config, {
type: "tool_use",
name: "use_subagents" as any,
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: any) => call.args[0] === "subagent")
assert.ok(subagentStatusCalls.length >= 2)
const finalCall = subagentStatusCalls[subagentStatusCalls.length - 1]
assert.equal(finalCall.args[4], false)
})
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,
},
}
}
return {
status: "completed",
result: "ok",
stats: {
toolCalls: 2,
inputTokens: 0,
outputTokens: 0,
cacheWriteTokens: 0,
cacheReadTokens: 0,
},
}
})
const handler = new UseSubagentsToolHandler()
const result = await handler.execute(config, {
type: "tool_use",
name: "use_subagents" as any,
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,408 @@
import { setTimeout as delay } from "node:timers/promises"
import { buildApiHandler } from "@core/api"
import { 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 { ClineStorageMessage, ClineTextContentBlock } from "@shared/messages"
import { Logger } from "@shared/services/Logger"
import { ClineDefaultTool } from "@shared/tools"
import { HostProvider } from "@/hosts/host-provider"
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
}
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.`
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 "{}"
}
}
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
}
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,
}
onProgress({ status: "running", stats })
try {
const mode = this.baseConfig.services.stateManager.getGlobalSettingsKey("mode")
const apiConfiguration = this.baseConfig.services.stateManager.getApiConfiguration()
const api = buildApiHandler(apiConfiguration, 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"),
}
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: true,
enableParallelToolCalling: false,
isSubagentRun: true,
}
const promptRegistry = PromptRegistry.getInstance()
const systemPrompt = (await promptRegistry.get(context)) + SUBAGENT_SYSTEM_SUFFIX
const nativeTools = this.buildNativeTools(context)
if (!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,
],
},
]
while (true) {
const streamHandler = new StreamResponseHandler()
const { toolUseHandler } = streamHandler.getHandlers()
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
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 finalizedToolCalls = toolUseHandler.getAllFinalizedToolUses()
const assistantContent = [] as any[]
if (assistantText.trim().length > 0) {
assistantContent.push({
type: "text",
text: assistantText,
signature: assistantTextSignature,
})
}
assistantContent.push(...finalizedToolCalls)
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.`)
toolResultBlocks.push({
type: "tool_result",
tool_use_id: call.id || call.call_id,
call_id: call.call_id,
content: deniedResult,
})
continue
}
const toolCallParams = toToolUseParams(call.input)
const toolCallBlock: ToolUse = {
type: "tool_use",
name: toolName,
params: toolCallParams,
partial: false,
isNativeToolCall: true,
call_id: call.call_id,
signature: call.signature,
}
if (call.call_id && call.id) {
state.toolUseIdMap.set(call.call_id, call.id)
}
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 } })
toolResultBlocks.push({
type: "tool_result",
tool_use_id: call.id || call.call_id,
call_id: call.call_id,
content: serializeToolResult(toolResult),
})
}
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,
callbacks: {
...baseCallbacks,
executeCommandTool: async (command: string, timeoutSeconds: number | undefined) => {
this.activeCommandExecutions += 1
try {
return await baseCallbacks.executeCommandTool(command, timeoutSeconds)
} 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))
}
}
+2
View File
@@ -39,6 +39,7 @@ export interface TaskConfig {
doubleCheckCompletionEnabled: boolean
vscodeTerminalExecutionMode: "vscodeTerminal" | "backgroundExec"
enableParallelToolCalling: boolean
isSubagentExecution: boolean
context: vscode.ExtensionContext
// Multi-workspace support (optional for backward compatibility)
@@ -105,6 +106,7 @@ export interface TaskCallbacks {
removeLastPartialMessageIfExistsWithType: (type: "ask" | "say", askOrSay: ClineAsk | ClineSay) => Promise<void>
executeCommandTool: (command: string, timeoutSeconds: number | undefined) => 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(" ")
}
+7 -29
View File
@@ -9,14 +9,10 @@
* - 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"
@@ -73,10 +69,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 +79,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 +87,22 @@ 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)
}
// 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 = this.terminalExecutionMode === "backgroundExec"
const manager = useStandalone ? this.standaloneManager : this.terminalManager
Logger.info(`Executing command in ${useStandalone ? "standalone" : "VSCode"} terminal: ${command}`)
@@ -154,12 +138,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) {
-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
// =============================================================================
@@ -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
+1 -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
}
/**
+32 -2
View File
@@ -65,7 +65,6 @@ export interface ExtensionState {
terminalReuseEnabled?: boolean
terminalOutputLineLimit: number
maxConsecutiveMistakes: number
subagentTerminalOutputLineLimit: number
defaultTerminalProfile?: string
vscodeTerminalExecutionMode: string
backgroundCommandRunning?: boolean
@@ -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,8 @@ export type ClineSay =
| "task_progress"
| "hook_status"
| "hook_output_stream"
| "subagent"
| "use_subagents"
| "conditional_rules_applied"
export interface ClineSayTool {
@@ -269,6 +270,31 @@ 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
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
items: SubagentStatusItem[]
}
export type BrowserActionResult = {
screenshot?: string
logs?: string
@@ -284,6 +310,10 @@ export interface ClineAskUseMcpServer {
uri?: string
}
export interface ClineAskUseSubagents {
prompts: string[]
}
export interface ClinePlanModeResponse {
response: string
options?: string[]
@@ -25,6 +25,7 @@ function convertClineAskToProtoEnum(ask: AppClineAsk | undefined): ClineAsk | un
condense: ClineAsk.CONDENSE,
summarize_task: ClineAsk.SUMMARIZE_TASK,
report_bug: ClineAsk.REPORT_BUG,
use_subagents: ClineAsk.USE_SUBAGENTS,
}
const result = mapping[ask]
@@ -57,6 +58,7 @@ function convertProtoEnumToClineAsk(ask: ClineAsk): AppClineAsk | undefined {
[ClineAsk.CONDENSE]: "condense",
[ClineAsk.SUMMARIZE_TASK]: "summarize_task",
[ClineAsk.REPORT_BUG]: "report_bug",
[ClineAsk.USE_SUBAGENTS]: "use_subagents",
}
return mapping[ask]
@@ -103,6 +105,8 @@ function convertClineSayToProtoEnum(say: AppClineSay | undefined): ClineSay | un
hook_status: ClineSay.HOOK_STATUS,
hook_output_stream: ClineSay.HOOK_OUTPUT_STREAM,
conditional_rules_applied: ClineSay.CONDITIONAL_RULES_APPLIED,
subagent: ClineSay.SUBAGENT_STATUS,
use_subagents: ClineSay.USE_SUBAGENTS_SAY,
generate_explanation: ClineSay.GENERATE_EXPLANATION,
}
@@ -152,6 +156,8 @@ function convertProtoEnumToClineSay(say: ClineSay): AppClineSay | undefined {
[ClineSay.HOOK_STATUS]: "hook_status",
[ClineSay.HOOK_OUTPUT_STREAM]: "hook_output_stream",
[ClineSay.CONDITIONAL_RULES_APPLIED]: "conditional_rules_applied",
[ClineSay.SUBAGENT_STATUS]: "subagent",
[ClineSay.USE_SUBAGENTS_SAY]: "use_subagents",
}
return mapping[say]
-6
View File
@@ -24,12 +24,6 @@ export const BASE_SLASH_COMMANDS: SlashCommand[] = [
section: "default",
cliCompatible: true,
},
{
name: "subagent",
description: "Invoke a Cline CLI subagent for focused research tasks",
section: "default",
cliCompatible: true,
},
{
name: "newrule",
description: "Create a new Cline rule based on your conversation",
-2
View File
@@ -251,7 +251,6 @@ const USER_SETTINGS_FIELDS = {
defaultTerminalProfile: { default: "default" as string },
terminalOutputLineLimit: { default: 500 as number },
maxConsecutiveMistakes: { default: 3 as number },
subagentTerminalOutputLineLimit: { default: 2000 as number },
strictPlanModeEnabled: { default: false as boolean },
yoloModeToggled: { default: false as boolean },
autoApproveAllToggled: { default: false as boolean },
@@ -267,7 +266,6 @@ const USER_SETTINGS_FIELDS = {
focusChainSettings: { default: DEFAULT_FOCUS_CHAIN_SETTINGS as FocusChainSettings },
customPrompt: { default: undefined as "compact" | undefined },
autoCondenseThreshold: { default: 0.75 as number }, // number from 0 to 1
subagentsEnabled: { default: false as boolean },
enableParallelToolCalling: { default: true as boolean },
backgroundEditEnabled: { default: false as boolean },
optOutOfRemoteConfig: { default: false as boolean },
+2
View File
@@ -32,6 +32,7 @@ export enum ClineDefaultTool {
APPLY_PATCH = "apply_patch",
GENERATE_EXPLANATION = "generate_explanation",
USE_SKILL = "use_skill",
USE_SUBAGENTS = "use_subagents",
}
// Array of all tool names for compatibility
@@ -50,4 +51,5 @@ export const READ_ONLY_TOOLS = [
ClineDefaultTool.WEB_SEARCH,
ClineDefaultTool.WEB_FETCH,
ClineDefaultTool.USE_SKILL,
ClineDefaultTool.USE_SUBAGENTS,
] as const
+6
View File
@@ -59,6 +59,12 @@ describe("getAvailableSlashCommands", () => {
}
})
it("should not include the deprecated subagent slash command", async () => {
const response = await getAvailableSlashCommands(mockController as Controller, EmptyRequest.create())
const deprecatedCommand = response.commands.find((cmd) => cmd.name === "subagent")
;(deprecatedCommand === undefined).should.be.true()
})
it("should mark base commands with section 'default'", async () => {
const response = await getAvailableSlashCommands(mockController as Controller, EmptyRequest.create())
-25
View File
@@ -3,16 +3,6 @@ import { promisify } from "util"
const execAsync = promisify(exec)
/**
* Parameters used to detect CLI subagent context
*/
interface CliSubagentDetectionParams {
yoloModeToggled: boolean
maxConsecutiveMistakes: number
isOneshot?: boolean
outputFormat?: string
}
/**
* Check if the Cline CLI tool is installed on the system
* @returns true if CLI is installed, false otherwise
@@ -34,18 +24,3 @@ export async function isClineCliInstalled(): Promise<boolean> {
return false
}
}
/**
* Detect if the current Cline instance is running as a CLI subagent.
* CLI subagents are identified by specific parameter patterns set by the transformClineCommand function.
* TODO - For now we are relying on the maxConsecutiveMistakes value, which will only ever be "3"
* unless users pass in "-s max_consecutive_mistakes=6" via Cline CLI. Would like better detection.
* @param params The current task parameters to analyze
* @returns true if this appears to be a CLI subagent context
*/
export function isCliSubagentContext(params: CliSubagentDetectionParams): boolean {
const hasYoloMode = params.yoloModeToggled === true
const hasHighMistakeLimit = params.maxConsecutiveMistakes === 6
return hasYoloMode && hasHighMistakeLimit
}
+13 -5
View File
@@ -61,6 +61,8 @@ import QuoteButton from "./QuoteButton"
import ReportBugPreview from "./ReportBugPreview"
import { RequestStartRow } from "./RequestStartRow"
import SearchResultsDisplay from "./SearchResultsDisplay"
import SubagentApprovalRow from "./SubagentApprovalRow"
import SubagentStatusRow from "./SubagentStatusRow"
import { ThinkingRow } from "./ThinkingRow"
import UserMessage from "./UserMessage"
@@ -113,7 +115,7 @@ const ChatRow = memo(
// NOTE: it's important we don't distinguish between partial or complete here since our scroll effects in chatview need to handle height change during partial -> complete
const isInitialRender = prevHeightRef.current === 0 // prevents scrolling when new element is added since we already scroll for that
// height starts off at Infinity
if (isLast && height !== 0 && height !== Infinity && height !== prevHeightRef.current) {
if (isLast && height !== 0 && height !== Number.POSITIVE_INFINITY && height !== prevHeightRef.current) {
if (!isInitialRender) {
onHeightChange(height > prevHeightRef.current)
}
@@ -421,7 +423,8 @@ export const ChatRowContent = memo(
marginBottom: "-1.5px",
transform: rotation ? `rotate(${rotation}deg)` : undefined,
}}
title={title}></span>
title={title}
/>
)
switch (tool.tool) {
@@ -763,6 +766,10 @@ export const ChatRowContent = memo(
)
}
if (message.ask === "use_subagents" || message.say === "use_subagents") {
return <SubagentApprovalRow message={message} />
}
if (message.ask === "use_mcp_server" || message.say === "use_mcp_server") {
const useMcpServer = JSON.parse(message.text || "{}") as ClineAskUseMcpServer
const server = mcpServers.find((server) => server.name === useMcpServer.serverName)
@@ -1084,6 +1091,8 @@ export const ChatRowContent = memo(
case "hook_output_stream":
// hook_output_stream messages are combined with hook_status messages, so we don't render them separately
return <InvisibleSpacer />
case "subagent":
return <SubagentStatusRow isLast={isLast} lastModifiedMessage={lastModifiedMessage} message={message} />
case "shell_integration_warning_with_suggestion":
const isBackgroundModeEnabled = vscodeTerminalExecutionMode === "backgroundExec"
return (
@@ -1158,10 +1167,9 @@ export const ChatRowContent = memo(
text={text || ""}
/>
)
} else {
// Virtuoso cannot handle zero-height items; render a spacer instead of null
return <InvisibleSpacer />
}
// Virtuoso cannot handle zero-height items; render a spacer instead of null
return <InvisibleSpacer />
case "followup":
let question: string | undefined
let options: string[] | undefined
@@ -2,9 +2,7 @@ import { COMMAND_OUTPUT_STRING, COMMAND_REQ_APP_STRING } from "@shared/combineCo
import { ClineMessage } from "@shared/ExtensionMessage"
import { StringRequest } from "@shared/proto/cline/common"
import { memo, useEffect, useRef } from "react"
import { ClineCompactIcon } from "@/assets/ClineCompactIcon"
import { Button } from "@/components/ui/button"
import { PLATFORM_CONFIG, PlatformType } from "@/config/platform.config"
import { cn } from "@/lib/utils"
import { FileServiceClient } from "@/services/grpc-client"
import CodeBlock from "../common/CodeBlock"
@@ -168,40 +166,10 @@ export const CommandOutputRow = memo(
const showCancelButton =
(isCommandExecuting || isCommandPending) && typeof onCancelCommand === "function" && isBackgroundExec
// Check if this is a Cline subagent command (only on VSCode platform, not JetBrains/standalone)
const isSubagentCommand = PLATFORM_CONFIG.type === PlatformType.VSCODE && command.trim().startsWith("cline ")
let subagentPrompt: string | undefined
if (isSubagentCommand) {
// Parse the cline command to extract prompt
// Format: cline "prompt"
const clineCommandRegex = /^cline\s+"([^"]+)"(?:\s+--no-interactive)?/
const match = command.match(clineCommandRegex)
if (match) {
subagentPrompt = match[1]
}
}
// Customize icon and title for subagent commands
const displayIcon = isSubagentCommand ? (
<span className="text-foreground mb-[-1.5px]">
<ClineCompactIcon />
</span>
) : (
icon
)
const displayTitle = isSubagentCommand ? (
<span className="text-foreground font-bold">Cline wants to use a subagent:</span>
) : (
title
)
const commandHeader = (
<div className="flex items-center gap-2.5 mb-3">
{displayIcon}
{displayTitle}
{icon}
{title}
</div>
)
@@ -253,19 +221,9 @@ export const CommandOutputRow = memo(
</div>
)}
{isSubagentCommand && subagentPrompt && (
<div className="p-2.5 border-b border-editor-group-border">
<div className="mb-0">
<strong>Prompt:</strong> <span className="ph-no-capture font-editor">{subagentPrompt}</span>
</div>
</div>
)}
{!isSubagentCommand && (
<div className="bg-code opacity-60 text-sm">
<CodeBlock forceWrap={true} source={`${"```"}shell\n${command}\n${"```"}`} />
</div>
)}
<div className="bg-code opacity-60 text-sm">
<CodeBlock forceWrap={true} source={`${"```"}shell\n${command}\n${"```"}`} />
</div>
{output.length > 0 && (
<CommandOutputContent
@@ -278,7 +236,7 @@ export const CommandOutputRow = memo(
</div>
{requestsApproval && (
<div className="flex items-center gap-2.5 p-2 text-[12px] text-editor-warning-foreground">
<i className="codicon codicon-warning"></i>
<i className="codicon codicon-warning" />
<span>The model has determined this command requires explicit approval.</span>
</div>
)}
@@ -0,0 +1,56 @@
import { ClineAskUseSubagents, ClineMessage } from "@shared/ExtensionMessage"
import { NetworkIcon } from "lucide-react"
import { useMemo } from "react"
interface SubagentApprovalRowProps {
message: ClineMessage
}
function parseSubagentApproval(message: ClineMessage): ClineAskUseSubagents | null {
if (!message.text) {
return null
}
try {
const parsed = JSON.parse(message.text) as ClineAskUseSubagents
if (!Array.isArray(parsed.prompts)) {
return null
}
const prompts = parsed.prompts.map((prompt) => prompt?.trim()).filter((prompt): prompt is string => !!prompt)
return { prompts }
} catch {
return null
}
}
export default function SubagentApprovalRow({ message }: SubagentApprovalRowProps) {
const data = useMemo(() => parseSubagentApproval(message), [message])
if (!data || data.prompts.length === 0) {
return <div className="text-foreground opacity-80">Subagent approval details are unavailable.</div>
}
const singular = data.prompts.length === 1
const title = singular ? "Cline wants to use a subagent:" : "Cline wants to use subagents:"
return (
<div>
<div className="flex items-center gap-2.5 mb-3">
<NetworkIcon className="size-2 text-foreground" />
<span className="font-bold text-foreground">{title}</span>
</div>
<div className="bg-code border border-editor-group-border rounded-sm py-2.5 px-3">
<div className="space-y-2">
{data.prompts.map((prompt, index) => (
<div
className="rounded-xs border border-editor-group-border bg-vscode-editor-background px-2 py-1.5"
key={index}>
<div className="text-xs font-semibold text-foreground">Prompt {index + 1}</div>
<div className="mt-1 text-xs font-editor whitespace-pre-wrap break-words">{prompt}</div>
</div>
))}
</div>
</div>
</div>
)
}
@@ -0,0 +1,108 @@
import { ClineMessage, ClineSaySubagentStatus, SubagentExecutionStatus } from "@shared/ExtensionMessage"
import { CheckIcon, CircleSlashIcon, CircleXIcon, LoaderCircleIcon } from "lucide-react"
import { useMemo } from "react"
interface SubagentStatusRowProps {
message: ClineMessage
isLast: boolean
lastModifiedMessage?: ClineMessage
}
const statusLabel = (status: SubagentExecutionStatus): string => {
switch (status) {
case "pending":
return "Pending"
case "running":
return "Running"
case "completed":
return "Completed"
case "failed":
return "Failed"
default:
return "Unknown"
}
}
const aggregateIcon = (status: ClineSaySubagentStatus["status"]) => {
switch (status) {
case "running":
return <LoaderCircleIcon className="size-2 mr-2 animate-spin text-link" />
case "completed":
return <CheckIcon className="size-2 mr-2 text-success" />
case "failed":
return <CircleXIcon className="size-2 mr-2 text-error" />
default:
return <LoaderCircleIcon className="size-2 mr-2 animate-spin text-link" />
}
}
const formatCount = (value: number | undefined): string => {
if (!Number.isFinite(value)) {
return "0"
}
return Intl.NumberFormat("en-US").format(value || 0)
}
export default function SubagentStatusRow({ message, isLast, lastModifiedMessage }: SubagentStatusRowProps) {
const data = useMemo(() => {
try {
if (!message.text) {
return null
}
return JSON.parse(message.text) as ClineSaySubagentStatus
} catch {
return null
}
}, [message.text])
if (!data) {
return <div className="text-foreground opacity-80">Subagent status update unavailable.</div>
}
const wasCancelled =
data.status === "running" &&
(!isLast || lastModifiedMessage?.ask === "resume_task" || lastModifiedMessage?.ask === "resume_completed_task")
const displayStatus: ClineSaySubagentStatus["status"] = wasCancelled ? "failed" : data.status
return (
<div className="bg-code border border-editor-group-border rounded-sm py-2.5 px-3">
<div className="flex items-center">
{wasCancelled ? <CircleSlashIcon className="size-2 mr-2" /> : aggregateIcon(displayStatus)}
<span className="font-semibold text-foreground">Subagent batch</span>
{wasCancelled && <span className="ml-2 text-xs opacity-80">Cancelled</span>}
<div className="ml-2 text-xs opacity-80">
{data.completed}/{data.total} complete, {data.successes} succeeded, {data.failures} failed
</div>
</div>
<div className="mt-2 text-xs opacity-75">
{formatCount(data.toolCalls)} tool calls, {formatCount(data.inputTokens)} input tokens,{" "}
{formatCount(data.outputTokens)} output tokens
</div>
<div className="mt-2 space-y-2">
{data.items.map((entry) => (
<div
className="rounded-xs border border-editor-group-border bg-vscode-editor-background px-2 py-1.5"
key={entry.index}>
<div className="flex items-center justify-between gap-2">
<div className="text-xs font-medium text-foreground">
[{entry.index}] {statusLabel(entry.status)}
</div>
<div className="text-[11px] opacity-70">
{formatCount(entry.toolCalls || 0)} tools, {formatCount(entry.inputTokens || 0)} in,{" "}
{formatCount(entry.outputTokens || 0)} out
</div>
</div>
<div className="mt-1 text-xs font-editor whitespace-pre-wrap break-words">{entry.prompt}</div>
{entry.result && entry.status === "completed" && (
<div className="mt-1 text-xs opacity-80 whitespace-pre-wrap break-words">{entry.result}</div>
)}
{entry.error && entry.status === "failed" && (
<div className="mt-1 text-xs text-error whitespace-pre-wrap break-words">{entry.error}</div>
)}
</div>
))}
</div>
</div>
)
}
@@ -63,7 +63,6 @@ export const WelcomeSection: React.FC<WelcomeSectionProps> = ({
navigateToSettings,
navigateToSettingsModelPicker,
navigateToWorktrees,
subagentsEnabled,
worktreesEnabled,
banners,
} = useExtensionState()
@@ -242,7 +241,7 @@ export const WelcomeSection: React.FC<WelcomeSectionProps> = ({
// Combine both sources: extension state banners first, then hardcoded banners
return [...extensionStateBanners, ...hardcodedBanners]
}, [bannerConfig, banners, clineUser, subagentsEnabled, handleBannerAction, handleBannerDismiss])
}, [bannerConfig, banners, clineUser, handleBannerAction, handleBannerDismiss])
return (
<div className="flex flex-col flex-1 w-full h-full p-0 m-0">
@@ -75,6 +75,7 @@ export function useMessageHandlers(messages: ClineMessage[], chatState: ChatStat
case "command":
case "command_output":
case "use_mcp_server":
case "use_subagents":
case "completion_result":
case "mistake_limit_reached":
case "api_req_failed":
@@ -99,6 +99,7 @@ describe("getButtonConfig", () => {
{ ask: "followup", expectedConfig: "followup" },
{ ask: "browser_action_launch", expectedConfig: "browser_action_launch" },
{ ask: "use_mcp_server", expectedConfig: "use_mcp_server" },
{ ask: "use_subagents", expectedConfig: "use_subagents" },
{ ask: "plan_mode_respond", expectedConfig: "plan_mode_respond" },
{ ask: "completion_result", expectedConfig: "completion_result" },
{ ask: "resume_task", expectedConfig: "resume_task" },
@@ -101,6 +101,14 @@ export const BUTTON_CONFIGS: Record<string, ButtonConfig> = {
primaryAction: "approve",
secondaryAction: "reject",
},
use_subagents: {
sendingDisabled: false,
enableButtons: true,
primaryText: "Approve",
secondaryText: "Reject",
primaryAction: "approve",
secondaryAction: "reject",
},
followup: {
sendingDisabled: false,
enableButtons: false,
@@ -261,6 +269,8 @@ export function getButtonConfig(message: ClineMessage | undefined, _mode: Mode =
return BUTTON_CONFIGS.browser_action_launch
case "use_mcp_server":
return BUTTON_CONFIGS.use_mcp_server
case "use_subagents":
return BUTTON_CONFIGS.use_subagents
case "plan_mode_respond":
return BUTTON_CONFIGS.plan_mode_respond
@@ -1,16 +1,10 @@
import { UpdateSettingsRequest } from "@shared/proto/cline/state"
import { EmptyRequest } from "@shared/proto/index.cline"
import { AlertCircleIcon } from "lucide-react"
import { memo, type ReactNode, useCallback, useEffect, useState } from "react"
import { Button } from "@/components/ui/button"
import { memo, type ReactNode, useCallback } from "react"
import { Label } from "@/components/ui/label"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
import { Switch } from "@/components/ui/switch"
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"
import { PLATFORM_CONFIG, PlatformType } from "@/config/platform.config"
import { useExtensionState } from "@/context/ExtensionStateContext"
import { StateServiceClient } from "@/services/grpc-client"
import { isMacOSOrLinux } from "@/utils/platformUtils"
import Section from "../Section"
import SettingsSlider from "../SettingsSlider"
import { updateSetting } from "../utils/settingsHandlers"
@@ -203,40 +197,12 @@ const FeatureSettingsSection = ({ renderSectionHeader }: FeatureSettingsSectionP
worktreesEnabled,
focusChainSettings,
remoteConfigSettings,
subagentsEnabled,
subagentTerminalOutputLineLimit,
nativeToolCallSetting,
enableParallelToolCalling,
backgroundEditEnabled,
doubleCheckCompletionEnabled,
} = useExtensionState()
const [isClineCliInstalled, setIsClineCliInstalled] = useState(false)
// Poll for CLI installation status while the component is mounted
useEffect(() => {
const checkInstallation = async () => {
try {
const result = await StateServiceClient.checkCliInstallation(EmptyRequest.create())
setIsClineCliInstalled(result.value)
} catch (error) {
console.error("Failed to check CLI installation:", error)
}
}
checkInstallation()
const pollInterval = setInterval(checkInstallation, 1500)
return () => clearInterval(pollInterval)
}, [])
const handleInstallCli = useCallback(async () => {
try {
await StateServiceClient.installClineCli(EmptyRequest.create())
} catch (error) {
console.error("Failed to initiate CLI installation:", error)
}
}, [])
const handleFocusChainIntervalChange = useCallback(
(value: number) => {
updateSetting("focusChainSettings", { ...focusChainSettings, remindClineInterval: value })
@@ -244,7 +210,6 @@ const FeatureSettingsSection = ({ renderSectionHeader }: FeatureSettingsSectionP
[focusChainSettings],
)
const showSubagents = isMacOSOrLinux() && PLATFORM_CONFIG.type === PlatformType.VSCODE
const isYoloRemoteLocked = remoteConfigSettings?.yoloModeToggled !== undefined
// State lookup for mapped features
@@ -336,45 +301,6 @@ const FeatureSettingsSection = ({ renderSectionHeader }: FeatureSettingsSectionP
<div
className="relative p-3 pt-0 my-3 rounded-md border border-editor-widget-border/50"
id="experimental-features">
{/* Subagents - Only show on macOS and Linux */}
{showSubagents && (
<>
<FeatureRow
checked={subagentsEnabled}
description="Delegate tasks to specialized sub-agents (experimental)"
disabled={!isClineCliInstalled}
label="Subagents"
onChange={(checked) => updateSetting("subagentsEnabled", checked)}
/>
<div className="mt-1.5 mb-2 px-2 pt-0.5 pb-1.5 rounded">
<p className="text-xs mb-2 flex items-start text-input-warning-foreground">
<span>
<AlertCircleIcon className="inline-flex !size-1 mr-1" />
Cline CLI is required for subagents. Install it with
<code className="px-1">npm install -g cline</code>, then run
<code className="px-1">cline auth</code>
to authenticate with Cline or configure an API provider.
</span>
</p>
{!isClineCliInstalled && (
<Button className="w-full" onClick={handleInstallCli} variant="secondary">
Install Now
</Button>
)}
</div>
{subagentsEnabled && (
<SettingsSlider
description="Maximum number of lines to include in output from CLI subagents. Truncates middle to save tokens."
label="Output Limit (100-5000)"
max={5000}
min={100}
onChange={(value) => updateSetting("subagentTerminalOutputLineLimit", value)}
step={100}
value={subagentTerminalOutputLineLimit ?? 2000}
/>
)}
</>
)}
{experimentalFeatures.map((feature) => (
<FeatureRow
checked={featureState[feature.stateKey]}
@@ -256,7 +256,6 @@ export const ExtensionStateContextProvider: React.FC<{
vscodeTerminalExecutionMode: "vscodeTerminal",
terminalOutputLineLimit: 500,
maxConsecutiveMistakes: 3,
subagentTerminalOutputLineLimit: 2000,
defaultTerminalProfile: "default",
isNewUser: false,
welcomeViewCompleted: false,
@@ -277,7 +276,6 @@ export const ExtensionStateContextProvider: React.FC<{
backgroundCommandRunning: false,
backgroundCommandTaskId: undefined,
lastDismissedCliBannerVersion: 0,
subagentsEnabled: false,
backgroundEditEnabled: false,
doubleCheckCompletionEnabled: false,
globalSkillsToggles: {},