mirror of
https://github.com/cline/cline.git
synced 2026-09-19 02:05:44 +08:00
feat: checkpoint subagent tool workflow and approval UX
This commit is contained in:
@@ -12,11 +12,7 @@
|
||||
|
||||
import type * as acp from "@agentclientprotocol/sdk"
|
||||
import type { TerminalHandle } from "@agentclientprotocol/sdk"
|
||||
import {
|
||||
DEFAULT_SUBAGENT_TERMINAL_OUTPUT_LINE_LIMIT,
|
||||
DEFAULT_TERMINAL_OUTPUT_LINE_LIMIT,
|
||||
PROCESS_HOT_TIMEOUT_NORMAL,
|
||||
} from "@integrations/terminal/constants"
|
||||
import { DEFAULT_TERMINAL_OUTPUT_LINE_LIMIT, PROCESS_HOT_TIMEOUT_NORMAL } from "@integrations/terminal/constants"
|
||||
import type {
|
||||
ITerminal,
|
||||
ITerminalManager,
|
||||
@@ -142,12 +138,12 @@ export interface ManagedTerminal {
|
||||
* Wraps ACP terminal operations and emits events compatible with ITerminalProcess.
|
||||
*/
|
||||
class AcpTerminalProcess extends EventEmitter<TerminalProcessEvents> implements ITerminalProcess {
|
||||
isHot: boolean = false
|
||||
waitForShellIntegration: boolean = false
|
||||
isHot = false
|
||||
waitForShellIntegration = false
|
||||
|
||||
private _unretrievedOutput: string = ""
|
||||
private _continued: boolean = false
|
||||
private _completed: boolean = false
|
||||
private _unretrievedOutput = ""
|
||||
private _continued = false
|
||||
private _completed = false
|
||||
private _hotTimeout: NodeJS.Timeout | null = null
|
||||
private _exitWaitTimeout: NodeJS.Timeout | null = null
|
||||
private readonly manager: AcpTerminalManager
|
||||
@@ -397,7 +393,7 @@ export class AcpTerminalManager implements ITerminalManager {
|
||||
private readonly numericIdToStringId: Map<number, string> = new Map()
|
||||
|
||||
/** Next numeric ID to assign */
|
||||
private nextNumericId: number = 1
|
||||
private nextNumericId = 1
|
||||
|
||||
/** Active processes indexed by numeric terminal ID */
|
||||
private readonly processes: Map<number, AcpTerminalProcess> = new Map()
|
||||
@@ -406,9 +402,8 @@ export class AcpTerminalManager implements ITerminalManager {
|
||||
private readonly terminalInfos: Map<number, TerminalInfo> = new Map()
|
||||
|
||||
// Configuration options for ITerminalManager
|
||||
private terminalReuseEnabled: boolean = true
|
||||
private terminalReuseEnabled = true
|
||||
private terminalOutputLineLimit: number = DEFAULT_TERMINAL_OUTPUT_LINE_LIMIT
|
||||
private subagentTerminalOutputLineLimit: number = DEFAULT_SUBAGENT_TERMINAL_OUTPUT_LINE_LIMIT
|
||||
|
||||
/**
|
||||
* Creates a new AcpTerminalManager.
|
||||
@@ -667,14 +662,6 @@ export class AcpTerminalManager implements ITerminalManager {
|
||||
this.terminalOutputLineLimit = limit
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the maximum number of output lines for subagent commands.
|
||||
* @param limit Maximum number of lines
|
||||
*/
|
||||
setSubagentTerminalOutputLineLimit(limit: number): void {
|
||||
this.subagentTerminalOutputLineLimit = limit
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the default terminal profile.
|
||||
* @param profile The profile identifier
|
||||
@@ -687,15 +674,10 @@ export class AcpTerminalManager implements ITerminalManager {
|
||||
* Process output lines, potentially truncating if over limit.
|
||||
* @param outputLines Array of output lines
|
||||
* @param overrideLimit Optional limit override
|
||||
* @param isSubagentCommand Whether this is a subagent command
|
||||
* @returns Processed output string
|
||||
*/
|
||||
processOutput(outputLines: string[], overrideLimit?: number, isSubagentCommand?: boolean): string {
|
||||
const limit = isSubagentCommand
|
||||
? overrideLimit !== undefined
|
||||
? overrideLimit
|
||||
: this.subagentTerminalOutputLineLimit
|
||||
: this.terminalOutputLineLimit
|
||||
processOutput(outputLines: string[], overrideLimit?: number): string {
|
||||
const limit = overrideLimit !== undefined ? overrideLimit : this.terminalOutputLineLimit
|
||||
|
||||
if (outputLines.length > limit) {
|
||||
const halfLimit = Math.floor(limit / 2)
|
||||
|
||||
@@ -46,14 +46,7 @@ export interface SkillInfo {
|
||||
enabled: boolean
|
||||
}
|
||||
|
||||
export const EXCLUDED_KEYS = new Set([
|
||||
"taskHistory",
|
||||
"primaryRootIndex",
|
||||
"subagentsEnabled",
|
||||
"subagentTerminalOutputLineLimit",
|
||||
"welcomeViewCompleted",
|
||||
"isNewUser",
|
||||
])
|
||||
export const EXCLUDED_KEYS = new Set(["taskHistory", "primaryRootIndex", "welcomeViewCompleted", "isNewUser"])
|
||||
|
||||
export const EDITABLE_TYPES: Set<ValueType> = new Set(["string", "number", "boolean"])
|
||||
export const MAX_VISIBLE = 12
|
||||
@@ -135,7 +128,7 @@ export function parseValue(input: string, type: ValueType): unknown {
|
||||
return input.toLowerCase() === "true" || input === "1"
|
||||
}
|
||||
if (type === "number") {
|
||||
const num = parseFloat(input)
|
||||
const num = Number.parseFloat(input)
|
||||
return Number.isNaN(num) ? 0 : num
|
||||
}
|
||||
if (type === "object") {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -250,7 +250,6 @@ export class Controller {
|
||||
const terminalReuseEnabled = this.stateManager.getGlobalStateKey("terminalReuseEnabled")
|
||||
const vscodeTerminalExecutionMode = this.stateManager.getGlobalStateKey("vscodeTerminalExecutionMode")
|
||||
const terminalOutputLineLimit = this.stateManager.getGlobalSettingsKey("terminalOutputLineLimit")
|
||||
const subagentTerminalOutputLineLimit = this.stateManager.getGlobalSettingsKey("subagentTerminalOutputLineLimit")
|
||||
const defaultTerminalProfile = this.stateManager.getGlobalSettingsKey("defaultTerminalProfile")
|
||||
const isNewUser = this.stateManager.getGlobalStateKey("isNewUser")
|
||||
const taskHistory = this.stateManager.getGlobalStateKey("taskHistory")
|
||||
@@ -314,7 +313,6 @@ export class Controller {
|
||||
shellIntegrationTimeout,
|
||||
terminalReuseEnabled: terminalReuseEnabled ?? true,
|
||||
terminalOutputLineLimit: terminalOutputLineLimit ?? 500,
|
||||
subagentTerminalOutputLineLimit: subagentTerminalOutputLineLimit ?? 2000,
|
||||
defaultTerminalProfile: defaultTerminalProfile ?? "default",
|
||||
vscodeTerminalExecutionMode,
|
||||
cwd,
|
||||
@@ -883,7 +881,6 @@ export class Controller {
|
||||
const mcpResponsesCollapsed = this.stateManager.getGlobalStateKey("mcpResponsesCollapsed")
|
||||
const terminalOutputLineLimit = this.stateManager.getGlobalSettingsKey("terminalOutputLineLimit")
|
||||
const maxConsecutiveMistakes = this.stateManager.getGlobalSettingsKey("maxConsecutiveMistakes")
|
||||
const subagentTerminalOutputLineLimit = this.stateManager.getGlobalSettingsKey("subagentTerminalOutputLineLimit")
|
||||
const favoritedModelIds = this.stateManager.getGlobalStateKey("favoritedModelIds")
|
||||
const lastDismissedInfoBannerVersion = this.stateManager.getGlobalStateKey("lastDismissedInfoBannerVersion") || 0
|
||||
const lastDismissedModelBannerVersion = this.stateManager.getGlobalStateKey("lastDismissedModelBannerVersion") || 0
|
||||
@@ -974,7 +971,6 @@ export class Controller {
|
||||
mcpResponsesCollapsed,
|
||||
terminalOutputLineLimit,
|
||||
maxConsecutiveMistakes,
|
||||
subagentTerminalOutputLineLimit,
|
||||
customPrompt,
|
||||
taskHistory: processedTaskHistory,
|
||||
shouldShowAnnouncement,
|
||||
@@ -1004,7 +1000,6 @@ export class Controller {
|
||||
remoteConfigSettings: this.stateManager.getRemoteConfigSettings(),
|
||||
lastDismissedCliBannerVersion,
|
||||
dismissedBanners,
|
||||
subagentsEnabled,
|
||||
nativeToolCallSetting: this.stateManager.getGlobalStateKey("nativeToolCallEnabled"),
|
||||
enableParallelToolCalling: this.stateManager.getGlobalSettingsKey("enableParallelToolCalling"),
|
||||
backgroundEditEnabled: this.stateManager.getGlobalSettingsKey("backgroundEditEnabled"),
|
||||
|
||||
@@ -1,165 +0,0 @@
|
||||
import { UpdateSettingsRequest } from "@shared/proto/cline/state"
|
||||
import * as assert from "assert"
|
||||
import * as sinon from "sinon"
|
||||
import { Controller } from ".."
|
||||
import { updateSettings } from "./updateSettings"
|
||||
|
||||
// Mock telemetryService
|
||||
const telemetryServiceMock = {
|
||||
captureSubagentToggle: sinon.stub(),
|
||||
}
|
||||
|
||||
describe("updateSettings platform validation", () => {
|
||||
let mockController: Controller
|
||||
let originalPlatform: NodeJS.Platform
|
||||
|
||||
beforeEach(() => {
|
||||
// Store original platform
|
||||
originalPlatform = process.platform
|
||||
|
||||
// Create mock controller
|
||||
mockController = {
|
||||
stateManager: {
|
||||
getGlobalSettingsKey: sinon.stub(),
|
||||
setGlobalState: sinon.stub(),
|
||||
setApiConfiguration: sinon.stub(),
|
||||
},
|
||||
postStateToWebview: sinon.stub().resolves({}),
|
||||
task: undefined,
|
||||
updateTelemetrySetting: sinon.stub(),
|
||||
} as unknown as Controller
|
||||
|
||||
// Clear telemetry service mock
|
||||
telemetryServiceMock.captureSubagentToggle.reset()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
// Restore original platform
|
||||
Object.defineProperty(process, "platform", {
|
||||
value: originalPlatform,
|
||||
writable: true,
|
||||
configurable: true,
|
||||
})
|
||||
sinon.restore()
|
||||
})
|
||||
|
||||
it("should allow enabling subagents on macOS (darwin)", async () => {
|
||||
// Set platform to macOS
|
||||
Object.defineProperty(process, "platform", { value: "darwin" })
|
||||
|
||||
;(mockController.stateManager.getGlobalSettingsKey as sinon.SinonStub).returns(false)
|
||||
|
||||
const request = UpdateSettingsRequest.create({
|
||||
subagentsEnabled: true,
|
||||
})
|
||||
|
||||
// Should not throw
|
||||
await updateSettings(mockController, request)
|
||||
|
||||
assert.ok(
|
||||
(mockController.stateManager.setGlobalState as sinon.SinonStub).calledWith("subagentsEnabled", true),
|
||||
"Should enable subagents on macOS",
|
||||
)
|
||||
})
|
||||
|
||||
it("should allow enabling subagents on Linux", async () => {
|
||||
// Set platform to Linux
|
||||
Object.defineProperty(process, "platform", { value: "linux" })
|
||||
|
||||
;(mockController.stateManager.getGlobalSettingsKey as sinon.SinonStub).returns(false)
|
||||
|
||||
const request = UpdateSettingsRequest.create({
|
||||
subagentsEnabled: true,
|
||||
})
|
||||
|
||||
// Should not throw
|
||||
await updateSettings(mockController, request)
|
||||
|
||||
assert.ok(
|
||||
(mockController.stateManager.setGlobalState as sinon.SinonStub).calledWith("subagentsEnabled", true),
|
||||
"Should enable subagents on Linux",
|
||||
)
|
||||
})
|
||||
|
||||
it("should throw error when trying to enable subagents on Windows", async () => {
|
||||
// Set platform to Windows
|
||||
Object.defineProperty(process, "platform", { value: "win32" })
|
||||
|
||||
;(mockController.stateManager.getGlobalSettingsKey as sinon.SinonStub).returns(false)
|
||||
|
||||
const request = UpdateSettingsRequest.create({
|
||||
subagentsEnabled: true,
|
||||
})
|
||||
|
||||
try {
|
||||
await updateSettings(mockController, request)
|
||||
assert.fail("Should have thrown an error")
|
||||
} catch (error) {
|
||||
assert.strictEqual(
|
||||
(error as Error).message,
|
||||
"CLI subagents are only supported on macOS and Linux platforms",
|
||||
"Should throw platform restriction error",
|
||||
)
|
||||
}
|
||||
|
||||
assert.ok(
|
||||
!(mockController.stateManager.setGlobalState as sinon.SinonStub).called,
|
||||
"Should not call setGlobalState when platform validation fails",
|
||||
)
|
||||
})
|
||||
|
||||
it("should allow disabling subagents on any platform", async () => {
|
||||
// Test on Windows
|
||||
Object.defineProperty(process, "platform", { value: "win32" })
|
||||
|
||||
;(mockController.stateManager.getGlobalSettingsKey as sinon.SinonStub).returns(true)
|
||||
|
||||
const request = UpdateSettingsRequest.create({
|
||||
subagentsEnabled: false,
|
||||
})
|
||||
|
||||
// Should not throw
|
||||
await updateSettings(mockController, request)
|
||||
|
||||
assert.ok(
|
||||
(mockController.stateManager.setGlobalState as sinon.SinonStub).calledWith("subagentsEnabled", false),
|
||||
"Should allow disabling subagents on any platform",
|
||||
)
|
||||
})
|
||||
|
||||
it("should allow keeping subagents disabled on non-macOS platforms", async () => {
|
||||
// Test on Windows with subagents already disabled
|
||||
Object.defineProperty(process, "platform", { value: "win32" })
|
||||
|
||||
;(mockController.stateManager.getGlobalSettingsKey as sinon.SinonStub).returns(false)
|
||||
|
||||
const request = UpdateSettingsRequest.create({
|
||||
subagentsEnabled: false,
|
||||
})
|
||||
|
||||
// Should not throw
|
||||
await updateSettings(mockController, request)
|
||||
|
||||
assert.ok(
|
||||
(mockController.stateManager.setGlobalState as sinon.SinonStub).calledWith("subagentsEnabled", false),
|
||||
"Should allow keeping subagents disabled on non-macOS platforms",
|
||||
)
|
||||
})
|
||||
|
||||
it("should not perform platform validation when subagentsEnabled is undefined", async () => {
|
||||
// Test on Windows but don't try to change subagents setting
|
||||
Object.defineProperty(process, "platform", { value: "win32" })
|
||||
|
||||
const request = UpdateSettingsRequest.create({
|
||||
strictPlanModeEnabled: true, // Some other setting
|
||||
})
|
||||
|
||||
// Should not throw error since subagentsEnabled is not being changed
|
||||
await updateSettings(mockController, request)
|
||||
|
||||
assert.ok(
|
||||
(mockController.postStateToWebview as sinon.SinonStub).called,
|
||||
"Should complete successfully when subagentsEnabled is not being changed",
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -128,22 +128,6 @@ export async function updateSettings(controller: Controller, request: UpdateSett
|
||||
)
|
||||
}
|
||||
|
||||
// Update subagent terminal output line limit
|
||||
if (request.subagentTerminalOutputLineLimit !== undefined) {
|
||||
controller.stateManager.setGlobalState(
|
||||
"subagentTerminalOutputLineLimit",
|
||||
Number(request.subagentTerminalOutputLineLimit),
|
||||
)
|
||||
}
|
||||
|
||||
// Update subagent terminal output line limit
|
||||
if (request.subagentTerminalOutputLineLimit !== undefined) {
|
||||
controller.stateManager.setGlobalState(
|
||||
"subagentTerminalOutputLineLimit",
|
||||
Number(request.subagentTerminalOutputLineLimit),
|
||||
)
|
||||
}
|
||||
|
||||
// Update max consecutive mistakes
|
||||
if (request.maxConsecutiveMistakes !== undefined) {
|
||||
controller.stateManager.setGlobalState("maxConsecutiveMistakes", Number(request.maxConsecutiveMistakes))
|
||||
@@ -314,25 +298,6 @@ export async function updateSettings(controller: Controller, request: UpdateSett
|
||||
controller.stateManager.setGlobalState("multiRootEnabled", !!request.multiRootEnabled)
|
||||
}
|
||||
|
||||
if (request.subagentsEnabled !== undefined) {
|
||||
const currentSettings = controller.stateManager.getGlobalSettingsKey("subagentsEnabled")
|
||||
const wasEnabled = currentSettings ?? false
|
||||
const isEnabled = !!request.subagentsEnabled
|
||||
|
||||
// Platform validation: Only allow enabling subagents on macOS and Linux
|
||||
if (isEnabled && process.platform !== "darwin" && process.platform !== "linux") {
|
||||
throw new Error("CLI subagents are only supported on macOS and Linux platforms")
|
||||
}
|
||||
|
||||
controller.stateManager.setGlobalState("subagentsEnabled", isEnabled)
|
||||
|
||||
// Capture telemetry when setting changes
|
||||
if (wasEnabled !== isEnabled) {
|
||||
telemetryService.captureSubagentToggle(isEnabled)
|
||||
}
|
||||
controller.stateManager.setGlobalState("subagentsEnabled", !!request.subagentsEnabled)
|
||||
}
|
||||
|
||||
if (request.nativeToolCallEnabled !== undefined) {
|
||||
controller.stateManager.setGlobalState("nativeToolCallEnabled", !!request.nativeToolCallEnabled)
|
||||
if (controller.task) {
|
||||
|
||||
@@ -226,15 +226,6 @@ Below is the user's input when they indicated that they wanted to submit a Githu
|
||||
</explicit_instructions>\n
|
||||
`
|
||||
|
||||
export const subagentToolResponse = () =>
|
||||
`<explicit_instructions type="subagent">
|
||||
The user has requested to invoke a Cline CLI subagent with the context below. You should execute a subagent command to handle this request using the CLI subagents feature.
|
||||
|
||||
Transform the user's request into a subagent command by executing:
|
||||
cline "<prompt>"
|
||||
</explicit_instructions>\n
|
||||
`
|
||||
|
||||
export const explainChangesToolResponse = () =>
|
||||
`<explicit_instructions type="explain_changes">
|
||||
The user has asked you to explain code changes. You have access to a tool called **generate_explanation** that opens a multi-file diff view with AI-generated inline comments explaining code changes between two git references.
|
||||
|
||||
+37
@@ -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": {
|
||||
|
||||
+37
@@ -417,6 +417,43 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "use_subagents",
|
||||
"description": "Run up to five focused in-process subagents in parallel. Each subagent gets its own prompt and returns a comprehensive research result with tool and token stats.",
|
||||
"strict": false,
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"prompt_1": {
|
||||
"type": "string",
|
||||
"description": "First subagent prompt."
|
||||
},
|
||||
"prompt_2": {
|
||||
"type": "string",
|
||||
"description": "Optional second subagent prompt."
|
||||
},
|
||||
"prompt_3": {
|
||||
"type": "string",
|
||||
"description": "Optional third subagent prompt."
|
||||
},
|
||||
"prompt_4": {
|
||||
"type": "string",
|
||||
"description": "Optional fourth subagent prompt."
|
||||
},
|
||||
"prompt_5": {
|
||||
"type": "string",
|
||||
"description": "Optional fifth subagent prompt."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"prompt_1"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
|
||||
@@ -368,6 +368,43 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "use_subagents",
|
||||
"description": "Run up to five focused in-process subagents in parallel. Each subagent gets its own prompt and returns a comprehensive research result with tool and token stats.",
|
||||
"strict": false,
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"prompt_1": {
|
||||
"type": "string",
|
||||
"description": "First subagent prompt."
|
||||
},
|
||||
"prompt_2": {
|
||||
"type": "string",
|
||||
"description": "Optional second subagent prompt."
|
||||
},
|
||||
"prompt_3": {
|
||||
"type": "string",
|
||||
"description": "Optional third subagent prompt."
|
||||
},
|
||||
"prompt_4": {
|
||||
"type": "string",
|
||||
"description": "Optional fourth subagent prompt."
|
||||
},
|
||||
"prompt_5": {
|
||||
"type": "string",
|
||||
"description": "Optional fifth subagent prompt."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"prompt_1"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
|
||||
@@ -352,6 +352,33 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "use_subagents",
|
||||
"description": "Run up to five focused in-process subagents in parallel. Each subagent gets its own prompt and returns a comprehensive research result with tool and token stats.",
|
||||
"parameters": {
|
||||
"type": "OBJECT",
|
||||
"properties": {
|
||||
"prompt_1": {
|
||||
"type": "STRING"
|
||||
},
|
||||
"prompt_2": {
|
||||
"type": "STRING"
|
||||
},
|
||||
"prompt_3": {
|
||||
"type": "STRING"
|
||||
},
|
||||
"prompt_4": {
|
||||
"type": "STRING"
|
||||
},
|
||||
"prompt_5": {
|
||||
"type": "STRING"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"prompt_1"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "12345670mcp0test_tool",
|
||||
"description": "test-server: A test tool",
|
||||
|
||||
@@ -1,60 +0,0 @@
|
||||
import { SystemPromptSection } from "../templates/placeholders"
|
||||
import { TemplateEngine } from "../templates/TemplateEngine"
|
||||
import type { PromptVariant, SystemPromptContext } from "../types"
|
||||
|
||||
const getCliSubagentsTemplateText = (_context: SystemPromptContext) => `USING THE CLINE CLI TOOL
|
||||
|
||||
The Cline CLI tool can be used to assign Cline AI agents with focused tasks. This can be used to keep you focused by delegating information-gathering and exploration to separate Cline instances. Use the Cline CLI tool to research large codebases, explore file structures, gather information from multiple files, analyze dependencies, or summarize code sections when the complete context may be too large or overwhelming.
|
||||
|
||||
## Creating Cline AI agents
|
||||
|
||||
Cline AI agents may be referred to as agents, subagents, or subtasks. Requests may not specifically invoke agents, but you may invoke them directly if warranted. Unless you are specifically asked to use this tool, only create agents when it seems likely you may be exploring across 10 or more files. If users specifically ask that you use this tool, you then must use this tool. Do not use subagents for editing code or executing commands- they should only be used for reading and research to help you better answer questions or build useful context for future coding tasks. If you are performing a search via search_files or the terminal (grep etc.), and the results are long and overwhleming, it is reccomended that you switch to use Cline CLI agents to perform this task. You may perform code edits directly using the write_to_file and replace_in_file tools, and commands using the execute_command tool.
|
||||
|
||||
## Command Syntax
|
||||
|
||||
You must use the following command syntax for creating Cline AI agents:
|
||||
|
||||
\`\`\`bash
|
||||
cline "your prompt here"
|
||||
\`\`\`
|
||||
|
||||
## Examples of how you might use this tool
|
||||
|
||||
\`\`\`bash
|
||||
# Find specific patterns
|
||||
cline "find all React components that use the useState hook and list their names"
|
||||
|
||||
# Analyze code structure
|
||||
cline "analyze the authentication flow. Reverse trace through all relevant functions and methods, and provide a summary of how it works. Include file/class references in your summary."
|
||||
|
||||
# Gather targeted information
|
||||
cline "list all API endpoints and their HTTP methods"
|
||||
|
||||
# Summarize directories
|
||||
cline "summarize the purpose of all files in the src/services directory"
|
||||
|
||||
# Research implementations
|
||||
cline "find how error handling is implemented across the application"
|
||||
\`\`\`
|
||||
|
||||
## Tips
|
||||
- Request brief, technically dense summaries over full file dumps.
|
||||
- Be specific with your instructions to get focused results.
|
||||
- Request summaries rather than full file contents. Encourage the agent to be brief, but specific and technically dense with their response.
|
||||
- If files you want to read are large or complicated, use Cline CLI agents for exploration before instead of reading these files.`
|
||||
|
||||
export async function getCliSubagentsSection(variant: PromptVariant, context: SystemPromptContext): Promise<string | undefined> {
|
||||
// If this is a CLI subagent, don't include CLI subagent instructions to prevent nesting/allignment concerns
|
||||
if (context.isCliSubagent) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
// Only include this section if CLI is installed and subagents are enabled
|
||||
if (!context.isSubagentsEnabledAndCliInstalled) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
const template = variant.componentOverrides?.[SystemPromptSection.CLI_SUBAGENTS]?.template || getCliSubagentsTemplateText
|
||||
|
||||
return new TemplateEngine().resolve(template, context, {})
|
||||
}
|
||||
@@ -2,7 +2,6 @@ import { SystemPromptSection } from "../templates/placeholders"
|
||||
import { getActVsPlanModeSection } from "./act_vs_plan_mode"
|
||||
import { getAgentRoleSection } from "./agent_role"
|
||||
import { getCapabilitiesSection } from "./capabilities"
|
||||
import { getCliSubagentsSection } from "./cli_subagents"
|
||||
import { getEditingFilesSection } from "./editing_files"
|
||||
import { getFeedbackSection } from "./feedback"
|
||||
import { getMcp } from "./mcp"
|
||||
@@ -44,10 +43,6 @@ export function getSystemPromptComponents() {
|
||||
id: SystemPromptSection.ACT_VS_PLAN,
|
||||
fn: getActVsPlanModeSection,
|
||||
},
|
||||
{
|
||||
id: SystemPromptSection.CLI_SUBAGENTS,
|
||||
fn: getCliSubagentsSection,
|
||||
},
|
||||
{
|
||||
id: SystemPromptSection.FEEDBACK,
|
||||
fn: getFeedbackSection,
|
||||
|
||||
@@ -5,7 +5,6 @@ export enum SystemPromptSection {
|
||||
MCP = "MCP_SECTION",
|
||||
EDITING_FILES = "EDITING_FILES_SECTION",
|
||||
ACT_VS_PLAN = "ACT_VS_PLAN_SECTION",
|
||||
CLI_SUBAGENTS = "CLI_SUBAGENTS_SECTION",
|
||||
TODO = "TODO_SECTION",
|
||||
CAPABILITIES = "CAPABILITIES_SECTION",
|
||||
SKILLS = "SKILLS_SECTION",
|
||||
|
||||
@@ -15,6 +15,7 @@ export * from "./plan_mode_respond"
|
||||
export * from "./read_file"
|
||||
export * from "./replace_in_file"
|
||||
export * from "./search_files"
|
||||
export * from "./subagent"
|
||||
export * from "./use_mcp_tool"
|
||||
export * from "./use_skill"
|
||||
export * from "./web_fetch"
|
||||
|
||||
@@ -17,6 +17,7 @@ import { plan_mode_respond_variants } from "./plan_mode_respond"
|
||||
import { read_file_variants } from "./read_file"
|
||||
import { replace_in_file_variants } from "./replace_in_file"
|
||||
import { search_files_variants } from "./search_files"
|
||||
import { subagent_variants } from "./subagent"
|
||||
import { use_mcp_tool_variants } from "./use_mcp_tool"
|
||||
import { use_skill_variants } from "./use_skill"
|
||||
import { web_fetch_variants } from "./web_fetch"
|
||||
@@ -47,6 +48,7 @@ export function registerClineToolSets(): void {
|
||||
...read_file_variants,
|
||||
...replace_in_file_variants,
|
||||
...search_files_variants,
|
||||
...subagent_variants,
|
||||
...use_mcp_tool_variants,
|
||||
...use_skill_variants,
|
||||
...web_fetch_variants,
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
import { ModelFamily } from "@/shared/prompts"
|
||||
import { ClineDefaultTool } from "@/shared/tools"
|
||||
import type { ClineToolSpec } from "../spec"
|
||||
|
||||
const id = ClineDefaultTool.USE_SUBAGENTS
|
||||
|
||||
const generic: ClineToolSpec = {
|
||||
variant: ModelFamily.GENERIC,
|
||||
id,
|
||||
name: "use_subagents",
|
||||
description:
|
||||
"Run up to five focused in-process subagents in parallel. Each subagent gets its own prompt and returns a comprehensive research result with tool and token stats.",
|
||||
contextRequirements: (context) => Boolean(context.enableNativeToolCalls) && !context.isSubagentRun,
|
||||
parameters: [
|
||||
{
|
||||
name: "prompt_1",
|
||||
required: true,
|
||||
instruction: "First subagent prompt.",
|
||||
},
|
||||
{
|
||||
name: "prompt_2",
|
||||
required: false,
|
||||
instruction: "Optional second subagent prompt.",
|
||||
},
|
||||
{
|
||||
name: "prompt_3",
|
||||
required: false,
|
||||
instruction: "Optional third subagent prompt.",
|
||||
},
|
||||
{
|
||||
name: "prompt_4",
|
||||
required: false,
|
||||
instruction: "Optional fourth subagent prompt.",
|
||||
},
|
||||
{
|
||||
name: "prompt_5",
|
||||
required: false,
|
||||
instruction: "Optional fifth subagent prompt.",
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
export const subagent_variants = [generic]
|
||||
@@ -120,6 +120,7 @@ export interface SystemPromptContext {
|
||||
readonly workspaceRoots?: Array<{ path: string; name: string; vcs?: string }>
|
||||
readonly isSubagentsEnabledAndCliInstalled?: boolean
|
||||
readonly isCliSubagent?: boolean
|
||||
readonly isSubagentRun?: boolean
|
||||
readonly isCliEnvironment?: boolean
|
||||
readonly enableNativeToolCalls?: boolean
|
||||
readonly enableParallelToolCalling?: boolean
|
||||
|
||||
@@ -37,7 +37,6 @@ export const config: Omit<PromptVariant, "id"> = createVariant(ModelFamily.GENER
|
||||
SystemPromptSection.MCP,
|
||||
SystemPromptSection.EDITING_FILES,
|
||||
SystemPromptSection.ACT_VS_PLAN,
|
||||
SystemPromptSection.CLI_SUBAGENTS,
|
||||
SystemPromptSection.TODO,
|
||||
SystemPromptSection.CAPABILITIES,
|
||||
SystemPromptSection.RULES,
|
||||
@@ -124,7 +123,6 @@ export const createAdvancedVariant = (family: ModelFamily) =>
|
||||
SystemPromptSection.MCP,
|
||||
SystemPromptSection.EDITING_FILES,
|
||||
SystemPromptSection.ACT_VS_PLAN,
|
||||
SystemPromptSection.CLI_SUBAGENTS,
|
||||
SystemPromptSection.TODO,
|
||||
SystemPromptSection.CAPABILITIES,
|
||||
SystemPromptSection.FEEDBACK,
|
||||
@@ -151,4 +149,5 @@ export const createAdvancedVariant = (family: ModelFamily) =>
|
||||
ClineDefaultTool.PLAN_MODE,
|
||||
ClineDefaultTool.MCP_DOCS,
|
||||
ClineDefaultTool.TODO,
|
||||
ClineDefaultTool.USE_SUBAGENTS,
|
||||
)
|
||||
|
||||
@@ -27,7 +27,6 @@ export const config = createVariant(ModelFamily.DEVSTRAL)
|
||||
SystemPromptSection.MCP,
|
||||
SystemPromptSection.EDITING_FILES,
|
||||
SystemPromptSection.ACT_VS_PLAN,
|
||||
SystemPromptSection.CLI_SUBAGENTS,
|
||||
SystemPromptSection.CAPABILITIES,
|
||||
SystemPromptSection.RULES,
|
||||
SystemPromptSection.SYSTEM_INFO,
|
||||
@@ -54,6 +53,7 @@ export const config = createVariant(ModelFamily.DEVSTRAL)
|
||||
ClineDefaultTool.MCP_DOCS,
|
||||
ClineDefaultTool.TODO,
|
||||
ClineDefaultTool.USE_SKILL,
|
||||
ClineDefaultTool.USE_SUBAGENTS,
|
||||
)
|
||||
.placeholders({
|
||||
MODEL_FAMILY: "devstral",
|
||||
|
||||
@@ -22,7 +22,6 @@ export const baseTemplate = `{{${SystemPromptSection.AGENT_ROLE}}}
|
||||
|
||||
====
|
||||
|
||||
{{${SystemPromptSection.CLI_SUBAGENTS}}}
|
||||
|
||||
====
|
||||
|
||||
|
||||
@@ -34,7 +34,6 @@ export const config = createVariant(ModelFamily.GEMINI_3)
|
||||
SystemPromptSection.TOOL_USE,
|
||||
SystemPromptSection.RULES,
|
||||
SystemPromptSection.ACT_VS_PLAN,
|
||||
SystemPromptSection.CLI_SUBAGENTS,
|
||||
SystemPromptSection.CAPABILITIES,
|
||||
SystemPromptSection.EDITING_FILES,
|
||||
SystemPromptSection.FEEDBACK,
|
||||
@@ -66,6 +65,7 @@ export const config = createVariant(ModelFamily.GEMINI_3)
|
||||
ClineDefaultTool.TODO,
|
||||
ClineDefaultTool.GENERATE_EXPLANATION,
|
||||
ClineDefaultTool.USE_SKILL,
|
||||
ClineDefaultTool.USE_SUBAGENTS,
|
||||
)
|
||||
.placeholders({
|
||||
MODEL_FAMILY: ModelFamily.GEMINI_3,
|
||||
|
||||
@@ -10,7 +10,6 @@ export const baseTemplate = `{{${SystemPromptSection.AGENT_ROLE}}}
|
||||
|
||||
====
|
||||
|
||||
{{${SystemPromptSection.CLI_SUBAGENTS}}}
|
||||
|
||||
====
|
||||
|
||||
|
||||
@@ -48,7 +48,6 @@ export const config = createVariant(ModelFamily.GENERIC)
|
||||
SystemPromptSection.MCP,
|
||||
SystemPromptSection.EDITING_FILES,
|
||||
SystemPromptSection.ACT_VS_PLAN,
|
||||
SystemPromptSection.CLI_SUBAGENTS,
|
||||
SystemPromptSection.CAPABILITIES,
|
||||
SystemPromptSection.RULES,
|
||||
SystemPromptSection.SYSTEM_INFO,
|
||||
@@ -74,6 +73,7 @@ export const config = createVariant(ModelFamily.GENERIC)
|
||||
ClineDefaultTool.TODO,
|
||||
ClineDefaultTool.GENERATE_EXPLANATION,
|
||||
ClineDefaultTool.USE_SKILL,
|
||||
ClineDefaultTool.USE_SUBAGENTS,
|
||||
)
|
||||
.placeholders({
|
||||
MODEL_FAMILY: "generic",
|
||||
|
||||
@@ -22,7 +22,6 @@ export const baseTemplate = `{{${SystemPromptSection.AGENT_ROLE}}}
|
||||
|
||||
====
|
||||
|
||||
{{${SystemPromptSection.CLI_SUBAGENTS}}}
|
||||
|
||||
====
|
||||
|
||||
|
||||
@@ -26,7 +26,6 @@ export const config = createVariant(ModelFamily.GLM)
|
||||
SystemPromptSection.TASK_PROGRESS,
|
||||
SystemPromptSection.RULES,
|
||||
SystemPromptSection.ACT_VS_PLAN,
|
||||
SystemPromptSection.CLI_SUBAGENTS,
|
||||
SystemPromptSection.CAPABILITIES,
|
||||
SystemPromptSection.EDITING_FILES,
|
||||
SystemPromptSection.TODO,
|
||||
@@ -54,6 +53,7 @@ export const config = createVariant(ModelFamily.GLM)
|
||||
ClineDefaultTool.TODO,
|
||||
ClineDefaultTool.GENERATE_EXPLANATION,
|
||||
ClineDefaultTool.USE_SKILL,
|
||||
ClineDefaultTool.USE_SUBAGENTS,
|
||||
)
|
||||
.placeholders({
|
||||
MODEL_FAMILY: ModelFamily.GLM,
|
||||
|
||||
@@ -10,7 +10,6 @@ export const baseTemplate = `{{${SystemPromptSection.AGENT_ROLE}}}
|
||||
|
||||
## {{${SystemPromptSection.ACT_VS_PLAN}}}
|
||||
|
||||
## {{${SystemPromptSection.CLI_SUBAGENTS}}}
|
||||
|
||||
## {{${SystemPromptSection.CAPABILITIES}}}
|
||||
|
||||
|
||||
@@ -36,7 +36,6 @@ export const config = createVariant(ModelFamily.GPT_5)
|
||||
SystemPromptSection.MCP,
|
||||
SystemPromptSection.EDITING_FILES,
|
||||
SystemPromptSection.ACT_VS_PLAN,
|
||||
SystemPromptSection.CLI_SUBAGENTS,
|
||||
SystemPromptSection.CAPABILITIES,
|
||||
SystemPromptSection.FEEDBACK,
|
||||
SystemPromptSection.RULES,
|
||||
@@ -65,6 +64,7 @@ export const config = createVariant(ModelFamily.GPT_5)
|
||||
ClineDefaultTool.TODO,
|
||||
ClineDefaultTool.GENERATE_EXPLANATION,
|
||||
ClineDefaultTool.USE_SKILL,
|
||||
ClineDefaultTool.USE_SUBAGENTS,
|
||||
)
|
||||
.placeholders({
|
||||
MODEL_FAMILY: ModelFamily.GPT_5,
|
||||
|
||||
@@ -27,7 +27,6 @@ export const BASE = `{{${SystemPromptSection.AGENT_ROLE}}}
|
||||
|
||||
====
|
||||
|
||||
{{${SystemPromptSection.CLI_SUBAGENTS}}}
|
||||
|
||||
====
|
||||
|
||||
@@ -68,7 +67,7 @@ const RULES = (context: SystemPromptContext) => `RULES
|
||||
- Be sure to consider the type of project (e.g. Python, JavaScript, web application) when determining the appropriate structure and files to include. Also consider what files may be most relevant to accomplishing the task, for example looking at a project's manifest file would help you understand the project's dependencies, which you could incorporate into any code you write.
|
||||
- When making changes to code, always consider the context in which the code is being used. Ensure that your changes are compatible with the existing codebase and that they follow the project's coding standards and best practices.
|
||||
- When you want to modify a file, use the replace_in_file or write_to_file tool directly with the desired changes. You do not need to display the changes before using the tool.
|
||||
- Use Markdown **only where semantically correct** (e.g., \`inline code\`, \`\`\`code fences\`\`\`, lists, tables). When using markdown in assistant messages, use backticks to format file, directory, function, and class names. Use \( and \) for inline math, \[ and \] for block math.
|
||||
- Use Markdown **only where semantically correct** (e.g., \`inline code\`, \`\`\`code fences\`\`\`, lists, tables). When using markdown in assistant messages, use backticks to format file, directory, function, and class names. Use ( and ) for inline math, [ and ] for block math.
|
||||
- Do not ask for more information than necessary. Use the tools provided to accomplish the user's request efficiently and effectively. When you've completed your task, you must use the attempt_completion tool to present the result to the user. The user may provide feedback, which you can use to make improvements and try again.
|
||||
- ${context.yoloModeToggled !== true ? "You are only allowed to ask the user questions using the ask_followup_question tool. Use this tool only when you need additional details to complete a task, and be sure to use a clear and concise question that will help you move forward with the task. However if you can use the available tools to avoid having to ask the user questions, you should do so" : "Use your available tools and apply your best judgment to accomplish the task without asking the user any followup questions, making reasonable assumptions from the provided context"}. For example, if the user mentions a file that may be in an outside directory like the Desktop, you should use the list_files tool to list the files in the Desktop and check if the file they are talking about is there, rather than asking the user to provide the file path themselves.${context.yoloModeToggled !== true ? "\n- When the user is being vague, you should be proactive about asking clarifying questions using the ask_followup_question tool to ensure you understand their request. However, if you can infer the user's intent based on the context and available tools, you should proceed without asking unnecessary questions" : ""}
|
||||
- When executing commands, if you don't see the expected output, assume the terminal executed the command successfully and proceed with the task. The user's terminal may be unable to stream the output back properly.${context.yoloModeToggled !== true ? " If you absolutely need to see the actual terminal output, use the ask_followup_question tool to request the user to copy and paste it back to you." : ""}
|
||||
|
||||
@@ -26,7 +26,6 @@ export const config = createVariant(ModelFamily.HERMES)
|
||||
SystemPromptSection.TOOL_USE,
|
||||
SystemPromptSection.RULES,
|
||||
SystemPromptSection.ACT_VS_PLAN,
|
||||
SystemPromptSection.CLI_SUBAGENTS,
|
||||
SystemPromptSection.CAPABILITIES,
|
||||
SystemPromptSection.EDITING_FILES,
|
||||
SystemPromptSection.TODO,
|
||||
@@ -56,6 +55,7 @@ export const config = createVariant(ModelFamily.HERMES)
|
||||
ClineDefaultTool.TODO,
|
||||
ClineDefaultTool.GENERATE_EXPLANATION,
|
||||
ClineDefaultTool.USE_SKILL,
|
||||
ClineDefaultTool.USE_SUBAGENTS,
|
||||
)
|
||||
.placeholders({
|
||||
MODEL_FAMILY: "hermes",
|
||||
|
||||
@@ -8,7 +8,6 @@ export const baseTemplate = `{{${SystemPromptSection.AGENT_ROLE}}}
|
||||
|
||||
## {{${SystemPromptSection.ACT_VS_PLAN}}}
|
||||
|
||||
## {{${SystemPromptSection.CLI_SUBAGENTS}}}
|
||||
|
||||
## {{${SystemPromptSection.CAPABILITIES}}}
|
||||
|
||||
|
||||
@@ -42,7 +42,6 @@ export const config = createVariant(ModelFamily.NATIVE_GPT_5_1)
|
||||
SystemPromptSection.TOOL_USE,
|
||||
SystemPromptSection.TASK_PROGRESS,
|
||||
SystemPromptSection.ACT_VS_PLAN,
|
||||
SystemPromptSection.CLI_SUBAGENTS,
|
||||
SystemPromptSection.CAPABILITIES,
|
||||
SystemPromptSection.FEEDBACK,
|
||||
SystemPromptSection.RULES,
|
||||
@@ -72,6 +71,7 @@ export const config = createVariant(ModelFamily.NATIVE_GPT_5_1)
|
||||
ClineDefaultTool.TODO,
|
||||
ClineDefaultTool.GENERATE_EXPLANATION,
|
||||
ClineDefaultTool.USE_SKILL,
|
||||
ClineDefaultTool.USE_SUBAGENTS,
|
||||
)
|
||||
.placeholders({
|
||||
MODEL_FAMILY: ModelFamily.NATIVE_GPT_5_1,
|
||||
|
||||
@@ -17,7 +17,6 @@ export const BASE = `{{${SystemPromptSection.AGENT_ROLE}}}
|
||||
|
||||
====
|
||||
|
||||
{{${SystemPromptSection.CLI_SUBAGENTS}}}
|
||||
|
||||
====
|
||||
|
||||
|
||||
@@ -48,7 +48,6 @@ export const config = createVariant(ModelFamily.NATIVE_GPT_5)
|
||||
SystemPromptSection.TOOL_USE,
|
||||
SystemPromptSection.TASK_PROGRESS,
|
||||
SystemPromptSection.ACT_VS_PLAN,
|
||||
SystemPromptSection.CLI_SUBAGENTS,
|
||||
SystemPromptSection.CAPABILITIES,
|
||||
SystemPromptSection.FEEDBACK,
|
||||
SystemPromptSection.RULES,
|
||||
@@ -78,6 +77,7 @@ export const config = createVariant(ModelFamily.NATIVE_GPT_5)
|
||||
ClineDefaultTool.TODO,
|
||||
ClineDefaultTool.GENERATE_EXPLANATION,
|
||||
ClineDefaultTool.USE_SKILL,
|
||||
ClineDefaultTool.USE_SUBAGENTS,
|
||||
)
|
||||
.placeholders({
|
||||
MODEL_FAMILY: ModelFamily.NATIVE_GPT_5,
|
||||
|
||||
@@ -17,7 +17,6 @@ export const BASE = `{{${SystemPromptSection.AGENT_ROLE}}}
|
||||
{{${SystemPromptSection.ACT_VS_PLAN}}}
|
||||
====
|
||||
|
||||
{{${SystemPromptSection.CLI_SUBAGENTS}}}
|
||||
|
||||
====
|
||||
|
||||
|
||||
@@ -63,6 +63,7 @@ export const config = createVariant(ModelFamily.NATIVE_NEXT_GEN)
|
||||
ClineDefaultTool.TODO,
|
||||
ClineDefaultTool.GENERATE_EXPLANATION,
|
||||
ClineDefaultTool.USE_SKILL,
|
||||
ClineDefaultTool.USE_SUBAGENTS,
|
||||
)
|
||||
.placeholders({
|
||||
MODEL_FAMILY: ModelFamily.NATIVE_NEXT_GEN,
|
||||
|
||||
@@ -39,7 +39,6 @@ export const config = createVariant(ModelFamily.NEXT_GEN)
|
||||
SystemPromptSection.MCP,
|
||||
SystemPromptSection.EDITING_FILES,
|
||||
SystemPromptSection.ACT_VS_PLAN,
|
||||
SystemPromptSection.CLI_SUBAGENTS,
|
||||
SystemPromptSection.CAPABILITIES,
|
||||
SystemPromptSection.FEEDBACK,
|
||||
SystemPromptSection.RULES,
|
||||
@@ -68,6 +67,7 @@ export const config = createVariant(ModelFamily.NEXT_GEN)
|
||||
ClineDefaultTool.TODO,
|
||||
ClineDefaultTool.GENERATE_EXPLANATION,
|
||||
ClineDefaultTool.USE_SKILL,
|
||||
ClineDefaultTool.USE_SUBAGENTS,
|
||||
)
|
||||
.placeholders({
|
||||
MODEL_FAMILY: ModelFamily.NEXT_GEN,
|
||||
|
||||
@@ -23,7 +23,6 @@ export const baseTemplate = `{{${SystemPromptSection.AGENT_ROLE}}}
|
||||
|
||||
====
|
||||
|
||||
{{${SystemPromptSection.CLI_SUBAGENTS}}}
|
||||
|
||||
====
|
||||
|
||||
|
||||
@@ -29,7 +29,6 @@ export const config = createVariant(ModelFamily.TRINITY)
|
||||
SystemPromptSection.MCP,
|
||||
SystemPromptSection.EDITING_FILES,
|
||||
SystemPromptSection.ACT_VS_PLAN,
|
||||
SystemPromptSection.CLI_SUBAGENTS,
|
||||
SystemPromptSection.CAPABILITIES,
|
||||
SystemPromptSection.RULES,
|
||||
SystemPromptSection.SYSTEM_INFO,
|
||||
@@ -55,6 +54,7 @@ export const config = createVariant(ModelFamily.TRINITY)
|
||||
ClineDefaultTool.TODO,
|
||||
ClineDefaultTool.GENERATE_EXPLANATION,
|
||||
ClineDefaultTool.USE_SKILL,
|
||||
ClineDefaultTool.USE_SUBAGENTS,
|
||||
)
|
||||
.placeholders({
|
||||
MODEL_FAMILY: ModelFamily.TRINITY,
|
||||
|
||||
@@ -22,7 +22,6 @@ export const baseTemplate = `{{${SystemPromptSection.AGENT_ROLE}}}
|
||||
|
||||
====
|
||||
|
||||
{{${SystemPromptSection.CLI_SUBAGENTS}}}
|
||||
|
||||
====
|
||||
|
||||
|
||||
@@ -33,7 +33,6 @@ export const config = createVariant(ModelFamily.XS)
|
||||
SystemPromptSection.TOOL_USE,
|
||||
SystemPromptSection.RULES,
|
||||
SystemPromptSection.ACT_VS_PLAN,
|
||||
SystemPromptSection.CLI_SUBAGENTS,
|
||||
SystemPromptSection.CAPABILITIES,
|
||||
SystemPromptSection.EDITING_FILES,
|
||||
SystemPromptSection.OBJECTIVE,
|
||||
@@ -50,6 +49,7 @@ export const config = createVariant(ModelFamily.XS)
|
||||
ClineDefaultTool.ASK,
|
||||
ClineDefaultTool.ATTEMPT,
|
||||
ClineDefaultTool.PLAN_MODE,
|
||||
ClineDefaultTool.USE_SUBAGENTS,
|
||||
)
|
||||
.placeholders({
|
||||
MODEL_FAMILY: ModelFamily.XS,
|
||||
@@ -63,9 +63,6 @@ export const config = createVariant(ModelFamily.XS)
|
||||
.overrideComponent(SystemPromptSection.RULES, {
|
||||
template: xsComponentOverrides.RULES,
|
||||
})
|
||||
.overrideComponent(SystemPromptSection.CLI_SUBAGENTS, {
|
||||
template: xsComponentOverrides.CLI_SUBAGENTS,
|
||||
})
|
||||
.overrideComponent(SystemPromptSection.ACT_VS_PLAN, {
|
||||
template: xsComponentOverrides.ACT_VS_PLAN,
|
||||
})
|
||||
|
||||
@@ -39,21 +39,6 @@ const XS_OBJECTIVES = `EXECUTION FLOW
|
||||
- Prefer replace_in_file; respect final formatted state.
|
||||
- When all steps succeed and are confirmed, call attempt_completion (optional demo command).`
|
||||
|
||||
const XS_CLI_SUBAGENTS = (context: SystemPromptContext) =>
|
||||
context.enableNativeToolCalls
|
||||
? ""
|
||||
: `USING THE CLINE CLI TOOL
|
||||
|
||||
The Cline CLI tool is installed and available for you to use to handle focused tasks without polluting your main context window. This can be done using
|
||||
\`\`\`bash
|
||||
cline t o "your prompt here"
|
||||
|
||||
This must only be used for searching and exploring code. It cannot be used to edit files or execute commands.
|
||||
Example:
|
||||
# Find specific patterns
|
||||
cline t o "find all React components that use the useState hook and list their names"
|
||||
\`\`\``
|
||||
|
||||
const XS_TOOLS_OVERRIDE = (context: SystemPromptContext) =>
|
||||
context.enableNativeToolCalls
|
||||
? `TOOLS
|
||||
@@ -118,7 +103,6 @@ export const xsComponentOverrides = {
|
||||
AGENT_ROLE:
|
||||
"You are Cline, a senior software engineer + precise task runner. Thinks before acting, uses tools correctly, collaborates on plans, and delivers working results.",
|
||||
RULES: XS_RULES,
|
||||
CLI_SUBAGENTS: XS_CLI_SUBAGENTS,
|
||||
ACT_VS_PLAN: XS_ACT_PLAN_MODE,
|
||||
CAPABILITIES: XS_CAPABILITIES,
|
||||
OBJECTIVE: XS_OBJECTIVES,
|
||||
|
||||
@@ -6,7 +6,6 @@ export const baseTemplate = `{{${SystemPromptSection.AGENT_ROLE}}}
|
||||
|
||||
## {{${SystemPromptSection.ACT_VS_PLAN}}}
|
||||
|
||||
## {{${SystemPromptSection.CLI_SUBAGENTS}}}
|
||||
|
||||
## {{${SystemPromptSection.CAPABILITIES}}}
|
||||
|
||||
|
||||
@@ -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}`)
|
||||
}
|
||||
|
||||
@@ -39,6 +39,7 @@ import { PlanModeRespondHandler } from "./tools/handlers/PlanModeRespondHandler"
|
||||
import { ReadFileToolHandler } from "./tools/handlers/ReadFileToolHandler"
|
||||
import { ReportBugHandler } from "./tools/handlers/ReportBugHandler"
|
||||
import { SearchFilesToolHandler } from "./tools/handlers/SearchFilesToolHandler"
|
||||
import { UseSubagentsToolHandler } from "./tools/handlers/SubagentToolHandler"
|
||||
import { SummarizeTaskHandler } from "./tools/handlers/SummarizeTaskHandler"
|
||||
import { UseMcpToolHandler } from "./tools/handlers/UseMcpToolHandler"
|
||||
import { UseSkillToolHandler } from "./tools/handlers/UseSkillToolHandler"
|
||||
@@ -117,6 +118,7 @@ export class ToolExecutor {
|
||||
private sayAndCreateMissingParamError: (toolName: ClineDefaultTool, paramName: string, relPath?: string) => Promise<any>,
|
||||
private removeLastPartialMessageIfExistsWithType: (type: "ask" | "say", askOrSay: ClineAsk | ClineSay) => Promise<void>,
|
||||
private executeCommandTool: (command: string, timeoutSeconds: number | undefined) => Promise<[boolean, any]>,
|
||||
private cancelRunningCommandTool: () => Promise<boolean>,
|
||||
private doesLatestTaskCompletionHaveNewChanges: () => Promise<boolean>,
|
||||
private updateFCListFromToolResponse: (taskProgress: string | undefined) => Promise<void>,
|
||||
private switchToActMode: () => Promise<boolean>,
|
||||
@@ -151,6 +153,7 @@ export class ToolExecutor {
|
||||
doubleCheckCompletionEnabled: this.stateManager.getGlobalSettingsKey("doubleCheckCompletionEnabled"),
|
||||
vscodeTerminalExecutionMode: this.vscodeTerminalExecutionMode,
|
||||
enableParallelToolCalling: this.isParallelToolCallingEnabled(),
|
||||
isSubagentExecution: false,
|
||||
cwd: this.cwd,
|
||||
workspaceManager: this.workspaceManager,
|
||||
isMultiRootEnabled: this.isMultiRootEnabled,
|
||||
@@ -181,6 +184,7 @@ export class ToolExecutor {
|
||||
cancelTask: this.cancelTask,
|
||||
updateTaskHistory: async (_: any) => [],
|
||||
executeCommandTool: this.executeCommandTool,
|
||||
cancelRunningCommandTool: this.cancelRunningCommandTool,
|
||||
doesLatestTaskCompletionHaveNewChanges: this.doesLatestTaskCompletionHaveNewChanges,
|
||||
updateFCListFromToolResponse: this.updateFCListFromToolResponse,
|
||||
sayAndCreateMissingParamError: this.sayAndCreateMissingParamError,
|
||||
@@ -238,6 +242,7 @@ export class ToolExecutor {
|
||||
this.coordinator.register(new ReportBugHandler())
|
||||
this.coordinator.register(new ApplyPatchHandler(validator))
|
||||
this.coordinator.register(new GenerateExplanationToolHandler())
|
||||
this.coordinator.register(new UseSubagentsToolHandler())
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+2
-20
@@ -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 ||
|
||||
|
||||
@@ -51,6 +51,7 @@ export class AutoApprove {
|
||||
case ClineDefaultTool.FILE_EDIT:
|
||||
case ClineDefaultTool.APPLY_PATCH:
|
||||
case ClineDefaultTool.BASH:
|
||||
case ClineDefaultTool.USE_SUBAGENTS:
|
||||
return [true, true]
|
||||
|
||||
case ClineDefaultTool.BROWSER:
|
||||
@@ -73,6 +74,7 @@ export class AutoApprove {
|
||||
case ClineDefaultTool.FILE_EDIT:
|
||||
case ClineDefaultTool.APPLY_PATCH:
|
||||
case ClineDefaultTool.BASH:
|
||||
case ClineDefaultTool.USE_SUBAGENTS:
|
||||
return [true, true]
|
||||
case ClineDefaultTool.BROWSER:
|
||||
case ClineDefaultTool.WEB_FETCH:
|
||||
@@ -90,6 +92,7 @@ export class AutoApprove {
|
||||
case ClineDefaultTool.LIST_FILES:
|
||||
case ClineDefaultTool.LIST_CODE_DEF:
|
||||
case ClineDefaultTool.SEARCH:
|
||||
case ClineDefaultTool.USE_SUBAGENTS:
|
||||
return [autoApprovalSettings.actions.readFiles, autoApprovalSettings.actions.readFilesExternally ?? false]
|
||||
case ClineDefaultTool.NEW_RULE:
|
||||
case ClineDefaultTool.FILE_NEW:
|
||||
@@ -127,7 +130,7 @@ export class AutoApprove {
|
||||
return true
|
||||
}
|
||||
|
||||
let isLocalRead: boolean = false
|
||||
let isLocalRead = false
|
||||
if (autoApproveActionpath) {
|
||||
// Use cached workspace info instead of fetching every time
|
||||
const { isMultiRootScenario } = await this.getWorkspaceInfo()
|
||||
@@ -159,8 +162,7 @@ export class AutoApprove {
|
||||
|
||||
if ((isLocalRead && autoApproveLocal) || (!isLocalRead && autoApproveLocal && autoApproveExternal)) {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,6 +67,9 @@ export class ExecuteCommandToolHandler implements IFullyManagedTool {
|
||||
|
||||
async handlePartialBlock(block: ToolUse, uiHelpers: StronglyTypedUIHelpers): Promise<void> {
|
||||
const command = block.params.command
|
||||
if (uiHelpers.getConfig().isSubagentExecution) {
|
||||
return
|
||||
}
|
||||
|
||||
// Check if this should be auto-approved to determine UI flow
|
||||
const shouldAutoApprove = uiHelpers.shouldAutoApproveTool(this.name)
|
||||
@@ -168,14 +171,18 @@ export class ExecuteCommandToolHandler implements IFullyManagedTool {
|
||||
`Command "${actualCommand}" was denied by CLINE_COMMAND_PERMISSIONS. ` +
|
||||
`Reason: ${permissionResult.reason}${matchedPattern}`
|
||||
}
|
||||
await config.callbacks.say("command_permission_denied", errorMessage)
|
||||
if (!config.isSubagentExecution) {
|
||||
await config.callbacks.say("command_permission_denied", errorMessage)
|
||||
}
|
||||
return formatResponse.toolError(formatResponse.permissionDeniedError(errorMessage))
|
||||
}
|
||||
|
||||
// Check clineignore validation for command
|
||||
const ignoredFileAttemptedToAccess = config.services.clineIgnoreController.validateCommand(actualCommand)
|
||||
if (ignoredFileAttemptedToAccess) {
|
||||
await config.callbacks.say("clineignore_error", ignoredFileAttemptedToAccess)
|
||||
if (!config.isSubagentExecution) {
|
||||
await config.callbacks.say("clineignore_error", ignoredFileAttemptedToAccess)
|
||||
}
|
||||
return formatResponse.toolError(formatResponse.clineIgnoreError(ignoredFileAttemptedToAccess))
|
||||
}
|
||||
|
||||
@@ -210,10 +217,16 @@ export class ExecuteCommandToolHandler implements IFullyManagedTool {
|
||||
)
|
||||
}
|
||||
|
||||
if ((!requiresApprovalPerLLM && autoApproveSafe) || (requiresApprovalPerLLM && autoApproveSafe && autoApproveAll)) {
|
||||
if (
|
||||
config.isSubagentExecution ||
|
||||
(!requiresApprovalPerLLM && autoApproveSafe) ||
|
||||
(requiresApprovalPerLLM && autoApproveSafe && autoApproveAll)
|
||||
) {
|
||||
// Auto-approve flow
|
||||
await config.callbacks.removeLastPartialMessageIfExistsWithType("ask", "command")
|
||||
await config.callbacks.say("command", actualCommand, undefined, undefined, false)
|
||||
if (!config.isSubagentExecution) {
|
||||
await config.callbacks.removeLastPartialMessageIfExistsWithType("ask", "command")
|
||||
await config.callbacks.say("command", actualCommand, undefined, undefined, false)
|
||||
}
|
||||
didAutoApprove = true
|
||||
telemetryService.captureToolUsage(
|
||||
config.ulid,
|
||||
@@ -276,7 +289,7 @@ export class ExecuteCommandToolHandler implements IFullyManagedTool {
|
||||
|
||||
// Setup timeout notification for long-running auto-approved commands
|
||||
let timeoutId: NodeJS.Timeout | undefined
|
||||
if (didAutoApprove && config.autoApprovalSettings.enableNotifications) {
|
||||
if (didAutoApprove && config.autoApprovalSettings.enableNotifications && !config.isSubagentExecution) {
|
||||
// if the command was auto-approved, and it's long running we need to notify the user after some time has passed without proceeding
|
||||
timeoutId = setTimeout(() => {
|
||||
showSystemNotification({
|
||||
|
||||
@@ -26,6 +26,9 @@ export class ListCodeDefinitionNamesToolHandler implements IFullyManagedTool {
|
||||
const relPath = block.params.path
|
||||
|
||||
const config = uiHelpers.getConfig()
|
||||
if (config.isSubagentExecution) {
|
||||
return
|
||||
}
|
||||
|
||||
// Create and show partial UI message
|
||||
const sharedMessageProps = {
|
||||
@@ -82,10 +85,14 @@ export class ListCodeDefinitionNamesToolHandler implements IFullyManagedTool {
|
||||
|
||||
const completeMessage = JSON.stringify(sharedMessageProps)
|
||||
|
||||
if (await config.callbacks.shouldAutoApproveToolWithPath(block.name, relDirPath)) {
|
||||
const shouldAutoApprove =
|
||||
config.isSubagentExecution || (await config.callbacks.shouldAutoApproveToolWithPath(block.name, relDirPath))
|
||||
if (shouldAutoApprove) {
|
||||
// Auto-approval flow
|
||||
await config.callbacks.removeLastPartialMessageIfExistsWithType("ask", "tool")
|
||||
await config.callbacks.say("tool", completeMessage, undefined, undefined, false)
|
||||
if (!config.isSubagentExecution) {
|
||||
await config.callbacks.removeLastPartialMessageIfExistsWithType("ask", "tool")
|
||||
await config.callbacks.say("tool", completeMessage, undefined, undefined, false)
|
||||
}
|
||||
|
||||
// Capture telemetry
|
||||
telemetryService.captureToolUsage(
|
||||
@@ -120,18 +127,17 @@ export class ListCodeDefinitionNamesToolHandler implements IFullyManagedTool {
|
||||
block.isNativeToolCall,
|
||||
)
|
||||
return formatResponse.toolDenied()
|
||||
} else {
|
||||
telemetryService.captureToolUsage(
|
||||
config.ulid,
|
||||
block.name,
|
||||
config.api.getModel().id,
|
||||
provider,
|
||||
false,
|
||||
true,
|
||||
undefined,
|
||||
block.isNativeToolCall,
|
||||
)
|
||||
}
|
||||
telemetryService.captureToolUsage(
|
||||
config.ulid,
|
||||
block.name,
|
||||
config.api.getModel().id,
|
||||
provider,
|
||||
false,
|
||||
true,
|
||||
undefined,
|
||||
block.isNativeToolCall,
|
||||
)
|
||||
}
|
||||
|
||||
// Run PreToolUse hook after approval but before execution
|
||||
|
||||
@@ -28,6 +28,9 @@ export class ListFilesToolHandler implements IFullyManagedTool {
|
||||
|
||||
// Get config access for services
|
||||
const config = uiHelpers.getConfig()
|
||||
if (config.isSubagentExecution) {
|
||||
return
|
||||
}
|
||||
|
||||
// Create and show partial UI message
|
||||
const recursiveRaw = block.params.recursive
|
||||
@@ -87,7 +90,9 @@ export class ListFilesToolHandler implements IFullyManagedTool {
|
||||
// Check clineignore access
|
||||
const accessValidation = this.validator.checkClineIgnorePath(relDirPath!)
|
||||
if (!accessValidation.ok) {
|
||||
await config.callbacks.say("clineignore_error", relDirPath)
|
||||
if (!config.isSubagentExecution) {
|
||||
await config.callbacks.say("clineignore_error", relDirPath)
|
||||
}
|
||||
return formatResponse.toolError(formatResponse.clineIgnoreError(relDirPath!))
|
||||
}
|
||||
|
||||
@@ -106,10 +111,14 @@ export class ListFilesToolHandler implements IFullyManagedTool {
|
||||
|
||||
const completeMessage = JSON.stringify(sharedMessageProps)
|
||||
|
||||
if (await config.callbacks.shouldAutoApproveToolWithPath(block.name, relDirPath)) {
|
||||
const shouldAutoApprove =
|
||||
config.isSubagentExecution || (await config.callbacks.shouldAutoApproveToolWithPath(block.name, relDirPath))
|
||||
if (shouldAutoApprove) {
|
||||
// Auto-approval flow
|
||||
await config.callbacks.removeLastPartialMessageIfExistsWithType("ask", "tool")
|
||||
await config.callbacks.say("tool", completeMessage, undefined, undefined, false)
|
||||
if (!config.isSubagentExecution) {
|
||||
await config.callbacks.removeLastPartialMessageIfExistsWithType("ask", "tool")
|
||||
await config.callbacks.say("tool", completeMessage, undefined, undefined, false)
|
||||
}
|
||||
|
||||
// Capture telemetry
|
||||
telemetryService.captureToolUsage(
|
||||
@@ -144,18 +153,17 @@ export class ListFilesToolHandler implements IFullyManagedTool {
|
||||
block.isNativeToolCall,
|
||||
)
|
||||
return formatResponse.toolDenied()
|
||||
} else {
|
||||
telemetryService.captureToolUsage(
|
||||
config.ulid,
|
||||
block.name,
|
||||
config.api.getModel().id,
|
||||
provider,
|
||||
false,
|
||||
true,
|
||||
workspaceContext,
|
||||
block.isNativeToolCall,
|
||||
)
|
||||
}
|
||||
telemetryService.captureToolUsage(
|
||||
config.ulid,
|
||||
block.name,
|
||||
config.api.getModel().id,
|
||||
provider,
|
||||
false,
|
||||
true,
|
||||
workspaceContext,
|
||||
block.isNativeToolCall,
|
||||
)
|
||||
}
|
||||
|
||||
// Run PreToolUse hook after approval but before execution
|
||||
|
||||
@@ -28,6 +28,9 @@ export class ReadFileToolHandler implements IFullyManagedTool {
|
||||
const relPath = block.params.path
|
||||
|
||||
const config = uiHelpers.getConfig()
|
||||
if (config.isSubagentExecution) {
|
||||
return
|
||||
}
|
||||
|
||||
// Create and show partial UI message
|
||||
const sharedMessageProps = {
|
||||
@@ -67,7 +70,9 @@ export class ReadFileToolHandler implements IFullyManagedTool {
|
||||
// Check clineignore access
|
||||
const accessValidation = this.validator.checkClineIgnorePath(relPath!)
|
||||
if (!accessValidation.ok) {
|
||||
await config.callbacks.say("clineignore_error", relPath)
|
||||
if (!config.isSubagentExecution) {
|
||||
await config.callbacks.say("clineignore_error", relPath)
|
||||
}
|
||||
return formatResponse.toolError(formatResponse.clineIgnoreError(relPath!))
|
||||
}
|
||||
|
||||
@@ -97,10 +102,14 @@ export class ReadFileToolHandler implements IFullyManagedTool {
|
||||
|
||||
const completeMessage = JSON.stringify(sharedMessageProps)
|
||||
|
||||
if (await config.callbacks.shouldAutoApproveToolWithPath(block.name, relPath)) {
|
||||
const shouldAutoApprove =
|
||||
config.isSubagentExecution || (await config.callbacks.shouldAutoApproveToolWithPath(block.name, relPath))
|
||||
if (shouldAutoApprove) {
|
||||
// Auto-approval flow
|
||||
await config.callbacks.removeLastPartialMessageIfExistsWithType("ask", "tool")
|
||||
await config.callbacks.say("tool", completeMessage, undefined, undefined, false)
|
||||
if (!config.isSubagentExecution) {
|
||||
await config.callbacks.removeLastPartialMessageIfExistsWithType("ask", "tool")
|
||||
await config.callbacks.say("tool", completeMessage, undefined, undefined, false)
|
||||
}
|
||||
|
||||
// Capture telemetry
|
||||
telemetryService.captureToolUsage(
|
||||
@@ -135,18 +144,17 @@ export class ReadFileToolHandler implements IFullyManagedTool {
|
||||
block.isNativeToolCall,
|
||||
)
|
||||
return formatResponse.toolDenied()
|
||||
} else {
|
||||
telemetryService.captureToolUsage(
|
||||
config.ulid,
|
||||
block.name,
|
||||
config.api.getModel().id,
|
||||
provider,
|
||||
false,
|
||||
true,
|
||||
workspaceContext,
|
||||
block.isNativeToolCall,
|
||||
)
|
||||
}
|
||||
telemetryService.captureToolUsage(
|
||||
config.ulid,
|
||||
block.name,
|
||||
config.api.getModel().id,
|
||||
provider,
|
||||
false,
|
||||
true,
|
||||
workspaceContext,
|
||||
block.isNativeToolCall,
|
||||
)
|
||||
}
|
||||
|
||||
// Run PreToolUse hook after approval but before execution
|
||||
|
||||
@@ -51,23 +51,21 @@ export class SearchFilesToolHandler implements IFullyManagedTool {
|
||||
const workspaceRoots = adapter.getWorkspaceRoots()
|
||||
const root = workspaceRoots.find((r) => r.name === workspaceHint)
|
||||
return [{ absolutePath, workspaceName: workspaceHint, workspaceRoot: root?.path }]
|
||||
} else {
|
||||
// As a fallback, perform the search across all available workspaces.
|
||||
// Typically, models should provide explicit hints to target specific workspaces for searching.
|
||||
const allPaths = adapter.getAllPossiblePaths(parsedPath)
|
||||
const workspaceRoots = adapter.getWorkspaceRoots()
|
||||
return allPaths.map((absPath, index) => ({
|
||||
absolutePath: absPath,
|
||||
workspaceName: workspaceRoots[index]?.name || path.basename(workspaceRoots[index]?.path || absPath),
|
||||
workspaceRoot: workspaceRoots[index]?.path,
|
||||
}))
|
||||
}
|
||||
} else {
|
||||
// Single-workspace mode (backward compatible)
|
||||
const pathResult = resolveWorkspacePath(config, originalPath, "SearchFilesTool.execute")
|
||||
const absolutePath = typeof pathResult === "string" ? pathResult : pathResult.absolutePath
|
||||
return [{ absolutePath, workspaceRoot: config.cwd }]
|
||||
// As a fallback, perform the search across all available workspaces.
|
||||
// Typically, models should provide explicit hints to target specific workspaces for searching.
|
||||
const allPaths = adapter.getAllPossiblePaths(parsedPath)
|
||||
const workspaceRoots = adapter.getWorkspaceRoots()
|
||||
return allPaths.map((absPath, index) => ({
|
||||
absolutePath: absPath,
|
||||
workspaceName: workspaceRoots[index]?.name || path.basename(workspaceRoots[index]?.path || absPath),
|
||||
workspaceRoot: workspaceRoots[index]?.path,
|
||||
}))
|
||||
}
|
||||
// Single-workspace mode (backward compatible)
|
||||
const pathResult = resolveWorkspacePath(config, originalPath, "SearchFilesTool.execute")
|
||||
const absolutePath = typeof pathResult === "string" ? pathResult : pathResult.absolutePath
|
||||
return [{ absolutePath, workspaceRoot: config.cwd }]
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -96,7 +94,7 @@ export class SearchFilesToolHandler implements IFullyManagedTool {
|
||||
// Parse the result count from the first line
|
||||
const firstLine = workspaceResults.split("\n")[0]
|
||||
const resultMatch = firstLine.match(/Found (\d+) result/)
|
||||
const resultCount = resultMatch ? parseInt(resultMatch[1], 10) : 0
|
||||
const resultCount = resultMatch ? Number.parseInt(resultMatch[1], 10) : 0
|
||||
|
||||
return {
|
||||
workspaceName,
|
||||
@@ -164,13 +162,11 @@ export class SearchFilesToolHandler implements IFullyManagedTool {
|
||||
// Multi-workspace search result
|
||||
if (totalResultCount === 0) {
|
||||
return "Found 0 results."
|
||||
} else {
|
||||
return `Found ${totalResultCount === 1 ? "1 result" : `${totalResultCount.toLocaleString()} results`} across ${searchPaths.length} workspace${searchPaths.length > 1 ? "s" : ""}.\n\n${allResults.join("\n\n")}`
|
||||
}
|
||||
} else {
|
||||
// Single workspace result
|
||||
return allResults[0] || "Found 0 results."
|
||||
return `Found ${totalResultCount === 1 ? "1 result" : `${totalResultCount.toLocaleString()} results`} across ${searchPaths.length} workspace${searchPaths.length > 1 ? "s" : ""}.\n\n${allResults.join("\n\n")}`
|
||||
}
|
||||
// Single workspace result
|
||||
return allResults[0] || "Found 0 results."
|
||||
}
|
||||
|
||||
async handlePartialBlock(block: ToolUse, uiHelpers: StronglyTypedUIHelpers): Promise<void> {
|
||||
@@ -178,6 +174,9 @@ export class SearchFilesToolHandler implements IFullyManagedTool {
|
||||
const regex = block.params.regex
|
||||
|
||||
const config = uiHelpers.getConfig()
|
||||
if (config.isSubagentExecution) {
|
||||
return
|
||||
}
|
||||
|
||||
// Create and show partial UI message
|
||||
const filePattern = block.params.file_pattern
|
||||
@@ -306,10 +305,14 @@ export class SearchFilesToolHandler implements IFullyManagedTool {
|
||||
|
||||
const completeMessage = JSON.stringify(sharedMessageProps)
|
||||
|
||||
if (await config.callbacks.shouldAutoApproveToolWithPath(block.name, relDirPath)) {
|
||||
const shouldAutoApprove =
|
||||
config.isSubagentExecution || (await config.callbacks.shouldAutoApproveToolWithPath(block.name, relDirPath))
|
||||
if (shouldAutoApprove) {
|
||||
// Auto-approval flow
|
||||
await config.callbacks.removeLastPartialMessageIfExistsWithType("ask", "tool")
|
||||
await config.callbacks.say("tool", completeMessage, undefined, undefined, false)
|
||||
if (!config.isSubagentExecution) {
|
||||
await config.callbacks.removeLastPartialMessageIfExistsWithType("ask", "tool")
|
||||
await config.callbacks.say("tool", completeMessage, undefined, undefined, false)
|
||||
}
|
||||
|
||||
// Capture telemetry
|
||||
telemetryService.captureToolUsage(
|
||||
@@ -344,18 +347,17 @@ export class SearchFilesToolHandler implements IFullyManagedTool {
|
||||
block.isNativeToolCall,
|
||||
)
|
||||
return formatResponse.toolDenied()
|
||||
} else {
|
||||
telemetryService.captureToolUsage(
|
||||
config.ulid,
|
||||
block.name,
|
||||
config.api.getModel().id,
|
||||
provider,
|
||||
false,
|
||||
true,
|
||||
workspaceContext,
|
||||
block.isNativeToolCall,
|
||||
)
|
||||
}
|
||||
telemetryService.captureToolUsage(
|
||||
config.ulid,
|
||||
block.name,
|
||||
config.api.getModel().id,
|
||||
provider,
|
||||
false,
|
||||
true,
|
||||
workspaceContext,
|
||||
block.isNativeToolCall,
|
||||
)
|
||||
}
|
||||
|
||||
// Run PreToolUse hook after approval but before execution
|
||||
|
||||
@@ -0,0 +1,224 @@
|
||||
import type { ToolUse } from "@core/assistant-message"
|
||||
import { formatResponse } from "@core/prompts/responses"
|
||||
import { ClineAskUseSubagents, ClineSaySubagentStatus, SubagentStatusItem } from "@shared/ExtensionMessage"
|
||||
import { telemetryService } from "@/services/telemetry"
|
||||
import { ClineDefaultTool } from "@/shared/tools"
|
||||
import type { ToolResponse } from "../../index"
|
||||
import { showNotificationForApproval } from "../../utils"
|
||||
import { SubagentRunner } from "../subagent/SubagentRunner"
|
||||
import type { IFullyManagedTool } from "../ToolExecutorCoordinator"
|
||||
import type { TaskConfig } from "../types/TaskConfig"
|
||||
import type { StronglyTypedUIHelpers } from "../types/UIHelpers"
|
||||
import { ToolResultUtils } from "../utils/ToolResultUtils"
|
||||
|
||||
const MAX_SUBAGENT_PROMPTS = 5
|
||||
const PROMPT_KEYS = ["prompt_1", "prompt_2", "prompt_3", "prompt_4", "prompt_5"] as const
|
||||
|
||||
function excerpt(text: string | undefined, maxChars = 1200): string {
|
||||
if (!text) {
|
||||
return ""
|
||||
}
|
||||
|
||||
const trimmed = text.trim()
|
||||
if (trimmed.length <= maxChars) {
|
||||
return trimmed
|
||||
}
|
||||
|
||||
return `${trimmed.slice(0, maxChars)}...`
|
||||
}
|
||||
|
||||
export class UseSubagentsToolHandler implements IFullyManagedTool {
|
||||
readonly name = ClineDefaultTool.USE_SUBAGENTS
|
||||
|
||||
getDescription(_block: ToolUse): string {
|
||||
return "[subagent batch]"
|
||||
}
|
||||
|
||||
async handlePartialBlock(_block: ToolUse, _uiHelpers: StronglyTypedUIHelpers): Promise<void> {
|
||||
return
|
||||
}
|
||||
|
||||
async execute(config: TaskConfig, block: ToolUse): Promise<ToolResponse> {
|
||||
const prompts = PROMPT_KEYS.map((key) => block.params[key]?.trim()).filter((prompt): prompt is string => !!prompt)
|
||||
|
||||
if (prompts.length === 0) {
|
||||
config.taskState.consecutiveMistakeCount++
|
||||
return await config.callbacks.sayAndCreateMissingParamError(this.name, "prompt_1")
|
||||
}
|
||||
|
||||
if (prompts.length > MAX_SUBAGENT_PROMPTS) {
|
||||
config.taskState.consecutiveMistakeCount++
|
||||
return formatResponse.toolError(
|
||||
`Too many subagent prompts provided (${prompts.length}). Maximum is ${MAX_SUBAGENT_PROMPTS}.`,
|
||||
)
|
||||
}
|
||||
|
||||
const apiConfig = config.services.stateManager.getApiConfiguration()
|
||||
const currentMode = config.services.stateManager.getGlobalSettingsKey("mode")
|
||||
const provider = (currentMode === "plan" ? apiConfig.planModeApiProvider : apiConfig.actModeApiProvider) as string
|
||||
const approvalPayload: ClineAskUseSubagents = { prompts }
|
||||
const approvalBody = JSON.stringify(approvalPayload)
|
||||
|
||||
const autoApproveResult = config.autoApprover?.shouldAutoApproveTool(this.name)
|
||||
const [autoApproveSafe] = Array.isArray(autoApproveResult) ? autoApproveResult : [autoApproveResult, false]
|
||||
const didAutoApprove = !!autoApproveSafe
|
||||
|
||||
if (didAutoApprove) {
|
||||
await config.callbacks.say("use_subagents", approvalBody, undefined, undefined, false)
|
||||
telemetryService.captureToolUsage(
|
||||
config.ulid,
|
||||
this.name,
|
||||
config.api.getModel().id,
|
||||
provider,
|
||||
true,
|
||||
true,
|
||||
undefined,
|
||||
block.isNativeToolCall,
|
||||
)
|
||||
} else {
|
||||
showNotificationForApproval(
|
||||
prompts.length === 1 ? "Cline wants to use a subagent" : `Cline wants to use ${prompts.length} subagents`,
|
||||
config.autoApprovalSettings.enableNotifications,
|
||||
)
|
||||
const didApprove = await ToolResultUtils.askApprovalAndPushFeedback("use_subagents", approvalBody, config)
|
||||
if (!didApprove) {
|
||||
telemetryService.captureToolUsage(
|
||||
config.ulid,
|
||||
this.name,
|
||||
config.api.getModel().id,
|
||||
provider,
|
||||
false,
|
||||
false,
|
||||
undefined,
|
||||
block.isNativeToolCall,
|
||||
)
|
||||
return formatResponse.toolDenied()
|
||||
}
|
||||
telemetryService.captureToolUsage(
|
||||
config.ulid,
|
||||
this.name,
|
||||
config.api.getModel().id,
|
||||
provider,
|
||||
false,
|
||||
true,
|
||||
undefined,
|
||||
block.isNativeToolCall,
|
||||
)
|
||||
}
|
||||
|
||||
config.taskState.consecutiveMistakeCount = 0
|
||||
|
||||
const entries: SubagentStatusItem[] = prompts.map((prompt, index) => ({
|
||||
index: index + 1,
|
||||
prompt,
|
||||
status: "pending",
|
||||
toolCalls: 0,
|
||||
inputTokens: 0,
|
||||
outputTokens: 0,
|
||||
}))
|
||||
|
||||
const emitStatus = async (status: ClineSaySubagentStatus["status"], partial: boolean) => {
|
||||
const completed = entries.filter((entry) => entry.status === "completed" || entry.status === "failed").length
|
||||
const successes = entries.filter((entry) => entry.status === "completed").length
|
||||
const failures = entries.filter((entry) => entry.status === "failed").length
|
||||
const toolCalls = entries.reduce((acc, entry) => acc + (entry.toolCalls || 0), 0)
|
||||
const inputTokens = entries.reduce((acc, entry) => acc + (entry.inputTokens || 0), 0)
|
||||
const outputTokens = entries.reduce((acc, entry) => acc + (entry.outputTokens || 0), 0)
|
||||
|
||||
const payload: ClineSaySubagentStatus = {
|
||||
status,
|
||||
total: entries.length,
|
||||
completed,
|
||||
successes,
|
||||
failures,
|
||||
toolCalls,
|
||||
inputTokens,
|
||||
outputTokens,
|
||||
items: entries,
|
||||
}
|
||||
|
||||
await config.callbacks.say("subagent", JSON.stringify(payload), undefined, undefined, partial)
|
||||
}
|
||||
|
||||
await config.callbacks.removeLastPartialMessageIfExistsWithType("say", "subagent")
|
||||
await emitStatus("running", true)
|
||||
|
||||
const runners = prompts.map(() => new SubagentRunner(config))
|
||||
const abortPollInterval = setInterval(() => {
|
||||
if (!config.taskState.abort) {
|
||||
return
|
||||
}
|
||||
clearInterval(abortPollInterval)
|
||||
void Promise.allSettled(runners.map((runner) => runner.abort()))
|
||||
}, 100)
|
||||
|
||||
const execution = prompts.map((prompt, index) =>
|
||||
runners[index].run(prompt, async (update) => {
|
||||
const current = entries[index]
|
||||
if (update.status === "running") {
|
||||
current.status = "running"
|
||||
}
|
||||
if (update.status === "completed") {
|
||||
current.status = "completed"
|
||||
}
|
||||
if (update.status === "failed") {
|
||||
current.status = "failed"
|
||||
}
|
||||
if (update.result !== undefined) {
|
||||
current.result = update.result
|
||||
}
|
||||
if (update.error !== undefined) {
|
||||
current.error = update.error
|
||||
}
|
||||
if (update.stats) {
|
||||
current.toolCalls = update.stats.toolCalls || 0
|
||||
current.inputTokens = update.stats.inputTokens || 0
|
||||
current.outputTokens = update.stats.outputTokens || 0
|
||||
}
|
||||
await emitStatus("running", true)
|
||||
}),
|
||||
)
|
||||
|
||||
const settled = await Promise.allSettled(execution)
|
||||
clearInterval(abortPollInterval)
|
||||
settled.forEach((result, index) => {
|
||||
if (result.status === "rejected") {
|
||||
entries[index].status = "failed"
|
||||
entries[index].error = (result.reason as Error)?.message || "Subagent execution failed"
|
||||
return
|
||||
}
|
||||
entries[index].status = result.value.status
|
||||
entries[index].result = result.value.result
|
||||
entries[index].error = result.value.error
|
||||
entries[index].toolCalls = result.value.stats.toolCalls || 0
|
||||
entries[index].inputTokens = result.value.stats.inputTokens || 0
|
||||
entries[index].outputTokens = result.value.stats.outputTokens || 0
|
||||
})
|
||||
|
||||
const failures = entries.filter((entry) => entry.status === "failed").length
|
||||
await emitStatus(failures > 0 ? "failed" : "completed", false)
|
||||
|
||||
const successCount = entries.length - failures
|
||||
const totalToolCalls = entries.reduce((acc, entry) => acc + (entry.toolCalls || 0), 0)
|
||||
const totalInputTokens = entries.reduce((acc, entry) => acc + (entry.inputTokens || 0), 0)
|
||||
const totalOutputTokens = entries.reduce((acc, entry) => acc + (entry.outputTokens || 0), 0)
|
||||
|
||||
const summary = [
|
||||
`Subagent batch complete.`,
|
||||
`Total: ${entries.length}`,
|
||||
`Succeeded: ${successCount}`,
|
||||
`Failed: ${failures}`,
|
||||
`Tool calls: ${totalToolCalls}`,
|
||||
`Input tokens: ${totalInputTokens}`,
|
||||
`Output tokens: ${totalOutputTokens}`,
|
||||
"",
|
||||
...entries.map((entry) => {
|
||||
const header = `[${entry.index}] ${entry.status.toUpperCase()} - ${entry.prompt}`
|
||||
const detail = entry.status === "completed" ? excerpt(entry.result) : excerpt(entry.error)
|
||||
return detail ? `${header}\n${detail}` : header
|
||||
}),
|
||||
].join("\n")
|
||||
|
||||
return formatResponse.toolResult(summary)
|
||||
}
|
||||
}
|
||||
@@ -20,6 +20,9 @@ export class UseSkillToolHandler implements IToolHandler, IPartialBlockHandler {
|
||||
|
||||
async handlePartialBlock(block: ToolUse, uiHelpers: StronglyTypedUIHelpers): Promise<void> {
|
||||
const skillName = block.params.skill_name
|
||||
if (uiHelpers.getConfig().isSubagentExecution) {
|
||||
return
|
||||
}
|
||||
const message = JSON.stringify({ tool: "useSkill", path: skillName || "" })
|
||||
await uiHelpers.say("tool", message, undefined, undefined, true)
|
||||
}
|
||||
@@ -58,7 +61,9 @@ export class UseSkillToolHandler implements IToolHandler, IPartialBlockHandler {
|
||||
|
||||
// Show tool message
|
||||
const message = JSON.stringify({ tool: "useSkill", path: skillName })
|
||||
await config.callbacks.say("tool", message, undefined, undefined, false)
|
||||
if (!config.isSubagentExecution) {
|
||||
await config.callbacks.say("tool", message, undefined, undefined, false)
|
||||
}
|
||||
|
||||
config.taskState.consecutiveMistakeCount = 0
|
||||
|
||||
|
||||
@@ -0,0 +1,263 @@
|
||||
import { strict as assert } from "node:assert"
|
||||
import { setTimeout as delay } from "node:timers/promises"
|
||||
import { afterEach, describe, it } from "mocha"
|
||||
import sinon from "sinon"
|
||||
import { TaskState } from "../../../TaskState"
|
||||
import { SubagentRunner } from "../../subagent/SubagentRunner"
|
||||
import { UseSubagentsToolHandler } from "../SubagentToolHandler"
|
||||
|
||||
function createConfig(options?: {
|
||||
autoApproveSafe?: boolean
|
||||
autoApproveAll?: boolean
|
||||
taskAskResponse?: "yesButtonClicked" | "noButtonClicked"
|
||||
}) {
|
||||
const taskState = new TaskState()
|
||||
const askResponse = options?.taskAskResponse ?? "yesButtonClicked"
|
||||
|
||||
const callbacks = {
|
||||
say: sinon.stub().resolves(undefined),
|
||||
ask: sinon.stub().resolves({ response: askResponse }),
|
||||
saveCheckpoint: sinon.stub().resolves(),
|
||||
sayAndCreateMissingParamError: sinon.stub().resolves("missing"),
|
||||
removeLastPartialMessageIfExistsWithType: sinon.stub().resolves(),
|
||||
executeCommandTool: sinon.stub().resolves([false, "ok"]),
|
||||
cancelRunningCommandTool: sinon.stub().resolves(false),
|
||||
doesLatestTaskCompletionHaveNewChanges: sinon.stub().resolves(false),
|
||||
updateFCListFromToolResponse: sinon.stub().resolves(),
|
||||
shouldAutoApproveTool: sinon.stub().returns([options?.autoApproveSafe ?? false, options?.autoApproveAll ?? false]),
|
||||
shouldAutoApproveToolWithPath: sinon.stub().resolves(false),
|
||||
postStateToWebview: sinon.stub().resolves(),
|
||||
reinitExistingTaskFromId: sinon.stub().resolves(),
|
||||
cancelTask: sinon.stub().resolves(),
|
||||
updateTaskHistory: sinon.stub().resolves([]),
|
||||
applyLatestBrowserSettings: sinon.stub().resolves(undefined),
|
||||
switchToActMode: sinon.stub().resolves(false),
|
||||
setActiveHookExecution: sinon.stub().resolves(),
|
||||
clearActiveHookExecution: sinon.stub().resolves(),
|
||||
getActiveHookExecution: sinon.stub().resolves(undefined),
|
||||
runUserPromptSubmitHook: sinon.stub().resolves({}),
|
||||
}
|
||||
|
||||
const config: any = {
|
||||
taskId: "task-1",
|
||||
ulid: "ulid-1",
|
||||
cwd: "/tmp",
|
||||
mode: "act",
|
||||
strictPlanModeEnabled: false,
|
||||
yoloModeToggled: false,
|
||||
vscodeTerminalExecutionMode: "backgroundExec",
|
||||
enableParallelToolCalling: true,
|
||||
context: {},
|
||||
taskState,
|
||||
messageState: {},
|
||||
api: {
|
||||
getModel: () => ({ id: "openai/gpt-5", info: {} }),
|
||||
},
|
||||
autoApprovalSettings: {
|
||||
enableNotifications: false,
|
||||
actions: {
|
||||
executeSafeCommands: false,
|
||||
executeAllCommands: false,
|
||||
},
|
||||
},
|
||||
autoApprover: {
|
||||
shouldAutoApproveTool: sinon.stub().returns([options?.autoApproveSafe ?? false, options?.autoApproveAll ?? false]),
|
||||
},
|
||||
browserSettings: {},
|
||||
focusChainSettings: {},
|
||||
services: {
|
||||
stateManager: {
|
||||
getGlobalStateKey: (key: string) => (key === "nativeToolCallEnabled" ? true : undefined),
|
||||
getGlobalSettingsKey: (key: string) => {
|
||||
if (key === "mode") {
|
||||
return "act"
|
||||
}
|
||||
if (key === "customPrompt") {
|
||||
return undefined
|
||||
}
|
||||
return undefined
|
||||
},
|
||||
getApiConfiguration: () => ({
|
||||
planModeApiProvider: "openai",
|
||||
actModeApiProvider: "openai",
|
||||
}),
|
||||
},
|
||||
mcpHub: {},
|
||||
},
|
||||
callbacks,
|
||||
coordinator: {
|
||||
getHandler: sinon.stub(),
|
||||
},
|
||||
}
|
||||
|
||||
return { config, callbacks, taskState }
|
||||
}
|
||||
|
||||
describe("SubagentToolHandler", () => {
|
||||
afterEach(() => {
|
||||
sinon.restore()
|
||||
})
|
||||
|
||||
it("returns missing parameter error when no prompts are provided", async () => {
|
||||
const { config, callbacks, taskState } = createConfig()
|
||||
const handler = new UseSubagentsToolHandler()
|
||||
|
||||
const result = await handler.execute(config, {
|
||||
type: "tool_use",
|
||||
name: "use_subagents" as any,
|
||||
params: {},
|
||||
partial: false,
|
||||
})
|
||||
|
||||
assert.equal(result, "missing")
|
||||
assert.equal(taskState.consecutiveMistakeCount, 1)
|
||||
sinon.assert.calledOnce(callbacks.sayAndCreateMissingParamError)
|
||||
})
|
||||
|
||||
it("uses one approval for the full batch and stops on denial", async () => {
|
||||
const { config, callbacks, taskState } = createConfig({ taskAskResponse: "noButtonClicked" })
|
||||
const runStub = sinon.stub(SubagentRunner.prototype, "run")
|
||||
const handler = new UseSubagentsToolHandler()
|
||||
|
||||
const result = await handler.execute(config, {
|
||||
type: "tool_use",
|
||||
name: "use_subagents" as any,
|
||||
params: {
|
||||
prompt_1: "one",
|
||||
prompt_2: "two",
|
||||
},
|
||||
partial: false,
|
||||
})
|
||||
|
||||
assert.equal(result, "The user denied this operation.")
|
||||
assert.equal(taskState.didRejectTool, true)
|
||||
sinon.assert.calledOnce(callbacks.ask)
|
||||
assert.equal(callbacks.ask.firstCall.args[0], "use_subagents")
|
||||
sinon.assert.notCalled(runStub)
|
||||
})
|
||||
|
||||
it("uses read-file auto-approve level (safe only) for approval bypass", async () => {
|
||||
const { config, callbacks } = createConfig({ autoApproveSafe: true, autoApproveAll: false })
|
||||
sinon.stub(SubagentRunner.prototype, "run").resolves({
|
||||
status: "completed",
|
||||
result: "done",
|
||||
stats: {
|
||||
toolCalls: 1,
|
||||
inputTokens: 2,
|
||||
outputTokens: 3,
|
||||
cacheWriteTokens: 0,
|
||||
cacheReadTokens: 0,
|
||||
},
|
||||
})
|
||||
|
||||
const handler = new UseSubagentsToolHandler()
|
||||
await handler.execute(config, {
|
||||
type: "tool_use",
|
||||
name: "use_subagents" as any,
|
||||
params: {
|
||||
prompt_1: "one",
|
||||
},
|
||||
partial: false,
|
||||
})
|
||||
|
||||
sinon.assert.notCalled(callbacks.ask)
|
||||
const approvalSayCalls = callbacks.say.getCalls().filter((call: any) => call.args[0] === "use_subagents")
|
||||
assert.ok(approvalSayCalls.length >= 1)
|
||||
})
|
||||
|
||||
it("fans out prompts in parallel and emits aggregated status", async () => {
|
||||
const { config, callbacks } = createConfig({ autoApproveSafe: true, autoApproveAll: true })
|
||||
let activeRuns = 0
|
||||
let maxActiveRuns = 0
|
||||
|
||||
sinon.stub(SubagentRunner.prototype, "run").callsFake(async (_prompt: string, onProgress: any) => {
|
||||
activeRuns++
|
||||
maxActiveRuns = Math.max(maxActiveRuns, activeRuns)
|
||||
onProgress({
|
||||
status: "running",
|
||||
stats: { toolCalls: 0, inputTokens: 0, outputTokens: 0, cacheWriteTokens: 0, cacheReadTokens: 0 },
|
||||
})
|
||||
await delay(10)
|
||||
activeRuns--
|
||||
return {
|
||||
status: "completed",
|
||||
result: "done",
|
||||
stats: {
|
||||
toolCalls: 1,
|
||||
inputTokens: 2,
|
||||
outputTokens: 3,
|
||||
cacheWriteTokens: 0,
|
||||
cacheReadTokens: 0,
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
const handler = new UseSubagentsToolHandler()
|
||||
const result = await handler.execute(config, {
|
||||
type: "tool_use",
|
||||
name: "use_subagents" as any,
|
||||
params: {
|
||||
prompt_1: "one",
|
||||
prompt_2: "two",
|
||||
prompt_3: "three",
|
||||
},
|
||||
partial: false,
|
||||
})
|
||||
|
||||
assert.equal(typeof result, "string")
|
||||
assert.ok((result as string).includes("Total: 3"))
|
||||
assert.ok(maxActiveRuns > 1)
|
||||
|
||||
const subagentStatusCalls = callbacks.say.getCalls().filter((call: any) => call.args[0] === "subagent")
|
||||
assert.ok(subagentStatusCalls.length >= 2)
|
||||
const finalCall = subagentStatusCalls[subagentStatusCalls.length - 1]
|
||||
assert.equal(finalCall.args[4], false)
|
||||
})
|
||||
|
||||
it("continues after per-subagent failures and reports both outcomes", async () => {
|
||||
const { config } = createConfig({ autoApproveSafe: true, autoApproveAll: true })
|
||||
|
||||
sinon.stub(SubagentRunner.prototype, "run").callsFake(async (prompt: string) => {
|
||||
if (prompt.includes("fail")) {
|
||||
return {
|
||||
status: "failed",
|
||||
error: "boom",
|
||||
stats: {
|
||||
toolCalls: 1,
|
||||
inputTokens: 0,
|
||||
outputTokens: 0,
|
||||
cacheWriteTokens: 0,
|
||||
cacheReadTokens: 0,
|
||||
},
|
||||
}
|
||||
}
|
||||
return {
|
||||
status: "completed",
|
||||
result: "ok",
|
||||
stats: {
|
||||
toolCalls: 2,
|
||||
inputTokens: 0,
|
||||
outputTokens: 0,
|
||||
cacheWriteTokens: 0,
|
||||
cacheReadTokens: 0,
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
const handler = new UseSubagentsToolHandler()
|
||||
const result = await handler.execute(config, {
|
||||
type: "tool_use",
|
||||
name: "use_subagents" as any,
|
||||
params: {
|
||||
prompt_1: "succeed",
|
||||
prompt_2: "fail",
|
||||
},
|
||||
partial: false,
|
||||
})
|
||||
|
||||
assert.equal(typeof result, "string")
|
||||
assert.ok((result as string).includes("Succeeded: 1"))
|
||||
assert.ok((result as string).includes("Failed: 1"))
|
||||
assert.ok((result as string).includes("boom"))
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,408 @@
|
||||
import { setTimeout as delay } from "node:timers/promises"
|
||||
import { buildApiHandler } from "@core/api"
|
||||
import { ToolUse } from "@core/assistant-message"
|
||||
import { discoverSkills, getAvailableSkills } from "@core/context/instructions/user-instructions/skills"
|
||||
import { formatResponse } from "@core/prompts/responses"
|
||||
import { PromptRegistry } from "@core/prompts/system-prompt"
|
||||
import { ClineToolSet } from "@core/prompts/system-prompt/registry/ClineToolSet"
|
||||
import type { SystemPromptContext } from "@core/prompts/system-prompt/types"
|
||||
import { StreamResponseHandler } from "@core/task/StreamResponseHandler"
|
||||
import { ClineStorageMessage, ClineTextContentBlock } from "@shared/messages"
|
||||
import { Logger } from "@shared/services/Logger"
|
||||
import { ClineDefaultTool } from "@shared/tools"
|
||||
import { HostProvider } from "@/hosts/host-provider"
|
||||
import { TaskState } from "../../TaskState"
|
||||
import type { TaskConfig } from "../types/TaskConfig"
|
||||
|
||||
const SUBAGENT_ALLOWED_TOOLS: ClineDefaultTool[] = [
|
||||
ClineDefaultTool.FILE_READ,
|
||||
ClineDefaultTool.LIST_FILES,
|
||||
ClineDefaultTool.SEARCH,
|
||||
ClineDefaultTool.LIST_CODE_DEF,
|
||||
ClineDefaultTool.BASH,
|
||||
ClineDefaultTool.USE_SKILL,
|
||||
]
|
||||
|
||||
export type SubagentRunStatus = "completed" | "failed"
|
||||
|
||||
export interface SubagentRunResult {
|
||||
status: SubagentRunStatus
|
||||
result?: string
|
||||
error?: string
|
||||
stats: SubagentRunStats
|
||||
}
|
||||
|
||||
interface SubagentProgressUpdate {
|
||||
stats?: SubagentRunStats
|
||||
status?: "running" | "completed" | "failed"
|
||||
result?: string
|
||||
error?: string
|
||||
}
|
||||
|
||||
interface SubagentRunStats {
|
||||
toolCalls: number
|
||||
inputTokens: number
|
||||
outputTokens: number
|
||||
cacheWriteTokens: number
|
||||
cacheReadTokens: number
|
||||
}
|
||||
|
||||
const SUBAGENT_SYSTEM_SUFFIX = `\n\n# Subagent Execution Mode
|
||||
You are running as a research subagent. Your job is to thoroughly explore the codebase and gather comprehensive information to answer the question.
|
||||
Explore broadly, read related files, trace through call chains, and build a complete picture before reporting back.
|
||||
You can read files, list directories, search for patterns, list code definitions, and run commands.
|
||||
Only use execute_command for readonly operations like ls, grep, git log, git diff, gh, etc.
|
||||
Do not run commands that modify files or system state.
|
||||
When you have a comprehensive answer, respond with your findings including file paths and line numbers.`
|
||||
|
||||
function serializeToolResult(result: unknown): string {
|
||||
if (typeof result === "string") {
|
||||
return result
|
||||
}
|
||||
|
||||
if (Array.isArray(result)) {
|
||||
return result
|
||||
.map((item) => {
|
||||
if (!item || typeof item !== "object") {
|
||||
return String(item)
|
||||
}
|
||||
|
||||
const maybeText = (item as { text?: string }).text
|
||||
if (typeof maybeText === "string") {
|
||||
return maybeText
|
||||
}
|
||||
|
||||
return JSON.stringify(item)
|
||||
})
|
||||
.join("\n")
|
||||
}
|
||||
|
||||
return JSON.stringify(result, null, 2)
|
||||
}
|
||||
|
||||
function toToolUseParams(input: unknown): Partial<Record<string, string>> {
|
||||
if (!input || typeof input !== "object") {
|
||||
return {}
|
||||
}
|
||||
|
||||
const params: Record<string, string> = {}
|
||||
for (const [key, value] of Object.entries(input)) {
|
||||
params[key] = typeof value === "string" ? value : JSON.stringify(value)
|
||||
}
|
||||
|
||||
return params
|
||||
}
|
||||
|
||||
function normalizeToolCallArguments(argumentsPayload: unknown): string {
|
||||
if (typeof argumentsPayload === "string") {
|
||||
return argumentsPayload
|
||||
}
|
||||
|
||||
try {
|
||||
return JSON.stringify(argumentsPayload ?? {})
|
||||
} catch {
|
||||
return "{}"
|
||||
}
|
||||
}
|
||||
|
||||
export class SubagentRunner {
|
||||
private activeApiAbort: (() => void) | undefined
|
||||
private abortRequested = false
|
||||
private activeCommandExecutions = 0
|
||||
private abortingCommands = false
|
||||
|
||||
constructor(private baseConfig: TaskConfig) {}
|
||||
|
||||
async abort(): Promise<void> {
|
||||
this.abortRequested = true
|
||||
|
||||
try {
|
||||
this.activeApiAbort?.()
|
||||
} catch (error) {
|
||||
Logger.error("[SubagentRunner] failed to abort active API stream", error)
|
||||
}
|
||||
|
||||
if (this.activeCommandExecutions > 0 && !this.abortingCommands && this.baseConfig.callbacks.cancelRunningCommandTool) {
|
||||
this.abortingCommands = true
|
||||
try {
|
||||
await this.baseConfig.callbacks.cancelRunningCommandTool()
|
||||
} catch (error) {
|
||||
Logger.error("[SubagentRunner] failed to cancel running command execution", error)
|
||||
} finally {
|
||||
this.abortingCommands = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private shouldAbort(): boolean {
|
||||
return this.abortRequested || this.baseConfig.taskState.abort
|
||||
}
|
||||
|
||||
async run(prompt: string, onProgress: (update: SubagentProgressUpdate) => void): Promise<SubagentRunResult> {
|
||||
this.abortRequested = false
|
||||
const state = new TaskState()
|
||||
const stats: SubagentRunStats = {
|
||||
toolCalls: 0,
|
||||
inputTokens: 0,
|
||||
outputTokens: 0,
|
||||
cacheWriteTokens: 0,
|
||||
cacheReadTokens: 0,
|
||||
}
|
||||
|
||||
onProgress({ status: "running", stats })
|
||||
|
||||
try {
|
||||
const mode = this.baseConfig.services.stateManager.getGlobalSettingsKey("mode")
|
||||
const apiConfiguration = this.baseConfig.services.stateManager.getApiConfiguration()
|
||||
const api = buildApiHandler(apiConfiguration, mode)
|
||||
this.activeApiAbort = api.abort?.bind(api)
|
||||
|
||||
const providerId = (
|
||||
mode === "plan" ? apiConfiguration.planModeApiProvider : apiConfiguration.actModeApiProvider
|
||||
) as string
|
||||
const providerInfo = {
|
||||
providerId,
|
||||
model: api.getModel(),
|
||||
mode,
|
||||
customPrompt: this.baseConfig.services.stateManager.getGlobalSettingsKey("customPrompt"),
|
||||
}
|
||||
|
||||
const host = await HostProvider.env.getHostVersion({})
|
||||
const discoveredSkills = await discoverSkills(this.baseConfig.cwd)
|
||||
const skills = getAvailableSkills(discoveredSkills)
|
||||
|
||||
const context: SystemPromptContext = {
|
||||
providerInfo,
|
||||
cwd: this.baseConfig.cwd,
|
||||
ide: host?.platform || "Unknown",
|
||||
skills,
|
||||
focusChainSettings: this.baseConfig.focusChainSettings,
|
||||
browserSettings: this.baseConfig.browserSettings,
|
||||
yoloModeToggled: false,
|
||||
enableNativeToolCalls: true,
|
||||
enableParallelToolCalling: false,
|
||||
isSubagentRun: true,
|
||||
}
|
||||
|
||||
const promptRegistry = PromptRegistry.getInstance()
|
||||
const systemPrompt = (await promptRegistry.get(context)) + SUBAGENT_SYSTEM_SUFFIX
|
||||
const nativeTools = this.buildNativeTools(context)
|
||||
|
||||
if (!nativeTools || nativeTools.length === 0) {
|
||||
const error = "Subagent tool requires native tool calling support."
|
||||
onProgress({ status: "failed", error, stats })
|
||||
return { status: "failed", error, stats }
|
||||
}
|
||||
|
||||
if (this.shouldAbort()) {
|
||||
await this.abort()
|
||||
const error = "Subagent run cancelled."
|
||||
onProgress({ status: "failed", error, stats: { ...stats } })
|
||||
return { status: "failed", error, stats }
|
||||
}
|
||||
|
||||
const conversation: ClineStorageMessage[] = [
|
||||
{
|
||||
role: "user",
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: prompt,
|
||||
} as ClineTextContentBlock,
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
while (true) {
|
||||
const streamHandler = new StreamResponseHandler()
|
||||
const { toolUseHandler } = streamHandler.getHandlers()
|
||||
|
||||
let assistantText = ""
|
||||
let assistantTextSignature: string | undefined
|
||||
let requestId: string | undefined
|
||||
|
||||
const stream = api.createMessage(systemPrompt, conversation, nativeTools)
|
||||
|
||||
for await (const chunk of stream) {
|
||||
switch (chunk.type) {
|
||||
case "usage":
|
||||
requestId = requestId ?? chunk.id
|
||||
stats.inputTokens += chunk.inputTokens || 0
|
||||
stats.outputTokens += chunk.outputTokens || 0
|
||||
stats.cacheWriteTokens += chunk.cacheWriteTokens || 0
|
||||
stats.cacheReadTokens += chunk.cacheReadTokens || 0
|
||||
onProgress({ stats: { ...stats } })
|
||||
break
|
||||
case "text":
|
||||
requestId = requestId ?? chunk.id
|
||||
assistantText += chunk.text || ""
|
||||
assistantTextSignature = chunk.signature || assistantTextSignature
|
||||
break
|
||||
case "tool_calls":
|
||||
requestId = requestId ?? chunk.id
|
||||
toolUseHandler.processToolUseDelta(
|
||||
{
|
||||
id: chunk.tool_call.function?.id,
|
||||
type: "tool_use",
|
||||
name: chunk.tool_call.function?.name,
|
||||
input: normalizeToolCallArguments(chunk.tool_call.function?.arguments),
|
||||
signature: chunk.signature,
|
||||
},
|
||||
chunk.tool_call.call_id,
|
||||
)
|
||||
break
|
||||
case "reasoning":
|
||||
requestId = requestId ?? chunk.id
|
||||
break
|
||||
}
|
||||
|
||||
if (this.shouldAbort()) {
|
||||
await this.abort()
|
||||
const error = "Subagent run cancelled."
|
||||
onProgress({ status: "failed", error, stats: { ...stats } })
|
||||
return { status: "failed", error, stats }
|
||||
}
|
||||
}
|
||||
|
||||
const finalizedToolCalls = toolUseHandler.getAllFinalizedToolUses()
|
||||
const assistantContent = [] as any[]
|
||||
if (assistantText.trim().length > 0) {
|
||||
assistantContent.push({
|
||||
type: "text",
|
||||
text: assistantText,
|
||||
signature: assistantTextSignature,
|
||||
})
|
||||
}
|
||||
assistantContent.push(...finalizedToolCalls)
|
||||
|
||||
if (assistantContent.length > 0) {
|
||||
conversation.push({
|
||||
role: "assistant",
|
||||
content: assistantContent,
|
||||
id: requestId,
|
||||
})
|
||||
}
|
||||
|
||||
if (finalizedToolCalls.length === 0) {
|
||||
if (assistantText.trim().length > 0) {
|
||||
onProgress({ status: "completed", result: assistantText.trim(), stats: { ...stats } })
|
||||
return { status: "completed", result: assistantText.trim(), stats }
|
||||
}
|
||||
|
||||
const error = "Subagent ended without a final text response."
|
||||
onProgress({ status: "failed", error, stats: { ...stats } })
|
||||
return { status: "failed", error, stats }
|
||||
}
|
||||
|
||||
const toolResultBlocks = [] as any[]
|
||||
for (const call of finalizedToolCalls) {
|
||||
const toolName = call.name as ClineDefaultTool
|
||||
|
||||
if (!SUBAGENT_ALLOWED_TOOLS.includes(toolName)) {
|
||||
const deniedResult = formatResponse.toolError(`Tool '${toolName}' is not available inside subagent runs.`)
|
||||
toolResultBlocks.push({
|
||||
type: "tool_result",
|
||||
tool_use_id: call.id || call.call_id,
|
||||
call_id: call.call_id,
|
||||
content: deniedResult,
|
||||
})
|
||||
continue
|
||||
}
|
||||
|
||||
const toolCallParams = toToolUseParams(call.input)
|
||||
|
||||
const toolCallBlock: ToolUse = {
|
||||
type: "tool_use",
|
||||
name: toolName,
|
||||
params: toolCallParams,
|
||||
partial: false,
|
||||
isNativeToolCall: true,
|
||||
call_id: call.call_id,
|
||||
signature: call.signature,
|
||||
}
|
||||
|
||||
if (call.call_id && call.id) {
|
||||
state.toolUseIdMap.set(call.call_id, call.id)
|
||||
}
|
||||
|
||||
const subagentConfig = this.createSubagentTaskConfig(state)
|
||||
const handler = this.baseConfig.coordinator.getHandler(toolName)
|
||||
let toolResult: unknown
|
||||
|
||||
if (!handler) {
|
||||
toolResult = formatResponse.toolError(`No handler registered for tool '${toolName}'.`)
|
||||
} else {
|
||||
try {
|
||||
toolResult = await handler.execute(subagentConfig, toolCallBlock)
|
||||
} catch (error) {
|
||||
toolResult = formatResponse.toolError((error as Error).message)
|
||||
}
|
||||
}
|
||||
|
||||
stats.toolCalls += 1
|
||||
onProgress({ stats: { ...stats } })
|
||||
|
||||
toolResultBlocks.push({
|
||||
type: "tool_result",
|
||||
tool_use_id: call.id || call.call_id,
|
||||
call_id: call.call_id,
|
||||
content: serializeToolResult(toolResult),
|
||||
})
|
||||
}
|
||||
|
||||
conversation.push({
|
||||
role: "user",
|
||||
content: toolResultBlocks,
|
||||
})
|
||||
|
||||
await delay(0)
|
||||
}
|
||||
} catch (error) {
|
||||
if (this.shouldAbort()) {
|
||||
const cancelledError = "Subagent run cancelled."
|
||||
onProgress({ status: "failed", error: cancelledError, stats: { ...stats } })
|
||||
return { status: "failed", error: cancelledError, stats }
|
||||
}
|
||||
|
||||
const errorText = (error as Error).message || "Subagent execution failed."
|
||||
Logger.error("[SubagentRunner] run failed", error)
|
||||
onProgress({ status: "failed", error: errorText, stats: { ...stats } })
|
||||
return { status: "failed", error: errorText, stats }
|
||||
} finally {
|
||||
this.activeApiAbort = undefined
|
||||
}
|
||||
}
|
||||
|
||||
private createSubagentTaskConfig(state: TaskState): TaskConfig {
|
||||
const baseCallbacks = this.baseConfig.callbacks
|
||||
|
||||
return {
|
||||
...this.baseConfig,
|
||||
taskState: state,
|
||||
isSubagentExecution: true,
|
||||
callbacks: {
|
||||
...baseCallbacks,
|
||||
executeCommandTool: async (command: string, timeoutSeconds: number | undefined) => {
|
||||
this.activeCommandExecutions += 1
|
||||
try {
|
||||
return await baseCallbacks.executeCommandTool(command, timeoutSeconds)
|
||||
} finally {
|
||||
this.activeCommandExecutions = Math.max(0, this.activeCommandExecutions - 1)
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
private buildNativeTools(context: SystemPromptContext) {
|
||||
const family = PromptRegistry.getInstance().getModelFamily(context)
|
||||
const toolSets = ClineToolSet.getToolsForVariantWithFallback(family, SUBAGENT_ALLOWED_TOOLS)
|
||||
const filteredToolSpecs = toolSets
|
||||
.map((toolSet) => toolSet.config)
|
||||
.filter((toolSpec) => !toolSpec.contextRequirements || toolSpec.contextRequirements(context))
|
||||
|
||||
const converter = ClineToolSet.getNativeConverter(context.providerInfo.providerId, context.providerInfo.model.id)
|
||||
|
||||
return filteredToolSpecs.map((tool) => converter(tool, context))
|
||||
}
|
||||
}
|
||||
@@ -39,6 +39,7 @@ export interface TaskConfig {
|
||||
doubleCheckCompletionEnabled: boolean
|
||||
vscodeTerminalExecutionMode: "vscodeTerminal" | "backgroundExec"
|
||||
enableParallelToolCalling: boolean
|
||||
isSubagentExecution: boolean
|
||||
context: vscode.ExtensionContext
|
||||
|
||||
// Multi-workspace support (optional for backward compatibility)
|
||||
@@ -105,6 +106,7 @@ export interface TaskCallbacks {
|
||||
removeLastPartialMessageIfExistsWithType: (type: "ask" | "say", askOrSay: ClineAsk | ClineSay) => Promise<void>
|
||||
|
||||
executeCommandTool: (command: string, timeoutSeconds: number | undefined) => Promise<[boolean, any]>
|
||||
cancelRunningCommandTool?: () => Promise<boolean>
|
||||
|
||||
doesLatestTaskCompletionHaveNewChanges: () => Promise<boolean>
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ export const TASK_CONFIG_KEYS = [
|
||||
"doubleCheckCompletionEnabled",
|
||||
"vscodeTerminalExecutionMode",
|
||||
"enableParallelToolCalling",
|
||||
"isSubagentExecution",
|
||||
"context",
|
||||
"taskState",
|
||||
"messageState",
|
||||
|
||||
@@ -123,6 +123,10 @@ export class ToolResultUtils {
|
||||
* Handles tool approval flow and processes any user feedback
|
||||
*/
|
||||
static async askApprovalAndPushFeedback(type: ClineAsk, completeMessage: string, config: TaskConfig) {
|
||||
if (config.isSubagentExecution) {
|
||||
return true
|
||||
}
|
||||
|
||||
const { response, text, images, files } = await config.callbacks.ask(type, completeMessage, false)
|
||||
|
||||
if (text || (images && images.length > 0) || (files && files.length > 0)) {
|
||||
@@ -139,9 +143,8 @@ export class ToolResultUtils {
|
||||
// User pressed reject button or responded with a message, which we treat as a rejection
|
||||
config.taskState.didRejectTool = true // Prevent further tool uses in this message
|
||||
return false
|
||||
} else {
|
||||
// User hit the approve button, and may have provided feedback
|
||||
return true
|
||||
}
|
||||
// User hit the approve button, and may have provided feedback
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -100,11 +100,10 @@ export class VscodeTerminalManager implements ITerminalManager {
|
||||
private terminalIds: Set<number> = new Set()
|
||||
private processes: Map<number, VscodeTerminalProcess> = new Map()
|
||||
private disposables: vscode.Disposable[] = []
|
||||
private shellIntegrationTimeout: number = 4000
|
||||
private terminalReuseEnabled: boolean = true
|
||||
private terminalOutputLineLimit: number = 500
|
||||
private subagentTerminalOutputLineLimit: number = 2000
|
||||
private defaultTerminalProfile: string = "default"
|
||||
private shellIntegrationTimeout = 4000
|
||||
private terminalReuseEnabled = true
|
||||
private terminalOutputLineLimit = 500
|
||||
private defaultTerminalProfile = "default"
|
||||
|
||||
constructor() {
|
||||
let disposable: vscode.Disposable | undefined
|
||||
@@ -364,16 +363,8 @@ export class VscodeTerminalManager implements ITerminalManager {
|
||||
this.terminalOutputLineLimit = limit
|
||||
}
|
||||
|
||||
setSubagentTerminalOutputLineLimit(limit: number): void {
|
||||
this.subagentTerminalOutputLineLimit = limit
|
||||
}
|
||||
|
||||
public processOutput(outputLines: string[], overrideLimit?: number, isSubagentCommand?: boolean): string {
|
||||
const limit = isSubagentCommand
|
||||
? overrideLimit !== undefined
|
||||
? overrideLimit
|
||||
: this.subagentTerminalOutputLineLimit
|
||||
: this.terminalOutputLineLimit
|
||||
public processOutput(outputLines: string[], overrideLimit?: number): string {
|
||||
const limit = overrideLimit !== undefined ? overrideLimit : this.terminalOutputLineLimit
|
||||
if (outputLines.length > limit) {
|
||||
const halfLimit = Math.floor(limit / 2)
|
||||
const start = outputLines.slice(0, halfLimit)
|
||||
@@ -425,7 +416,7 @@ export class VscodeTerminalManager implements ITerminalManager {
|
||||
* @param force If true, closes even busy terminals (with warning)
|
||||
* @returns Number of terminals closed
|
||||
*/
|
||||
closeTerminals(filterFn: (terminal: TerminalInfo) => boolean, force: boolean = false): number {
|
||||
closeTerminals(filterFn: (terminal: TerminalInfo) => boolean, force = false): number {
|
||||
const terminalsToClose = this.filterTerminals(filterFn)
|
||||
let closedCount = 0
|
||||
|
||||
|
||||
@@ -1,78 +0,0 @@
|
||||
/**
|
||||
* Pattern to match simplified Cline CLI syntax: cline "prompt" or cline 'prompt'
|
||||
* with optional additional flags after the closing quote
|
||||
*/
|
||||
const CLINE_COMMAND_PATTERN = /^cline\s+(['"])(.+?)\1(\s+.*)?$/
|
||||
|
||||
/**
|
||||
* Detects if a command is a Cline CLI subagent command.
|
||||
*
|
||||
* Matches the simplified syntax: cline "prompt" or cline 'prompt'
|
||||
* This allows the system to apply subagent-specific settings like autonomous execution.
|
||||
*
|
||||
* @param command - The command string to check
|
||||
* @returns True if the command is a Cline CLI subagent command, false otherwise
|
||||
*/
|
||||
export function isSubagentCommand(command: string): boolean {
|
||||
// Match simplified syntaxes
|
||||
// cline "prompt"
|
||||
// cline 'prompt'
|
||||
return CLINE_COMMAND_PATTERN.test(command)
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms simplified Cline CLI command syntax with subagent settings.
|
||||
*
|
||||
* Converts: cline "prompt" or cline 'prompt'
|
||||
* To: cline "prompt" --json -y
|
||||
*
|
||||
* Preserves additional flags like --cwd:
|
||||
* cline "prompt" --cwd ./path → cline "prompt" --json -y --cwd ./path
|
||||
*
|
||||
* This enables autonomous subagent execution with proper CLI flags for automation.
|
||||
*
|
||||
* @param command - The command string to potentially transform
|
||||
* @returns The transformed command if it matches the pattern, otherwise the original command
|
||||
*/
|
||||
export function transformClineCommand(command: string): string {
|
||||
if (!isSubagentCommand(command)) {
|
||||
return command
|
||||
}
|
||||
|
||||
// Inject subagent-specific command structure and settings
|
||||
const commandWithSettings = injectSubagentSettings(command)
|
||||
|
||||
return commandWithSettings
|
||||
}
|
||||
|
||||
/**
|
||||
* Injects subagent-specific command structure and settings into Cline CLI commands.
|
||||
*
|
||||
* @param command - The Cline CLI command (simplified or full syntax)
|
||||
* @returns The command with injected flags and settings
|
||||
*/
|
||||
function injectSubagentSettings(command: string): string {
|
||||
// No pre-prompt flags needed - use standard "cline 'prompt'" syntax
|
||||
const prePromptFlags: string[] = []
|
||||
|
||||
// Flags/settings to insert after the prompt
|
||||
const postPromptFlags = ["--json", "-y"]
|
||||
|
||||
const match = command.match(CLINE_COMMAND_PATTERN)
|
||||
|
||||
if (match) {
|
||||
const quote = match[1]
|
||||
const prompt = match[2]
|
||||
const additionalFlags = match[3] || ""
|
||||
const prePromptPart = prePromptFlags.length > 0 ? prePromptFlags.join(" ") + " " : ""
|
||||
return `cline ${prePromptPart}${quote}${prompt}${quote} ${postPromptFlags.join(" ")}${additionalFlags}`
|
||||
}
|
||||
|
||||
// Already full format: just inject settings after prompt
|
||||
const parts = command.split(" ")
|
||||
const promptEndIndex = parts.findIndex((p) => p.endsWith('"') || p.endsWith("'"))
|
||||
if (promptEndIndex !== -1) {
|
||||
parts.splice(promptEndIndex + 1, 0, ...postPromptFlags)
|
||||
}
|
||||
return parts.join(" ")
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -68,9 +68,6 @@ export const TRUNCATE_KEEP_LINES = 100
|
||||
/** Default max lines for command output */
|
||||
export const DEFAULT_TERMINAL_OUTPUT_LINE_LIMIT = 500
|
||||
|
||||
/** Max lines for subagent commands (more context needed) */
|
||||
export const DEFAULT_SUBAGENT_TERMINAL_OUTPUT_LINE_LIMIT = 2000
|
||||
|
||||
// =============================================================================
|
||||
// Background Command Tracking
|
||||
// =============================================================================
|
||||
|
||||
@@ -14,11 +14,7 @@
|
||||
|
||||
import { ClineTempManager } from "@services/temp"
|
||||
import * as fs from "fs"
|
||||
import {
|
||||
BACKGROUND_COMMAND_TIMEOUT_MS,
|
||||
DEFAULT_SUBAGENT_TERMINAL_OUTPUT_LINE_LIMIT,
|
||||
DEFAULT_TERMINAL_OUTPUT_LINE_LIMIT,
|
||||
} from "../constants"
|
||||
import { BACKGROUND_COMMAND_TIMEOUT_MS, DEFAULT_TERMINAL_OUTPUT_LINE_LIMIT } from "../constants"
|
||||
import type { BackgroundCommand, ITerminalManager, TerminalInfo, TerminalProcessResultPromise } from "../types"
|
||||
import { StandaloneTerminalProcess } from "./StandaloneTerminalProcess"
|
||||
import { StandaloneTerminalRegistry } from "./StandaloneTerminalRegistry"
|
||||
@@ -81,9 +77,6 @@ export class StandaloneTerminalManager implements ITerminalManager {
|
||||
/** Maximum output lines to keep */
|
||||
private terminalOutputLineLimit: number = DEFAULT_TERMINAL_OUTPUT_LINE_LIMIT
|
||||
|
||||
/** Maximum output lines for subagent commands */
|
||||
private subagentTerminalOutputLineLimit: number = DEFAULT_SUBAGENT_TERMINAL_OUTPUT_LINE_LIMIT
|
||||
|
||||
/** Default terminal profile */
|
||||
private defaultTerminalProfile = "default"
|
||||
|
||||
@@ -227,15 +220,10 @@ export class StandaloneTerminalManager implements ITerminalManager {
|
||||
* Process output lines, potentially truncating if over limit.
|
||||
* @param outputLines Array of output lines
|
||||
* @param overrideLimit Optional limit override
|
||||
* @param isSubagentCommand Whether this is a subagent command
|
||||
* @returns Processed output string
|
||||
*/
|
||||
processOutput(outputLines: string[], overrideLimit?: number, isSubagentCommand?: boolean): string {
|
||||
const limit = isSubagentCommand
|
||||
? overrideLimit !== undefined
|
||||
? overrideLimit
|
||||
: this.subagentTerminalOutputLineLimit
|
||||
: this.terminalOutputLineLimit
|
||||
processOutput(outputLines: string[], overrideLimit?: number): string {
|
||||
const limit = overrideLimit !== undefined ? overrideLimit : this.terminalOutputLineLimit
|
||||
if (outputLines.length > limit) {
|
||||
const halfLimit = Math.floor(limit / 2)
|
||||
const start = outputLines.slice(0, halfLimit)
|
||||
@@ -295,14 +283,6 @@ export class StandaloneTerminalManager implements ITerminalManager {
|
||||
this.terminalOutputLineLimit = limit
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the maximum number of output lines for subagent commands.
|
||||
* @param limit Maximum number of lines
|
||||
*/
|
||||
setSubagentTerminalOutputLineLimit(limit: number): void {
|
||||
this.subagentTerminalOutputLineLimit = limit
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the default terminal profile.
|
||||
* @param profile The profile identifier
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -65,7 +65,6 @@ export interface ExtensionState {
|
||||
terminalReuseEnabled?: boolean
|
||||
terminalOutputLineLimit: number
|
||||
maxConsecutiveMistakes: number
|
||||
subagentTerminalOutputLineLimit: number
|
||||
defaultTerminalProfile?: string
|
||||
vscodeTerminalExecutionMode: string
|
||||
backgroundCommandRunning?: boolean
|
||||
@@ -105,7 +104,6 @@ export interface ExtensionState {
|
||||
dismissedBanners?: Array<{ bannerId: string; dismissedAt: number }>
|
||||
hooksEnabled?: boolean
|
||||
remoteConfigSettings?: Partial<RemoteConfigFields>
|
||||
subagentsEnabled?: boolean
|
||||
globalSkillsToggles?: Record<string, boolean>
|
||||
localSkillsToggles?: Record<string, boolean>
|
||||
nativeToolCallSetting?: boolean
|
||||
@@ -154,6 +152,7 @@ export type ClineAsk =
|
||||
| "condense"
|
||||
| "summarize_task"
|
||||
| "report_bug"
|
||||
| "use_subagents"
|
||||
|
||||
export type ClineSay =
|
||||
| "task"
|
||||
@@ -190,6 +189,8 @@ export type ClineSay =
|
||||
| "task_progress"
|
||||
| "hook_status"
|
||||
| "hook_output_stream"
|
||||
| "subagent"
|
||||
| "use_subagents"
|
||||
| "conditional_rules_applied"
|
||||
|
||||
export interface ClineSayTool {
|
||||
@@ -269,6 +270,31 @@ export interface ClineSayGenerateExplanation {
|
||||
error?: string
|
||||
}
|
||||
|
||||
export type SubagentExecutionStatus = "pending" | "running" | "completed" | "failed"
|
||||
|
||||
export interface SubagentStatusItem {
|
||||
index: number
|
||||
prompt: string
|
||||
status: SubagentExecutionStatus
|
||||
toolCalls: number
|
||||
inputTokens: number
|
||||
outputTokens: number
|
||||
result?: string
|
||||
error?: string
|
||||
}
|
||||
|
||||
export interface ClineSaySubagentStatus {
|
||||
status: "running" | "completed" | "failed"
|
||||
total: number
|
||||
completed: number
|
||||
successes: number
|
||||
failures: number
|
||||
toolCalls: number
|
||||
inputTokens: number
|
||||
outputTokens: number
|
||||
items: SubagentStatusItem[]
|
||||
}
|
||||
|
||||
export type BrowserActionResult = {
|
||||
screenshot?: string
|
||||
logs?: string
|
||||
@@ -284,6 +310,10 @@ export interface ClineAskUseMcpServer {
|
||||
uri?: string
|
||||
}
|
||||
|
||||
export interface ClineAskUseSubagents {
|
||||
prompts: string[]
|
||||
}
|
||||
|
||||
export interface ClinePlanModeResponse {
|
||||
response: string
|
||||
options?: string[]
|
||||
|
||||
@@ -25,6 +25,7 @@ function convertClineAskToProtoEnum(ask: AppClineAsk | undefined): ClineAsk | un
|
||||
condense: ClineAsk.CONDENSE,
|
||||
summarize_task: ClineAsk.SUMMARIZE_TASK,
|
||||
report_bug: ClineAsk.REPORT_BUG,
|
||||
use_subagents: ClineAsk.USE_SUBAGENTS,
|
||||
}
|
||||
|
||||
const result = mapping[ask]
|
||||
@@ -57,6 +58,7 @@ function convertProtoEnumToClineAsk(ask: ClineAsk): AppClineAsk | undefined {
|
||||
[ClineAsk.CONDENSE]: "condense",
|
||||
[ClineAsk.SUMMARIZE_TASK]: "summarize_task",
|
||||
[ClineAsk.REPORT_BUG]: "report_bug",
|
||||
[ClineAsk.USE_SUBAGENTS]: "use_subagents",
|
||||
}
|
||||
|
||||
return mapping[ask]
|
||||
@@ -103,6 +105,8 @@ function convertClineSayToProtoEnum(say: AppClineSay | undefined): ClineSay | un
|
||||
hook_status: ClineSay.HOOK_STATUS,
|
||||
hook_output_stream: ClineSay.HOOK_OUTPUT_STREAM,
|
||||
conditional_rules_applied: ClineSay.CONDITIONAL_RULES_APPLIED,
|
||||
subagent: ClineSay.SUBAGENT_STATUS,
|
||||
use_subagents: ClineSay.USE_SUBAGENTS_SAY,
|
||||
generate_explanation: ClineSay.GENERATE_EXPLANATION,
|
||||
}
|
||||
|
||||
@@ -152,6 +156,8 @@ function convertProtoEnumToClineSay(say: ClineSay): AppClineSay | undefined {
|
||||
[ClineSay.HOOK_STATUS]: "hook_status",
|
||||
[ClineSay.HOOK_OUTPUT_STREAM]: "hook_output_stream",
|
||||
[ClineSay.CONDITIONAL_RULES_APPLIED]: "conditional_rules_applied",
|
||||
[ClineSay.SUBAGENT_STATUS]: "subagent",
|
||||
[ClineSay.USE_SUBAGENTS_SAY]: "use_subagents",
|
||||
}
|
||||
|
||||
return mapping[say]
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 },
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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())
|
||||
|
||||
|
||||
@@ -3,16 +3,6 @@ import { promisify } from "util"
|
||||
|
||||
const execAsync = promisify(exec)
|
||||
|
||||
/**
|
||||
* Parameters used to detect CLI subagent context
|
||||
*/
|
||||
interface CliSubagentDetectionParams {
|
||||
yoloModeToggled: boolean
|
||||
maxConsecutiveMistakes: number
|
||||
isOneshot?: boolean
|
||||
outputFormat?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the Cline CLI tool is installed on the system
|
||||
* @returns true if CLI is installed, false otherwise
|
||||
@@ -34,18 +24,3 @@ export async function isClineCliInstalled(): Promise<boolean> {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect if the current Cline instance is running as a CLI subagent.
|
||||
* CLI subagents are identified by specific parameter patterns set by the transformClineCommand function.
|
||||
* TODO - For now we are relying on the maxConsecutiveMistakes value, which will only ever be "3"
|
||||
* unless users pass in "-s max_consecutive_mistakes=6" via Cline CLI. Would like better detection.
|
||||
* @param params The current task parameters to analyze
|
||||
* @returns true if this appears to be a CLI subagent context
|
||||
*/
|
||||
export function isCliSubagentContext(params: CliSubagentDetectionParams): boolean {
|
||||
const hasYoloMode = params.yoloModeToggled === true
|
||||
const hasHighMistakeLimit = params.maxConsecutiveMistakes === 6
|
||||
|
||||
return hasYoloMode && hasHighMistakeLimit
|
||||
}
|
||||
|
||||
@@ -61,6 +61,8 @@ import QuoteButton from "./QuoteButton"
|
||||
import ReportBugPreview from "./ReportBugPreview"
|
||||
import { RequestStartRow } from "./RequestStartRow"
|
||||
import SearchResultsDisplay from "./SearchResultsDisplay"
|
||||
import SubagentApprovalRow from "./SubagentApprovalRow"
|
||||
import SubagentStatusRow from "./SubagentStatusRow"
|
||||
import { ThinkingRow } from "./ThinkingRow"
|
||||
import UserMessage from "./UserMessage"
|
||||
|
||||
@@ -113,7 +115,7 @@ const ChatRow = memo(
|
||||
// NOTE: it's important we don't distinguish between partial or complete here since our scroll effects in chatview need to handle height change during partial -> complete
|
||||
const isInitialRender = prevHeightRef.current === 0 // prevents scrolling when new element is added since we already scroll for that
|
||||
// height starts off at Infinity
|
||||
if (isLast && height !== 0 && height !== Infinity && height !== prevHeightRef.current) {
|
||||
if (isLast && height !== 0 && height !== Number.POSITIVE_INFINITY && height !== prevHeightRef.current) {
|
||||
if (!isInitialRender) {
|
||||
onHeightChange(height > prevHeightRef.current)
|
||||
}
|
||||
@@ -421,7 +423,8 @@ export const ChatRowContent = memo(
|
||||
marginBottom: "-1.5px",
|
||||
transform: rotation ? `rotate(${rotation}deg)` : undefined,
|
||||
}}
|
||||
title={title}></span>
|
||||
title={title}
|
||||
/>
|
||||
)
|
||||
|
||||
switch (tool.tool) {
|
||||
@@ -763,6 +766,10 @@ export const ChatRowContent = memo(
|
||||
)
|
||||
}
|
||||
|
||||
if (message.ask === "use_subagents" || message.say === "use_subagents") {
|
||||
return <SubagentApprovalRow message={message} />
|
||||
}
|
||||
|
||||
if (message.ask === "use_mcp_server" || message.say === "use_mcp_server") {
|
||||
const useMcpServer = JSON.parse(message.text || "{}") as ClineAskUseMcpServer
|
||||
const server = mcpServers.find((server) => server.name === useMcpServer.serverName)
|
||||
@@ -1084,6 +1091,8 @@ export const ChatRowContent = memo(
|
||||
case "hook_output_stream":
|
||||
// hook_output_stream messages are combined with hook_status messages, so we don't render them separately
|
||||
return <InvisibleSpacer />
|
||||
case "subagent":
|
||||
return <SubagentStatusRow isLast={isLast} lastModifiedMessage={lastModifiedMessage} message={message} />
|
||||
case "shell_integration_warning_with_suggestion":
|
||||
const isBackgroundModeEnabled = vscodeTerminalExecutionMode === "backgroundExec"
|
||||
return (
|
||||
@@ -1158,10 +1167,9 @@ export const ChatRowContent = memo(
|
||||
text={text || ""}
|
||||
/>
|
||||
)
|
||||
} else {
|
||||
// Virtuoso cannot handle zero-height items; render a spacer instead of null
|
||||
return <InvisibleSpacer />
|
||||
}
|
||||
// Virtuoso cannot handle zero-height items; render a spacer instead of null
|
||||
return <InvisibleSpacer />
|
||||
case "followup":
|
||||
let question: string | undefined
|
||||
let options: string[] | undefined
|
||||
|
||||
@@ -2,9 +2,7 @@ import { COMMAND_OUTPUT_STRING, COMMAND_REQ_APP_STRING } from "@shared/combineCo
|
||||
import { ClineMessage } from "@shared/ExtensionMessage"
|
||||
import { StringRequest } from "@shared/proto/cline/common"
|
||||
import { memo, useEffect, useRef } from "react"
|
||||
import { ClineCompactIcon } from "@/assets/ClineCompactIcon"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { PLATFORM_CONFIG, PlatformType } from "@/config/platform.config"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { FileServiceClient } from "@/services/grpc-client"
|
||||
import CodeBlock from "../common/CodeBlock"
|
||||
@@ -168,40 +166,10 @@ export const CommandOutputRow = memo(
|
||||
const showCancelButton =
|
||||
(isCommandExecuting || isCommandPending) && typeof onCancelCommand === "function" && isBackgroundExec
|
||||
|
||||
// Check if this is a Cline subagent command (only on VSCode platform, not JetBrains/standalone)
|
||||
const isSubagentCommand = PLATFORM_CONFIG.type === PlatformType.VSCODE && command.trim().startsWith("cline ")
|
||||
let subagentPrompt: string | undefined
|
||||
|
||||
if (isSubagentCommand) {
|
||||
// Parse the cline command to extract prompt
|
||||
// Format: cline "prompt"
|
||||
const clineCommandRegex = /^cline\s+"([^"]+)"(?:\s+--no-interactive)?/
|
||||
const match = command.match(clineCommandRegex)
|
||||
|
||||
if (match) {
|
||||
subagentPrompt = match[1]
|
||||
}
|
||||
}
|
||||
|
||||
// Customize icon and title for subagent commands
|
||||
const displayIcon = isSubagentCommand ? (
|
||||
<span className="text-foreground mb-[-1.5px]">
|
||||
<ClineCompactIcon />
|
||||
</span>
|
||||
) : (
|
||||
icon
|
||||
)
|
||||
|
||||
const displayTitle = isSubagentCommand ? (
|
||||
<span className="text-foreground font-bold">Cline wants to use a subagent:</span>
|
||||
) : (
|
||||
title
|
||||
)
|
||||
|
||||
const commandHeader = (
|
||||
<div className="flex items-center gap-2.5 mb-3">
|
||||
{displayIcon}
|
||||
{displayTitle}
|
||||
{icon}
|
||||
{title}
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -253,19 +221,9 @@ export const CommandOutputRow = memo(
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isSubagentCommand && subagentPrompt && (
|
||||
<div className="p-2.5 border-b border-editor-group-border">
|
||||
<div className="mb-0">
|
||||
<strong>Prompt:</strong> <span className="ph-no-capture font-editor">{subagentPrompt}</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!isSubagentCommand && (
|
||||
<div className="bg-code opacity-60 text-sm">
|
||||
<CodeBlock forceWrap={true} source={`${"```"}shell\n${command}\n${"```"}`} />
|
||||
</div>
|
||||
)}
|
||||
<div className="bg-code opacity-60 text-sm">
|
||||
<CodeBlock forceWrap={true} source={`${"```"}shell\n${command}\n${"```"}`} />
|
||||
</div>
|
||||
|
||||
{output.length > 0 && (
|
||||
<CommandOutputContent
|
||||
@@ -278,7 +236,7 @@ export const CommandOutputRow = memo(
|
||||
</div>
|
||||
{requestsApproval && (
|
||||
<div className="flex items-center gap-2.5 p-2 text-[12px] text-editor-warning-foreground">
|
||||
<i className="codicon codicon-warning"></i>
|
||||
<i className="codicon codicon-warning" />
|
||||
<span>The model has determined this command requires explicit approval.</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
import { ClineAskUseSubagents, ClineMessage } from "@shared/ExtensionMessage"
|
||||
import { NetworkIcon } from "lucide-react"
|
||||
import { useMemo } from "react"
|
||||
|
||||
interface SubagentApprovalRowProps {
|
||||
message: ClineMessage
|
||||
}
|
||||
|
||||
function parseSubagentApproval(message: ClineMessage): ClineAskUseSubagents | null {
|
||||
if (!message.text) {
|
||||
return null
|
||||
}
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(message.text) as ClineAskUseSubagents
|
||||
if (!Array.isArray(parsed.prompts)) {
|
||||
return null
|
||||
}
|
||||
const prompts = parsed.prompts.map((prompt) => prompt?.trim()).filter((prompt): prompt is string => !!prompt)
|
||||
return { prompts }
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export default function SubagentApprovalRow({ message }: SubagentApprovalRowProps) {
|
||||
const data = useMemo(() => parseSubagentApproval(message), [message])
|
||||
|
||||
if (!data || data.prompts.length === 0) {
|
||||
return <div className="text-foreground opacity-80">Subagent approval details are unavailable.</div>
|
||||
}
|
||||
|
||||
const singular = data.prompts.length === 1
|
||||
const title = singular ? "Cline wants to use a subagent:" : "Cline wants to use subagents:"
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex items-center gap-2.5 mb-3">
|
||||
<NetworkIcon className="size-2 text-foreground" />
|
||||
<span className="font-bold text-foreground">{title}</span>
|
||||
</div>
|
||||
<div className="bg-code border border-editor-group-border rounded-sm py-2.5 px-3">
|
||||
<div className="space-y-2">
|
||||
{data.prompts.map((prompt, index) => (
|
||||
<div
|
||||
className="rounded-xs border border-editor-group-border bg-vscode-editor-background px-2 py-1.5"
|
||||
key={index}>
|
||||
<div className="text-xs font-semibold text-foreground">Prompt {index + 1}</div>
|
||||
<div className="mt-1 text-xs font-editor whitespace-pre-wrap break-words">{prompt}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
import { ClineMessage, ClineSaySubagentStatus, SubagentExecutionStatus } from "@shared/ExtensionMessage"
|
||||
import { CheckIcon, CircleSlashIcon, CircleXIcon, LoaderCircleIcon } from "lucide-react"
|
||||
import { useMemo } from "react"
|
||||
|
||||
interface SubagentStatusRowProps {
|
||||
message: ClineMessage
|
||||
isLast: boolean
|
||||
lastModifiedMessage?: ClineMessage
|
||||
}
|
||||
|
||||
const statusLabel = (status: SubagentExecutionStatus): string => {
|
||||
switch (status) {
|
||||
case "pending":
|
||||
return "Pending"
|
||||
case "running":
|
||||
return "Running"
|
||||
case "completed":
|
||||
return "Completed"
|
||||
case "failed":
|
||||
return "Failed"
|
||||
default:
|
||||
return "Unknown"
|
||||
}
|
||||
}
|
||||
|
||||
const aggregateIcon = (status: ClineSaySubagentStatus["status"]) => {
|
||||
switch (status) {
|
||||
case "running":
|
||||
return <LoaderCircleIcon className="size-2 mr-2 animate-spin text-link" />
|
||||
case "completed":
|
||||
return <CheckIcon className="size-2 mr-2 text-success" />
|
||||
case "failed":
|
||||
return <CircleXIcon className="size-2 mr-2 text-error" />
|
||||
default:
|
||||
return <LoaderCircleIcon className="size-2 mr-2 animate-spin text-link" />
|
||||
}
|
||||
}
|
||||
|
||||
const formatCount = (value: number | undefined): string => {
|
||||
if (!Number.isFinite(value)) {
|
||||
return "0"
|
||||
}
|
||||
|
||||
return Intl.NumberFormat("en-US").format(value || 0)
|
||||
}
|
||||
|
||||
export default function SubagentStatusRow({ message, isLast, lastModifiedMessage }: SubagentStatusRowProps) {
|
||||
const data = useMemo(() => {
|
||||
try {
|
||||
if (!message.text) {
|
||||
return null
|
||||
}
|
||||
return JSON.parse(message.text) as ClineSaySubagentStatus
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}, [message.text])
|
||||
|
||||
if (!data) {
|
||||
return <div className="text-foreground opacity-80">Subagent status update unavailable.</div>
|
||||
}
|
||||
|
||||
const wasCancelled =
|
||||
data.status === "running" &&
|
||||
(!isLast || lastModifiedMessage?.ask === "resume_task" || lastModifiedMessage?.ask === "resume_completed_task")
|
||||
const displayStatus: ClineSaySubagentStatus["status"] = wasCancelled ? "failed" : data.status
|
||||
|
||||
return (
|
||||
<div className="bg-code border border-editor-group-border rounded-sm py-2.5 px-3">
|
||||
<div className="flex items-center">
|
||||
{wasCancelled ? <CircleSlashIcon className="size-2 mr-2" /> : aggregateIcon(displayStatus)}
|
||||
<span className="font-semibold text-foreground">Subagent batch</span>
|
||||
{wasCancelled && <span className="ml-2 text-xs opacity-80">Cancelled</span>}
|
||||
<div className="ml-2 text-xs opacity-80">
|
||||
{data.completed}/{data.total} complete, {data.successes} succeeded, {data.failures} failed
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-2 text-xs opacity-75">
|
||||
{formatCount(data.toolCalls)} tool calls, {formatCount(data.inputTokens)} input tokens,{" "}
|
||||
{formatCount(data.outputTokens)} output tokens
|
||||
</div>
|
||||
<div className="mt-2 space-y-2">
|
||||
{data.items.map((entry) => (
|
||||
<div
|
||||
className="rounded-xs border border-editor-group-border bg-vscode-editor-background px-2 py-1.5"
|
||||
key={entry.index}>
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<div className="text-xs font-medium text-foreground">
|
||||
[{entry.index}] {statusLabel(entry.status)}
|
||||
</div>
|
||||
<div className="text-[11px] opacity-70">
|
||||
{formatCount(entry.toolCalls || 0)} tools, {formatCount(entry.inputTokens || 0)} in,{" "}
|
||||
{formatCount(entry.outputTokens || 0)} out
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-1 text-xs font-editor whitespace-pre-wrap break-words">{entry.prompt}</div>
|
||||
{entry.result && entry.status === "completed" && (
|
||||
<div className="mt-1 text-xs opacity-80 whitespace-pre-wrap break-words">{entry.result}</div>
|
||||
)}
|
||||
{entry.error && entry.status === "failed" && (
|
||||
<div className="mt-1 text-xs text-error whitespace-pre-wrap break-words">{entry.error}</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -63,7 +63,6 @@ export const WelcomeSection: React.FC<WelcomeSectionProps> = ({
|
||||
navigateToSettings,
|
||||
navigateToSettingsModelPicker,
|
||||
navigateToWorktrees,
|
||||
subagentsEnabled,
|
||||
worktreesEnabled,
|
||||
banners,
|
||||
} = useExtensionState()
|
||||
@@ -242,7 +241,7 @@ export const WelcomeSection: React.FC<WelcomeSectionProps> = ({
|
||||
|
||||
// Combine both sources: extension state banners first, then hardcoded banners
|
||||
return [...extensionStateBanners, ...hardcodedBanners]
|
||||
}, [bannerConfig, banners, clineUser, subagentsEnabled, handleBannerAction, handleBannerDismiss])
|
||||
}, [bannerConfig, banners, clineUser, handleBannerAction, handleBannerDismiss])
|
||||
|
||||
return (
|
||||
<div className="flex flex-col flex-1 w-full h-full p-0 m-0">
|
||||
|
||||
@@ -75,6 +75,7 @@ export function useMessageHandlers(messages: ClineMessage[], chatState: ChatStat
|
||||
case "command":
|
||||
case "command_output":
|
||||
case "use_mcp_server":
|
||||
case "use_subagents":
|
||||
case "completion_result":
|
||||
case "mistake_limit_reached":
|
||||
case "api_req_failed":
|
||||
|
||||
@@ -99,6 +99,7 @@ describe("getButtonConfig", () => {
|
||||
{ ask: "followup", expectedConfig: "followup" },
|
||||
{ ask: "browser_action_launch", expectedConfig: "browser_action_launch" },
|
||||
{ ask: "use_mcp_server", expectedConfig: "use_mcp_server" },
|
||||
{ ask: "use_subagents", expectedConfig: "use_subagents" },
|
||||
{ ask: "plan_mode_respond", expectedConfig: "plan_mode_respond" },
|
||||
{ ask: "completion_result", expectedConfig: "completion_result" },
|
||||
{ ask: "resume_task", expectedConfig: "resume_task" },
|
||||
|
||||
@@ -101,6 +101,14 @@ export const BUTTON_CONFIGS: Record<string, ButtonConfig> = {
|
||||
primaryAction: "approve",
|
||||
secondaryAction: "reject",
|
||||
},
|
||||
use_subagents: {
|
||||
sendingDisabled: false,
|
||||
enableButtons: true,
|
||||
primaryText: "Approve",
|
||||
secondaryText: "Reject",
|
||||
primaryAction: "approve",
|
||||
secondaryAction: "reject",
|
||||
},
|
||||
followup: {
|
||||
sendingDisabled: false,
|
||||
enableButtons: false,
|
||||
@@ -261,6 +269,8 @@ export function getButtonConfig(message: ClineMessage | undefined, _mode: Mode =
|
||||
return BUTTON_CONFIGS.browser_action_launch
|
||||
case "use_mcp_server":
|
||||
return BUTTON_CONFIGS.use_mcp_server
|
||||
case "use_subagents":
|
||||
return BUTTON_CONFIGS.use_subagents
|
||||
case "plan_mode_respond":
|
||||
return BUTTON_CONFIGS.plan_mode_respond
|
||||
|
||||
|
||||
@@ -1,16 +1,10 @@
|
||||
import { UpdateSettingsRequest } from "@shared/proto/cline/state"
|
||||
import { EmptyRequest } from "@shared/proto/index.cline"
|
||||
import { AlertCircleIcon } from "lucide-react"
|
||||
import { memo, type ReactNode, useCallback, useEffect, useState } from "react"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { memo, type ReactNode, useCallback } from "react"
|
||||
import { Label } from "@/components/ui/label"
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
|
||||
import { Switch } from "@/components/ui/switch"
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"
|
||||
import { PLATFORM_CONFIG, PlatformType } from "@/config/platform.config"
|
||||
import { useExtensionState } from "@/context/ExtensionStateContext"
|
||||
import { StateServiceClient } from "@/services/grpc-client"
|
||||
import { isMacOSOrLinux } from "@/utils/platformUtils"
|
||||
import Section from "../Section"
|
||||
import SettingsSlider from "../SettingsSlider"
|
||||
import { updateSetting } from "../utils/settingsHandlers"
|
||||
@@ -203,40 +197,12 @@ const FeatureSettingsSection = ({ renderSectionHeader }: FeatureSettingsSectionP
|
||||
worktreesEnabled,
|
||||
focusChainSettings,
|
||||
remoteConfigSettings,
|
||||
subagentsEnabled,
|
||||
subagentTerminalOutputLineLimit,
|
||||
nativeToolCallSetting,
|
||||
enableParallelToolCalling,
|
||||
backgroundEditEnabled,
|
||||
doubleCheckCompletionEnabled,
|
||||
} = useExtensionState()
|
||||
|
||||
const [isClineCliInstalled, setIsClineCliInstalled] = useState(false)
|
||||
|
||||
// Poll for CLI installation status while the component is mounted
|
||||
useEffect(() => {
|
||||
const checkInstallation = async () => {
|
||||
try {
|
||||
const result = await StateServiceClient.checkCliInstallation(EmptyRequest.create())
|
||||
setIsClineCliInstalled(result.value)
|
||||
} catch (error) {
|
||||
console.error("Failed to check CLI installation:", error)
|
||||
}
|
||||
}
|
||||
|
||||
checkInstallation()
|
||||
const pollInterval = setInterval(checkInstallation, 1500)
|
||||
return () => clearInterval(pollInterval)
|
||||
}, [])
|
||||
|
||||
const handleInstallCli = useCallback(async () => {
|
||||
try {
|
||||
await StateServiceClient.installClineCli(EmptyRequest.create())
|
||||
} catch (error) {
|
||||
console.error("Failed to initiate CLI installation:", error)
|
||||
}
|
||||
}, [])
|
||||
|
||||
const handleFocusChainIntervalChange = useCallback(
|
||||
(value: number) => {
|
||||
updateSetting("focusChainSettings", { ...focusChainSettings, remindClineInterval: value })
|
||||
@@ -244,7 +210,6 @@ const FeatureSettingsSection = ({ renderSectionHeader }: FeatureSettingsSectionP
|
||||
[focusChainSettings],
|
||||
)
|
||||
|
||||
const showSubagents = isMacOSOrLinux() && PLATFORM_CONFIG.type === PlatformType.VSCODE
|
||||
const isYoloRemoteLocked = remoteConfigSettings?.yoloModeToggled !== undefined
|
||||
|
||||
// State lookup for mapped features
|
||||
@@ -336,45 +301,6 @@ const FeatureSettingsSection = ({ renderSectionHeader }: FeatureSettingsSectionP
|
||||
<div
|
||||
className="relative p-3 pt-0 my-3 rounded-md border border-editor-widget-border/50"
|
||||
id="experimental-features">
|
||||
{/* Subagents - Only show on macOS and Linux */}
|
||||
{showSubagents && (
|
||||
<>
|
||||
<FeatureRow
|
||||
checked={subagentsEnabled}
|
||||
description="Delegate tasks to specialized sub-agents (experimental)"
|
||||
disabled={!isClineCliInstalled}
|
||||
label="Subagents"
|
||||
onChange={(checked) => updateSetting("subagentsEnabled", checked)}
|
||||
/>
|
||||
<div className="mt-1.5 mb-2 px-2 pt-0.5 pb-1.5 rounded">
|
||||
<p className="text-xs mb-2 flex items-start text-input-warning-foreground">
|
||||
<span>
|
||||
<AlertCircleIcon className="inline-flex !size-1 mr-1" />
|
||||
Cline CLI is required for subagents. Install it with
|
||||
<code className="px-1">npm install -g cline</code>, then run
|
||||
<code className="px-1">cline auth</code>
|
||||
to authenticate with Cline or configure an API provider.
|
||||
</span>
|
||||
</p>
|
||||
{!isClineCliInstalled && (
|
||||
<Button className="w-full" onClick={handleInstallCli} variant="secondary">
|
||||
Install Now
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
{subagentsEnabled && (
|
||||
<SettingsSlider
|
||||
description="Maximum number of lines to include in output from CLI subagents. Truncates middle to save tokens."
|
||||
label="Output Limit (100-5000)"
|
||||
max={5000}
|
||||
min={100}
|
||||
onChange={(value) => updateSetting("subagentTerminalOutputLineLimit", value)}
|
||||
step={100}
|
||||
value={subagentTerminalOutputLineLimit ?? 2000}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{experimentalFeatures.map((feature) => (
|
||||
<FeatureRow
|
||||
checked={featureState[feature.stateKey]}
|
||||
|
||||
@@ -256,7 +256,6 @@ export const ExtensionStateContextProvider: React.FC<{
|
||||
vscodeTerminalExecutionMode: "vscodeTerminal",
|
||||
terminalOutputLineLimit: 500,
|
||||
maxConsecutiveMistakes: 3,
|
||||
subagentTerminalOutputLineLimit: 2000,
|
||||
defaultTerminalProfile: "default",
|
||||
isNewUser: false,
|
||||
welcomeViewCompleted: false,
|
||||
@@ -277,7 +276,6 @@ export const ExtensionStateContextProvider: React.FC<{
|
||||
backgroundCommandRunning: false,
|
||||
backgroundCommandTaskId: undefined,
|
||||
lastDismissedCliBannerVersion: 0,
|
||||
subagentsEnabled: false,
|
||||
backgroundEditEnabled: false,
|
||||
doubleCheckCompletionEnabled: false,
|
||||
globalSkillsToggles: {},
|
||||
|
||||
Reference in New Issue
Block a user