diff --git a/cli/src/acp/AcpTerminalManager.ts b/cli/src/acp/AcpTerminalManager.ts index b900b2fbf4..e2e852ada0 100644 --- a/cli/src/acp/AcpTerminalManager.ts +++ b/cli/src/acp/AcpTerminalManager.ts @@ -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 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 = 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 = new Map() @@ -406,9 +402,8 @@ export class AcpTerminalManager implements ITerminalManager { private readonly terminalInfos: Map = 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) diff --git a/cli/src/components/ConfigViewComponents.tsx b/cli/src/components/ConfigViewComponents.tsx index 925b8b8778..6778c92a65 100644 --- a/cli/src/components/ConfigViewComponents.tsx +++ b/cli/src/components/ConfigViewComponents.tsx @@ -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 = 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") { diff --git a/proto/cline/state.proto b/proto/cline/state.proto index 5d135a2eae..dfd89e8109 100644 --- a/proto/cline/state.proto +++ b/proto/cline/state.proto @@ -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; diff --git a/proto/cline/ui.proto b/proto/cline/ui.proto index e56d25c100..fe2496907a 100644 --- a/proto/cline/ui.proto +++ b/proto/cline/ui.proto @@ -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 diff --git a/src/core/assistant-message/index.ts b/src/core/assistant-message/index.ts index bf31eec6fb..d8b264bee0 100644 --- a/src/core/assistant-message/index.ts +++ b/src/core/assistant-message/index.ts @@ -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] diff --git a/src/core/controller/index.ts b/src/core/controller/index.ts index f5f55eb06a..9f5078ce32 100644 --- a/src/core/controller/index.ts +++ b/src/core/controller/index.ts @@ -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"), diff --git a/src/core/controller/state/updateSettings.test.ts b/src/core/controller/state/updateSettings.test.ts deleted file mode 100644 index e6365cbe4e..0000000000 --- a/src/core/controller/state/updateSettings.test.ts +++ /dev/null @@ -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", - ) - }) -}) diff --git a/src/core/controller/state/updateSettings.ts b/src/core/controller/state/updateSettings.ts index 0a70bb2bc7..4a34b85dd9 100644 --- a/src/core/controller/state/updateSettings.ts +++ b/src/core/controller/state/updateSettings.ts @@ -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) { diff --git a/src/core/prompts/commands.ts b/src/core/prompts/commands.ts index 674ac19ee9..1a98f2efcd 100644 --- a/src/core/prompts/commands.ts +++ b/src/core/prompts/commands.ts @@ -226,15 +226,6 @@ Below is the user's input when they indicated that they wanted to submit a Githu \n ` -export const subagentToolResponse = () => - ` -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 "" -\n -` - export const explainChangesToolResponse = () => ` 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. diff --git a/src/core/prompts/system-prompt/__tests__/__snapshots__/cline_native_next_gen.tools.snap b/src/core/prompts/system-prompt/__tests__/__snapshots__/cline_native_next_gen.tools.snap index b09556fb92..2764914ae5 100644 --- a/src/core/prompts/system-prompt/__tests__/__snapshots__/cline_native_next_gen.tools.snap +++ b/src/core/prompts/system-prompt/__tests__/__snapshots__/cline_native_next_gen.tools.snap @@ -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": { diff --git a/src/core/prompts/system-prompt/__tests__/__snapshots__/openai_gpt_5_1_native.tools.snap b/src/core/prompts/system-prompt/__tests__/__snapshots__/openai_gpt_5_1_native.tools.snap index a455aa4ad6..4ee05b7afb 100644 --- a/src/core/prompts/system-prompt/__tests__/__snapshots__/openai_gpt_5_1_native.tools.snap +++ b/src/core/prompts/system-prompt/__tests__/__snapshots__/openai_gpt_5_1_native.tools.snap @@ -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": { diff --git a/src/core/prompts/system-prompt/__tests__/__snapshots__/openai_gpt_5_native.tools.snap b/src/core/prompts/system-prompt/__tests__/__snapshots__/openai_gpt_5_native.tools.snap index 7be3c5bf35..aeb8a39674 100644 --- a/src/core/prompts/system-prompt/__tests__/__snapshots__/openai_gpt_5_native.tools.snap +++ b/src/core/prompts/system-prompt/__tests__/__snapshots__/openai_gpt_5_native.tools.snap @@ -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": { diff --git a/src/core/prompts/system-prompt/__tests__/__snapshots__/vertex_gemini3.tools.snap b/src/core/prompts/system-prompt/__tests__/__snapshots__/vertex_gemini3.tools.snap index d9b5de6593..453cfc8f0f 100644 --- a/src/core/prompts/system-prompt/__tests__/__snapshots__/vertex_gemini3.tools.snap +++ b/src/core/prompts/system-prompt/__tests__/__snapshots__/vertex_gemini3.tools.snap @@ -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", diff --git a/src/core/prompts/system-prompt/components/cli_subagents.ts b/src/core/prompts/system-prompt/components/cli_subagents.ts deleted file mode 100644 index 2bbd25d946..0000000000 --- a/src/core/prompts/system-prompt/components/cli_subagents.ts +++ /dev/null @@ -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 { - // 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, {}) -} diff --git a/src/core/prompts/system-prompt/components/index.ts b/src/core/prompts/system-prompt/components/index.ts index e70e6fcd34..6f1696b923 100644 --- a/src/core/prompts/system-prompt/components/index.ts +++ b/src/core/prompts/system-prompt/components/index.ts @@ -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, diff --git a/src/core/prompts/system-prompt/templates/placeholders.ts b/src/core/prompts/system-prompt/templates/placeholders.ts index 2090b0c505..9947eba3a5 100644 --- a/src/core/prompts/system-prompt/templates/placeholders.ts +++ b/src/core/prompts/system-prompt/templates/placeholders.ts @@ -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", diff --git a/src/core/prompts/system-prompt/tools/index.ts b/src/core/prompts/system-prompt/tools/index.ts index b909611773..c54cf2b4b1 100644 --- a/src/core/prompts/system-prompt/tools/index.ts +++ b/src/core/prompts/system-prompt/tools/index.ts @@ -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" diff --git a/src/core/prompts/system-prompt/tools/init.ts b/src/core/prompts/system-prompt/tools/init.ts index 7e47b95e2a..b0b7be6d86 100644 --- a/src/core/prompts/system-prompt/tools/init.ts +++ b/src/core/prompts/system-prompt/tools/init.ts @@ -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, diff --git a/src/core/prompts/system-prompt/tools/subagent.ts b/src/core/prompts/system-prompt/tools/subagent.ts new file mode 100644 index 0000000000..dbcd4473b4 --- /dev/null +++ b/src/core/prompts/system-prompt/tools/subagent.ts @@ -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] diff --git a/src/core/prompts/system-prompt/types.ts b/src/core/prompts/system-prompt/types.ts index 6c7dc2205e..230fa14936 100644 --- a/src/core/prompts/system-prompt/types.ts +++ b/src/core/prompts/system-prompt/types.ts @@ -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 diff --git a/src/core/prompts/system-prompt/variants/config.template.ts b/src/core/prompts/system-prompt/variants/config.template.ts index ce384930e5..cc970af78c 100644 --- a/src/core/prompts/system-prompt/variants/config.template.ts +++ b/src/core/prompts/system-prompt/variants/config.template.ts @@ -37,7 +37,6 @@ export const config: Omit = 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, ) diff --git a/src/core/prompts/system-prompt/variants/devstral/config.ts b/src/core/prompts/system-prompt/variants/devstral/config.ts index bbedaf57f7..db7a51e6a7 100644 --- a/src/core/prompts/system-prompt/variants/devstral/config.ts +++ b/src/core/prompts/system-prompt/variants/devstral/config.ts @@ -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", diff --git a/src/core/prompts/system-prompt/variants/devstral/template.ts b/src/core/prompts/system-prompt/variants/devstral/template.ts index 16de747cd5..e833cfada4 100644 --- a/src/core/prompts/system-prompt/variants/devstral/template.ts +++ b/src/core/prompts/system-prompt/variants/devstral/template.ts @@ -22,7 +22,6 @@ export const baseTemplate = `{{${SystemPromptSection.AGENT_ROLE}}} ==== -{{${SystemPromptSection.CLI_SUBAGENTS}}} ==== diff --git a/src/core/prompts/system-prompt/variants/gemini-3/config.ts b/src/core/prompts/system-prompt/variants/gemini-3/config.ts index bb5ca64434..fbc33119a9 100644 --- a/src/core/prompts/system-prompt/variants/gemini-3/config.ts +++ b/src/core/prompts/system-prompt/variants/gemini-3/config.ts @@ -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, diff --git a/src/core/prompts/system-prompt/variants/gemini-3/template.ts b/src/core/prompts/system-prompt/variants/gemini-3/template.ts index f2362df5cf..bd996be178 100644 --- a/src/core/prompts/system-prompt/variants/gemini-3/template.ts +++ b/src/core/prompts/system-prompt/variants/gemini-3/template.ts @@ -10,7 +10,6 @@ export const baseTemplate = `{{${SystemPromptSection.AGENT_ROLE}}} ==== -{{${SystemPromptSection.CLI_SUBAGENTS}}} ==== diff --git a/src/core/prompts/system-prompt/variants/generic/config.ts b/src/core/prompts/system-prompt/variants/generic/config.ts index 46b6b7559f..f999c0e568 100644 --- a/src/core/prompts/system-prompt/variants/generic/config.ts +++ b/src/core/prompts/system-prompt/variants/generic/config.ts @@ -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", diff --git a/src/core/prompts/system-prompt/variants/generic/template.ts b/src/core/prompts/system-prompt/variants/generic/template.ts index 16de747cd5..e833cfada4 100644 --- a/src/core/prompts/system-prompt/variants/generic/template.ts +++ b/src/core/prompts/system-prompt/variants/generic/template.ts @@ -22,7 +22,6 @@ export const baseTemplate = `{{${SystemPromptSection.AGENT_ROLE}}} ==== -{{${SystemPromptSection.CLI_SUBAGENTS}}} ==== diff --git a/src/core/prompts/system-prompt/variants/glm/config.ts b/src/core/prompts/system-prompt/variants/glm/config.ts index 154694d65f..42984160f3 100644 --- a/src/core/prompts/system-prompt/variants/glm/config.ts +++ b/src/core/prompts/system-prompt/variants/glm/config.ts @@ -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, diff --git a/src/core/prompts/system-prompt/variants/glm/template.ts b/src/core/prompts/system-prompt/variants/glm/template.ts index e7d558822c..30b56ecdb8 100644 --- a/src/core/prompts/system-prompt/variants/glm/template.ts +++ b/src/core/prompts/system-prompt/variants/glm/template.ts @@ -10,7 +10,6 @@ export const baseTemplate = `{{${SystemPromptSection.AGENT_ROLE}}} ## {{${SystemPromptSection.ACT_VS_PLAN}}} -## {{${SystemPromptSection.CLI_SUBAGENTS}}} ## {{${SystemPromptSection.CAPABILITIES}}} diff --git a/src/core/prompts/system-prompt/variants/gpt-5/config.ts b/src/core/prompts/system-prompt/variants/gpt-5/config.ts index 7310b1cc87..812b4a0565 100644 --- a/src/core/prompts/system-prompt/variants/gpt-5/config.ts +++ b/src/core/prompts/system-prompt/variants/gpt-5/config.ts @@ -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, diff --git a/src/core/prompts/system-prompt/variants/gpt-5/template.ts b/src/core/prompts/system-prompt/variants/gpt-5/template.ts index 35dafaab03..b81aab5060 100644 --- a/src/core/prompts/system-prompt/variants/gpt-5/template.ts +++ b/src/core/prompts/system-prompt/variants/gpt-5/template.ts @@ -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." : ""} diff --git a/src/core/prompts/system-prompt/variants/hermes/config.ts b/src/core/prompts/system-prompt/variants/hermes/config.ts index 4cb3d1070d..c918340f6c 100644 --- a/src/core/prompts/system-prompt/variants/hermes/config.ts +++ b/src/core/prompts/system-prompt/variants/hermes/config.ts @@ -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", diff --git a/src/core/prompts/system-prompt/variants/hermes/template.ts b/src/core/prompts/system-prompt/variants/hermes/template.ts index b60718a8f4..592ccb1205 100644 --- a/src/core/prompts/system-prompt/variants/hermes/template.ts +++ b/src/core/prompts/system-prompt/variants/hermes/template.ts @@ -8,7 +8,6 @@ export const baseTemplate = `{{${SystemPromptSection.AGENT_ROLE}}} ## {{${SystemPromptSection.ACT_VS_PLAN}}} -## {{${SystemPromptSection.CLI_SUBAGENTS}}} ## {{${SystemPromptSection.CAPABILITIES}}} diff --git a/src/core/prompts/system-prompt/variants/native-gpt-5-1/config.ts b/src/core/prompts/system-prompt/variants/native-gpt-5-1/config.ts index fd82a35483..114be668a7 100644 --- a/src/core/prompts/system-prompt/variants/native-gpt-5-1/config.ts +++ b/src/core/prompts/system-prompt/variants/native-gpt-5-1/config.ts @@ -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, diff --git a/src/core/prompts/system-prompt/variants/native-gpt-5-1/template.ts b/src/core/prompts/system-prompt/variants/native-gpt-5-1/template.ts index 89cfbf7e96..2da4df51d9 100644 --- a/src/core/prompts/system-prompt/variants/native-gpt-5-1/template.ts +++ b/src/core/prompts/system-prompt/variants/native-gpt-5-1/template.ts @@ -17,7 +17,6 @@ export const BASE = `{{${SystemPromptSection.AGENT_ROLE}}} ==== -{{${SystemPromptSection.CLI_SUBAGENTS}}} ==== diff --git a/src/core/prompts/system-prompt/variants/native-gpt-5/config.ts b/src/core/prompts/system-prompt/variants/native-gpt-5/config.ts index 6d7070953d..4025eec402 100644 --- a/src/core/prompts/system-prompt/variants/native-gpt-5/config.ts +++ b/src/core/prompts/system-prompt/variants/native-gpt-5/config.ts @@ -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, diff --git a/src/core/prompts/system-prompt/variants/native-gpt-5/template.ts b/src/core/prompts/system-prompt/variants/native-gpt-5/template.ts index 48c650290e..bf6fd37c32 100644 --- a/src/core/prompts/system-prompt/variants/native-gpt-5/template.ts +++ b/src/core/prompts/system-prompt/variants/native-gpt-5/template.ts @@ -17,7 +17,6 @@ export const BASE = `{{${SystemPromptSection.AGENT_ROLE}}} {{${SystemPromptSection.ACT_VS_PLAN}}} ==== -{{${SystemPromptSection.CLI_SUBAGENTS}}} ==== diff --git a/src/core/prompts/system-prompt/variants/native-next-gen/config.ts b/src/core/prompts/system-prompt/variants/native-next-gen/config.ts index 0b7833ed0d..1db5204ccb 100644 --- a/src/core/prompts/system-prompt/variants/native-next-gen/config.ts +++ b/src/core/prompts/system-prompt/variants/native-next-gen/config.ts @@ -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, diff --git a/src/core/prompts/system-prompt/variants/next-gen/config.ts b/src/core/prompts/system-prompt/variants/next-gen/config.ts index 21fec4c84e..49117097cd 100644 --- a/src/core/prompts/system-prompt/variants/next-gen/config.ts +++ b/src/core/prompts/system-prompt/variants/next-gen/config.ts @@ -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, diff --git a/src/core/prompts/system-prompt/variants/next-gen/template.ts b/src/core/prompts/system-prompt/variants/next-gen/template.ts index 6ae26e7399..1335ccaeb5 100644 --- a/src/core/prompts/system-prompt/variants/next-gen/template.ts +++ b/src/core/prompts/system-prompt/variants/next-gen/template.ts @@ -23,7 +23,6 @@ export const baseTemplate = `{{${SystemPromptSection.AGENT_ROLE}}} ==== -{{${SystemPromptSection.CLI_SUBAGENTS}}} ==== diff --git a/src/core/prompts/system-prompt/variants/trinity/config.ts b/src/core/prompts/system-prompt/variants/trinity/config.ts index db3978b0e1..0920820f27 100644 --- a/src/core/prompts/system-prompt/variants/trinity/config.ts +++ b/src/core/prompts/system-prompt/variants/trinity/config.ts @@ -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, diff --git a/src/core/prompts/system-prompt/variants/trinity/template.ts b/src/core/prompts/system-prompt/variants/trinity/template.ts index 16de747cd5..e833cfada4 100644 --- a/src/core/prompts/system-prompt/variants/trinity/template.ts +++ b/src/core/prompts/system-prompt/variants/trinity/template.ts @@ -22,7 +22,6 @@ export const baseTemplate = `{{${SystemPromptSection.AGENT_ROLE}}} ==== -{{${SystemPromptSection.CLI_SUBAGENTS}}} ==== diff --git a/src/core/prompts/system-prompt/variants/xs/config.ts b/src/core/prompts/system-prompt/variants/xs/config.ts index b8688b1c5a..e778a8f3b2 100644 --- a/src/core/prompts/system-prompt/variants/xs/config.ts +++ b/src/core/prompts/system-prompt/variants/xs/config.ts @@ -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, }) diff --git a/src/core/prompts/system-prompt/variants/xs/overrides.ts b/src/core/prompts/system-prompt/variants/xs/overrides.ts index 929a9a97eb..ec4a219170 100644 --- a/src/core/prompts/system-prompt/variants/xs/overrides.ts +++ b/src/core/prompts/system-prompt/variants/xs/overrides.ts @@ -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, diff --git a/src/core/prompts/system-prompt/variants/xs/template.ts b/src/core/prompts/system-prompt/variants/xs/template.ts index f99854903c..8c6f73747d 100644 --- a/src/core/prompts/system-prompt/variants/xs/template.ts +++ b/src/core/prompts/system-prompt/variants/xs/template.ts @@ -6,7 +6,6 @@ export const baseTemplate = `{{${SystemPromptSection.AGENT_ROLE}}} ## {{${SystemPromptSection.ACT_VS_PLAN}}} -## {{${SystemPromptSection.CLI_SUBAGENTS}}} ## {{${SystemPromptSection.CAPABILITIES}}} diff --git a/src/core/slash-commands/index.ts b/src/core/slash-commands/index.ts index 287cd3ac47..676d0d1d56 100644 --- a/src/core/slash-commands/index.ts +++ b/src/core/slash-commands/index.ts @@ -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}`) } diff --git a/src/core/task/ToolExecutor.ts b/src/core/task/ToolExecutor.ts index 7534fcbfd8..17d8bea801 100644 --- a/src/core/task/ToolExecutor.ts +++ b/src/core/task/ToolExecutor.ts @@ -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, private removeLastPartialMessageIfExistsWithType: (type: "ask" | "say", askOrSay: ClineAsk | ClineSay) => Promise, private executeCommandTool: (command: string, timeoutSeconds: number | undefined) => Promise<[boolean, any]>, + private cancelRunningCommandTool: () => Promise, private doesLatestTaskCompletionHaveNewChanges: () => Promise, private updateFCListFromToolResponse: (taskProgress: string | undefined) => Promise, private switchToActMode: () => Promise, @@ -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()) } /** diff --git a/src/core/task/index.ts b/src/core/task/index.ts index 7c25279c51..b59a243418 100644 --- a/src/core/task/index.ts +++ b/src/core/task/index.ts @@ -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 || diff --git a/src/core/task/tools/autoApprove.ts b/src/core/task/tools/autoApprove.ts index 1b0e7f4793..382a5250ce 100644 --- a/src/core/task/tools/autoApprove.ts +++ b/src/core/task/tools/autoApprove.ts @@ -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 } } diff --git a/src/core/task/tools/handlers/ExecuteCommandToolHandler.ts b/src/core/task/tools/handlers/ExecuteCommandToolHandler.ts index 3f58dacec8..483761d909 100644 --- a/src/core/task/tools/handlers/ExecuteCommandToolHandler.ts +++ b/src/core/task/tools/handlers/ExecuteCommandToolHandler.ts @@ -67,6 +67,9 @@ export class ExecuteCommandToolHandler implements IFullyManagedTool { async handlePartialBlock(block: ToolUse, uiHelpers: StronglyTypedUIHelpers): Promise { 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({ diff --git a/src/core/task/tools/handlers/ListCodeDefinitionNamesToolHandler.ts b/src/core/task/tools/handlers/ListCodeDefinitionNamesToolHandler.ts index cc4d6e00c4..99a2e11a13 100644 --- a/src/core/task/tools/handlers/ListCodeDefinitionNamesToolHandler.ts +++ b/src/core/task/tools/handlers/ListCodeDefinitionNamesToolHandler.ts @@ -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 diff --git a/src/core/task/tools/handlers/ListFilesToolHandler.ts b/src/core/task/tools/handlers/ListFilesToolHandler.ts index dbf4e2587b..b676f28ae3 100644 --- a/src/core/task/tools/handlers/ListFilesToolHandler.ts +++ b/src/core/task/tools/handlers/ListFilesToolHandler.ts @@ -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 diff --git a/src/core/task/tools/handlers/ReadFileToolHandler.ts b/src/core/task/tools/handlers/ReadFileToolHandler.ts index 0530d1a928..62f8e9e0b5 100644 --- a/src/core/task/tools/handlers/ReadFileToolHandler.ts +++ b/src/core/task/tools/handlers/ReadFileToolHandler.ts @@ -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 diff --git a/src/core/task/tools/handlers/SearchFilesToolHandler.ts b/src/core/task/tools/handlers/SearchFilesToolHandler.ts index b290e95afe..172208056e 100644 --- a/src/core/task/tools/handlers/SearchFilesToolHandler.ts +++ b/src/core/task/tools/handlers/SearchFilesToolHandler.ts @@ -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 { @@ -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 diff --git a/src/core/task/tools/handlers/SubagentToolHandler.ts b/src/core/task/tools/handlers/SubagentToolHandler.ts new file mode 100644 index 0000000000..16c1baa0b0 --- /dev/null +++ b/src/core/task/tools/handlers/SubagentToolHandler.ts @@ -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 { + return + } + + async execute(config: TaskConfig, block: ToolUse): Promise { + 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) + } +} diff --git a/src/core/task/tools/handlers/UseSkillToolHandler.ts b/src/core/task/tools/handlers/UseSkillToolHandler.ts index e5c82669b4..f1335f8497 100644 --- a/src/core/task/tools/handlers/UseSkillToolHandler.ts +++ b/src/core/task/tools/handlers/UseSkillToolHandler.ts @@ -20,6 +20,9 @@ export class UseSkillToolHandler implements IToolHandler, IPartialBlockHandler { async handlePartialBlock(block: ToolUse, uiHelpers: StronglyTypedUIHelpers): Promise { 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 diff --git a/src/core/task/tools/handlers/__tests__/SubagentToolHandler.test.ts b/src/core/task/tools/handlers/__tests__/SubagentToolHandler.test.ts new file mode 100644 index 0000000000..3d23f6afbe --- /dev/null +++ b/src/core/task/tools/handlers/__tests__/SubagentToolHandler.test.ts @@ -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")) + }) +}) diff --git a/src/core/task/tools/subagent/SubagentRunner.ts b/src/core/task/tools/subagent/SubagentRunner.ts new file mode 100644 index 0000000000..119c65bbd3 --- /dev/null +++ b/src/core/task/tools/subagent/SubagentRunner.ts @@ -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> { + if (!input || typeof input !== "object") { + return {} + } + + const params: Record = {} + 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 { + 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 { + 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)) + } +} diff --git a/src/core/task/tools/types/TaskConfig.ts b/src/core/task/tools/types/TaskConfig.ts index 48596ee2d0..7ac264b43e 100644 --- a/src/core/task/tools/types/TaskConfig.ts +++ b/src/core/task/tools/types/TaskConfig.ts @@ -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 executeCommandTool: (command: string, timeoutSeconds: number | undefined) => Promise<[boolean, any]> + cancelRunningCommandTool?: () => Promise doesLatestTaskCompletionHaveNewChanges: () => Promise diff --git a/src/core/task/tools/utils/ToolConstants.ts b/src/core/task/tools/utils/ToolConstants.ts index 3bb800b7e7..8d127eea74 100644 --- a/src/core/task/tools/utils/ToolConstants.ts +++ b/src/core/task/tools/utils/ToolConstants.ts @@ -19,6 +19,7 @@ export const TASK_CONFIG_KEYS = [ "doubleCheckCompletionEnabled", "vscodeTerminalExecutionMode", "enableParallelToolCalling", + "isSubagentExecution", "context", "taskState", "messageState", diff --git a/src/core/task/tools/utils/ToolResultUtils.ts b/src/core/task/tools/utils/ToolResultUtils.ts index 217144aa07..2f0412e633 100644 --- a/src/core/task/tools/utils/ToolResultUtils.ts +++ b/src/core/task/tools/utils/ToolResultUtils.ts @@ -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 } } diff --git a/src/hosts/vscode/terminal/VscodeTerminalManager.ts b/src/hosts/vscode/terminal/VscodeTerminalManager.ts index 114f48a249..201ba8078c 100644 --- a/src/hosts/vscode/terminal/VscodeTerminalManager.ts +++ b/src/hosts/vscode/terminal/VscodeTerminalManager.ts @@ -100,11 +100,10 @@ export class VscodeTerminalManager implements ITerminalManager { private terminalIds: Set = new Set() private processes: Map = 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 diff --git a/src/integrations/cli-subagents/subagent_command.ts b/src/integrations/cli-subagents/subagent_command.ts deleted file mode 100644 index 8aa32b8dcb..0000000000 --- a/src/integrations/cli-subagents/subagent_command.ts +++ /dev/null @@ -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(" ") -} diff --git a/src/integrations/terminal/CommandExecutor.ts b/src/integrations/terminal/CommandExecutor.ts index 47f318d745..33a1fd7729 100644 --- a/src/integrations/terminal/CommandExecutor.ts +++ b/src/integrations/terminal/CommandExecutor.ts @@ -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) { diff --git a/src/integrations/terminal/constants.ts b/src/integrations/terminal/constants.ts index 541db12728..244aefe0a6 100644 --- a/src/integrations/terminal/constants.ts +++ b/src/integrations/terminal/constants.ts @@ -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 // ============================================================================= diff --git a/src/integrations/terminal/standalone/StandaloneTerminalManager.ts b/src/integrations/terminal/standalone/StandaloneTerminalManager.ts index edbe34024c..270eecdf5e 100644 --- a/src/integrations/terminal/standalone/StandaloneTerminalManager.ts +++ b/src/integrations/terminal/standalone/StandaloneTerminalManager.ts @@ -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 diff --git a/src/integrations/terminal/types.ts b/src/integrations/terminal/types.ts index 0beb4aec0a..7aaaf524c7 100644 --- a/src/integrations/terminal/types.ts +++ b/src/integrations/terminal/types.ts @@ -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 } /** diff --git a/src/shared/ExtensionMessage.ts b/src/shared/ExtensionMessage.ts index 318f8652b0..c78a123c47 100644 --- a/src/shared/ExtensionMessage.ts +++ b/src/shared/ExtensionMessage.ts @@ -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 - subagentsEnabled?: boolean globalSkillsToggles?: Record localSkillsToggles?: Record 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[] diff --git a/src/shared/proto-conversions/cline-message.ts b/src/shared/proto-conversions/cline-message.ts index 9b35351d26..698f95f2ee 100644 --- a/src/shared/proto-conversions/cline-message.ts +++ b/src/shared/proto-conversions/cline-message.ts @@ -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] diff --git a/src/shared/slashCommands.ts b/src/shared/slashCommands.ts index d937d7440a..8041460334 100644 --- a/src/shared/slashCommands.ts +++ b/src/shared/slashCommands.ts @@ -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", diff --git a/src/shared/storage/state-keys.ts b/src/shared/storage/state-keys.ts index b138388d0e..a7597fdaf5 100644 --- a/src/shared/storage/state-keys.ts +++ b/src/shared/storage/state-keys.ts @@ -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 }, diff --git a/src/shared/tools.ts b/src/shared/tools.ts index 860c8f0786..05f1d98f9e 100644 --- a/src/shared/tools.ts +++ b/src/shared/tools.ts @@ -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 diff --git a/src/test/slash-commands.test.ts b/src/test/slash-commands.test.ts index 067c6167cb..754dc9b8ac 100644 --- a/src/test/slash-commands.test.ts +++ b/src/test/slash-commands.test.ts @@ -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()) diff --git a/src/utils/cli-detector.ts b/src/utils/cli-detector.ts index 10b2407697..28af1e6e35 100644 --- a/src/utils/cli-detector.ts +++ b/src/utils/cli-detector.ts @@ -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 { 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 -} diff --git a/webview-ui/src/components/chat/ChatRow.tsx b/webview-ui/src/components/chat/ChatRow.tsx index 03d30459e9..ed919a3a93 100644 --- a/webview-ui/src/components/chat/ChatRow.tsx +++ b/webview-ui/src/components/chat/ChatRow.tsx @@ -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}> + title={title} + /> ) switch (tool.tool) { @@ -763,6 +766,10 @@ export const ChatRowContent = memo( ) } + if (message.ask === "use_subagents" || message.say === "use_subagents") { + return + } + 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 + case "subagent": + return 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 } + // Virtuoso cannot handle zero-height items; render a spacer instead of null + return case "followup": let question: string | undefined let options: string[] | undefined diff --git a/webview-ui/src/components/chat/CommandOutputRow.tsx b/webview-ui/src/components/chat/CommandOutputRow.tsx index 243ca29c47..ddc29a9cda 100644 --- a/webview-ui/src/components/chat/CommandOutputRow.tsx +++ b/webview-ui/src/components/chat/CommandOutputRow.tsx @@ -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 ? ( - - - - ) : ( - icon - ) - - const displayTitle = isSubagentCommand ? ( - Cline wants to use a subagent: - ) : ( - title - ) - const commandHeader = (
- {displayIcon} - {displayTitle} + {icon} + {title}
) @@ -253,19 +221,9 @@ export const CommandOutputRow = memo( )} - {isSubagentCommand && subagentPrompt && ( -
-
- Prompt: {subagentPrompt} -
-
- )} - - {!isSubagentCommand && ( -
- -
- )} +
+ +
{output.length > 0 && ( {requestsApproval && (
- + The model has determined this command requires explicit approval.
)} diff --git a/webview-ui/src/components/chat/SubagentApprovalRow.tsx b/webview-ui/src/components/chat/SubagentApprovalRow.tsx new file mode 100644 index 0000000000..a6def81970 --- /dev/null +++ b/webview-ui/src/components/chat/SubagentApprovalRow.tsx @@ -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
Subagent approval details are unavailable.
+ } + + const singular = data.prompts.length === 1 + const title = singular ? "Cline wants to use a subagent:" : "Cline wants to use subagents:" + + return ( +
+
+ + {title} +
+
+
+ {data.prompts.map((prompt, index) => ( +
+
Prompt {index + 1}
+
{prompt}
+
+ ))} +
+
+
+ ) +} diff --git a/webview-ui/src/components/chat/SubagentStatusRow.tsx b/webview-ui/src/components/chat/SubagentStatusRow.tsx new file mode 100644 index 0000000000..4b744f4a52 --- /dev/null +++ b/webview-ui/src/components/chat/SubagentStatusRow.tsx @@ -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 + case "completed": + return + case "failed": + return + default: + return + } +} + +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
Subagent status update unavailable.
+ } + + const wasCancelled = + data.status === "running" && + (!isLast || lastModifiedMessage?.ask === "resume_task" || lastModifiedMessage?.ask === "resume_completed_task") + const displayStatus: ClineSaySubagentStatus["status"] = wasCancelled ? "failed" : data.status + + return ( +
+
+ {wasCancelled ? : aggregateIcon(displayStatus)} + Subagent batch + {wasCancelled && Cancelled} +
+ {data.completed}/{data.total} complete, {data.successes} succeeded, {data.failures} failed +
+
+
+ {formatCount(data.toolCalls)} tool calls, {formatCount(data.inputTokens)} input tokens,{" "} + {formatCount(data.outputTokens)} output tokens +
+
+ {data.items.map((entry) => ( +
+
+
+ [{entry.index}] {statusLabel(entry.status)} +
+
+ {formatCount(entry.toolCalls || 0)} tools, {formatCount(entry.inputTokens || 0)} in,{" "} + {formatCount(entry.outputTokens || 0)} out +
+
+
{entry.prompt}
+ {entry.result && entry.status === "completed" && ( +
{entry.result}
+ )} + {entry.error && entry.status === "failed" && ( +
{entry.error}
+ )} +
+ ))} +
+
+ ) +} diff --git a/webview-ui/src/components/chat/chat-view/components/layout/WelcomeSection.tsx b/webview-ui/src/components/chat/chat-view/components/layout/WelcomeSection.tsx index 9ac4573030..a98ab15f41 100644 --- a/webview-ui/src/components/chat/chat-view/components/layout/WelcomeSection.tsx +++ b/webview-ui/src/components/chat/chat-view/components/layout/WelcomeSection.tsx @@ -63,7 +63,6 @@ export const WelcomeSection: React.FC = ({ navigateToSettings, navigateToSettingsModelPicker, navigateToWorktrees, - subagentsEnabled, worktreesEnabled, banners, } = useExtensionState() @@ -242,7 +241,7 @@ export const WelcomeSection: React.FC = ({ // 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 (
diff --git a/webview-ui/src/components/chat/chat-view/hooks/useMessageHandlers.ts b/webview-ui/src/components/chat/chat-view/hooks/useMessageHandlers.ts index 0a1dddf63f..fc407183cc 100644 --- a/webview-ui/src/components/chat/chat-view/hooks/useMessageHandlers.ts +++ b/webview-ui/src/components/chat/chat-view/hooks/useMessageHandlers.ts @@ -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": diff --git a/webview-ui/src/components/chat/chat-view/shared/buttonConfig.test.ts b/webview-ui/src/components/chat/chat-view/shared/buttonConfig.test.ts index af0fcbcf36..5becac73be 100644 --- a/webview-ui/src/components/chat/chat-view/shared/buttonConfig.test.ts +++ b/webview-ui/src/components/chat/chat-view/shared/buttonConfig.test.ts @@ -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" }, diff --git a/webview-ui/src/components/chat/chat-view/shared/buttonConfig.ts b/webview-ui/src/components/chat/chat-view/shared/buttonConfig.ts index e32a03f92a..21a31cc0de 100644 --- a/webview-ui/src/components/chat/chat-view/shared/buttonConfig.ts +++ b/webview-ui/src/components/chat/chat-view/shared/buttonConfig.ts @@ -101,6 +101,14 @@ export const BUTTON_CONFIGS: Record = { 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 diff --git a/webview-ui/src/components/settings/sections/FeatureSettingsSection.tsx b/webview-ui/src/components/settings/sections/FeatureSettingsSection.tsx index db65db51aa..c33e0bb75d 100644 --- a/webview-ui/src/components/settings/sections/FeatureSettingsSection.tsx +++ b/webview-ui/src/components/settings/sections/FeatureSettingsSection.tsx @@ -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
- {/* Subagents - Only show on macOS and Linux */} - {showSubagents && ( - <> - updateSetting("subagentsEnabled", checked)} - /> -
-

- - - Cline CLI is required for subagents. Install it with - npm install -g cline, then run - cline auth - to authenticate with Cline or configure an API provider. - -

- {!isClineCliInstalled && ( - - )} -
- {subagentsEnabled && ( - updateSetting("subagentTerminalOutputLineLimit", value)} - step={100} - value={subagentTerminalOutputLineLimit ?? 2000} - /> - )} - - )} {experimentalFeatures.map((feature) => (