mirror of
https://github.com/cline/cline.git
synced 2026-09-17 06:47:31 +08:00
Compare commits
18
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c1e275030f | ||
|
|
7a63ff1348 | ||
|
|
741ead7da1 | ||
|
|
b3a4f77d37 | ||
|
|
d0e41fe779 | ||
|
|
7c993284dc | ||
|
|
ace7bec246 | ||
|
|
2fbaf35b64 | ||
|
|
a335cb5aee | ||
|
|
8bb4ede283 | ||
|
|
18ce14cff8 | ||
|
|
453196ac56 | ||
|
|
9ee428926f | ||
|
|
a197b0d06e | ||
|
|
ba7ee64abc | ||
|
|
aa9448bf75 | ||
|
|
8d30992ea0 | ||
|
|
7a725934d3 |
@@ -147,6 +147,17 @@ Required steps:
|
||||
|
||||
Common mistake: Adding only the return value without the `context.globalState.get()` call. This compiles but the value is always `undefined` on load.
|
||||
|
||||
Settings plumbing gotcha: if a key is user-toggleable from settings, wire both controller update paths:
|
||||
- `src/core/controller/state/updateSettings.ts` for webview `updateSetting(...)`
|
||||
- `src/core/controller/state/updateSettingsCli.ts` for CLI/ACP settings updates
|
||||
Missing one path causes a toggle to appear to change in one surface while the backend state stays unchanged.
|
||||
|
||||
Webview toggle gotcha: settings changes must also round-trip back in state payloads.
|
||||
- Add the field to `UpdateSettingsRequest` in `proto/cline/state.proto` (for webview update requests), then run `npm run protos`
|
||||
- Include the key in `Controller.getStateToPostToWebview()` (`src/core/controller/index.ts`)
|
||||
- Ensure `ExtensionState` and webview defaults include the key (`src/shared/ExtensionMessage.ts`, `webview-ui/src/context/ExtensionStateContext.tsx`)
|
||||
If this round-trip wiring is missing, the backend value can update but the toggle in webview appears stuck or reverts.
|
||||
|
||||
## StateManager Cache vs Direct globalState Access
|
||||
StateManager uses an in-memory cache populated during `StateManager.initialize(context)` in `common.ts`. For most state, use `controller.stateManager.setGlobalState()`/`getGlobalStateKey()`.
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -312,6 +312,10 @@ function translateSayMessage(
|
||||
// API request finished - no specific update needed
|
||||
break
|
||||
|
||||
case "subagent_usage":
|
||||
// Hidden aggregate metrics event used for task-level accounting.
|
||||
break
|
||||
|
||||
case "task":
|
||||
// Task started - don't echo the user's prompt back to them
|
||||
// The ACP client already knows what they typed
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
import type { ClineMessage } from "@shared/ExtensionMessage"
|
||||
import { render } from "ink-testing-library"
|
||||
import React from "react"
|
||||
import { describe, expect, it, vi } from "vitest"
|
||||
import { ChatMessage } from "./ChatMessage"
|
||||
|
||||
vi.mock("../hooks/useTerminalSize", () => ({
|
||||
useTerminalSize: () => ({
|
||||
columns: 120,
|
||||
rows: 40,
|
||||
resizeKey: 0,
|
||||
}),
|
||||
}))
|
||||
|
||||
describe("ChatMessage subagent rendering", () => {
|
||||
it("renders subagent approval prompts as a tree", () => {
|
||||
const message: ClineMessage = {
|
||||
ts: Date.now(),
|
||||
type: "ask",
|
||||
ask: "use_subagents",
|
||||
text: JSON.stringify({
|
||||
prompts: [
|
||||
"Find codebase stats and size",
|
||||
"Find funny comments and easter eggs",
|
||||
"Find unusual patterns and history",
|
||||
],
|
||||
}),
|
||||
}
|
||||
|
||||
const { lastFrame } = render(React.createElement(ChatMessage, { message, mode: "act" }))
|
||||
const frame = lastFrame() || ""
|
||||
|
||||
expect(frame).toContain("Cline wants to run subagents")
|
||||
expect(frame).toContain("├─ Find codebase stats and size")
|
||||
expect(frame).toContain("├─ Find funny comments and easter eggs")
|
||||
expect(frame).toContain("└─ Find unusual patterns and history")
|
||||
})
|
||||
|
||||
it("renders subagent progress rows with compact token stats and completion checks", () => {
|
||||
const message: ClineMessage = {
|
||||
ts: Date.now(),
|
||||
type: "say",
|
||||
say: "subagent",
|
||||
text: JSON.stringify({
|
||||
status: "running",
|
||||
total: 3,
|
||||
completed: 1,
|
||||
successes: 1,
|
||||
failures: 0,
|
||||
toolCalls: 21,
|
||||
inputTokens: 0,
|
||||
outputTokens: 0,
|
||||
contextWindow: 0,
|
||||
maxContextTokens: 0,
|
||||
maxContextUsagePercentage: 0,
|
||||
items: [
|
||||
{
|
||||
index: 1,
|
||||
prompt: "Find codebase stats and size",
|
||||
status: "completed",
|
||||
toolCalls: 5,
|
||||
inputTokens: 0,
|
||||
outputTokens: 0,
|
||||
totalCost: 0.034,
|
||||
contextTokens: 24400,
|
||||
contextWindow: 200000,
|
||||
contextUsagePercentage: 12.2,
|
||||
},
|
||||
{
|
||||
index: 2,
|
||||
prompt: "Find funny comments and easter eggs",
|
||||
status: "running",
|
||||
toolCalls: 11,
|
||||
inputTokens: 0,
|
||||
outputTokens: 0,
|
||||
totalCost: 0.056,
|
||||
contextTokens: 31600,
|
||||
contextWindow: 200000,
|
||||
contextUsagePercentage: 15.8,
|
||||
},
|
||||
{
|
||||
index: 3,
|
||||
prompt: "Find unusual patterns and history",
|
||||
status: "pending",
|
||||
toolCalls: 5,
|
||||
inputTokens: 0,
|
||||
outputTokens: 0,
|
||||
totalCost: 0,
|
||||
contextTokens: 28900,
|
||||
contextWindow: 200000,
|
||||
contextUsagePercentage: 14.4,
|
||||
},
|
||||
],
|
||||
}),
|
||||
}
|
||||
|
||||
const { lastFrame } = render(React.createElement(ChatMessage, { isStreaming: true, message, mode: "act" }))
|
||||
const frame = lastFrame() || ""
|
||||
|
||||
expect(frame).toContain("Cline is running subagents")
|
||||
expect(frame).toContain("✓ Find codebase stats and size")
|
||||
expect(frame).toContain("5 tool uses · 24.4k tokens · $0.03")
|
||||
expect(frame).toContain("11 tool uses · 31.6k tokens · $0.06")
|
||||
expect(frame).toContain("5 tool uses · 28.9k tokens · $0.00")
|
||||
})
|
||||
})
|
||||
@@ -17,6 +17,7 @@ import { useTerminalSize } from "../hooks/useTerminalSize"
|
||||
import { jsonParseSafe } from "../utils/parser"
|
||||
import { getToolDescription, isFileEditTool, parseToolFromMessage } from "../utils/tools"
|
||||
import { DiffView } from "./DiffView"
|
||||
import { SubagentMessage } from "./SubagentMessage"
|
||||
|
||||
/**
|
||||
* Add "(Tab)" hint after "Act mode" mentions.
|
||||
@@ -24,7 +25,7 @@ import { DiffView } from "./DiffView"
|
||||
* Matches just "Act mode" without requiring "to " prefix because markdown
|
||||
* processing may split "toggle to **Act mode**" into separate text chunks.
|
||||
*/
|
||||
function addActModeHint(text: string): React.ReactNode[] {
|
||||
function addActModeHint(text: string, keyPrefix: string): React.ReactNode[] {
|
||||
// Match "Act mode" in various capitalizations, but not if already followed by (Tab)
|
||||
const actModeRegex = /\bact\s+mode\b(?!\s*\(tab\))/gi
|
||||
const parts = text.split(actModeRegex)
|
||||
@@ -41,7 +42,7 @@ function addActModeHint(text: string): React.ReactNode[] {
|
||||
}
|
||||
if (matches[i]) {
|
||||
nodes.push(
|
||||
<React.Fragment key={`act-mode-${i}`}>
|
||||
<React.Fragment key={`${keyPrefix}-act-mode-${i}`}>
|
||||
{matches[i]}
|
||||
<Text color="gray"> (Tab)</Text>
|
||||
</React.Fragment>,
|
||||
@@ -59,6 +60,8 @@ function addActModeHint(text: string): React.ReactNode[] {
|
||||
*/
|
||||
function renderInlineMarkdown(text: string): React.ReactNode[] {
|
||||
const nodes: React.ReactNode[] = []
|
||||
let hintCallIndex = 0
|
||||
const addHintedText = (value: string) => addActModeHint(value, `hint-${hintCallIndex++}`)
|
||||
// Match **bold**, *italic*, or `code` - order matters (** before *)
|
||||
const regex = /(\*\*[^*]+\*\*|\*[^*]+\*|`[^`]+`)/g
|
||||
let lastIndex = 0
|
||||
@@ -68,7 +71,7 @@ function renderInlineMarkdown(text: string): React.ReactNode[] {
|
||||
// Add text before match (with Act Mode hint processing)
|
||||
if (match.index > lastIndex) {
|
||||
const beforeText = text.slice(lastIndex, match.index)
|
||||
nodes.push(...addActModeHint(beforeText))
|
||||
nodes.push(...addHintedText(beforeText))
|
||||
}
|
||||
|
||||
const fullMatch = match[0]
|
||||
@@ -77,7 +80,7 @@ function renderInlineMarkdown(text: string): React.ReactNode[] {
|
||||
if (fullMatch.startsWith("**") && fullMatch.endsWith("**")) {
|
||||
// Bold - also process for Act Mode hints inside bold text
|
||||
const boldContent = fullMatch.slice(2, -2)
|
||||
const hintedContent = addActModeHint(boldContent)
|
||||
const hintedContent = addHintedText(boldContent)
|
||||
nodes.push(
|
||||
<Text bold key={key}>
|
||||
{hintedContent}
|
||||
@@ -100,10 +103,10 @@ function renderInlineMarkdown(text: string): React.ReactNode[] {
|
||||
|
||||
// Add remaining text (with Act Mode hint processing)
|
||||
if (lastIndex < text.length) {
|
||||
nodes.push(...addActModeHint(text.slice(lastIndex)))
|
||||
nodes.push(...addHintedText(text.slice(lastIndex)))
|
||||
}
|
||||
|
||||
return nodes.length > 0 ? nodes : addActModeHint(text)
|
||||
return nodes.length > 0 ? nodes : addHintedText(text)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -224,7 +227,7 @@ function truncate(text: string, maxLength: number): string {
|
||||
/**
|
||||
* Format tool result for display
|
||||
*/
|
||||
function formatToolResult(result: string, maxLines: number = 5): string[] {
|
||||
function formatToolResult(result: string, maxLines = 5): string[] {
|
||||
const lines = result.split("\n")
|
||||
if (lines.length <= maxLines) {
|
||||
return lines
|
||||
@@ -446,6 +449,10 @@ export const ChatMessage: React.FC<ChatMessageProps> = ({ message, mode, isStrea
|
||||
)
|
||||
}
|
||||
|
||||
if ((type === "ask" && ask === "use_subagents") || say === "use_subagents" || say === "subagent") {
|
||||
return <SubagentMessage isStreaming={isStreaming} message={message} mode={mode} />
|
||||
}
|
||||
|
||||
// MCP response
|
||||
if (say === "mcp_server_response" && text) {
|
||||
const lines = formatToolResult(text, 8)
|
||||
@@ -800,6 +807,8 @@ export const ChatMessageList: React.FC<ChatMessageListProps> = ({ messages, maxM
|
||||
const displayMessages = messages.filter((m) => {
|
||||
// Skip api_req_finished, they're just markers
|
||||
if (m.say === "api_req_finished") return false
|
||||
// Skip hidden aggregated usage messages
|
||||
if (m.say === "subagent_usage") return false
|
||||
// Skip empty text messages
|
||||
if (m.say === "text" && !m.text?.trim()) return false
|
||||
// Skip checkpoint messages
|
||||
|
||||
@@ -46,14 +46,7 @@ export interface SkillInfo {
|
||||
enabled: boolean
|
||||
}
|
||||
|
||||
export const EXCLUDED_KEYS = new Set([
|
||||
"taskHistory",
|
||||
"primaryRootIndex",
|
||||
"subagentsEnabled",
|
||||
"subagentTerminalOutputLineLimit",
|
||||
"welcomeViewCompleted",
|
||||
"isNewUser",
|
||||
])
|
||||
export const EXCLUDED_KEYS = new Set(["taskHistory", "primaryRootIndex", "welcomeViewCompleted", "isNewUser"])
|
||||
|
||||
export const EDITABLE_TYPES: Set<ValueType> = new Set(["string", "number", "boolean"])
|
||||
export const MAX_VISIBLE = 12
|
||||
@@ -135,7 +128,7 @@ export function parseValue(input: string, type: ValueType): unknown {
|
||||
return input.toLowerCase() === "true" || input === "1"
|
||||
}
|
||||
if (type === "number") {
|
||||
const num = parseFloat(input)
|
||||
const num = Number.parseFloat(input)
|
||||
return Number.isNaN(num) ? 0 : num
|
||||
}
|
||||
if (type === "object") {
|
||||
|
||||
@@ -82,6 +82,12 @@ const TABS: PanelTab[] = [
|
||||
|
||||
// Settings configuration for simple boolean toggles
|
||||
const FEATURE_SETTINGS = {
|
||||
subagents: {
|
||||
stateKey: "subagentsEnabled",
|
||||
default: false,
|
||||
label: "Subagents",
|
||||
description: "Let Cline run focused subagents in parallel to explore the codebase for you",
|
||||
},
|
||||
autoCondense: {
|
||||
stateKey: "useAutoCondense",
|
||||
default: false,
|
||||
|
||||
@@ -0,0 +1,343 @@
|
||||
import type { ClineAskUseSubagents, ClineMessage, ClineSaySubagentStatus } from "@shared/ExtensionMessage"
|
||||
import { Box, Text } from "ink"
|
||||
import Spinner from "ink-spinner"
|
||||
import React from "react"
|
||||
import { COLORS } from "../constants/colors"
|
||||
import { useTerminalSize } from "../hooks/useTerminalSize"
|
||||
import { jsonParseSafe } from "../utils/parser"
|
||||
|
||||
interface SubagentMessageProps {
|
||||
message: ClineMessage
|
||||
isStreaming?: boolean
|
||||
mode?: "act" | "plan"
|
||||
}
|
||||
|
||||
const TREE_PREFIX_WIDTH = 5
|
||||
const MIN_PROMPT_WIDTH = 20
|
||||
|
||||
const DotRow: React.FC<{ children: React.ReactNode; color?: string; flashing?: boolean }> = ({
|
||||
children,
|
||||
color,
|
||||
flashing = false,
|
||||
}) => (
|
||||
<Box flexDirection="row">
|
||||
<Box width={2}>
|
||||
{flashing ? (
|
||||
<Text color={color}>
|
||||
<Spinner type="toggle8" />
|
||||
</Text>
|
||||
) : (
|
||||
<Text color={color}>⏺</Text>
|
||||
)}
|
||||
</Box>
|
||||
<Box flexGrow={1}>{children}</Box>
|
||||
</Box>
|
||||
)
|
||||
|
||||
function formatCompactTokens(tokens: number | undefined): string {
|
||||
const value = Number.isFinite(tokens) ? Math.max(0, tokens || 0) : 0
|
||||
return new Intl.NumberFormat("en-US", {
|
||||
notation: "compact",
|
||||
maximumFractionDigits: 1,
|
||||
})
|
||||
.format(value)
|
||||
.toLowerCase()
|
||||
}
|
||||
|
||||
function formatCompactCost(cost: number | undefined): string {
|
||||
const value = Number.isFinite(cost) ? Math.max(0, cost || 0) : 0
|
||||
const maximumFractionDigits = value >= 0.01 ? 2 : 4
|
||||
return new Intl.NumberFormat("en-US", {
|
||||
style: "currency",
|
||||
currency: "USD",
|
||||
minimumFractionDigits: 2,
|
||||
maximumFractionDigits,
|
||||
}).format(value)
|
||||
}
|
||||
|
||||
function formatSubagentStatsValues(
|
||||
toolCalls: number | undefined,
|
||||
contextTokens: number | undefined,
|
||||
totalCost: number | undefined,
|
||||
) {
|
||||
const safeToolCalls = Number.isFinite(toolCalls) ? Math.max(0, toolCalls || 0) : 0
|
||||
const toolUses = safeToolCalls === 1 ? "tool use" : "tool uses"
|
||||
const tokensUsed = formatCompactTokens(contextTokens || 0)
|
||||
const formattedCost = formatCompactCost(totalCost || 0)
|
||||
return `${safeToolCalls} ${toolUses} · ${tokensUsed} tokens · ${formattedCost}`
|
||||
}
|
||||
|
||||
function wrapPrompt(text: string, width: number): string[] {
|
||||
if (!text) {
|
||||
return [""]
|
||||
}
|
||||
|
||||
const normalizedWidth = Math.max(1, width)
|
||||
const wrappedLines: string[] = []
|
||||
const paragraphs = text.split("\n")
|
||||
|
||||
for (const paragraph of paragraphs) {
|
||||
const words = paragraph.trim().split(/\s+/).filter(Boolean)
|
||||
if (words.length === 0) {
|
||||
wrappedLines.push("")
|
||||
continue
|
||||
}
|
||||
|
||||
let line = ""
|
||||
for (const word of words) {
|
||||
if (!line) {
|
||||
if (word.length <= normalizedWidth) {
|
||||
line = word
|
||||
continue
|
||||
}
|
||||
|
||||
let remaining = word
|
||||
while (remaining.length > normalizedWidth) {
|
||||
wrappedLines.push(remaining.slice(0, normalizedWidth))
|
||||
remaining = remaining.slice(normalizedWidth)
|
||||
}
|
||||
line = remaining
|
||||
continue
|
||||
}
|
||||
|
||||
if (line.length + 1 + word.length <= normalizedWidth) {
|
||||
line = `${line} ${word}`
|
||||
continue
|
||||
}
|
||||
|
||||
wrappedLines.push(line)
|
||||
|
||||
if (word.length <= normalizedWidth) {
|
||||
line = word
|
||||
continue
|
||||
}
|
||||
|
||||
let remaining = word
|
||||
while (remaining.length > normalizedWidth) {
|
||||
wrappedLines.push(remaining.slice(0, normalizedWidth))
|
||||
remaining = remaining.slice(normalizedWidth)
|
||||
}
|
||||
line = remaining
|
||||
}
|
||||
|
||||
if (line) {
|
||||
wrappedLines.push(line)
|
||||
}
|
||||
}
|
||||
|
||||
return wrappedLines.length > 0 ? wrappedLines : [text]
|
||||
}
|
||||
|
||||
const TreePromptRow: React.FC<{
|
||||
prefix: React.ReactNode
|
||||
continuationPrefix: string
|
||||
prompt: string
|
||||
promptWidth: number
|
||||
color?: string
|
||||
}> = ({ prefix, continuationPrefix, prompt, promptWidth, color }) => {
|
||||
const lines = wrapPrompt(prompt, promptWidth)
|
||||
|
||||
return (
|
||||
<Box flexDirection="column" width="100%">
|
||||
{lines.map((line, index) => (
|
||||
<Box flexDirection="row" key={`${line}-${index}`} width="100%">
|
||||
<Box flexShrink={0} width={TREE_PREFIX_WIDTH}>
|
||||
{index === 0 ? prefix : <Text color="gray">{continuationPrefix}</Text>}
|
||||
</Box>
|
||||
<Box flexGrow={1}>
|
||||
<Text color={color}>{line}</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
const TreeStatsRow: React.FC<{ prefix: string; stats: string }> = ({ prefix, stats }) => (
|
||||
<Box flexDirection="row" width="100%">
|
||||
<Box flexShrink={0} width={TREE_PREFIX_WIDTH}>
|
||||
<Text color="gray">{prefix}</Text>
|
||||
</Box>
|
||||
<Box flexGrow={1}>
|
||||
<Text color="gray">⎿ {stats}</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
)
|
||||
|
||||
export const SubagentMessage: React.FC<SubagentMessageProps> = ({ message, mode, isStreaming }) => {
|
||||
const { type, ask, say, text, partial } = message
|
||||
const toolColor = mode === "plan" ? "yellow" : COLORS.primaryBlue
|
||||
const { columns } = useTerminalSize()
|
||||
const promptWidth = Math.max(MIN_PROMPT_WIDTH, columns - 2 - TREE_PREFIX_WIDTH)
|
||||
|
||||
if ((type === "ask" && ask === "use_subagents") || say === "use_subagents") {
|
||||
const parsed = text
|
||||
? jsonParseSafe<ClineAskUseSubagents>(text, {
|
||||
prompts: [],
|
||||
})
|
||||
: { prompts: [] }
|
||||
|
||||
const prompts = (parsed.prompts || []).map((prompt) => prompt?.trim()).filter(Boolean)
|
||||
if (prompts.length === 0) {
|
||||
return (
|
||||
<Box flexDirection="column" marginBottom={1} width="100%">
|
||||
<DotRow color={toolColor}>
|
||||
<Text color={toolColor}>Cline wants to run subagents:</Text>
|
||||
</DotRow>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
const singular = prompts.length === 1
|
||||
return (
|
||||
<Box flexDirection="column" marginBottom={1} width="100%">
|
||||
<DotRow color={toolColor} flashing={partial === true && isStreaming}>
|
||||
<Text color={toolColor}>{singular ? "Cline wants to run a subagent:" : "Cline wants to run subagents:"}</Text>
|
||||
</DotRow>
|
||||
<Box flexDirection="column" marginLeft={2} width="100%">
|
||||
{prompts.map((prompt, index) => {
|
||||
const isLastPrompt = index === prompts.length - 1
|
||||
const branch = isLastPrompt ? "└─" : "├─"
|
||||
const continuationPrefix = isLastPrompt ? " " : "│ "
|
||||
const shouldShowPromptStats = partial !== true || !isLastPrompt
|
||||
return (
|
||||
<Box flexDirection="column" key={`${prompt}-${index}`}>
|
||||
<TreePromptRow
|
||||
color={toolColor}
|
||||
continuationPrefix={continuationPrefix}
|
||||
prefix={<Text color={toolColor}>{`${branch} `}</Text>}
|
||||
prompt={prompt}
|
||||
promptWidth={promptWidth}
|
||||
/>
|
||||
{shouldShowPromptStats && (
|
||||
<TreeStatsRow
|
||||
prefix={continuationPrefix}
|
||||
stats={formatSubagentStatsValues(undefined, undefined, undefined)}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
)
|
||||
})}
|
||||
</Box>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
if (say === "subagent" && text) {
|
||||
const parsed = jsonParseSafe<ClineSaySubagentStatus>(text, {
|
||||
status: "running",
|
||||
total: 0,
|
||||
completed: 0,
|
||||
successes: 0,
|
||||
failures: 0,
|
||||
toolCalls: 0,
|
||||
inputTokens: 0,
|
||||
outputTokens: 0,
|
||||
contextWindow: 0,
|
||||
maxContextTokens: 0,
|
||||
maxContextUsagePercentage: 0,
|
||||
items: [],
|
||||
})
|
||||
|
||||
const items = parsed.items || []
|
||||
if (items.length === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<Box flexDirection="column" marginBottom={1} width="100%">
|
||||
<DotRow color={toolColor} flashing={partial === true && isStreaming}>
|
||||
<Text color={toolColor}>
|
||||
{items.length === 1 ? "Cline is running a subagent:" : "Cline is running subagents:"}
|
||||
</Text>
|
||||
</DotRow>
|
||||
<Box flexDirection="column" marginLeft={2} width="100%">
|
||||
{items.map((entry, index) => {
|
||||
const isLastEntry = index === items.length - 1
|
||||
const branch = isLastEntry ? "└─" : "├─"
|
||||
const continuationPrefix = isLastEntry ? " " : "│ "
|
||||
const key = `${entry.index}-${index}`
|
||||
const shouldShowStats = true
|
||||
|
||||
if (entry.status === "completed") {
|
||||
return (
|
||||
<Box flexDirection="column" key={key}>
|
||||
<TreePromptRow
|
||||
color="green"
|
||||
continuationPrefix={continuationPrefix}
|
||||
prefix={
|
||||
<Box flexDirection="row">
|
||||
<Text color="gray">{`${branch} `}</Text>
|
||||
<Text color="green">✓</Text>
|
||||
</Box>
|
||||
}
|
||||
prompt={entry.prompt}
|
||||
promptWidth={promptWidth}
|
||||
/>
|
||||
<TreeStatsRow
|
||||
prefix={continuationPrefix}
|
||||
stats={formatSubagentStatsValues(entry.toolCalls, entry.contextTokens, entry.totalCost)}
|
||||
/>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
if (entry.status === "failed") {
|
||||
return (
|
||||
<Box flexDirection="column" key={key}>
|
||||
<TreePromptRow
|
||||
color="red"
|
||||
continuationPrefix={continuationPrefix}
|
||||
prefix={
|
||||
<Box flexDirection="row">
|
||||
<Text color="gray">{`${branch} `}</Text>
|
||||
<Text color="red">✗</Text>
|
||||
</Box>
|
||||
}
|
||||
prompt={entry.prompt}
|
||||
promptWidth={promptWidth}
|
||||
/>
|
||||
<TreeStatsRow
|
||||
prefix={continuationPrefix}
|
||||
stats={formatSubagentStatsValues(entry.toolCalls, entry.contextTokens, entry.totalCost)}
|
||||
/>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Box flexDirection="column" key={key}>
|
||||
<TreePromptRow
|
||||
color={toolColor}
|
||||
continuationPrefix={continuationPrefix}
|
||||
prefix={
|
||||
<Box flexDirection="row">
|
||||
<Text color="gray">{branch} </Text>
|
||||
{entry.status === "running" ? (
|
||||
<Text color={toolColor}>
|
||||
<Spinner type="dots" />
|
||||
</Text>
|
||||
) : (
|
||||
<Text color={toolColor}>•</Text>
|
||||
)}
|
||||
</Box>
|
||||
}
|
||||
prompt={entry.prompt}
|
||||
promptWidth={promptWidth}
|
||||
/>
|
||||
{shouldShowStats && (
|
||||
<TreeStatsRow
|
||||
prefix={continuationPrefix}
|
||||
stats={formatSubagentStatsValues(entry.toolCalls, entry.contextTokens, entry.totalCost)}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
)
|
||||
})}
|
||||
</Box>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
Generated
+78
-43
@@ -3128,7 +3128,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.14.3.tgz",
|
||||
"integrity": "sha512-Iq8QQQ/7X3Sac15oB6p0FmUg/klxQvXLeileoqrTRGJYLV+/9tubbr9ipz0GKHjmXVsgFPo/+W+2cA8eNcR+XA==",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@grpc/proto-loader": "^0.8.0",
|
||||
"@js-sdsl/ordered-map": "^4.4.2"
|
||||
@@ -4031,7 +4030,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.26.0.tgz",
|
||||
"integrity": "sha512-Y5RmPncpiDtTXDbLKswIJzTqu2hyBKxTNsgKqKclDbhIgg1wgtf1fRuvxgTnRfcnxtvvgbIEcqUOzZrJ6iSReg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@hono/node-server": "^1.19.9",
|
||||
"ajv": "^8.17.1",
|
||||
@@ -4107,7 +4105,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz",
|
||||
"integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=8.0.0"
|
||||
}
|
||||
@@ -5798,7 +5795,8 @@
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
]
|
||||
],
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@rollup/rollup-android-arm64": {
|
||||
"version": "4.57.1",
|
||||
@@ -5811,7 +5809,8 @@
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
]
|
||||
],
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@rollup/rollup-darwin-arm64": {
|
||||
"version": "4.57.1",
|
||||
@@ -5824,7 +5823,8 @@
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
]
|
||||
],
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@rollup/rollup-darwin-x64": {
|
||||
"version": "4.57.1",
|
||||
@@ -5837,7 +5837,8 @@
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
]
|
||||
],
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@rollup/rollup-freebsd-arm64": {
|
||||
"version": "4.57.1",
|
||||
@@ -5850,7 +5851,8 @@
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
]
|
||||
],
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@rollup/rollup-freebsd-x64": {
|
||||
"version": "4.57.1",
|
||||
@@ -5863,7 +5865,8 @@
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
]
|
||||
],
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
|
||||
"version": "4.57.1",
|
||||
@@ -5876,7 +5879,8 @@
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
],
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
|
||||
"version": "4.57.1",
|
||||
@@ -5889,7 +5893,8 @@
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
],
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm64-gnu": {
|
||||
"version": "4.57.1",
|
||||
@@ -5902,7 +5907,8 @@
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
],
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm64-musl": {
|
||||
"version": "4.57.1",
|
||||
@@ -5915,7 +5921,8 @@
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
],
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-loong64-gnu": {
|
||||
"version": "4.57.1",
|
||||
@@ -5928,7 +5935,8 @@
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
],
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-loong64-musl": {
|
||||
"version": "4.57.1",
|
||||
@@ -5941,7 +5949,8 @@
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
],
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-ppc64-gnu": {
|
||||
"version": "4.57.1",
|
||||
@@ -5954,7 +5963,8 @@
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
],
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-ppc64-musl": {
|
||||
"version": "4.57.1",
|
||||
@@ -5967,7 +5977,8 @@
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
],
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
|
||||
"version": "4.57.1",
|
||||
@@ -5980,7 +5991,8 @@
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
],
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-riscv64-musl": {
|
||||
"version": "4.57.1",
|
||||
@@ -5993,7 +6005,8 @@
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
],
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-s390x-gnu": {
|
||||
"version": "4.57.1",
|
||||
@@ -6006,7 +6019,8 @@
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
],
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-x64-gnu": {
|
||||
"version": "4.57.1",
|
||||
@@ -6019,7 +6033,8 @@
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
],
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-x64-musl": {
|
||||
"version": "4.57.1",
|
||||
@@ -6032,7 +6047,8 @@
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
],
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@rollup/rollup-openbsd-x64": {
|
||||
"version": "4.57.1",
|
||||
@@ -6045,7 +6061,8 @@
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openbsd"
|
||||
]
|
||||
],
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@rollup/rollup-openharmony-arm64": {
|
||||
"version": "4.57.1",
|
||||
@@ -6058,7 +6075,8 @@
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openharmony"
|
||||
]
|
||||
],
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-arm64-msvc": {
|
||||
"version": "4.57.1",
|
||||
@@ -6071,7 +6089,8 @@
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
]
|
||||
],
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-ia32-msvc": {
|
||||
"version": "4.57.1",
|
||||
@@ -6084,7 +6103,8 @@
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
]
|
||||
],
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-x64-gnu": {
|
||||
"version": "4.57.1",
|
||||
@@ -6097,7 +6117,8 @@
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
]
|
||||
],
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-x64-msvc": {
|
||||
"version": "4.57.1",
|
||||
@@ -6110,7 +6131,8 @@
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
]
|
||||
],
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@sap-ai-sdk/ai-api": {
|
||||
"version": "2.6.0",
|
||||
@@ -7782,7 +7804,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.32.tgz",
|
||||
"integrity": "sha512-Ez8QE4DMfhjjTsES9K2dwfV258qBui7qxUsoaixZDiTzbde4U12e1pXGNu/ECsUIOi5/zoCxAQxIhQnaUQ2VvA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"undici-types": "~6.21.0"
|
||||
}
|
||||
@@ -7845,7 +7866,6 @@
|
||||
"integrity": "sha512-KkiJeU6VbYbUOp5ITMIc7kBfqlYkKA5KhEHVrGMmUUMt7NeaZg65ojdPk+FtNrBAOXNVM5QM72jnADjM+XVRAQ==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"csstype": "^3.2.2"
|
||||
}
|
||||
@@ -8780,7 +8800,6 @@
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
|
||||
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
},
|
||||
@@ -9745,7 +9764,6 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"baseline-browser-mapping": "^2.9.0",
|
||||
"caniuse-lite": "^1.0.30001759",
|
||||
@@ -11144,8 +11162,7 @@
|
||||
"version": "0.0.1367902",
|
||||
"resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1367902.tgz",
|
||||
"integrity": "sha512-XxtPuC3PGakY6PD7dG66/o8KwJ/LkH2/EKe19Dcw58w53dv4/vSQEkn/SzuyhHE2q4zPgCkxQBxus3VV4ql+Pg==",
|
||||
"license": "BSD-3-Clause",
|
||||
"peer": true
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/diff": {
|
||||
"version": "5.2.2",
|
||||
@@ -12162,7 +12179,6 @@
|
||||
"resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz",
|
||||
"integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"accepts": "^2.0.0",
|
||||
"body-parser": "^2.2.1",
|
||||
@@ -13470,7 +13486,6 @@
|
||||
"resolved": "https://registry.npmjs.org/hono/-/hono-4.11.7.tgz",
|
||||
"integrity": "sha512-l7qMiNee7t82bH3SeyUCt9UF15EVmaBvsppY2zQtrbIhl/yzBTny+YUxsVjSjQ6gaqaeVtZmGocom8TzBlA4Yw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=16.9.0"
|
||||
}
|
||||
@@ -13749,7 +13764,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@jrichman/ink/-/ink-6.4.7.tgz",
|
||||
"integrity": "sha512-QHyxhNF5VonF5cRmdAJD/UPucB9nRx3FozWMjQrDGfBxfAL9lpyu72/MlFPgloS1TMTGsOt7YN6dTPPA6mh0Aw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@alcalzone/ansi-tokenize": "^0.2.1",
|
||||
"ansi-escapes": "^7.0.0",
|
||||
@@ -15003,7 +15017,6 @@
|
||||
"resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz",
|
||||
"integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"jiti": "lib/jiti-cli.mjs"
|
||||
}
|
||||
@@ -15353,7 +15366,6 @@
|
||||
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.2.tgz",
|
||||
"integrity": "sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==",
|
||||
"license": "MPL-2.0",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"detect-libc": "^2.0.3"
|
||||
},
|
||||
@@ -19090,7 +19102,6 @@
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz",
|
||||
"integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
@@ -21798,7 +21809,6 @@
|
||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
@@ -22120,7 +22130,6 @@
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz",
|
||||
"integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"esbuild": "^0.27.0",
|
||||
"fdir": "^6.5.0",
|
||||
@@ -22202,6 +22211,7 @@
|
||||
"os": [
|
||||
"aix"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
@@ -22218,6 +22228,7 @@
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
@@ -22234,6 +22245,7 @@
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
@@ -22250,6 +22262,7 @@
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
@@ -22266,6 +22279,7 @@
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
@@ -22282,6 +22296,7 @@
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
@@ -22298,6 +22313,7 @@
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
@@ -22314,6 +22330,7 @@
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
@@ -22330,6 +22347,7 @@
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
@@ -22346,6 +22364,7 @@
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
@@ -22362,6 +22381,7 @@
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
@@ -22378,6 +22398,7 @@
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
@@ -22394,6 +22415,7 @@
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
@@ -22410,6 +22432,7 @@
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
@@ -22426,6 +22449,7 @@
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
@@ -22442,6 +22466,7 @@
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
@@ -22458,6 +22483,7 @@
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
@@ -22474,6 +22500,7 @@
|
||||
"os": [
|
||||
"netbsd"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
@@ -22490,6 +22517,7 @@
|
||||
"os": [
|
||||
"netbsd"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
@@ -22506,6 +22534,7 @@
|
||||
"os": [
|
||||
"openbsd"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
@@ -22522,6 +22551,7 @@
|
||||
"os": [
|
||||
"openbsd"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
@@ -22538,6 +22568,7 @@
|
||||
"os": [
|
||||
"openharmony"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
@@ -22554,6 +22585,7 @@
|
||||
"os": [
|
||||
"sunos"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
@@ -22570,6 +22602,7 @@
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
@@ -22586,6 +22619,7 @@
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
@@ -22602,6 +22636,7 @@
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
@@ -22657,6 +22692,7 @@
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||
}
|
||||
@@ -23591,7 +23627,6 @@
|
||||
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
|
||||
"integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/colinhacks"
|
||||
}
|
||||
|
||||
@@ -33,6 +33,7 @@ enum ClineAsk {
|
||||
REPORT_BUG = 14;
|
||||
SUMMARIZE_TASK = 15;
|
||||
ACT_MODE_RESPOND = 16;
|
||||
USE_SUBAGENTS = 17;
|
||||
}
|
||||
|
||||
// Enum for ClineSay types
|
||||
@@ -71,6 +72,9 @@ enum ClineSay {
|
||||
HOOK_OUTPUT_STREAM = 31;
|
||||
COMMAND_PERMISSION_DENIED = 32;
|
||||
CONDITIONAL_RULES_APPLIED = 33;
|
||||
SUBAGENT_STATUS = 34;
|
||||
USE_SUBAGENTS_SAY = 35;
|
||||
SUBAGENT_USAGE = 36;
|
||||
}
|
||||
|
||||
// Enum for ClineSayTool tool types
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -241,8 +241,10 @@ function extractTaskInformation(clineMessages: ClineMessage[], metadata: any): T
|
||||
let cacheReads = 0
|
||||
let totalCost = 0
|
||||
|
||||
// Look for api_req_started messages with token info
|
||||
const apiReqMessages = clineMessages.filter((msg) => msg.type === "say" && msg.say === "api_req_started" && msg.text)
|
||||
// Look for usage-carrying messages with token info
|
||||
const apiReqMessages = clineMessages.filter(
|
||||
(msg) => msg.type === "say" && (msg.say === "api_req_started" || msg.say === "subagent_usage") && msg.text,
|
||||
)
|
||||
|
||||
for (const msg of apiReqMessages) {
|
||||
try {
|
||||
|
||||
@@ -250,7 +250,6 @@ export class Controller {
|
||||
const terminalReuseEnabled = this.stateManager.getGlobalStateKey("terminalReuseEnabled")
|
||||
const vscodeTerminalExecutionMode = this.stateManager.getGlobalStateKey("vscodeTerminalExecutionMode")
|
||||
const terminalOutputLineLimit = this.stateManager.getGlobalSettingsKey("terminalOutputLineLimit")
|
||||
const subagentTerminalOutputLineLimit = this.stateManager.getGlobalSettingsKey("subagentTerminalOutputLineLimit")
|
||||
const defaultTerminalProfile = this.stateManager.getGlobalSettingsKey("defaultTerminalProfile")
|
||||
const isNewUser = this.stateManager.getGlobalStateKey("isNewUser")
|
||||
const taskHistory = this.stateManager.getGlobalStateKey("taskHistory")
|
||||
@@ -314,7 +313,6 @@ export class Controller {
|
||||
shellIntegrationTimeout,
|
||||
terminalReuseEnabled: terminalReuseEnabled ?? true,
|
||||
terminalOutputLineLimit: terminalOutputLineLimit ?? 500,
|
||||
subagentTerminalOutputLineLimit: subagentTerminalOutputLineLimit ?? 2000,
|
||||
defaultTerminalProfile: defaultTerminalProfile ?? "default",
|
||||
vscodeTerminalExecutionMode,
|
||||
cwd,
|
||||
@@ -859,6 +857,7 @@ export class Controller {
|
||||
const strictPlanModeEnabled = this.stateManager.getGlobalSettingsKey("strictPlanModeEnabled")
|
||||
const yoloModeToggled = this.stateManager.getGlobalSettingsKey("yoloModeToggled")
|
||||
const useAutoCondense = this.stateManager.getGlobalSettingsKey("useAutoCondense")
|
||||
const subagentsEnabled = this.stateManager.getGlobalSettingsKey("subagentsEnabled")
|
||||
const userInfo = this.stateManager.getGlobalStateKey("userInfo")
|
||||
const mcpMarketplaceEnabled = this.stateManager.getGlobalStateKey("mcpMarketplaceEnabled")
|
||||
const mcpDisplayMode = this.stateManager.getGlobalStateKey("mcpDisplayMode")
|
||||
@@ -883,13 +882,11 @@ export class Controller {
|
||||
const mcpResponsesCollapsed = this.stateManager.getGlobalStateKey("mcpResponsesCollapsed")
|
||||
const terminalOutputLineLimit = this.stateManager.getGlobalSettingsKey("terminalOutputLineLimit")
|
||||
const maxConsecutiveMistakes = this.stateManager.getGlobalSettingsKey("maxConsecutiveMistakes")
|
||||
const subagentTerminalOutputLineLimit = this.stateManager.getGlobalSettingsKey("subagentTerminalOutputLineLimit")
|
||||
const favoritedModelIds = this.stateManager.getGlobalStateKey("favoritedModelIds")
|
||||
const lastDismissedInfoBannerVersion = this.stateManager.getGlobalStateKey("lastDismissedInfoBannerVersion") || 0
|
||||
const lastDismissedModelBannerVersion = this.stateManager.getGlobalStateKey("lastDismissedModelBannerVersion") || 0
|
||||
const lastDismissedCliBannerVersion = this.stateManager.getGlobalStateKey("lastDismissedCliBannerVersion") || 0
|
||||
const dismissedBanners = this.stateManager.getGlobalStateKey("dismissedBanners")
|
||||
const subagentsEnabled = this.stateManager.getGlobalSettingsKey("subagentsEnabled")
|
||||
const doubleCheckCompletionEnabled = this.stateManager.getGlobalSettingsKey("doubleCheckCompletionEnabled")
|
||||
|
||||
const localClineRulesToggles = this.stateManager.getWorkspaceStateKey("localClineRulesToggles")
|
||||
@@ -944,6 +941,7 @@ export class Controller {
|
||||
strictPlanModeEnabled,
|
||||
yoloModeToggled,
|
||||
useAutoCondense,
|
||||
subagentsEnabled,
|
||||
userInfo,
|
||||
mcpMarketplaceEnabled,
|
||||
mcpDisplayMode,
|
||||
@@ -974,7 +972,6 @@ export class Controller {
|
||||
mcpResponsesCollapsed,
|
||||
terminalOutputLineLimit,
|
||||
maxConsecutiveMistakes,
|
||||
subagentTerminalOutputLineLimit,
|
||||
customPrompt,
|
||||
taskHistory: processedTaskHistory,
|
||||
shouldShowAnnouncement,
|
||||
@@ -1004,7 +1001,6 @@ export class Controller {
|
||||
remoteConfigSettings: this.stateManager.getRemoteConfigSettings(),
|
||||
lastDismissedCliBannerVersion,
|
||||
dismissedBanners,
|
||||
subagentsEnabled,
|
||||
nativeToolCallSetting: this.stateManager.getGlobalStateKey("nativeToolCallEnabled"),
|
||||
enableParallelToolCalling: this.stateManager.getGlobalSettingsKey("enableParallelToolCalling"),
|
||||
backgroundEditEnabled: this.stateManager.getGlobalSettingsKey("backgroundEditEnabled"),
|
||||
|
||||
@@ -1,165 +0,0 @@
|
||||
import { UpdateSettingsRequest } from "@shared/proto/cline/state"
|
||||
import * as assert from "assert"
|
||||
import * as sinon from "sinon"
|
||||
import { Controller } from ".."
|
||||
import { updateSettings } from "./updateSettings"
|
||||
|
||||
// Mock telemetryService
|
||||
const telemetryServiceMock = {
|
||||
captureSubagentToggle: sinon.stub(),
|
||||
}
|
||||
|
||||
describe("updateSettings platform validation", () => {
|
||||
let mockController: Controller
|
||||
let originalPlatform: NodeJS.Platform
|
||||
|
||||
beforeEach(() => {
|
||||
// Store original platform
|
||||
originalPlatform = process.platform
|
||||
|
||||
// Create mock controller
|
||||
mockController = {
|
||||
stateManager: {
|
||||
getGlobalSettingsKey: sinon.stub(),
|
||||
setGlobalState: sinon.stub(),
|
||||
setApiConfiguration: sinon.stub(),
|
||||
},
|
||||
postStateToWebview: sinon.stub().resolves({}),
|
||||
task: undefined,
|
||||
updateTelemetrySetting: sinon.stub(),
|
||||
} as unknown as Controller
|
||||
|
||||
// Clear telemetry service mock
|
||||
telemetryServiceMock.captureSubagentToggle.reset()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
// Restore original platform
|
||||
Object.defineProperty(process, "platform", {
|
||||
value: originalPlatform,
|
||||
writable: true,
|
||||
configurable: true,
|
||||
})
|
||||
sinon.restore()
|
||||
})
|
||||
|
||||
it("should allow enabling subagents on macOS (darwin)", async () => {
|
||||
// Set platform to macOS
|
||||
Object.defineProperty(process, "platform", { value: "darwin" })
|
||||
|
||||
;(mockController.stateManager.getGlobalSettingsKey as sinon.SinonStub).returns(false)
|
||||
|
||||
const request = UpdateSettingsRequest.create({
|
||||
subagentsEnabled: true,
|
||||
})
|
||||
|
||||
// Should not throw
|
||||
await updateSettings(mockController, request)
|
||||
|
||||
assert.ok(
|
||||
(mockController.stateManager.setGlobalState as sinon.SinonStub).calledWith("subagentsEnabled", true),
|
||||
"Should enable subagents on macOS",
|
||||
)
|
||||
})
|
||||
|
||||
it("should allow enabling subagents on Linux", async () => {
|
||||
// Set platform to Linux
|
||||
Object.defineProperty(process, "platform", { value: "linux" })
|
||||
|
||||
;(mockController.stateManager.getGlobalSettingsKey as sinon.SinonStub).returns(false)
|
||||
|
||||
const request = UpdateSettingsRequest.create({
|
||||
subagentsEnabled: true,
|
||||
})
|
||||
|
||||
// Should not throw
|
||||
await updateSettings(mockController, request)
|
||||
|
||||
assert.ok(
|
||||
(mockController.stateManager.setGlobalState as sinon.SinonStub).calledWith("subagentsEnabled", true),
|
||||
"Should enable subagents on Linux",
|
||||
)
|
||||
})
|
||||
|
||||
it("should throw error when trying to enable subagents on Windows", async () => {
|
||||
// Set platform to Windows
|
||||
Object.defineProperty(process, "platform", { value: "win32" })
|
||||
|
||||
;(mockController.stateManager.getGlobalSettingsKey as sinon.SinonStub).returns(false)
|
||||
|
||||
const request = UpdateSettingsRequest.create({
|
||||
subagentsEnabled: true,
|
||||
})
|
||||
|
||||
try {
|
||||
await updateSettings(mockController, request)
|
||||
assert.fail("Should have thrown an error")
|
||||
} catch (error) {
|
||||
assert.strictEqual(
|
||||
(error as Error).message,
|
||||
"CLI subagents are only supported on macOS and Linux platforms",
|
||||
"Should throw platform restriction error",
|
||||
)
|
||||
}
|
||||
|
||||
assert.ok(
|
||||
!(mockController.stateManager.setGlobalState as sinon.SinonStub).called,
|
||||
"Should not call setGlobalState when platform validation fails",
|
||||
)
|
||||
})
|
||||
|
||||
it("should allow disabling subagents on any platform", async () => {
|
||||
// Test on Windows
|
||||
Object.defineProperty(process, "platform", { value: "win32" })
|
||||
|
||||
;(mockController.stateManager.getGlobalSettingsKey as sinon.SinonStub).returns(true)
|
||||
|
||||
const request = UpdateSettingsRequest.create({
|
||||
subagentsEnabled: false,
|
||||
})
|
||||
|
||||
// Should not throw
|
||||
await updateSettings(mockController, request)
|
||||
|
||||
assert.ok(
|
||||
(mockController.stateManager.setGlobalState as sinon.SinonStub).calledWith("subagentsEnabled", false),
|
||||
"Should allow disabling subagents on any platform",
|
||||
)
|
||||
})
|
||||
|
||||
it("should allow keeping subagents disabled on non-macOS platforms", async () => {
|
||||
// Test on Windows with subagents already disabled
|
||||
Object.defineProperty(process, "platform", { value: "win32" })
|
||||
|
||||
;(mockController.stateManager.getGlobalSettingsKey as sinon.SinonStub).returns(false)
|
||||
|
||||
const request = UpdateSettingsRequest.create({
|
||||
subagentsEnabled: false,
|
||||
})
|
||||
|
||||
// Should not throw
|
||||
await updateSettings(mockController, request)
|
||||
|
||||
assert.ok(
|
||||
(mockController.stateManager.setGlobalState as sinon.SinonStub).calledWith("subagentsEnabled", false),
|
||||
"Should allow keeping subagents disabled on non-macOS platforms",
|
||||
)
|
||||
})
|
||||
|
||||
it("should not perform platform validation when subagentsEnabled is undefined", async () => {
|
||||
// Test on Windows but don't try to change subagents setting
|
||||
Object.defineProperty(process, "platform", { value: "win32" })
|
||||
|
||||
const request = UpdateSettingsRequest.create({
|
||||
strictPlanModeEnabled: true, // Some other setting
|
||||
})
|
||||
|
||||
// Should not throw error since subagentsEnabled is not being changed
|
||||
await updateSettings(mockController, request)
|
||||
|
||||
assert.ok(
|
||||
(mockController.postStateToWebview as sinon.SinonStub).called,
|
||||
"Should complete successfully when subagentsEnabled is not being changed",
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -128,22 +128,6 @@ export async function updateSettings(controller: Controller, request: UpdateSett
|
||||
)
|
||||
}
|
||||
|
||||
// Update subagent terminal output line limit
|
||||
if (request.subagentTerminalOutputLineLimit !== undefined) {
|
||||
controller.stateManager.setGlobalState(
|
||||
"subagentTerminalOutputLineLimit",
|
||||
Number(request.subagentTerminalOutputLineLimit),
|
||||
)
|
||||
}
|
||||
|
||||
// Update subagent terminal output line limit
|
||||
if (request.subagentTerminalOutputLineLimit !== undefined) {
|
||||
controller.stateManager.setGlobalState(
|
||||
"subagentTerminalOutputLineLimit",
|
||||
Number(request.subagentTerminalOutputLineLimit),
|
||||
)
|
||||
}
|
||||
|
||||
// Update max consecutive mistakes
|
||||
if (request.maxConsecutiveMistakes !== undefined) {
|
||||
controller.stateManager.setGlobalState("maxConsecutiveMistakes", Number(request.maxConsecutiveMistakes))
|
||||
@@ -174,6 +158,18 @@ export async function updateSettings(controller: Controller, request: UpdateSett
|
||||
controller.stateManager.setGlobalState("worktreesEnabled", request.worktreesEnabled)
|
||||
}
|
||||
|
||||
// Update subagents setting
|
||||
if (request.subagentsEnabled !== undefined) {
|
||||
const wasEnabled = controller.stateManager.getGlobalSettingsKey("subagentsEnabled") ?? false
|
||||
const isEnabled = !!request.subagentsEnabled
|
||||
controller.stateManager.setGlobalState("subagentsEnabled", isEnabled)
|
||||
|
||||
// Capture telemetry when setting changes
|
||||
if (wasEnabled !== isEnabled) {
|
||||
telemetryService.captureSubagentToggle(isEnabled)
|
||||
}
|
||||
}
|
||||
|
||||
if (request.dictationSettings !== undefined) {
|
||||
// Convert from protobuf format (snake_case) to TypeScript format (camelCase)
|
||||
const dictationSettings = {
|
||||
@@ -314,25 +310,6 @@ export async function updateSettings(controller: Controller, request: UpdateSett
|
||||
controller.stateManager.setGlobalState("multiRootEnabled", !!request.multiRootEnabled)
|
||||
}
|
||||
|
||||
if (request.subagentsEnabled !== undefined) {
|
||||
const currentSettings = controller.stateManager.getGlobalSettingsKey("subagentsEnabled")
|
||||
const wasEnabled = currentSettings ?? false
|
||||
const isEnabled = !!request.subagentsEnabled
|
||||
|
||||
// Platform validation: Only allow enabling subagents on macOS and Linux
|
||||
if (isEnabled && process.platform !== "darwin" && process.platform !== "linux") {
|
||||
throw new Error("CLI subagents are only supported on macOS and Linux platforms")
|
||||
}
|
||||
|
||||
controller.stateManager.setGlobalState("subagentsEnabled", isEnabled)
|
||||
|
||||
// Capture telemetry when setting changes
|
||||
if (wasEnabled !== isEnabled) {
|
||||
telemetryService.captureSubagentToggle(isEnabled)
|
||||
}
|
||||
controller.stateManager.setGlobalState("subagentsEnabled", !!request.subagentsEnabled)
|
||||
}
|
||||
|
||||
if (request.nativeToolCallEnabled !== undefined) {
|
||||
controller.stateManager.setGlobalState("nativeToolCallEnabled", !!request.nativeToolCallEnabled)
|
||||
if (controller.task) {
|
||||
|
||||
@@ -50,6 +50,7 @@ export async function updateSettingsCli(controller: Controller, request: UpdateS
|
||||
useAutoCondense,
|
||||
clineWebToolsEnabled,
|
||||
worktreesEnabled,
|
||||
subagentsEnabled,
|
||||
focusChainSettings,
|
||||
browserSettings,
|
||||
defaultTerminalProfile,
|
||||
@@ -162,6 +163,17 @@ export async function updateSettingsCli(controller: Controller, request: UpdateS
|
||||
controller.stateManager.setGlobalState("worktreesEnabled", worktreesEnabled)
|
||||
}
|
||||
|
||||
// Update subagents setting (requires telemetry on state change)
|
||||
if (subagentsEnabled !== undefined) {
|
||||
const wasEnabled = controller.stateManager.getGlobalSettingsKey("subagentsEnabled") ?? false
|
||||
const isEnabled = !!subagentsEnabled
|
||||
controller.stateManager.setGlobalState("subagentsEnabled", isEnabled)
|
||||
|
||||
if (wasEnabled !== isEnabled) {
|
||||
telemetryService.captureSubagentToggle(isEnabled)
|
||||
}
|
||||
}
|
||||
|
||||
// Update focus chain settings (requires telemetry on state change)
|
||||
if (focusChainSettings !== undefined) {
|
||||
const currentSettings = controller.stateManager.getGlobalSettingsKey("focusChainSettings")
|
||||
|
||||
@@ -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.
|
||||
|
||||
+17
@@ -266,6 +266,23 @@ Usage:
|
||||
<to_ref>HEAD</to_ref>
|
||||
</generate_explanation>
|
||||
|
||||
## use_subagents
|
||||
Description: Run up to five focused in-process subagents in parallel. Each subagent gets its own prompt and returns a comprehensive research result with tool and token stats. Use this for broad exploration when reading many files would consume the main agent's context window.
|
||||
Parameters:
|
||||
- prompt_1: (required) First subagent prompt.
|
||||
- prompt_2: (optional) Optional second subagent prompt.
|
||||
- prompt_3: (optional) Optional third subagent prompt.
|
||||
- prompt_4: (optional) Optional fourth subagent prompt.
|
||||
- prompt_5: (optional) Optional fifth subagent prompt.
|
||||
Usage:
|
||||
<use_subagents>
|
||||
<prompt_1></prompt_1>
|
||||
<prompt_2></prompt_2>
|
||||
<prompt_3></prompt_3>
|
||||
<prompt_4></prompt_4>
|
||||
<prompt_5></prompt_5>
|
||||
</use_subagents>
|
||||
|
||||
# Tool Use Examples
|
||||
|
||||
## Example 1: Requesting to execute a command
|
||||
|
||||
+17
@@ -232,6 +232,23 @@ Usage:
|
||||
<to_ref>HEAD</to_ref>
|
||||
</generate_explanation>
|
||||
|
||||
## use_subagents
|
||||
Description: Run up to five focused in-process subagents in parallel. Each subagent gets its own prompt and returns a comprehensive research result with tool and token stats. Use this for broad exploration when reading many files would consume the main agent's context window.
|
||||
Parameters:
|
||||
- prompt_1: (required) First subagent prompt.
|
||||
- prompt_2: (optional) Optional second subagent prompt.
|
||||
- prompt_3: (optional) Optional third subagent prompt.
|
||||
- prompt_4: (optional) Optional fourth subagent prompt.
|
||||
- prompt_5: (optional) Optional fifth subagent prompt.
|
||||
Usage:
|
||||
<use_subagents>
|
||||
<prompt_1></prompt_1>
|
||||
<prompt_2></prompt_2>
|
||||
<prompt_3></prompt_3>
|
||||
<prompt_4></prompt_4>
|
||||
<prompt_5></prompt_5>
|
||||
</use_subagents>
|
||||
|
||||
# Tool Use Examples
|
||||
|
||||
## Example 1: Requesting to execute a command
|
||||
|
||||
+17
@@ -240,6 +240,23 @@ Usage:
|
||||
<to_ref>HEAD</to_ref>
|
||||
</generate_explanation>
|
||||
|
||||
## use_subagents
|
||||
Description: Run up to five focused in-process subagents in parallel. Each subagent gets its own prompt and returns a comprehensive research result with tool and token stats. Use this for broad exploration when reading many files would consume the main agent's context window.
|
||||
Parameters:
|
||||
- prompt_1: (required) First subagent prompt.
|
||||
- prompt_2: (optional) Optional second subagent prompt.
|
||||
- prompt_3: (optional) Optional third subagent prompt.
|
||||
- prompt_4: (optional) Optional fourth subagent prompt.
|
||||
- prompt_5: (optional) Optional fifth subagent prompt.
|
||||
Usage:
|
||||
<use_subagents>
|
||||
<prompt_1></prompt_1>
|
||||
<prompt_2></prompt_2>
|
||||
<prompt_3></prompt_3>
|
||||
<prompt_4></prompt_4>
|
||||
<prompt_5></prompt_5>
|
||||
</use_subagents>
|
||||
|
||||
# Tool Use Examples
|
||||
|
||||
## Example 1: Requesting to execute a command
|
||||
|
||||
+17
@@ -266,6 +266,23 @@ Usage:
|
||||
<to_ref>HEAD</to_ref>
|
||||
</generate_explanation>
|
||||
|
||||
## use_subagents
|
||||
Description: Run up to five focused in-process subagents in parallel. Each subagent gets its own prompt and returns a comprehensive research result with tool and token stats. Use this for broad exploration when reading many files would consume the main agent's context window.
|
||||
Parameters:
|
||||
- prompt_1: (required) First subagent prompt.
|
||||
- prompt_2: (optional) Optional second subagent prompt.
|
||||
- prompt_3: (optional) Optional third subagent prompt.
|
||||
- prompt_4: (optional) Optional fourth subagent prompt.
|
||||
- prompt_5: (optional) Optional fifth subagent prompt.
|
||||
Usage:
|
||||
<use_subagents>
|
||||
<prompt_1></prompt_1>
|
||||
<prompt_2></prompt_2>
|
||||
<prompt_3></prompt_3>
|
||||
<prompt_4></prompt_4>
|
||||
<prompt_5></prompt_5>
|
||||
</use_subagents>
|
||||
|
||||
# Tool Use Examples
|
||||
|
||||
## Example 1: Requesting to execute a command
|
||||
|
||||
@@ -297,6 +297,23 @@ Usage:
|
||||
<load_mcp_documentation>
|
||||
</load_mcp_documentation>
|
||||
|
||||
## use_subagents
|
||||
Description: Run up to five focused in-process subagents in parallel. Each subagent gets its own prompt and returns a comprehensive research result with tool and token stats. Use this for broad exploration when reading many files would consume the main agent's context window.
|
||||
Parameters:
|
||||
- prompt_1: (required) First subagent prompt.
|
||||
- prompt_2: (optional) Optional second subagent prompt.
|
||||
- prompt_3: (optional) Optional third subagent prompt.
|
||||
- prompt_4: (optional) Optional fourth subagent prompt.
|
||||
- prompt_5: (optional) Optional fifth subagent prompt.
|
||||
Usage:
|
||||
<use_subagents>
|
||||
<prompt_1></prompt_1>
|
||||
<prompt_2></prompt_2>
|
||||
<prompt_3></prompt_3>
|
||||
<prompt_4></prompt_4>
|
||||
<prompt_5></prompt_5>
|
||||
</use_subagents>
|
||||
|
||||
# Tool Use Examples
|
||||
|
||||
## Example 1: Requesting to execute a command
|
||||
|
||||
@@ -263,6 +263,23 @@ Usage:
|
||||
<load_mcp_documentation>
|
||||
</load_mcp_documentation>
|
||||
|
||||
## use_subagents
|
||||
Description: Run up to five focused in-process subagents in parallel. Each subagent gets its own prompt and returns a comprehensive research result with tool and token stats. Use this for broad exploration when reading many files would consume the main agent's context window.
|
||||
Parameters:
|
||||
- prompt_1: (required) First subagent prompt.
|
||||
- prompt_2: (optional) Optional second subagent prompt.
|
||||
- prompt_3: (optional) Optional third subagent prompt.
|
||||
- prompt_4: (optional) Optional fourth subagent prompt.
|
||||
- prompt_5: (optional) Optional fifth subagent prompt.
|
||||
Usage:
|
||||
<use_subagents>
|
||||
<prompt_1></prompt_1>
|
||||
<prompt_2></prompt_2>
|
||||
<prompt_3></prompt_3>
|
||||
<prompt_4></prompt_4>
|
||||
<prompt_5></prompt_5>
|
||||
</use_subagents>
|
||||
|
||||
# Tool Use Examples
|
||||
|
||||
## Example 1: Requesting to execute a command
|
||||
|
||||
+17
@@ -267,6 +267,23 @@ Usage:
|
||||
<load_mcp_documentation>
|
||||
</load_mcp_documentation>
|
||||
|
||||
## use_subagents
|
||||
Description: Run up to five focused in-process subagents in parallel. Each subagent gets its own prompt and returns a comprehensive research result with tool and token stats. Use this for broad exploration when reading many files would consume the main agent's context window.
|
||||
Parameters:
|
||||
- prompt_1: (required) First subagent prompt.
|
||||
- prompt_2: (optional) Optional second subagent prompt.
|
||||
- prompt_3: (optional) Optional third subagent prompt.
|
||||
- prompt_4: (optional) Optional fourth subagent prompt.
|
||||
- prompt_5: (optional) Optional fifth subagent prompt.
|
||||
Usage:
|
||||
<use_subagents>
|
||||
<prompt_1></prompt_1>
|
||||
<prompt_2></prompt_2>
|
||||
<prompt_3></prompt_3>
|
||||
<prompt_4></prompt_4>
|
||||
<prompt_5></prompt_5>
|
||||
</use_subagents>
|
||||
|
||||
# Tool Use Examples
|
||||
|
||||
## Example 1: Requesting to execute a command
|
||||
|
||||
@@ -297,6 +297,23 @@ Usage:
|
||||
<load_mcp_documentation>
|
||||
</load_mcp_documentation>
|
||||
|
||||
## use_subagents
|
||||
Description: Run up to five focused in-process subagents in parallel. Each subagent gets its own prompt and returns a comprehensive research result with tool and token stats. Use this for broad exploration when reading many files would consume the main agent's context window.
|
||||
Parameters:
|
||||
- prompt_1: (required) First subagent prompt.
|
||||
- prompt_2: (optional) Optional second subagent prompt.
|
||||
- prompt_3: (optional) Optional third subagent prompt.
|
||||
- prompt_4: (optional) Optional fourth subagent prompt.
|
||||
- prompt_5: (optional) Optional fifth subagent prompt.
|
||||
Usage:
|
||||
<use_subagents>
|
||||
<prompt_1></prompt_1>
|
||||
<prompt_2></prompt_2>
|
||||
<prompt_3></prompt_3>
|
||||
<prompt_4></prompt_4>
|
||||
<prompt_5></prompt_5>
|
||||
</use_subagents>
|
||||
|
||||
# Tool Use Examples
|
||||
|
||||
## Example 1: Requesting to execute a command
|
||||
|
||||
+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. Use this for broad exploration when reading many files would consume the main agent's context window.",
|
||||
"strict": false,
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"prompt_1": {
|
||||
"type": "string",
|
||||
"description": "First subagent prompt."
|
||||
},
|
||||
"prompt_2": {
|
||||
"type": "string",
|
||||
"description": "Optional second subagent prompt."
|
||||
},
|
||||
"prompt_3": {
|
||||
"type": "string",
|
||||
"description": "Optional third subagent prompt."
|
||||
},
|
||||
"prompt_4": {
|
||||
"type": "string",
|
||||
"description": "Optional fourth subagent prompt."
|
||||
},
|
||||
"prompt_5": {
|
||||
"type": "string",
|
||||
"description": "Optional fifth subagent prompt."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"prompt_1"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
|
||||
@@ -266,6 +266,23 @@ Usage:
|
||||
<to_ref>HEAD</to_ref>
|
||||
</generate_explanation>
|
||||
|
||||
## use_subagents
|
||||
Description: Run up to five focused in-process subagents in parallel. Each subagent gets its own prompt and returns a comprehensive research result with tool and token stats. Use this for broad exploration when reading many files would consume the main agent's context window.
|
||||
Parameters:
|
||||
- prompt_1: (required) First subagent prompt.
|
||||
- prompt_2: (optional) Optional second subagent prompt.
|
||||
- prompt_3: (optional) Optional third subagent prompt.
|
||||
- prompt_4: (optional) Optional fourth subagent prompt.
|
||||
- prompt_5: (optional) Optional fifth subagent prompt.
|
||||
Usage:
|
||||
<use_subagents>
|
||||
<prompt_1></prompt_1>
|
||||
<prompt_2></prompt_2>
|
||||
<prompt_3></prompt_3>
|
||||
<prompt_4></prompt_4>
|
||||
<prompt_5></prompt_5>
|
||||
</use_subagents>
|
||||
|
||||
# Tool Use Examples
|
||||
|
||||
## Example 1: Requesting to execute a command
|
||||
|
||||
@@ -232,6 +232,23 @@ Usage:
|
||||
<to_ref>HEAD</to_ref>
|
||||
</generate_explanation>
|
||||
|
||||
## use_subagents
|
||||
Description: Run up to five focused in-process subagents in parallel. Each subagent gets its own prompt and returns a comprehensive research result with tool and token stats. Use this for broad exploration when reading many files would consume the main agent's context window.
|
||||
Parameters:
|
||||
- prompt_1: (required) First subagent prompt.
|
||||
- prompt_2: (optional) Optional second subagent prompt.
|
||||
- prompt_3: (optional) Optional third subagent prompt.
|
||||
- prompt_4: (optional) Optional fourth subagent prompt.
|
||||
- prompt_5: (optional) Optional fifth subagent prompt.
|
||||
Usage:
|
||||
<use_subagents>
|
||||
<prompt_1></prompt_1>
|
||||
<prompt_2></prompt_2>
|
||||
<prompt_3></prompt_3>
|
||||
<prompt_4></prompt_4>
|
||||
<prompt_5></prompt_5>
|
||||
</use_subagents>
|
||||
|
||||
# Tool Use Examples
|
||||
|
||||
## Example 1: Requesting to execute a command
|
||||
|
||||
+17
@@ -240,6 +240,23 @@ Usage:
|
||||
<to_ref>HEAD</to_ref>
|
||||
</generate_explanation>
|
||||
|
||||
## use_subagents
|
||||
Description: Run up to five focused in-process subagents in parallel. Each subagent gets its own prompt and returns a comprehensive research result with tool and token stats. Use this for broad exploration when reading many files would consume the main agent's context window.
|
||||
Parameters:
|
||||
- prompt_1: (required) First subagent prompt.
|
||||
- prompt_2: (optional) Optional second subagent prompt.
|
||||
- prompt_3: (optional) Optional third subagent prompt.
|
||||
- prompt_4: (optional) Optional fourth subagent prompt.
|
||||
- prompt_5: (optional) Optional fifth subagent prompt.
|
||||
Usage:
|
||||
<use_subagents>
|
||||
<prompt_1></prompt_1>
|
||||
<prompt_2></prompt_2>
|
||||
<prompt_3></prompt_3>
|
||||
<prompt_4></prompt_4>
|
||||
<prompt_5></prompt_5>
|
||||
</use_subagents>
|
||||
|
||||
# Tool Use Examples
|
||||
|
||||
## Example 1: Requesting to execute a command
|
||||
|
||||
@@ -266,6 +266,23 @@ Usage:
|
||||
<to_ref>HEAD</to_ref>
|
||||
</generate_explanation>
|
||||
|
||||
## use_subagents
|
||||
Description: Run up to five focused in-process subagents in parallel. Each subagent gets its own prompt and returns a comprehensive research result with tool and token stats. Use this for broad exploration when reading many files would consume the main agent's context window.
|
||||
Parameters:
|
||||
- prompt_1: (required) First subagent prompt.
|
||||
- prompt_2: (optional) Optional second subagent prompt.
|
||||
- prompt_3: (optional) Optional third subagent prompt.
|
||||
- prompt_4: (optional) Optional fourth subagent prompt.
|
||||
- prompt_5: (optional) Optional fifth subagent prompt.
|
||||
Usage:
|
||||
<use_subagents>
|
||||
<prompt_1></prompt_1>
|
||||
<prompt_2></prompt_2>
|
||||
<prompt_3></prompt_3>
|
||||
<prompt_4></prompt_4>
|
||||
<prompt_5></prompt_5>
|
||||
</use_subagents>
|
||||
|
||||
# Tool Use Examples
|
||||
|
||||
## Example 1: Requesting to execute a command
|
||||
|
||||
@@ -266,6 +266,23 @@ Usage:
|
||||
<to_ref>HEAD</to_ref>
|
||||
</generate_explanation>
|
||||
|
||||
## use_subagents
|
||||
Description: Run up to five focused in-process subagents in parallel. Each subagent gets its own prompt and returns a comprehensive research result with tool and token stats. Use this for broad exploration when reading many files would consume the main agent's context window.
|
||||
Parameters:
|
||||
- prompt_1: (required) First subagent prompt.
|
||||
- prompt_2: (optional) Optional second subagent prompt.
|
||||
- prompt_3: (optional) Optional third subagent prompt.
|
||||
- prompt_4: (optional) Optional fourth subagent prompt.
|
||||
- prompt_5: (optional) Optional fifth subagent prompt.
|
||||
Usage:
|
||||
<use_subagents>
|
||||
<prompt_1></prompt_1>
|
||||
<prompt_2></prompt_2>
|
||||
<prompt_3></prompt_3>
|
||||
<prompt_4></prompt_4>
|
||||
<prompt_5></prompt_5>
|
||||
</use_subagents>
|
||||
|
||||
# Tool Use Examples
|
||||
|
||||
## Example 1: Requesting to execute a command
|
||||
|
||||
@@ -232,6 +232,23 @@ Usage:
|
||||
<to_ref>HEAD</to_ref>
|
||||
</generate_explanation>
|
||||
|
||||
## use_subagents
|
||||
Description: Run up to five focused in-process subagents in parallel. Each subagent gets its own prompt and returns a comprehensive research result with tool and token stats. Use this for broad exploration when reading many files would consume the main agent's context window.
|
||||
Parameters:
|
||||
- prompt_1: (required) First subagent prompt.
|
||||
- prompt_2: (optional) Optional second subagent prompt.
|
||||
- prompt_3: (optional) Optional third subagent prompt.
|
||||
- prompt_4: (optional) Optional fourth subagent prompt.
|
||||
- prompt_5: (optional) Optional fifth subagent prompt.
|
||||
Usage:
|
||||
<use_subagents>
|
||||
<prompt_1></prompt_1>
|
||||
<prompt_2></prompt_2>
|
||||
<prompt_3></prompt_3>
|
||||
<prompt_4></prompt_4>
|
||||
<prompt_5></prompt_5>
|
||||
</use_subagents>
|
||||
|
||||
# Tool Use Examples
|
||||
|
||||
## Example 1: Requesting to execute a command
|
||||
|
||||
+17
@@ -240,6 +240,23 @@ Usage:
|
||||
<to_ref>HEAD</to_ref>
|
||||
</generate_explanation>
|
||||
|
||||
## use_subagents
|
||||
Description: Run up to five focused in-process subagents in parallel. Each subagent gets its own prompt and returns a comprehensive research result with tool and token stats. Use this for broad exploration when reading many files would consume the main agent's context window.
|
||||
Parameters:
|
||||
- prompt_1: (required) First subagent prompt.
|
||||
- prompt_2: (optional) Optional second subagent prompt.
|
||||
- prompt_3: (optional) Optional third subagent prompt.
|
||||
- prompt_4: (optional) Optional fourth subagent prompt.
|
||||
- prompt_5: (optional) Optional fifth subagent prompt.
|
||||
Usage:
|
||||
<use_subagents>
|
||||
<prompt_1></prompt_1>
|
||||
<prompt_2></prompt_2>
|
||||
<prompt_3></prompt_3>
|
||||
<prompt_4></prompt_4>
|
||||
<prompt_5></prompt_5>
|
||||
</use_subagents>
|
||||
|
||||
# Tool Use Examples
|
||||
|
||||
## Example 1: Requesting to execute a command
|
||||
|
||||
@@ -266,6 +266,23 @@ Usage:
|
||||
<to_ref>HEAD</to_ref>
|
||||
</generate_explanation>
|
||||
|
||||
## use_subagents
|
||||
Description: Run up to five focused in-process subagents in parallel. Each subagent gets its own prompt and returns a comprehensive research result with tool and token stats. Use this for broad exploration when reading many files would consume the main agent's context window.
|
||||
Parameters:
|
||||
- prompt_1: (required) First subagent prompt.
|
||||
- prompt_2: (optional) Optional second subagent prompt.
|
||||
- prompt_3: (optional) Optional third subagent prompt.
|
||||
- prompt_4: (optional) Optional fourth subagent prompt.
|
||||
- prompt_5: (optional) Optional fifth subagent prompt.
|
||||
Usage:
|
||||
<use_subagents>
|
||||
<prompt_1></prompt_1>
|
||||
<prompt_2></prompt_2>
|
||||
<prompt_3></prompt_3>
|
||||
<prompt_4></prompt_4>
|
||||
<prompt_5></prompt_5>
|
||||
</use_subagents>
|
||||
|
||||
# Tool Use Examples
|
||||
|
||||
## Example 1: Requesting to execute a command
|
||||
|
||||
+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. Use this for broad exploration when reading many files would consume the main agent's context window.",
|
||||
"strict": false,
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"prompt_1": {
|
||||
"type": "string",
|
||||
"description": "First subagent prompt."
|
||||
},
|
||||
"prompt_2": {
|
||||
"type": "string",
|
||||
"description": "Optional second subagent prompt."
|
||||
},
|
||||
"prompt_3": {
|
||||
"type": "string",
|
||||
"description": "Optional third subagent prompt."
|
||||
},
|
||||
"prompt_4": {
|
||||
"type": "string",
|
||||
"description": "Optional fourth subagent prompt."
|
||||
},
|
||||
"prompt_5": {
|
||||
"type": "string",
|
||||
"description": "Optional fifth subagent prompt."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"prompt_1"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
|
||||
@@ -368,6 +368,43 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "use_subagents",
|
||||
"description": "Run up to five focused in-process subagents in parallel. Each subagent gets its own prompt and returns a comprehensive research result with tool and token stats. Use this for broad exploration when reading many files would consume the main agent's context window.",
|
||||
"strict": false,
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"prompt_1": {
|
||||
"type": "string",
|
||||
"description": "First subagent prompt."
|
||||
},
|
||||
"prompt_2": {
|
||||
"type": "string",
|
||||
"description": "Optional second subagent prompt."
|
||||
},
|
||||
"prompt_3": {
|
||||
"type": "string",
|
||||
"description": "Optional third subagent prompt."
|
||||
},
|
||||
"prompt_4": {
|
||||
"type": "string",
|
||||
"description": "Optional fourth subagent prompt."
|
||||
},
|
||||
"prompt_5": {
|
||||
"type": "string",
|
||||
"description": "Optional fifth subagent prompt."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"prompt_1"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
|
||||
+17
@@ -275,6 +275,23 @@ Usage:
|
||||
<to_ref>HEAD</to_ref>
|
||||
</generate_explanation>
|
||||
|
||||
## use_subagents
|
||||
Description: Run up to five focused in-process subagents in parallel. Each subagent gets its own prompt and returns a comprehensive research result with tool and token stats. Use this for broad exploration when reading many files would consume the main agent's context window.
|
||||
Parameters:
|
||||
- prompt_1: (required) First subagent prompt.
|
||||
- prompt_2: (optional) Optional second subagent prompt.
|
||||
- prompt_3: (optional) Optional third subagent prompt.
|
||||
- prompt_4: (optional) Optional fourth subagent prompt.
|
||||
- prompt_5: (optional) Optional fifth subagent prompt.
|
||||
Usage:
|
||||
<use_subagents>
|
||||
<prompt_1></prompt_1>
|
||||
<prompt_2></prompt_2>
|
||||
<prompt_3></prompt_3>
|
||||
<prompt_4></prompt_4>
|
||||
<prompt_5></prompt_5>
|
||||
</use_subagents>
|
||||
|
||||
# Tool Use Examples
|
||||
|
||||
## Example 1: Requesting to execute a command
|
||||
|
||||
+17
@@ -241,6 +241,23 @@ Usage:
|
||||
<to_ref>HEAD</to_ref>
|
||||
</generate_explanation>
|
||||
|
||||
## use_subagents
|
||||
Description: Run up to five focused in-process subagents in parallel. Each subagent gets its own prompt and returns a comprehensive research result with tool and token stats. Use this for broad exploration when reading many files would consume the main agent's context window.
|
||||
Parameters:
|
||||
- prompt_1: (required) First subagent prompt.
|
||||
- prompt_2: (optional) Optional second subagent prompt.
|
||||
- prompt_3: (optional) Optional third subagent prompt.
|
||||
- prompt_4: (optional) Optional fourth subagent prompt.
|
||||
- prompt_5: (optional) Optional fifth subagent prompt.
|
||||
Usage:
|
||||
<use_subagents>
|
||||
<prompt_1></prompt_1>
|
||||
<prompt_2></prompt_2>
|
||||
<prompt_3></prompt_3>
|
||||
<prompt_4></prompt_4>
|
||||
<prompt_5></prompt_5>
|
||||
</use_subagents>
|
||||
|
||||
# Tool Use Examples
|
||||
|
||||
## Example 1: Requesting to execute a command
|
||||
|
||||
+17
@@ -249,6 +249,23 @@ Usage:
|
||||
<to_ref>HEAD</to_ref>
|
||||
</generate_explanation>
|
||||
|
||||
## use_subagents
|
||||
Description: Run up to five focused in-process subagents in parallel. Each subagent gets its own prompt and returns a comprehensive research result with tool and token stats. Use this for broad exploration when reading many files would consume the main agent's context window.
|
||||
Parameters:
|
||||
- prompt_1: (required) First subagent prompt.
|
||||
- prompt_2: (optional) Optional second subagent prompt.
|
||||
- prompt_3: (optional) Optional third subagent prompt.
|
||||
- prompt_4: (optional) Optional fourth subagent prompt.
|
||||
- prompt_5: (optional) Optional fifth subagent prompt.
|
||||
Usage:
|
||||
<use_subagents>
|
||||
<prompt_1></prompt_1>
|
||||
<prompt_2></prompt_2>
|
||||
<prompt_3></prompt_3>
|
||||
<prompt_4></prompt_4>
|
||||
<prompt_5></prompt_5>
|
||||
</use_subagents>
|
||||
|
||||
# Tool Use Examples
|
||||
|
||||
## Example 1: Requesting to execute a command
|
||||
|
||||
+17
@@ -275,6 +275,23 @@ Usage:
|
||||
<to_ref>HEAD</to_ref>
|
||||
</generate_explanation>
|
||||
|
||||
## use_subagents
|
||||
Description: Run up to five focused in-process subagents in parallel. Each subagent gets its own prompt and returns a comprehensive research result with tool and token stats. Use this for broad exploration when reading many files would consume the main agent's context window.
|
||||
Parameters:
|
||||
- prompt_1: (required) First subagent prompt.
|
||||
- prompt_2: (optional) Optional second subagent prompt.
|
||||
- prompt_3: (optional) Optional third subagent prompt.
|
||||
- prompt_4: (optional) Optional fourth subagent prompt.
|
||||
- prompt_5: (optional) Optional fifth subagent prompt.
|
||||
Usage:
|
||||
<use_subagents>
|
||||
<prompt_1></prompt_1>
|
||||
<prompt_2></prompt_2>
|
||||
<prompt_3></prompt_3>
|
||||
<prompt_4></prompt_4>
|
||||
<prompt_5></prompt_5>
|
||||
</use_subagents>
|
||||
|
||||
# Tool Use Examples
|
||||
|
||||
## Example 1: Requesting to execute a command
|
||||
|
||||
@@ -352,6 +352,33 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "use_subagents",
|
||||
"description": "Run up to five focused in-process subagents in parallel. Each subagent gets its own prompt and returns a comprehensive research result with tool and token stats. Use this for broad exploration when reading many files would consume the main agent's context window.",
|
||||
"parameters": {
|
||||
"type": "OBJECT",
|
||||
"properties": {
|
||||
"prompt_1": {
|
||||
"type": "STRING"
|
||||
},
|
||||
"prompt_2": {
|
||||
"type": "STRING"
|
||||
},
|
||||
"prompt_3": {
|
||||
"type": "STRING"
|
||||
},
|
||||
"prompt_4": {
|
||||
"type": "STRING"
|
||||
},
|
||||
"prompt_5": {
|
||||
"type": "STRING"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"prompt_1"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "12345670mcp0test_tool",
|
||||
"description": "test-server: A test tool",
|
||||
|
||||
@@ -117,7 +117,7 @@ export const mockProviderInfo = {
|
||||
mode: "act" as const,
|
||||
}
|
||||
|
||||
const makeProviderInfo = (modelId: string, providerId: string = "test") => ({
|
||||
const makeProviderInfo = (modelId: string, providerId = "test") => ({
|
||||
providerId: modelId.includes("ollama") ? "ollama" : providerId,
|
||||
model: { ...mockProviderInfo.model, id: modelId },
|
||||
mode: "act" as const,
|
||||
@@ -129,6 +129,7 @@ const baseContext: SystemPromptContext = {
|
||||
ide: "TestIde",
|
||||
supportsBrowserUse: true,
|
||||
clineWebToolsEnabled: true,
|
||||
subagentsEnabled: true,
|
||||
mcpHub: {
|
||||
getServers: () => [
|
||||
{
|
||||
|
||||
@@ -1,60 +0,0 @@
|
||||
import { SystemPromptSection } from "../templates/placeholders"
|
||||
import { TemplateEngine } from "../templates/TemplateEngine"
|
||||
import type { PromptVariant, SystemPromptContext } from "../types"
|
||||
|
||||
const getCliSubagentsTemplateText = (_context: SystemPromptContext) => `USING THE CLINE CLI TOOL
|
||||
|
||||
The Cline CLI tool can be used to assign Cline AI agents with focused tasks. This can be used to keep you focused by delegating information-gathering and exploration to separate Cline instances. Use the Cline CLI tool to research large codebases, explore file structures, gather information from multiple files, analyze dependencies, or summarize code sections when the complete context may be too large or overwhelming.
|
||||
|
||||
## Creating Cline AI agents
|
||||
|
||||
Cline AI agents may be referred to as agents, subagents, or subtasks. Requests may not specifically invoke agents, but you may invoke them directly if warranted. Unless you are specifically asked to use this tool, only create agents when it seems likely you may be exploring across 10 or more files. If users specifically ask that you use this tool, you then must use this tool. Do not use subagents for editing code or executing commands- they should only be used for reading and research to help you better answer questions or build useful context for future coding tasks. If you are performing a search via search_files or the terminal (grep etc.), and the results are long and overwhleming, it is reccomended that you switch to use Cline CLI agents to perform this task. You may perform code edits directly using the write_to_file and replace_in_file tools, and commands using the execute_command tool.
|
||||
|
||||
## Command Syntax
|
||||
|
||||
You must use the following command syntax for creating Cline AI agents:
|
||||
|
||||
\`\`\`bash
|
||||
cline "your prompt here"
|
||||
\`\`\`
|
||||
|
||||
## Examples of how you might use this tool
|
||||
|
||||
\`\`\`bash
|
||||
# Find specific patterns
|
||||
cline "find all React components that use the useState hook and list their names"
|
||||
|
||||
# Analyze code structure
|
||||
cline "analyze the authentication flow. Reverse trace through all relevant functions and methods, and provide a summary of how it works. Include file/class references in your summary."
|
||||
|
||||
# Gather targeted information
|
||||
cline "list all API endpoints and their HTTP methods"
|
||||
|
||||
# Summarize directories
|
||||
cline "summarize the purpose of all files in the src/services directory"
|
||||
|
||||
# Research implementations
|
||||
cline "find how error handling is implemented across the application"
|
||||
\`\`\`
|
||||
|
||||
## Tips
|
||||
- Request brief, technically dense summaries over full file dumps.
|
||||
- Be specific with your instructions to get focused results.
|
||||
- Request summaries rather than full file contents. Encourage the agent to be brief, but specific and technically dense with their response.
|
||||
- If files you want to read are large or complicated, use Cline CLI agents for exploration before instead of reading these files.`
|
||||
|
||||
export async function getCliSubagentsSection(variant: PromptVariant, context: SystemPromptContext): Promise<string | undefined> {
|
||||
// If this is a CLI subagent, don't include CLI subagent instructions to prevent nesting/allignment concerns
|
||||
if (context.isCliSubagent) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
// Only include this section if CLI is installed and subagents are enabled
|
||||
if (!context.isSubagentsEnabledAndCliInstalled) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
const template = variant.componentOverrides?.[SystemPromptSection.CLI_SUBAGENTS]?.template || getCliSubagentsTemplateText
|
||||
|
||||
return new TemplateEngine().resolve(template, context, {})
|
||||
}
|
||||
@@ -2,7 +2,6 @@ import { SystemPromptSection } from "../templates/placeholders"
|
||||
import { getActVsPlanModeSection } from "./act_vs_plan_mode"
|
||||
import { getAgentRoleSection } from "./agent_role"
|
||||
import { getCapabilitiesSection } from "./capabilities"
|
||||
import { getCliSubagentsSection } from "./cli_subagents"
|
||||
import { getEditingFilesSection } from "./editing_files"
|
||||
import { getFeedbackSection } from "./feedback"
|
||||
import { getMcp } from "./mcp"
|
||||
@@ -44,10 +43,6 @@ export function getSystemPromptComponents() {
|
||||
id: SystemPromptSection.ACT_VS_PLAN,
|
||||
fn: getActVsPlanModeSection,
|
||||
},
|
||||
{
|
||||
id: SystemPromptSection.CLI_SUBAGENTS,
|
||||
fn: getCliSubagentsSection,
|
||||
},
|
||||
{
|
||||
id: SystemPromptSection.FEEDBACK,
|
||||
fn: getFeedbackSection,
|
||||
|
||||
@@ -5,7 +5,6 @@ export enum SystemPromptSection {
|
||||
MCP = "MCP_SECTION",
|
||||
EDITING_FILES = "EDITING_FILES_SECTION",
|
||||
ACT_VS_PLAN = "ACT_VS_PLAN_SECTION",
|
||||
CLI_SUBAGENTS = "CLI_SUBAGENTS_SECTION",
|
||||
TODO = "TODO_SECTION",
|
||||
CAPABILITIES = "CAPABILITIES_SECTION",
|
||||
SKILLS = "SKILLS_SECTION",
|
||||
|
||||
@@ -15,6 +15,7 @@ export * from "./plan_mode_respond"
|
||||
export * from "./read_file"
|
||||
export * from "./replace_in_file"
|
||||
export * from "./search_files"
|
||||
export * from "./subagent"
|
||||
export * from "./use_mcp_tool"
|
||||
export * from "./use_skill"
|
||||
export * from "./web_fetch"
|
||||
|
||||
@@ -17,6 +17,7 @@ import { plan_mode_respond_variants } from "./plan_mode_respond"
|
||||
import { read_file_variants } from "./read_file"
|
||||
import { replace_in_file_variants } from "./replace_in_file"
|
||||
import { search_files_variants } from "./search_files"
|
||||
import { subagent_variants } from "./subagent"
|
||||
import { use_mcp_tool_variants } from "./use_mcp_tool"
|
||||
import { use_skill_variants } from "./use_skill"
|
||||
import { web_fetch_variants } from "./web_fetch"
|
||||
@@ -47,6 +48,7 @@ export function registerClineToolSets(): void {
|
||||
...read_file_variants,
|
||||
...replace_in_file_variants,
|
||||
...search_files_variants,
|
||||
...subagent_variants,
|
||||
...use_mcp_tool_variants,
|
||||
...use_skill_variants,
|
||||
...web_fetch_variants,
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
import { ModelFamily } from "@/shared/prompts"
|
||||
import { ClineDefaultTool } from "@/shared/tools"
|
||||
import type { ClineToolSpec } from "../spec"
|
||||
|
||||
const id = ClineDefaultTool.USE_SUBAGENTS
|
||||
|
||||
const generic: ClineToolSpec = {
|
||||
variant: ModelFamily.GENERIC,
|
||||
id,
|
||||
name: "use_subagents",
|
||||
description:
|
||||
"Run up to five focused in-process subagents in parallel. Each subagent gets its own prompt and returns a comprehensive research result with tool and token stats. Use this for broad exploration when reading many files would consume the main agent's context window.",
|
||||
contextRequirements: (context) => context.subagentsEnabled === true && !context.isSubagentRun,
|
||||
parameters: [
|
||||
{
|
||||
name: "prompt_1",
|
||||
required: true,
|
||||
instruction: "First subagent prompt.",
|
||||
},
|
||||
{
|
||||
name: "prompt_2",
|
||||
required: false,
|
||||
instruction: "Optional second subagent prompt.",
|
||||
},
|
||||
{
|
||||
name: "prompt_3",
|
||||
required: false,
|
||||
instruction: "Optional third subagent prompt.",
|
||||
},
|
||||
{
|
||||
name: "prompt_4",
|
||||
required: false,
|
||||
instruction: "Optional fourth subagent prompt.",
|
||||
},
|
||||
{
|
||||
name: "prompt_5",
|
||||
required: false,
|
||||
instruction: "Optional fifth subagent prompt.",
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
export const subagent_variants = [generic]
|
||||
@@ -115,11 +115,13 @@ export interface SystemPromptContext {
|
||||
readonly isTesting?: boolean
|
||||
readonly runtimePlaceholders?: Readonly<Record<string, unknown>>
|
||||
readonly yoloModeToggled?: boolean
|
||||
readonly subagentsEnabled?: boolean
|
||||
readonly clineWebToolsEnabled?: boolean
|
||||
readonly isMultiRootEnabled?: boolean
|
||||
readonly workspaceRoots?: Array<{ path: string; name: string; vcs?: string }>
|
||||
readonly isSubagentsEnabledAndCliInstalled?: boolean
|
||||
readonly isCliSubagent?: boolean
|
||||
readonly isSubagentRun?: boolean
|
||||
readonly isCliEnvironment?: boolean
|
||||
readonly enableNativeToolCalls?: boolean
|
||||
readonly enableParallelToolCalling?: boolean
|
||||
|
||||
@@ -37,7 +37,6 @@ export const config: Omit<PromptVariant, "id"> = createVariant(ModelFamily.GENER
|
||||
SystemPromptSection.MCP,
|
||||
SystemPromptSection.EDITING_FILES,
|
||||
SystemPromptSection.ACT_VS_PLAN,
|
||||
SystemPromptSection.CLI_SUBAGENTS,
|
||||
SystemPromptSection.TODO,
|
||||
SystemPromptSection.CAPABILITIES,
|
||||
SystemPromptSection.RULES,
|
||||
@@ -124,7 +123,6 @@ export const createAdvancedVariant = (family: ModelFamily) =>
|
||||
SystemPromptSection.MCP,
|
||||
SystemPromptSection.EDITING_FILES,
|
||||
SystemPromptSection.ACT_VS_PLAN,
|
||||
SystemPromptSection.CLI_SUBAGENTS,
|
||||
SystemPromptSection.TODO,
|
||||
SystemPromptSection.CAPABILITIES,
|
||||
SystemPromptSection.FEEDBACK,
|
||||
@@ -151,4 +149,5 @@ export const createAdvancedVariant = (family: ModelFamily) =>
|
||||
ClineDefaultTool.PLAN_MODE,
|
||||
ClineDefaultTool.MCP_DOCS,
|
||||
ClineDefaultTool.TODO,
|
||||
ClineDefaultTool.USE_SUBAGENTS,
|
||||
)
|
||||
|
||||
@@ -27,7 +27,6 @@ export const config = createVariant(ModelFamily.DEVSTRAL)
|
||||
SystemPromptSection.MCP,
|
||||
SystemPromptSection.EDITING_FILES,
|
||||
SystemPromptSection.ACT_VS_PLAN,
|
||||
SystemPromptSection.CLI_SUBAGENTS,
|
||||
SystemPromptSection.CAPABILITIES,
|
||||
SystemPromptSection.RULES,
|
||||
SystemPromptSection.SYSTEM_INFO,
|
||||
@@ -54,6 +53,7 @@ export const config = createVariant(ModelFamily.DEVSTRAL)
|
||||
ClineDefaultTool.MCP_DOCS,
|
||||
ClineDefaultTool.TODO,
|
||||
ClineDefaultTool.USE_SKILL,
|
||||
ClineDefaultTool.USE_SUBAGENTS,
|
||||
)
|
||||
.placeholders({
|
||||
MODEL_FAMILY: "devstral",
|
||||
|
||||
@@ -22,7 +22,6 @@ export const baseTemplate = `{{${SystemPromptSection.AGENT_ROLE}}}
|
||||
|
||||
====
|
||||
|
||||
{{${SystemPromptSection.CLI_SUBAGENTS}}}
|
||||
|
||||
====
|
||||
|
||||
|
||||
@@ -34,7 +34,6 @@ export const config = createVariant(ModelFamily.GEMINI_3)
|
||||
SystemPromptSection.TOOL_USE,
|
||||
SystemPromptSection.RULES,
|
||||
SystemPromptSection.ACT_VS_PLAN,
|
||||
SystemPromptSection.CLI_SUBAGENTS,
|
||||
SystemPromptSection.CAPABILITIES,
|
||||
SystemPromptSection.EDITING_FILES,
|
||||
SystemPromptSection.FEEDBACK,
|
||||
@@ -66,6 +65,7 @@ export const config = createVariant(ModelFamily.GEMINI_3)
|
||||
ClineDefaultTool.TODO,
|
||||
ClineDefaultTool.GENERATE_EXPLANATION,
|
||||
ClineDefaultTool.USE_SKILL,
|
||||
ClineDefaultTool.USE_SUBAGENTS,
|
||||
)
|
||||
.placeholders({
|
||||
MODEL_FAMILY: ModelFamily.GEMINI_3,
|
||||
|
||||
@@ -10,7 +10,6 @@ export const baseTemplate = `{{${SystemPromptSection.AGENT_ROLE}}}
|
||||
|
||||
====
|
||||
|
||||
{{${SystemPromptSection.CLI_SUBAGENTS}}}
|
||||
|
||||
====
|
||||
|
||||
|
||||
@@ -48,7 +48,6 @@ export const config = createVariant(ModelFamily.GENERIC)
|
||||
SystemPromptSection.MCP,
|
||||
SystemPromptSection.EDITING_FILES,
|
||||
SystemPromptSection.ACT_VS_PLAN,
|
||||
SystemPromptSection.CLI_SUBAGENTS,
|
||||
SystemPromptSection.CAPABILITIES,
|
||||
SystemPromptSection.RULES,
|
||||
SystemPromptSection.SYSTEM_INFO,
|
||||
@@ -74,6 +73,7 @@ export const config = createVariant(ModelFamily.GENERIC)
|
||||
ClineDefaultTool.TODO,
|
||||
ClineDefaultTool.GENERATE_EXPLANATION,
|
||||
ClineDefaultTool.USE_SKILL,
|
||||
ClineDefaultTool.USE_SUBAGENTS,
|
||||
)
|
||||
.placeholders({
|
||||
MODEL_FAMILY: "generic",
|
||||
|
||||
@@ -22,7 +22,6 @@ export const baseTemplate = `{{${SystemPromptSection.AGENT_ROLE}}}
|
||||
|
||||
====
|
||||
|
||||
{{${SystemPromptSection.CLI_SUBAGENTS}}}
|
||||
|
||||
====
|
||||
|
||||
|
||||
@@ -26,7 +26,6 @@ export const config = createVariant(ModelFamily.GLM)
|
||||
SystemPromptSection.TASK_PROGRESS,
|
||||
SystemPromptSection.RULES,
|
||||
SystemPromptSection.ACT_VS_PLAN,
|
||||
SystemPromptSection.CLI_SUBAGENTS,
|
||||
SystemPromptSection.CAPABILITIES,
|
||||
SystemPromptSection.EDITING_FILES,
|
||||
SystemPromptSection.TODO,
|
||||
@@ -54,6 +53,7 @@ export const config = createVariant(ModelFamily.GLM)
|
||||
ClineDefaultTool.TODO,
|
||||
ClineDefaultTool.GENERATE_EXPLANATION,
|
||||
ClineDefaultTool.USE_SKILL,
|
||||
ClineDefaultTool.USE_SUBAGENTS,
|
||||
)
|
||||
.placeholders({
|
||||
MODEL_FAMILY: ModelFamily.GLM,
|
||||
|
||||
@@ -10,7 +10,6 @@ export const baseTemplate = `{{${SystemPromptSection.AGENT_ROLE}}}
|
||||
|
||||
## {{${SystemPromptSection.ACT_VS_PLAN}}}
|
||||
|
||||
## {{${SystemPromptSection.CLI_SUBAGENTS}}}
|
||||
|
||||
## {{${SystemPromptSection.CAPABILITIES}}}
|
||||
|
||||
|
||||
@@ -36,7 +36,6 @@ export const config = createVariant(ModelFamily.GPT_5)
|
||||
SystemPromptSection.MCP,
|
||||
SystemPromptSection.EDITING_FILES,
|
||||
SystemPromptSection.ACT_VS_PLAN,
|
||||
SystemPromptSection.CLI_SUBAGENTS,
|
||||
SystemPromptSection.CAPABILITIES,
|
||||
SystemPromptSection.FEEDBACK,
|
||||
SystemPromptSection.RULES,
|
||||
@@ -65,6 +64,7 @@ export const config = createVariant(ModelFamily.GPT_5)
|
||||
ClineDefaultTool.TODO,
|
||||
ClineDefaultTool.GENERATE_EXPLANATION,
|
||||
ClineDefaultTool.USE_SKILL,
|
||||
ClineDefaultTool.USE_SUBAGENTS,
|
||||
)
|
||||
.placeholders({
|
||||
MODEL_FAMILY: ModelFamily.GPT_5,
|
||||
|
||||
@@ -27,7 +27,6 @@ export const BASE = `{{${SystemPromptSection.AGENT_ROLE}}}
|
||||
|
||||
====
|
||||
|
||||
{{${SystemPromptSection.CLI_SUBAGENTS}}}
|
||||
|
||||
====
|
||||
|
||||
@@ -68,7 +67,7 @@ const RULES = (context: SystemPromptContext) => `RULES
|
||||
- Be sure to consider the type of project (e.g. Python, JavaScript, web application) when determining the appropriate structure and files to include. Also consider what files may be most relevant to accomplishing the task, for example looking at a project's manifest file would help you understand the project's dependencies, which you could incorporate into any code you write.
|
||||
- When making changes to code, always consider the context in which the code is being used. Ensure that your changes are compatible with the existing codebase and that they follow the project's coding standards and best practices.
|
||||
- When you want to modify a file, use the replace_in_file or write_to_file tool directly with the desired changes. You do not need to display the changes before using the tool.
|
||||
- Use Markdown **only where semantically correct** (e.g., \`inline code\`, \`\`\`code fences\`\`\`, lists, tables). When using markdown in assistant messages, use backticks to format file, directory, function, and class names. Use \( and \) for inline math, \[ and \] for block math.
|
||||
- Use Markdown **only where semantically correct** (e.g., \`inline code\`, \`\`\`code fences\`\`\`, lists, tables). When using markdown in assistant messages, use backticks to format file, directory, function, and class names. Use ( and ) for inline math, [ and ] for block math.
|
||||
- Do not ask for more information than necessary. Use the tools provided to accomplish the user's request efficiently and effectively. When you've completed your task, you must use the attempt_completion tool to present the result to the user. The user may provide feedback, which you can use to make improvements and try again.
|
||||
- ${context.yoloModeToggled !== true ? "You are only allowed to ask the user questions using the ask_followup_question tool. Use this tool only when you need additional details to complete a task, and be sure to use a clear and concise question that will help you move forward with the task. However if you can use the available tools to avoid having to ask the user questions, you should do so" : "Use your available tools and apply your best judgment to accomplish the task without asking the user any followup questions, making reasonable assumptions from the provided context"}. For example, if the user mentions a file that may be in an outside directory like the Desktop, you should use the list_files tool to list the files in the Desktop and check if the file they are talking about is there, rather than asking the user to provide the file path themselves.${context.yoloModeToggled !== true ? "\n- When the user is being vague, you should be proactive about asking clarifying questions using the ask_followup_question tool to ensure you understand their request. However, if you can infer the user's intent based on the context and available tools, you should proceed without asking unnecessary questions" : ""}
|
||||
- When executing commands, if you don't see the expected output, assume the terminal executed the command successfully and proceed with the task. The user's terminal may be unable to stream the output back properly.${context.yoloModeToggled !== true ? " If you absolutely need to see the actual terminal output, use the ask_followup_question tool to request the user to copy and paste it back to you." : ""}
|
||||
|
||||
@@ -26,7 +26,6 @@ export const config = createVariant(ModelFamily.HERMES)
|
||||
SystemPromptSection.TOOL_USE,
|
||||
SystemPromptSection.RULES,
|
||||
SystemPromptSection.ACT_VS_PLAN,
|
||||
SystemPromptSection.CLI_SUBAGENTS,
|
||||
SystemPromptSection.CAPABILITIES,
|
||||
SystemPromptSection.EDITING_FILES,
|
||||
SystemPromptSection.TODO,
|
||||
@@ -56,6 +55,7 @@ export const config = createVariant(ModelFamily.HERMES)
|
||||
ClineDefaultTool.TODO,
|
||||
ClineDefaultTool.GENERATE_EXPLANATION,
|
||||
ClineDefaultTool.USE_SKILL,
|
||||
ClineDefaultTool.USE_SUBAGENTS,
|
||||
)
|
||||
.placeholders({
|
||||
MODEL_FAMILY: "hermes",
|
||||
|
||||
@@ -8,7 +8,6 @@ export const baseTemplate = `{{${SystemPromptSection.AGENT_ROLE}}}
|
||||
|
||||
## {{${SystemPromptSection.ACT_VS_PLAN}}}
|
||||
|
||||
## {{${SystemPromptSection.CLI_SUBAGENTS}}}
|
||||
|
||||
## {{${SystemPromptSection.CAPABILITIES}}}
|
||||
|
||||
|
||||
@@ -42,7 +42,6 @@ export const config = createVariant(ModelFamily.NATIVE_GPT_5_1)
|
||||
SystemPromptSection.TOOL_USE,
|
||||
SystemPromptSection.TASK_PROGRESS,
|
||||
SystemPromptSection.ACT_VS_PLAN,
|
||||
SystemPromptSection.CLI_SUBAGENTS,
|
||||
SystemPromptSection.CAPABILITIES,
|
||||
SystemPromptSection.FEEDBACK,
|
||||
SystemPromptSection.RULES,
|
||||
@@ -72,6 +71,7 @@ export const config = createVariant(ModelFamily.NATIVE_GPT_5_1)
|
||||
ClineDefaultTool.TODO,
|
||||
ClineDefaultTool.GENERATE_EXPLANATION,
|
||||
ClineDefaultTool.USE_SKILL,
|
||||
ClineDefaultTool.USE_SUBAGENTS,
|
||||
)
|
||||
.placeholders({
|
||||
MODEL_FAMILY: ModelFamily.NATIVE_GPT_5_1,
|
||||
|
||||
@@ -17,7 +17,6 @@ export const BASE = `{{${SystemPromptSection.AGENT_ROLE}}}
|
||||
|
||||
====
|
||||
|
||||
{{${SystemPromptSection.CLI_SUBAGENTS}}}
|
||||
|
||||
====
|
||||
|
||||
|
||||
@@ -48,7 +48,6 @@ export const config = createVariant(ModelFamily.NATIVE_GPT_5)
|
||||
SystemPromptSection.TOOL_USE,
|
||||
SystemPromptSection.TASK_PROGRESS,
|
||||
SystemPromptSection.ACT_VS_PLAN,
|
||||
SystemPromptSection.CLI_SUBAGENTS,
|
||||
SystemPromptSection.CAPABILITIES,
|
||||
SystemPromptSection.FEEDBACK,
|
||||
SystemPromptSection.RULES,
|
||||
@@ -78,6 +77,7 @@ export const config = createVariant(ModelFamily.NATIVE_GPT_5)
|
||||
ClineDefaultTool.TODO,
|
||||
ClineDefaultTool.GENERATE_EXPLANATION,
|
||||
ClineDefaultTool.USE_SKILL,
|
||||
ClineDefaultTool.USE_SUBAGENTS,
|
||||
)
|
||||
.placeholders({
|
||||
MODEL_FAMILY: ModelFamily.NATIVE_GPT_5,
|
||||
|
||||
@@ -17,7 +17,6 @@ export const BASE = `{{${SystemPromptSection.AGENT_ROLE}}}
|
||||
{{${SystemPromptSection.ACT_VS_PLAN}}}
|
||||
====
|
||||
|
||||
{{${SystemPromptSection.CLI_SUBAGENTS}}}
|
||||
|
||||
====
|
||||
|
||||
|
||||
@@ -63,6 +63,7 @@ export const config = createVariant(ModelFamily.NATIVE_NEXT_GEN)
|
||||
ClineDefaultTool.TODO,
|
||||
ClineDefaultTool.GENERATE_EXPLANATION,
|
||||
ClineDefaultTool.USE_SKILL,
|
||||
ClineDefaultTool.USE_SUBAGENTS,
|
||||
)
|
||||
.placeholders({
|
||||
MODEL_FAMILY: ModelFamily.NATIVE_NEXT_GEN,
|
||||
|
||||
@@ -39,7 +39,6 @@ export const config = createVariant(ModelFamily.NEXT_GEN)
|
||||
SystemPromptSection.MCP,
|
||||
SystemPromptSection.EDITING_FILES,
|
||||
SystemPromptSection.ACT_VS_PLAN,
|
||||
SystemPromptSection.CLI_SUBAGENTS,
|
||||
SystemPromptSection.CAPABILITIES,
|
||||
SystemPromptSection.FEEDBACK,
|
||||
SystemPromptSection.RULES,
|
||||
@@ -68,6 +67,7 @@ export const config = createVariant(ModelFamily.NEXT_GEN)
|
||||
ClineDefaultTool.TODO,
|
||||
ClineDefaultTool.GENERATE_EXPLANATION,
|
||||
ClineDefaultTool.USE_SKILL,
|
||||
ClineDefaultTool.USE_SUBAGENTS,
|
||||
)
|
||||
.placeholders({
|
||||
MODEL_FAMILY: ModelFamily.NEXT_GEN,
|
||||
|
||||
@@ -23,7 +23,6 @@ export const baseTemplate = `{{${SystemPromptSection.AGENT_ROLE}}}
|
||||
|
||||
====
|
||||
|
||||
{{${SystemPromptSection.CLI_SUBAGENTS}}}
|
||||
|
||||
====
|
||||
|
||||
|
||||
@@ -29,7 +29,6 @@ export const config = createVariant(ModelFamily.TRINITY)
|
||||
SystemPromptSection.MCP,
|
||||
SystemPromptSection.EDITING_FILES,
|
||||
SystemPromptSection.ACT_VS_PLAN,
|
||||
SystemPromptSection.CLI_SUBAGENTS,
|
||||
SystemPromptSection.CAPABILITIES,
|
||||
SystemPromptSection.RULES,
|
||||
SystemPromptSection.SYSTEM_INFO,
|
||||
@@ -55,6 +54,7 @@ export const config = createVariant(ModelFamily.TRINITY)
|
||||
ClineDefaultTool.TODO,
|
||||
ClineDefaultTool.GENERATE_EXPLANATION,
|
||||
ClineDefaultTool.USE_SKILL,
|
||||
ClineDefaultTool.USE_SUBAGENTS,
|
||||
)
|
||||
.placeholders({
|
||||
MODEL_FAMILY: ModelFamily.TRINITY,
|
||||
|
||||
@@ -22,7 +22,6 @@ export const baseTemplate = `{{${SystemPromptSection.AGENT_ROLE}}}
|
||||
|
||||
====
|
||||
|
||||
{{${SystemPromptSection.CLI_SUBAGENTS}}}
|
||||
|
||||
====
|
||||
|
||||
|
||||
@@ -33,7 +33,6 @@ export const config = createVariant(ModelFamily.XS)
|
||||
SystemPromptSection.TOOL_USE,
|
||||
SystemPromptSection.RULES,
|
||||
SystemPromptSection.ACT_VS_PLAN,
|
||||
SystemPromptSection.CLI_SUBAGENTS,
|
||||
SystemPromptSection.CAPABILITIES,
|
||||
SystemPromptSection.EDITING_FILES,
|
||||
SystemPromptSection.OBJECTIVE,
|
||||
@@ -50,6 +49,7 @@ export const config = createVariant(ModelFamily.XS)
|
||||
ClineDefaultTool.ASK,
|
||||
ClineDefaultTool.ATTEMPT,
|
||||
ClineDefaultTool.PLAN_MODE,
|
||||
ClineDefaultTool.USE_SUBAGENTS,
|
||||
)
|
||||
.placeholders({
|
||||
MODEL_FAMILY: ModelFamily.XS,
|
||||
@@ -63,9 +63,6 @@ export const config = createVariant(ModelFamily.XS)
|
||||
.overrideComponent(SystemPromptSection.RULES, {
|
||||
template: xsComponentOverrides.RULES,
|
||||
})
|
||||
.overrideComponent(SystemPromptSection.CLI_SUBAGENTS, {
|
||||
template: xsComponentOverrides.CLI_SUBAGENTS,
|
||||
})
|
||||
.overrideComponent(SystemPromptSection.ACT_VS_PLAN, {
|
||||
template: xsComponentOverrides.ACT_VS_PLAN,
|
||||
})
|
||||
|
||||
@@ -39,21 +39,6 @@ const XS_OBJECTIVES = `EXECUTION FLOW
|
||||
- Prefer replace_in_file; respect final formatted state.
|
||||
- When all steps succeed and are confirmed, call attempt_completion (optional demo command).`
|
||||
|
||||
const XS_CLI_SUBAGENTS = (context: SystemPromptContext) =>
|
||||
context.enableNativeToolCalls
|
||||
? ""
|
||||
: `USING THE CLINE CLI TOOL
|
||||
|
||||
The Cline CLI tool is installed and available for you to use to handle focused tasks without polluting your main context window. This can be done using
|
||||
\`\`\`bash
|
||||
cline t o "your prompt here"
|
||||
|
||||
This must only be used for searching and exploring code. It cannot be used to edit files or execute commands.
|
||||
Example:
|
||||
# Find specific patterns
|
||||
cline t o "find all React components that use the useState hook and list their names"
|
||||
\`\`\``
|
||||
|
||||
const XS_TOOLS_OVERRIDE = (context: SystemPromptContext) =>
|
||||
context.enableNativeToolCalls
|
||||
? `TOOLS
|
||||
@@ -118,7 +103,6 @@ export const xsComponentOverrides = {
|
||||
AGENT_ROLE:
|
||||
"You are Cline, a senior software engineer + precise task runner. Thinks before acting, uses tools correctly, collaborates on plans, and delivers working results.",
|
||||
RULES: XS_RULES,
|
||||
CLI_SUBAGENTS: XS_CLI_SUBAGENTS,
|
||||
ACT_VS_PLAN: XS_ACT_PLAN_MODE,
|
||||
CAPABILITIES: XS_CAPABILITIES,
|
||||
OBJECTIVE: XS_OBJECTIVES,
|
||||
|
||||
@@ -6,7 +6,6 @@ export const baseTemplate = `{{${SystemPromptSection.AGENT_ROLE}}}
|
||||
|
||||
## {{${SystemPromptSection.ACT_VS_PLAN}}}
|
||||
|
||||
## {{${SystemPromptSection.CLI_SUBAGENTS}}}
|
||||
|
||||
## {{${SystemPromptSection.CAPABILITIES}}}
|
||||
|
||||
|
||||
@@ -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}`)
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import { getHooksEnabledSafe } from "@core/hooks/hooks-utils"
|
||||
import { ClineIgnoreController } from "@core/ignore/ClineIgnoreController"
|
||||
import { CommandPermissionController } from "@core/permissions"
|
||||
import { DiffViewProvider } from "@integrations/editor/DiffViewProvider"
|
||||
import type { CommandExecutionOptions } from "@integrations/terminal"
|
||||
import { BrowserSession } from "@services/browser/BrowserSession"
|
||||
import { UrlContentFetcher } from "@services/browser/UrlContentFetcher"
|
||||
import { McpHub } from "@services/mcp/McpHub"
|
||||
@@ -39,6 +40,7 @@ import { PlanModeRespondHandler } from "./tools/handlers/PlanModeRespondHandler"
|
||||
import { ReadFileToolHandler } from "./tools/handlers/ReadFileToolHandler"
|
||||
import { ReportBugHandler } from "./tools/handlers/ReportBugHandler"
|
||||
import { SearchFilesToolHandler } from "./tools/handlers/SearchFilesToolHandler"
|
||||
import { UseSubagentsToolHandler } from "./tools/handlers/SubagentToolHandler"
|
||||
import { SummarizeTaskHandler } from "./tools/handlers/SummarizeTaskHandler"
|
||||
import { UseMcpToolHandler } from "./tools/handlers/UseMcpToolHandler"
|
||||
import { UseSkillToolHandler } from "./tools/handlers/UseSkillToolHandler"
|
||||
@@ -116,7 +118,12 @@ export class ToolExecutor {
|
||||
private saveCheckpoint: (isAttemptCompletionMessage?: boolean, completionMessageTs?: number) => Promise<void>,
|
||||
private sayAndCreateMissingParamError: (toolName: ClineDefaultTool, paramName: string, relPath?: string) => Promise<any>,
|
||||
private removeLastPartialMessageIfExistsWithType: (type: "ask" | "say", askOrSay: ClineAsk | ClineSay) => Promise<void>,
|
||||
private executeCommandTool: (command: string, timeoutSeconds: number | undefined) => Promise<[boolean, any]>,
|
||||
private executeCommandTool: (
|
||||
command: string,
|
||||
timeoutSeconds: number | undefined,
|
||||
options?: CommandExecutionOptions,
|
||||
) => Promise<[boolean, any]>,
|
||||
private cancelRunningCommandTool: () => Promise<boolean>,
|
||||
private doesLatestTaskCompletionHaveNewChanges: () => Promise<boolean>,
|
||||
private updateFCListFromToolResponse: (taskProgress: string | undefined) => Promise<void>,
|
||||
private switchToActMode: () => Promise<boolean>,
|
||||
@@ -151,6 +158,7 @@ export class ToolExecutor {
|
||||
doubleCheckCompletionEnabled: this.stateManager.getGlobalSettingsKey("doubleCheckCompletionEnabled"),
|
||||
vscodeTerminalExecutionMode: this.vscodeTerminalExecutionMode,
|
||||
enableParallelToolCalling: this.isParallelToolCallingEnabled(),
|
||||
isSubagentExecution: false,
|
||||
cwd: this.cwd,
|
||||
workspaceManager: this.workspaceManager,
|
||||
isMultiRootEnabled: this.isMultiRootEnabled,
|
||||
@@ -181,6 +189,7 @@ export class ToolExecutor {
|
||||
cancelTask: this.cancelTask,
|
||||
updateTaskHistory: async (_: any) => [],
|
||||
executeCommandTool: this.executeCommandTool,
|
||||
cancelRunningCommandTool: this.cancelRunningCommandTool,
|
||||
doesLatestTaskCompletionHaveNewChanges: this.doesLatestTaskCompletionHaveNewChanges,
|
||||
updateFCListFromToolResponse: this.updateFCListFromToolResponse,
|
||||
sayAndCreateMissingParamError: this.sayAndCreateMissingParamError,
|
||||
@@ -238,6 +247,7 @@ export class ToolExecutor {
|
||||
this.coordinator.register(new ReportBugHandler())
|
||||
this.coordinator.register(new ApplyPatchHandler(validator))
|
||||
this.coordinator.register(new GenerateExplanationToolHandler())
|
||||
this.coordinator.register(new UseSubagentsToolHandler())
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+10
-22
@@ -76,6 +76,7 @@ import { getSystemPrompt } from "@/core/prompts/system-prompt"
|
||||
import { HostProvider } from "@/hosts/host-provider"
|
||||
import { FileEditProvider } from "@/integrations/editor/FileEditProvider"
|
||||
import {
|
||||
type CommandExecutionOptions,
|
||||
CommandExecutor,
|
||||
CommandExecutorCallbacks,
|
||||
FullCommandExecutorConfig,
|
||||
@@ -98,7 +99,6 @@ import { ApiFormat } from "@/shared/proto/cline/models"
|
||||
import { ShowMessageType } from "@/shared/proto/index.host"
|
||||
import { Logger } from "@/shared/services/Logger"
|
||||
import { Session } from "@/shared/services/Session"
|
||||
import { isClineCliInstalled, isCliSubagentContext } from "@/utils/cli-detector"
|
||||
import { RuleContextBuilder } from "../context/instructions/user-instructions/RuleContextBuilder"
|
||||
import { ensureLocalClineDirExists } from "../context/instructions/user-instructions/rule-helpers"
|
||||
import { discoverSkills, getAvailableSkills } from "../context/instructions/user-instructions/skills"
|
||||
@@ -126,7 +126,6 @@ type TaskParams = {
|
||||
shellIntegrationTimeout: number
|
||||
terminalReuseEnabled: boolean
|
||||
terminalOutputLineLimit: number
|
||||
subagentTerminalOutputLineLimit: number
|
||||
defaultTerminalProfile: string
|
||||
vscodeTerminalExecutionMode: "vscodeTerminal" | "backgroundExec"
|
||||
cwd: string
|
||||
@@ -261,7 +260,6 @@ export class Task {
|
||||
shellIntegrationTimeout,
|
||||
terminalReuseEnabled,
|
||||
terminalOutputLineLimit,
|
||||
subagentTerminalOutputLineLimit,
|
||||
defaultTerminalProfile,
|
||||
vscodeTerminalExecutionMode,
|
||||
cwd,
|
||||
@@ -303,7 +301,6 @@ export class Task {
|
||||
this.terminalManager.setShellIntegrationTimeout(shellIntegrationTimeout)
|
||||
this.terminalManager.setTerminalReuseEnabled(terminalReuseEnabled ?? true)
|
||||
this.terminalManager.setTerminalOutputLineLimit(terminalOutputLineLimit)
|
||||
this.terminalManager.setSubagentTerminalOutputLineLimit(subagentTerminalOutputLineLimit)
|
||||
this.terminalManager.setDefaultTerminalProfile(defaultTerminalProfile)
|
||||
|
||||
this.urlContentFetcher = new UrlContentFetcher(controller.context)
|
||||
@@ -552,6 +549,7 @@ export class Task {
|
||||
this.sayAndCreateMissingParamError.bind(this),
|
||||
this.removeLastPartialMessageIfExistsWithType.bind(this),
|
||||
this.executeCommandTool.bind(this),
|
||||
this.cancelBackgroundCommand.bind(this),
|
||||
() => this.checkpointManager?.doesLatestTaskCompletionHaveNewChanges() ?? Promise.resolve(false),
|
||||
this.FocusChainManager?.updateFCListFromToolResponse.bind(this.FocusChainManager) || (async () => {}),
|
||||
this.switchToActModeCallback.bind(this),
|
||||
@@ -1552,8 +1550,12 @@ export class Task {
|
||||
}
|
||||
|
||||
// Tools
|
||||
async executeCommandTool(command: string, timeoutSeconds: number | undefined): Promise<[boolean, ClineToolResponseContent]> {
|
||||
return this.commandExecutor.execute(command, timeoutSeconds)
|
||||
async executeCommandTool(
|
||||
command: string,
|
||||
timeoutSeconds: number | undefined,
|
||||
options?: CommandExecutionOptions,
|
||||
): Promise<[boolean, ClineToolResponseContent]> {
|
||||
return this.commandExecutor.execute(command, timeoutSeconds, options)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1757,14 +1759,6 @@ export class Task {
|
||||
? `# Preferred Language\n\nSpeak in ${preferredLanguage}.`
|
||||
: ""
|
||||
|
||||
// Check CLI installation status only if subagents are enabled
|
||||
const subagentsEnabled = this.stateManager.getGlobalSettingsKey("subagentsEnabled")
|
||||
let isSubagentsEnabledAndCliInstalled = false
|
||||
if (subagentsEnabled) {
|
||||
const clineCliInstalled = await isClineCliInstalled()
|
||||
isSubagentsEnabledAndCliInstalled = subagentsEnabled && clineCliInstalled
|
||||
}
|
||||
|
||||
const { globalToggles, localToggles } = await refreshClineRulesToggles(this.controller, this.cwd)
|
||||
const { windsurfLocalToggles, cursorLocalToggles, agentsLocalToggles } = await refreshExternalRulesToggles(
|
||||
this.controller,
|
||||
@@ -1808,12 +1802,6 @@ export class Task {
|
||||
}))
|
||||
}
|
||||
|
||||
// Detect if this is a CLI subagent to prevent nested subagent creation
|
||||
const isCliSubagent = isCliSubagentContext({
|
||||
yoloModeToggled: this.stateManager.getGlobalSettingsKey("yoloModeToggled"),
|
||||
maxConsecutiveMistakes: this.stateManager.getGlobalSettingsKey("maxConsecutiveMistakes"),
|
||||
})
|
||||
|
||||
// Discover and filter available skills
|
||||
const allSkills = await discoverSkills(this.cwd)
|
||||
const resolvedSkills = getAvailableSkills(allSkills)
|
||||
@@ -1856,12 +1844,12 @@ export class Task {
|
||||
preferredLanguageInstructions,
|
||||
browserSettings: this.stateManager.getGlobalSettingsKey("browserSettings"),
|
||||
yoloModeToggled: this.stateManager.getGlobalSettingsKey("yoloModeToggled"),
|
||||
subagentsEnabled: this.stateManager.getGlobalSettingsKey("subagentsEnabled"),
|
||||
clineWebToolsEnabled:
|
||||
this.stateManager.getGlobalSettingsKey("clineWebToolsEnabled") && featureFlagsService.getWebtoolsEnabled(),
|
||||
isMultiRootEnabled: multiRootEnabled,
|
||||
workspaceRoots,
|
||||
isSubagentsEnabledAndCliInstalled,
|
||||
isCliSubagent,
|
||||
isSubagentRun: false,
|
||||
isCliEnvironment,
|
||||
enableNativeToolCalls:
|
||||
providerInfo.model.info.apiFormat === ApiFormat.OPENAI_RESPONSES ||
|
||||
|
||||
@@ -51,6 +51,7 @@ export class AutoApprove {
|
||||
case ClineDefaultTool.FILE_EDIT:
|
||||
case ClineDefaultTool.APPLY_PATCH:
|
||||
case ClineDefaultTool.BASH:
|
||||
case ClineDefaultTool.USE_SUBAGENTS:
|
||||
return [true, true]
|
||||
|
||||
case ClineDefaultTool.BROWSER:
|
||||
@@ -73,6 +74,7 @@ export class AutoApprove {
|
||||
case ClineDefaultTool.FILE_EDIT:
|
||||
case ClineDefaultTool.APPLY_PATCH:
|
||||
case ClineDefaultTool.BASH:
|
||||
case ClineDefaultTool.USE_SUBAGENTS:
|
||||
return [true, true]
|
||||
case ClineDefaultTool.BROWSER:
|
||||
case ClineDefaultTool.WEB_FETCH:
|
||||
@@ -90,6 +92,7 @@ export class AutoApprove {
|
||||
case ClineDefaultTool.LIST_FILES:
|
||||
case ClineDefaultTool.LIST_CODE_DEF:
|
||||
case ClineDefaultTool.SEARCH:
|
||||
case ClineDefaultTool.USE_SUBAGENTS:
|
||||
return [autoApprovalSettings.actions.readFiles, autoApprovalSettings.actions.readFilesExternally ?? false]
|
||||
case ClineDefaultTool.NEW_RULE:
|
||||
case ClineDefaultTool.FILE_NEW:
|
||||
@@ -127,7 +130,7 @@ export class AutoApprove {
|
||||
return true
|
||||
}
|
||||
|
||||
let isLocalRead: boolean = false
|
||||
let isLocalRead = false
|
||||
if (autoApproveActionpath) {
|
||||
// Use cached workspace info instead of fetching every time
|
||||
const { isMultiRootScenario } = await this.getWorkspaceInfo()
|
||||
@@ -159,8 +162,7 @@ export class AutoApprove {
|
||||
|
||||
if ((isLocalRead && autoApproveLocal) || (!isLocalRead && autoApproveLocal && autoApproveExternal)) {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,6 +67,9 @@ export class ExecuteCommandToolHandler implements IFullyManagedTool {
|
||||
|
||||
async handlePartialBlock(block: ToolUse, uiHelpers: StronglyTypedUIHelpers): Promise<void> {
|
||||
const command = block.params.command
|
||||
if (uiHelpers.getConfig().isSubagentExecution) {
|
||||
return
|
||||
}
|
||||
|
||||
// Check if this should be auto-approved to determine UI flow
|
||||
const shouldAutoApprove = uiHelpers.shouldAutoApproveTool(this.name)
|
||||
@@ -168,14 +171,18 @@ export class ExecuteCommandToolHandler implements IFullyManagedTool {
|
||||
`Command "${actualCommand}" was denied by CLINE_COMMAND_PERMISSIONS. ` +
|
||||
`Reason: ${permissionResult.reason}${matchedPattern}`
|
||||
}
|
||||
await config.callbacks.say("command_permission_denied", errorMessage)
|
||||
if (!config.isSubagentExecution) {
|
||||
await config.callbacks.say("command_permission_denied", errorMessage)
|
||||
}
|
||||
return formatResponse.toolError(formatResponse.permissionDeniedError(errorMessage))
|
||||
}
|
||||
|
||||
// Check clineignore validation for command
|
||||
const ignoredFileAttemptedToAccess = config.services.clineIgnoreController.validateCommand(actualCommand)
|
||||
if (ignoredFileAttemptedToAccess) {
|
||||
await config.callbacks.say("clineignore_error", ignoredFileAttemptedToAccess)
|
||||
if (!config.isSubagentExecution) {
|
||||
await config.callbacks.say("clineignore_error", ignoredFileAttemptedToAccess)
|
||||
}
|
||||
return formatResponse.toolError(formatResponse.clineIgnoreError(ignoredFileAttemptedToAccess))
|
||||
}
|
||||
|
||||
@@ -210,10 +217,16 @@ export class ExecuteCommandToolHandler implements IFullyManagedTool {
|
||||
)
|
||||
}
|
||||
|
||||
if ((!requiresApprovalPerLLM && autoApproveSafe) || (requiresApprovalPerLLM && autoApproveSafe && autoApproveAll)) {
|
||||
if (
|
||||
config.isSubagentExecution ||
|
||||
(!requiresApprovalPerLLM && autoApproveSafe) ||
|
||||
(requiresApprovalPerLLM && autoApproveSafe && autoApproveAll)
|
||||
) {
|
||||
// Auto-approve flow
|
||||
await config.callbacks.removeLastPartialMessageIfExistsWithType("ask", "command")
|
||||
await config.callbacks.say("command", actualCommand, undefined, undefined, false)
|
||||
if (!config.isSubagentExecution) {
|
||||
await config.callbacks.removeLastPartialMessageIfExistsWithType("ask", "command")
|
||||
await config.callbacks.say("command", actualCommand, undefined, undefined, false)
|
||||
}
|
||||
didAutoApprove = true
|
||||
telemetryService.captureToolUsage(
|
||||
config.ulid,
|
||||
@@ -276,7 +289,7 @@ export class ExecuteCommandToolHandler implements IFullyManagedTool {
|
||||
|
||||
// Setup timeout notification for long-running auto-approved commands
|
||||
let timeoutId: NodeJS.Timeout | undefined
|
||||
if (didAutoApprove && config.autoApprovalSettings.enableNotifications) {
|
||||
if (didAutoApprove && config.autoApprovalSettings.enableNotifications && !config.isSubagentExecution) {
|
||||
// if the command was auto-approved, and it's long running we need to notify the user after some time has passed without proceeding
|
||||
timeoutId = setTimeout(() => {
|
||||
showSystemNotification({
|
||||
|
||||
@@ -26,6 +26,9 @@ export class ListCodeDefinitionNamesToolHandler implements IFullyManagedTool {
|
||||
const relPath = block.params.path
|
||||
|
||||
const config = uiHelpers.getConfig()
|
||||
if (config.isSubagentExecution) {
|
||||
return
|
||||
}
|
||||
|
||||
// Create and show partial UI message
|
||||
const sharedMessageProps = {
|
||||
@@ -82,10 +85,14 @@ export class ListCodeDefinitionNamesToolHandler implements IFullyManagedTool {
|
||||
|
||||
const completeMessage = JSON.stringify(sharedMessageProps)
|
||||
|
||||
if (await config.callbacks.shouldAutoApproveToolWithPath(block.name, relDirPath)) {
|
||||
const shouldAutoApprove =
|
||||
config.isSubagentExecution || (await config.callbacks.shouldAutoApproveToolWithPath(block.name, relDirPath))
|
||||
if (shouldAutoApprove) {
|
||||
// Auto-approval flow
|
||||
await config.callbacks.removeLastPartialMessageIfExistsWithType("ask", "tool")
|
||||
await config.callbacks.say("tool", completeMessage, undefined, undefined, false)
|
||||
if (!config.isSubagentExecution) {
|
||||
await config.callbacks.removeLastPartialMessageIfExistsWithType("ask", "tool")
|
||||
await config.callbacks.say("tool", completeMessage, undefined, undefined, false)
|
||||
}
|
||||
|
||||
// Capture telemetry
|
||||
telemetryService.captureToolUsage(
|
||||
@@ -120,18 +127,17 @@ export class ListCodeDefinitionNamesToolHandler implements IFullyManagedTool {
|
||||
block.isNativeToolCall,
|
||||
)
|
||||
return formatResponse.toolDenied()
|
||||
} else {
|
||||
telemetryService.captureToolUsage(
|
||||
config.ulid,
|
||||
block.name,
|
||||
config.api.getModel().id,
|
||||
provider,
|
||||
false,
|
||||
true,
|
||||
undefined,
|
||||
block.isNativeToolCall,
|
||||
)
|
||||
}
|
||||
telemetryService.captureToolUsage(
|
||||
config.ulid,
|
||||
block.name,
|
||||
config.api.getModel().id,
|
||||
provider,
|
||||
false,
|
||||
true,
|
||||
undefined,
|
||||
block.isNativeToolCall,
|
||||
)
|
||||
}
|
||||
|
||||
// Run PreToolUse hook after approval but before execution
|
||||
|
||||
@@ -28,6 +28,9 @@ export class ListFilesToolHandler implements IFullyManagedTool {
|
||||
|
||||
// Get config access for services
|
||||
const config = uiHelpers.getConfig()
|
||||
if (config.isSubagentExecution) {
|
||||
return
|
||||
}
|
||||
|
||||
// Create and show partial UI message
|
||||
const recursiveRaw = block.params.recursive
|
||||
@@ -87,7 +90,9 @@ export class ListFilesToolHandler implements IFullyManagedTool {
|
||||
// Check clineignore access
|
||||
const accessValidation = this.validator.checkClineIgnorePath(relDirPath!)
|
||||
if (!accessValidation.ok) {
|
||||
await config.callbacks.say("clineignore_error", relDirPath)
|
||||
if (!config.isSubagentExecution) {
|
||||
await config.callbacks.say("clineignore_error", relDirPath)
|
||||
}
|
||||
return formatResponse.toolError(formatResponse.clineIgnoreError(relDirPath!))
|
||||
}
|
||||
|
||||
@@ -106,10 +111,14 @@ export class ListFilesToolHandler implements IFullyManagedTool {
|
||||
|
||||
const completeMessage = JSON.stringify(sharedMessageProps)
|
||||
|
||||
if (await config.callbacks.shouldAutoApproveToolWithPath(block.name, relDirPath)) {
|
||||
const shouldAutoApprove =
|
||||
config.isSubagentExecution || (await config.callbacks.shouldAutoApproveToolWithPath(block.name, relDirPath))
|
||||
if (shouldAutoApprove) {
|
||||
// Auto-approval flow
|
||||
await config.callbacks.removeLastPartialMessageIfExistsWithType("ask", "tool")
|
||||
await config.callbacks.say("tool", completeMessage, undefined, undefined, false)
|
||||
if (!config.isSubagentExecution) {
|
||||
await config.callbacks.removeLastPartialMessageIfExistsWithType("ask", "tool")
|
||||
await config.callbacks.say("tool", completeMessage, undefined, undefined, false)
|
||||
}
|
||||
|
||||
// Capture telemetry
|
||||
telemetryService.captureToolUsage(
|
||||
@@ -144,18 +153,17 @@ export class ListFilesToolHandler implements IFullyManagedTool {
|
||||
block.isNativeToolCall,
|
||||
)
|
||||
return formatResponse.toolDenied()
|
||||
} else {
|
||||
telemetryService.captureToolUsage(
|
||||
config.ulid,
|
||||
block.name,
|
||||
config.api.getModel().id,
|
||||
provider,
|
||||
false,
|
||||
true,
|
||||
workspaceContext,
|
||||
block.isNativeToolCall,
|
||||
)
|
||||
}
|
||||
telemetryService.captureToolUsage(
|
||||
config.ulid,
|
||||
block.name,
|
||||
config.api.getModel().id,
|
||||
provider,
|
||||
false,
|
||||
true,
|
||||
workspaceContext,
|
||||
block.isNativeToolCall,
|
||||
)
|
||||
}
|
||||
|
||||
// Run PreToolUse hook after approval but before execution
|
||||
|
||||
@@ -28,6 +28,9 @@ export class ReadFileToolHandler implements IFullyManagedTool {
|
||||
const relPath = block.params.path
|
||||
|
||||
const config = uiHelpers.getConfig()
|
||||
if (config.isSubagentExecution) {
|
||||
return
|
||||
}
|
||||
|
||||
// Create and show partial UI message
|
||||
const sharedMessageProps = {
|
||||
@@ -67,7 +70,9 @@ export class ReadFileToolHandler implements IFullyManagedTool {
|
||||
// Check clineignore access
|
||||
const accessValidation = this.validator.checkClineIgnorePath(relPath!)
|
||||
if (!accessValidation.ok) {
|
||||
await config.callbacks.say("clineignore_error", relPath)
|
||||
if (!config.isSubagentExecution) {
|
||||
await config.callbacks.say("clineignore_error", relPath)
|
||||
}
|
||||
return formatResponse.toolError(formatResponse.clineIgnoreError(relPath!))
|
||||
}
|
||||
|
||||
@@ -97,10 +102,14 @@ export class ReadFileToolHandler implements IFullyManagedTool {
|
||||
|
||||
const completeMessage = JSON.stringify(sharedMessageProps)
|
||||
|
||||
if (await config.callbacks.shouldAutoApproveToolWithPath(block.name, relPath)) {
|
||||
const shouldAutoApprove =
|
||||
config.isSubagentExecution || (await config.callbacks.shouldAutoApproveToolWithPath(block.name, relPath))
|
||||
if (shouldAutoApprove) {
|
||||
// Auto-approval flow
|
||||
await config.callbacks.removeLastPartialMessageIfExistsWithType("ask", "tool")
|
||||
await config.callbacks.say("tool", completeMessage, undefined, undefined, false)
|
||||
if (!config.isSubagentExecution) {
|
||||
await config.callbacks.removeLastPartialMessageIfExistsWithType("ask", "tool")
|
||||
await config.callbacks.say("tool", completeMessage, undefined, undefined, false)
|
||||
}
|
||||
|
||||
// Capture telemetry
|
||||
telemetryService.captureToolUsage(
|
||||
@@ -135,18 +144,17 @@ export class ReadFileToolHandler implements IFullyManagedTool {
|
||||
block.isNativeToolCall,
|
||||
)
|
||||
return formatResponse.toolDenied()
|
||||
} else {
|
||||
telemetryService.captureToolUsage(
|
||||
config.ulid,
|
||||
block.name,
|
||||
config.api.getModel().id,
|
||||
provider,
|
||||
false,
|
||||
true,
|
||||
workspaceContext,
|
||||
block.isNativeToolCall,
|
||||
)
|
||||
}
|
||||
telemetryService.captureToolUsage(
|
||||
config.ulid,
|
||||
block.name,
|
||||
config.api.getModel().id,
|
||||
provider,
|
||||
false,
|
||||
true,
|
||||
workspaceContext,
|
||||
block.isNativeToolCall,
|
||||
)
|
||||
}
|
||||
|
||||
// Run PreToolUse hook after approval but before execution
|
||||
|
||||
@@ -51,23 +51,21 @@ export class SearchFilesToolHandler implements IFullyManagedTool {
|
||||
const workspaceRoots = adapter.getWorkspaceRoots()
|
||||
const root = workspaceRoots.find((r) => r.name === workspaceHint)
|
||||
return [{ absolutePath, workspaceName: workspaceHint, workspaceRoot: root?.path }]
|
||||
} else {
|
||||
// As a fallback, perform the search across all available workspaces.
|
||||
// Typically, models should provide explicit hints to target specific workspaces for searching.
|
||||
const allPaths = adapter.getAllPossiblePaths(parsedPath)
|
||||
const workspaceRoots = adapter.getWorkspaceRoots()
|
||||
return allPaths.map((absPath, index) => ({
|
||||
absolutePath: absPath,
|
||||
workspaceName: workspaceRoots[index]?.name || path.basename(workspaceRoots[index]?.path || absPath),
|
||||
workspaceRoot: workspaceRoots[index]?.path,
|
||||
}))
|
||||
}
|
||||
} else {
|
||||
// Single-workspace mode (backward compatible)
|
||||
const pathResult = resolveWorkspacePath(config, originalPath, "SearchFilesTool.execute")
|
||||
const absolutePath = typeof pathResult === "string" ? pathResult : pathResult.absolutePath
|
||||
return [{ absolutePath, workspaceRoot: config.cwd }]
|
||||
// As a fallback, perform the search across all available workspaces.
|
||||
// Typically, models should provide explicit hints to target specific workspaces for searching.
|
||||
const allPaths = adapter.getAllPossiblePaths(parsedPath)
|
||||
const workspaceRoots = adapter.getWorkspaceRoots()
|
||||
return allPaths.map((absPath, index) => ({
|
||||
absolutePath: absPath,
|
||||
workspaceName: workspaceRoots[index]?.name || path.basename(workspaceRoots[index]?.path || absPath),
|
||||
workspaceRoot: workspaceRoots[index]?.path,
|
||||
}))
|
||||
}
|
||||
// Single-workspace mode (backward compatible)
|
||||
const pathResult = resolveWorkspacePath(config, originalPath, "SearchFilesTool.execute")
|
||||
const absolutePath = typeof pathResult === "string" ? pathResult : pathResult.absolutePath
|
||||
return [{ absolutePath, workspaceRoot: config.cwd }]
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -96,7 +94,7 @@ export class SearchFilesToolHandler implements IFullyManagedTool {
|
||||
// Parse the result count from the first line
|
||||
const firstLine = workspaceResults.split("\n")[0]
|
||||
const resultMatch = firstLine.match(/Found (\d+) result/)
|
||||
const resultCount = resultMatch ? parseInt(resultMatch[1], 10) : 0
|
||||
const resultCount = resultMatch ? Number.parseInt(resultMatch[1], 10) : 0
|
||||
|
||||
return {
|
||||
workspaceName,
|
||||
@@ -164,13 +162,11 @@ export class SearchFilesToolHandler implements IFullyManagedTool {
|
||||
// Multi-workspace search result
|
||||
if (totalResultCount === 0) {
|
||||
return "Found 0 results."
|
||||
} else {
|
||||
return `Found ${totalResultCount === 1 ? "1 result" : `${totalResultCount.toLocaleString()} results`} across ${searchPaths.length} workspace${searchPaths.length > 1 ? "s" : ""}.\n\n${allResults.join("\n\n")}`
|
||||
}
|
||||
} else {
|
||||
// Single workspace result
|
||||
return allResults[0] || "Found 0 results."
|
||||
return `Found ${totalResultCount === 1 ? "1 result" : `${totalResultCount.toLocaleString()} results`} across ${searchPaths.length} workspace${searchPaths.length > 1 ? "s" : ""}.\n\n${allResults.join("\n\n")}`
|
||||
}
|
||||
// Single workspace result
|
||||
return allResults[0] || "Found 0 results."
|
||||
}
|
||||
|
||||
async handlePartialBlock(block: ToolUse, uiHelpers: StronglyTypedUIHelpers): Promise<void> {
|
||||
@@ -178,6 +174,9 @@ export class SearchFilesToolHandler implements IFullyManagedTool {
|
||||
const regex = block.params.regex
|
||||
|
||||
const config = uiHelpers.getConfig()
|
||||
if (config.isSubagentExecution) {
|
||||
return
|
||||
}
|
||||
|
||||
// Create and show partial UI message
|
||||
const filePattern = block.params.file_pattern
|
||||
@@ -306,10 +305,14 @@ export class SearchFilesToolHandler implements IFullyManagedTool {
|
||||
|
||||
const completeMessage = JSON.stringify(sharedMessageProps)
|
||||
|
||||
if (await config.callbacks.shouldAutoApproveToolWithPath(block.name, relDirPath)) {
|
||||
const shouldAutoApprove =
|
||||
config.isSubagentExecution || (await config.callbacks.shouldAutoApproveToolWithPath(block.name, relDirPath))
|
||||
if (shouldAutoApprove) {
|
||||
// Auto-approval flow
|
||||
await config.callbacks.removeLastPartialMessageIfExistsWithType("ask", "tool")
|
||||
await config.callbacks.say("tool", completeMessage, undefined, undefined, false)
|
||||
if (!config.isSubagentExecution) {
|
||||
await config.callbacks.removeLastPartialMessageIfExistsWithType("ask", "tool")
|
||||
await config.callbacks.say("tool", completeMessage, undefined, undefined, false)
|
||||
}
|
||||
|
||||
// Capture telemetry
|
||||
telemetryService.captureToolUsage(
|
||||
@@ -344,18 +347,17 @@ export class SearchFilesToolHandler implements IFullyManagedTool {
|
||||
block.isNativeToolCall,
|
||||
)
|
||||
return formatResponse.toolDenied()
|
||||
} else {
|
||||
telemetryService.captureToolUsage(
|
||||
config.ulid,
|
||||
block.name,
|
||||
config.api.getModel().id,
|
||||
provider,
|
||||
false,
|
||||
true,
|
||||
workspaceContext,
|
||||
block.isNativeToolCall,
|
||||
)
|
||||
}
|
||||
telemetryService.captureToolUsage(
|
||||
config.ulid,
|
||||
block.name,
|
||||
config.api.getModel().id,
|
||||
provider,
|
||||
false,
|
||||
true,
|
||||
workspaceContext,
|
||||
block.isNativeToolCall,
|
||||
)
|
||||
}
|
||||
|
||||
// Run PreToolUse hook after approval but before execution
|
||||
|
||||
@@ -0,0 +1,296 @@
|
||||
import type { ToolUse } from "@core/assistant-message"
|
||||
import { formatResponse } from "@core/prompts/responses"
|
||||
import {
|
||||
ClineAskUseSubagents,
|
||||
ClineSaySubagentStatus,
|
||||
ClineSubagentUsageInfo,
|
||||
SubagentStatusItem,
|
||||
} from "@shared/ExtensionMessage"
|
||||
import { telemetryService } from "@/services/telemetry"
|
||||
import { ClineDefaultTool } from "@/shared/tools"
|
||||
import type { ToolResponse } from "../../index"
|
||||
import { showNotificationForApproval } from "../../utils"
|
||||
import { SubagentRunner } from "../subagent/SubagentRunner"
|
||||
import type { IFullyManagedTool } from "../ToolExecutorCoordinator"
|
||||
import type { TaskConfig } from "../types/TaskConfig"
|
||||
import type { StronglyTypedUIHelpers } from "../types/UIHelpers"
|
||||
import { ToolResultUtils } from "../utils/ToolResultUtils"
|
||||
|
||||
const MAX_SUBAGENT_PROMPTS = 5
|
||||
const PROMPT_KEYS = ["prompt_1", "prompt_2", "prompt_3", "prompt_4", "prompt_5"] as const
|
||||
|
||||
function excerpt(text: string | undefined, maxChars = 1200): string {
|
||||
if (!text) {
|
||||
return ""
|
||||
}
|
||||
|
||||
const trimmed = text.trim()
|
||||
if (trimmed.length <= maxChars) {
|
||||
return trimmed
|
||||
}
|
||||
|
||||
return `${trimmed.slice(0, maxChars)}...`
|
||||
}
|
||||
|
||||
export class UseSubagentsToolHandler implements IFullyManagedTool {
|
||||
readonly name = ClineDefaultTool.USE_SUBAGENTS
|
||||
|
||||
getDescription(_block: ToolUse): string {
|
||||
return "[subagents]"
|
||||
}
|
||||
|
||||
async handlePartialBlock(block: ToolUse, uiHelpers: StronglyTypedUIHelpers): Promise<void> {
|
||||
const prompts = PROMPT_KEYS.map((key) => uiHelpers.removeClosingTag(block, key, block.params[key]?.trim()))
|
||||
.map((prompt) => prompt?.trim())
|
||||
.filter((prompt): prompt is string => !!prompt)
|
||||
|
||||
if (prompts.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
const partialMessage = JSON.stringify({ prompts } satisfies ClineAskUseSubagents)
|
||||
const autoApproveResult = uiHelpers.shouldAutoApproveTool(this.name)
|
||||
const [shouldAutoApprove] = Array.isArray(autoApproveResult) ? autoApproveResult : [autoApproveResult, false]
|
||||
|
||||
if (shouldAutoApprove) {
|
||||
await uiHelpers.removeLastPartialMessageIfExistsWithType("ask", "use_subagents")
|
||||
await uiHelpers.say("use_subagents", partialMessage, undefined, undefined, block.partial)
|
||||
} else {
|
||||
await uiHelpers.removeLastPartialMessageIfExistsWithType("say", "use_subagents")
|
||||
await uiHelpers.ask("use_subagents", partialMessage, block.partial).catch(() => {})
|
||||
}
|
||||
}
|
||||
|
||||
async execute(config: TaskConfig, block: ToolUse): Promise<ToolResponse> {
|
||||
const subagentsEnabled = config.services.stateManager.getGlobalSettingsKey("subagentsEnabled")
|
||||
if (!subagentsEnabled) {
|
||||
return formatResponse.toolError("Subagents are disabled. Enable them in Settings > Features to use this tool.")
|
||||
}
|
||||
|
||||
const prompts = PROMPT_KEYS.map((key) => block.params[key]?.trim()).filter((prompt): prompt is string => !!prompt)
|
||||
|
||||
if (prompts.length === 0) {
|
||||
config.taskState.consecutiveMistakeCount++
|
||||
return await config.callbacks.sayAndCreateMissingParamError(this.name, "prompt_1")
|
||||
}
|
||||
|
||||
if (prompts.length > MAX_SUBAGENT_PROMPTS) {
|
||||
config.taskState.consecutiveMistakeCount++
|
||||
return formatResponse.toolError(
|
||||
`Too many subagent prompts provided (${prompts.length}). Maximum is ${MAX_SUBAGENT_PROMPTS}.`,
|
||||
)
|
||||
}
|
||||
|
||||
const apiConfig = config.services.stateManager.getApiConfiguration()
|
||||
const currentMode = config.services.stateManager.getGlobalSettingsKey("mode")
|
||||
const provider = (currentMode === "plan" ? apiConfig.planModeApiProvider : apiConfig.actModeApiProvider) as string
|
||||
const approvalPayload: ClineAskUseSubagents = { prompts }
|
||||
const approvalBody = JSON.stringify(approvalPayload)
|
||||
|
||||
const autoApproveResult = config.autoApprover?.shouldAutoApproveTool(this.name)
|
||||
const [autoApproveSafe] = Array.isArray(autoApproveResult) ? autoApproveResult : [autoApproveResult, false]
|
||||
const didAutoApprove = !!autoApproveSafe
|
||||
|
||||
if (didAutoApprove) {
|
||||
telemetryService.captureToolUsage(
|
||||
config.ulid,
|
||||
this.name,
|
||||
config.api.getModel().id,
|
||||
provider,
|
||||
true,
|
||||
true,
|
||||
undefined,
|
||||
block.isNativeToolCall,
|
||||
)
|
||||
} else {
|
||||
showNotificationForApproval(
|
||||
prompts.length === 1 ? "Cline wants to use a subagent" : `Cline wants to use ${prompts.length} subagents`,
|
||||
config.autoApprovalSettings.enableNotifications,
|
||||
)
|
||||
const didApprove = await ToolResultUtils.askApprovalAndPushFeedback("use_subagents", approvalBody, config)
|
||||
if (!didApprove) {
|
||||
telemetryService.captureToolUsage(
|
||||
config.ulid,
|
||||
this.name,
|
||||
config.api.getModel().id,
|
||||
provider,
|
||||
false,
|
||||
false,
|
||||
undefined,
|
||||
block.isNativeToolCall,
|
||||
)
|
||||
return formatResponse.toolDenied()
|
||||
}
|
||||
telemetryService.captureToolUsage(
|
||||
config.ulid,
|
||||
this.name,
|
||||
config.api.getModel().id,
|
||||
provider,
|
||||
false,
|
||||
true,
|
||||
undefined,
|
||||
block.isNativeToolCall,
|
||||
)
|
||||
}
|
||||
|
||||
config.taskState.consecutiveMistakeCount = 0
|
||||
|
||||
const entries: SubagentStatusItem[] = prompts.map((prompt, index) => ({
|
||||
index: index + 1,
|
||||
prompt,
|
||||
status: "pending",
|
||||
toolCalls: 0,
|
||||
inputTokens: 0,
|
||||
outputTokens: 0,
|
||||
totalCost: 0,
|
||||
contextTokens: 0,
|
||||
contextWindow: 0,
|
||||
contextUsagePercentage: 0,
|
||||
}))
|
||||
|
||||
const emitStatus = async (status: ClineSaySubagentStatus["status"], partial: boolean) => {
|
||||
const completed = entries.filter((entry) => entry.status === "completed" || entry.status === "failed").length
|
||||
const successes = entries.filter((entry) => entry.status === "completed").length
|
||||
const failures = entries.filter((entry) => entry.status === "failed").length
|
||||
const toolCalls = entries.reduce((acc, entry) => acc + (entry.toolCalls || 0), 0)
|
||||
const inputTokens = entries.reduce((acc, entry) => acc + (entry.inputTokens || 0), 0)
|
||||
const outputTokens = entries.reduce((acc, entry) => acc + (entry.outputTokens || 0), 0)
|
||||
const contextWindow = entries.reduce((acc, entry) => Math.max(acc, entry.contextWindow || 0), 0)
|
||||
const maxContextTokens = entries.reduce((acc, entry) => Math.max(acc, entry.contextTokens || 0), 0)
|
||||
const maxContextUsagePercentage = entries.reduce((acc, entry) => Math.max(acc, entry.contextUsagePercentage || 0), 0)
|
||||
|
||||
const payload: ClineSaySubagentStatus = {
|
||||
status,
|
||||
total: entries.length,
|
||||
completed,
|
||||
successes,
|
||||
failures,
|
||||
toolCalls,
|
||||
inputTokens,
|
||||
outputTokens,
|
||||
contextWindow,
|
||||
maxContextTokens,
|
||||
maxContextUsagePercentage,
|
||||
items: entries,
|
||||
}
|
||||
|
||||
await config.callbacks.say("subagent", JSON.stringify(payload), undefined, undefined, partial)
|
||||
}
|
||||
|
||||
let statusUpdateQueue: Promise<void> = Promise.resolve()
|
||||
const queueStatusUpdate = (status: ClineSaySubagentStatus["status"], partial: boolean): Promise<void> => {
|
||||
statusUpdateQueue = statusUpdateQueue.catch(() => undefined).then(() => emitStatus(status, partial))
|
||||
return statusUpdateQueue
|
||||
}
|
||||
|
||||
await config.callbacks.removeLastPartialMessageIfExistsWithType("say", "subagent")
|
||||
await queueStatusUpdate("running", true)
|
||||
|
||||
const runners = prompts.map(() => new SubagentRunner(config))
|
||||
const abortPollInterval = setInterval(() => {
|
||||
if (!config.taskState.abort) {
|
||||
return
|
||||
}
|
||||
clearInterval(abortPollInterval)
|
||||
void Promise.allSettled(runners.map((runner) => runner.abort()))
|
||||
}, 100)
|
||||
|
||||
const execution = prompts.map((prompt, index) =>
|
||||
runners[index].run(prompt, async (update) => {
|
||||
const current = entries[index]
|
||||
if (update.status === "running") {
|
||||
current.status = "running"
|
||||
}
|
||||
if (update.status === "completed") {
|
||||
current.status = "completed"
|
||||
}
|
||||
if (update.status === "failed") {
|
||||
current.status = "failed"
|
||||
}
|
||||
if (update.result !== undefined) {
|
||||
current.result = update.result
|
||||
}
|
||||
if (update.error !== undefined) {
|
||||
current.error = update.error
|
||||
}
|
||||
if (update.stats) {
|
||||
current.toolCalls = update.stats.toolCalls || 0
|
||||
current.inputTokens = update.stats.inputTokens || 0
|
||||
current.outputTokens = update.stats.outputTokens || 0
|
||||
current.totalCost = update.stats.totalCost || 0
|
||||
current.contextTokens = update.stats.contextTokens || 0
|
||||
current.contextWindow = update.stats.contextWindow || 0
|
||||
current.contextUsagePercentage = update.stats.contextUsagePercentage || 0
|
||||
}
|
||||
await queueStatusUpdate("running", true)
|
||||
}),
|
||||
)
|
||||
|
||||
const settled = await Promise.allSettled(execution)
|
||||
clearInterval(abortPollInterval)
|
||||
let usageTokensIn = 0
|
||||
let usageTokensOut = 0
|
||||
let usageCacheWrites = 0
|
||||
let usageCacheReads = 0
|
||||
let usageCost = 0
|
||||
settled.forEach((result, index) => {
|
||||
if (result.status === "rejected") {
|
||||
entries[index].status = "failed"
|
||||
entries[index].error = (result.reason as Error)?.message || "Subagent execution failed"
|
||||
return
|
||||
}
|
||||
entries[index].status = result.value.status
|
||||
entries[index].result = result.value.result
|
||||
entries[index].error = result.value.error
|
||||
entries[index].toolCalls = result.value.stats.toolCalls || 0
|
||||
entries[index].inputTokens = result.value.stats.inputTokens || 0
|
||||
entries[index].outputTokens = result.value.stats.outputTokens || 0
|
||||
entries[index].totalCost = result.value.stats.totalCost || 0
|
||||
entries[index].contextTokens = result.value.stats.contextTokens || 0
|
||||
entries[index].contextWindow = result.value.stats.contextWindow || 0
|
||||
entries[index].contextUsagePercentage = result.value.stats.contextUsagePercentage || 0
|
||||
|
||||
usageTokensIn += result.value.stats.inputTokens || 0
|
||||
usageTokensOut += result.value.stats.outputTokens || 0
|
||||
usageCacheWrites += result.value.stats.cacheWriteTokens || 0
|
||||
usageCacheReads += result.value.stats.cacheReadTokens || 0
|
||||
usageCost += result.value.stats.totalCost || 0
|
||||
})
|
||||
|
||||
const failures = entries.filter((entry) => entry.status === "failed").length
|
||||
await queueStatusUpdate(failures > 0 ? "failed" : "completed", false)
|
||||
|
||||
const subagentUsagePayload: ClineSubagentUsageInfo = {
|
||||
source: "subagents",
|
||||
tokensIn: usageTokensIn,
|
||||
tokensOut: usageTokensOut,
|
||||
cacheWrites: usageCacheWrites,
|
||||
cacheReads: usageCacheReads,
|
||||
cost: usageCost,
|
||||
}
|
||||
await config.callbacks.say("subagent_usage", JSON.stringify(subagentUsagePayload))
|
||||
|
||||
const successCount = entries.length - failures
|
||||
const totalToolCalls = entries.reduce((acc, entry) => acc + (entry.toolCalls || 0), 0)
|
||||
const maxContextUsagePercentage = entries.reduce((acc, entry) => Math.max(acc, entry.contextUsagePercentage || 0), 0)
|
||||
const maxContextTokens = entries.reduce((acc, entry) => Math.max(acc, entry.contextTokens || 0), 0)
|
||||
const contextWindow = entries.reduce((acc, entry) => Math.max(acc, entry.contextWindow || 0), 0)
|
||||
|
||||
const summary = [
|
||||
"Subagent results:",
|
||||
`Total: ${entries.length}`,
|
||||
`Succeeded: ${successCount}`,
|
||||
`Failed: ${failures}`,
|
||||
`Tool calls: ${totalToolCalls}`,
|
||||
`Peak context usage: ${maxContextTokens.toLocaleString()} / ${contextWindow.toLocaleString()} (${maxContextUsagePercentage.toFixed(1)}%)`,
|
||||
"",
|
||||
...entries.map((entry) => {
|
||||
const header = `[${entry.index}] ${entry.status.toUpperCase()} - ${entry.prompt}`
|
||||
const detail = entry.status === "completed" ? excerpt(entry.result) : excerpt(entry.error)
|
||||
return detail ? `${header}\n${detail}` : header
|
||||
}),
|
||||
].join("\n")
|
||||
|
||||
return formatResponse.toolResult(summary)
|
||||
}
|
||||
}
|
||||
@@ -20,6 +20,9 @@ export class UseSkillToolHandler implements IToolHandler, IPartialBlockHandler {
|
||||
|
||||
async handlePartialBlock(block: ToolUse, uiHelpers: StronglyTypedUIHelpers): Promise<void> {
|
||||
const skillName = block.params.skill_name
|
||||
if (uiHelpers.getConfig().isSubagentExecution) {
|
||||
return
|
||||
}
|
||||
const message = JSON.stringify({ tool: "useSkill", path: skillName || "" })
|
||||
await uiHelpers.say("tool", message, undefined, undefined, true)
|
||||
}
|
||||
@@ -58,7 +61,9 @@ export class UseSkillToolHandler implements IToolHandler, IPartialBlockHandler {
|
||||
|
||||
// Show tool message
|
||||
const message = JSON.stringify({ tool: "useSkill", path: skillName })
|
||||
await config.callbacks.say("tool", message, undefined, undefined, false)
|
||||
if (!config.isSubagentExecution) {
|
||||
await config.callbacks.say("tool", message, undefined, undefined, false)
|
||||
}
|
||||
|
||||
config.taskState.consecutiveMistakeCount = 0
|
||||
|
||||
|
||||
@@ -0,0 +1,383 @@
|
||||
import { strict as assert } from "node:assert"
|
||||
import { setTimeout as delay } from "node:timers/promises"
|
||||
import { ClineSubagentUsageInfo } from "@shared/ExtensionMessage"
|
||||
import { ClineDefaultTool } from "@shared/tools"
|
||||
import { afterEach, describe, it } from "mocha"
|
||||
import sinon from "sinon"
|
||||
import { TaskState } from "../../../TaskState"
|
||||
import { SubagentRunner } from "../../subagent/SubagentRunner"
|
||||
import type { TaskConfig } from "../../types/TaskConfig"
|
||||
import { createUIHelpers } from "../../types/UIHelpers"
|
||||
import { UseSubagentsToolHandler } from "../SubagentToolHandler"
|
||||
|
||||
function createConfig(options?: {
|
||||
autoApproveSafe?: boolean
|
||||
autoApproveAll?: boolean
|
||||
taskAskResponse?: "yesButtonClicked" | "noButtonClicked"
|
||||
subagentsEnabled?: boolean
|
||||
}) {
|
||||
const taskState = new TaskState()
|
||||
const askResponse = options?.taskAskResponse ?? "yesButtonClicked"
|
||||
const subagentsEnabled = options?.subagentsEnabled ?? true
|
||||
|
||||
const callbacks = {
|
||||
say: sinon.stub().resolves(undefined),
|
||||
ask: sinon.stub().resolves({ response: askResponse }),
|
||||
saveCheckpoint: sinon.stub().resolves(),
|
||||
sayAndCreateMissingParamError: sinon.stub().resolves("missing"),
|
||||
removeLastPartialMessageIfExistsWithType: sinon.stub().resolves(),
|
||||
executeCommandTool: sinon.stub().resolves([false, "ok"]),
|
||||
cancelRunningCommandTool: sinon.stub().resolves(false),
|
||||
doesLatestTaskCompletionHaveNewChanges: sinon.stub().resolves(false),
|
||||
updateFCListFromToolResponse: sinon.stub().resolves(),
|
||||
shouldAutoApproveTool: sinon.stub().returns([options?.autoApproveSafe ?? false, options?.autoApproveAll ?? false]),
|
||||
shouldAutoApproveToolWithPath: sinon.stub().resolves(false),
|
||||
postStateToWebview: sinon.stub().resolves(),
|
||||
reinitExistingTaskFromId: sinon.stub().resolves(),
|
||||
cancelTask: sinon.stub().resolves(),
|
||||
updateTaskHistory: sinon.stub().resolves([]),
|
||||
applyLatestBrowserSettings: sinon.stub().resolves(undefined),
|
||||
switchToActMode: sinon.stub().resolves(false),
|
||||
setActiveHookExecution: sinon.stub().resolves(),
|
||||
clearActiveHookExecution: sinon.stub().resolves(),
|
||||
getActiveHookExecution: sinon.stub().resolves(undefined),
|
||||
runUserPromptSubmitHook: sinon.stub().resolves({}),
|
||||
}
|
||||
|
||||
const config = {
|
||||
taskId: "task-1",
|
||||
ulid: "ulid-1",
|
||||
cwd: "/tmp",
|
||||
mode: "act",
|
||||
strictPlanModeEnabled: false,
|
||||
yoloModeToggled: false,
|
||||
vscodeTerminalExecutionMode: "backgroundExec",
|
||||
enableParallelToolCalling: true,
|
||||
context: {},
|
||||
taskState,
|
||||
messageState: {},
|
||||
api: {
|
||||
getModel: () => ({ id: "openai/gpt-5", info: {} }),
|
||||
},
|
||||
autoApprovalSettings: {
|
||||
enableNotifications: false,
|
||||
actions: {
|
||||
executeSafeCommands: false,
|
||||
executeAllCommands: false,
|
||||
},
|
||||
},
|
||||
autoApprover: {
|
||||
shouldAutoApproveTool: sinon.stub().returns([options?.autoApproveSafe ?? false, options?.autoApproveAll ?? false]),
|
||||
},
|
||||
browserSettings: {},
|
||||
focusChainSettings: {},
|
||||
services: {
|
||||
stateManager: {
|
||||
getGlobalStateKey: (key: string) => (key === "nativeToolCallEnabled" ? true : undefined),
|
||||
getGlobalSettingsKey: (key: string) => {
|
||||
if (key === "mode") {
|
||||
return "act"
|
||||
}
|
||||
if (key === "customPrompt") {
|
||||
return undefined
|
||||
}
|
||||
if (key === "subagentsEnabled") {
|
||||
return subagentsEnabled
|
||||
}
|
||||
return undefined
|
||||
},
|
||||
getApiConfiguration: () => ({
|
||||
planModeApiProvider: "openai",
|
||||
actModeApiProvider: "openai",
|
||||
}),
|
||||
},
|
||||
mcpHub: {},
|
||||
},
|
||||
callbacks,
|
||||
coordinator: {
|
||||
getHandler: sinon.stub(),
|
||||
},
|
||||
} as unknown as TaskConfig
|
||||
|
||||
return { config, callbacks, taskState }
|
||||
}
|
||||
|
||||
describe("SubagentToolHandler", () => {
|
||||
afterEach(() => {
|
||||
sinon.restore()
|
||||
})
|
||||
|
||||
it("returns missing parameter error when no prompts are provided", async () => {
|
||||
const { config, callbacks, taskState } = createConfig()
|
||||
const handler = new UseSubagentsToolHandler()
|
||||
|
||||
const result = await handler.execute(config, {
|
||||
type: "tool_use",
|
||||
name: ClineDefaultTool.USE_SUBAGENTS,
|
||||
params: {},
|
||||
partial: false,
|
||||
})
|
||||
|
||||
assert.equal(result, "missing")
|
||||
assert.equal(taskState.consecutiveMistakeCount, 1)
|
||||
sinon.assert.calledOnce(callbacks.sayAndCreateMissingParamError)
|
||||
})
|
||||
|
||||
it("returns an error when subagents are disabled", async () => {
|
||||
const { config } = createConfig({ subagentsEnabled: false })
|
||||
const handler = new UseSubagentsToolHandler()
|
||||
|
||||
const result = await handler.execute(config, {
|
||||
type: "tool_use",
|
||||
name: ClineDefaultTool.USE_SUBAGENTS,
|
||||
params: {
|
||||
prompt_1: "first prompt",
|
||||
},
|
||||
partial: false,
|
||||
})
|
||||
|
||||
assert.equal(
|
||||
result,
|
||||
"The tool execution failed with the following error:\n<error>\nSubagents are disabled. Enable them in Settings > Features to use this tool.\n</error>",
|
||||
)
|
||||
})
|
||||
|
||||
it("streams partial use_subagents approval as ask when not auto-approved", async () => {
|
||||
const { config, callbacks } = createConfig({ autoApproveSafe: false, autoApproveAll: false })
|
||||
const handler = new UseSubagentsToolHandler()
|
||||
const uiHelpers = createUIHelpers(config)
|
||||
|
||||
await handler.handlePartialBlock(
|
||||
{
|
||||
type: "tool_use",
|
||||
name: ClineDefaultTool.USE_SUBAGENTS,
|
||||
params: {
|
||||
prompt_1: "first prompt",
|
||||
prompt_2: "second prompt",
|
||||
},
|
||||
partial: true,
|
||||
},
|
||||
uiHelpers,
|
||||
)
|
||||
|
||||
sinon.assert.calledOnce(callbacks.removeLastPartialMessageIfExistsWithType)
|
||||
sinon.assert.calledWithExactly(callbacks.removeLastPartialMessageIfExistsWithType, "say", "use_subagents")
|
||||
sinon.assert.calledOnce(callbacks.ask)
|
||||
sinon.assert.calledWithMatch(callbacks.ask, "use_subagents", sinon.match.string, true)
|
||||
|
||||
const payload = JSON.parse(callbacks.ask.firstCall.args[1])
|
||||
assert.deepEqual(payload.prompts, ["first prompt", "second prompt"])
|
||||
sinon.assert.notCalled(callbacks.say)
|
||||
})
|
||||
|
||||
it("streams partial use_subagents approval as say when auto-approved", async () => {
|
||||
const { config, callbacks } = createConfig({ autoApproveSafe: true, autoApproveAll: false })
|
||||
const handler = new UseSubagentsToolHandler()
|
||||
const uiHelpers = createUIHelpers(config)
|
||||
|
||||
await handler.handlePartialBlock(
|
||||
{
|
||||
type: "tool_use",
|
||||
name: ClineDefaultTool.USE_SUBAGENTS,
|
||||
params: {
|
||||
prompt_1: "first prompt",
|
||||
prompt_2: "second prompt",
|
||||
},
|
||||
partial: true,
|
||||
},
|
||||
uiHelpers,
|
||||
)
|
||||
|
||||
sinon.assert.calledOnce(callbacks.removeLastPartialMessageIfExistsWithType)
|
||||
sinon.assert.calledWithExactly(callbacks.removeLastPartialMessageIfExistsWithType, "ask", "use_subagents")
|
||||
sinon.assert.calledOnce(callbacks.say)
|
||||
sinon.assert.calledWithMatch(callbacks.say, "use_subagents", sinon.match.string, undefined, undefined, true)
|
||||
|
||||
const payload = JSON.parse(callbacks.say.firstCall.args[1])
|
||||
assert.deepEqual(payload.prompts, ["first prompt", "second prompt"])
|
||||
sinon.assert.notCalled(callbacks.ask)
|
||||
})
|
||||
|
||||
it("uses one approval for the full batch and stops on denial", async () => {
|
||||
const { config, callbacks, taskState } = createConfig({ taskAskResponse: "noButtonClicked" })
|
||||
const runStub = sinon.stub(SubagentRunner.prototype, "run")
|
||||
const handler = new UseSubagentsToolHandler()
|
||||
|
||||
const result = await handler.execute(config, {
|
||||
type: "tool_use",
|
||||
name: ClineDefaultTool.USE_SUBAGENTS,
|
||||
params: {
|
||||
prompt_1: "one",
|
||||
prompt_2: "two",
|
||||
},
|
||||
partial: false,
|
||||
})
|
||||
|
||||
assert.equal(result, "The user denied this operation.")
|
||||
assert.equal(taskState.didRejectTool, true)
|
||||
sinon.assert.calledOnce(callbacks.ask)
|
||||
assert.equal(callbacks.ask.firstCall.args[0], "use_subagents")
|
||||
sinon.assert.notCalled(runStub)
|
||||
})
|
||||
|
||||
it("uses read-file auto-approve level (safe only) for approval bypass", async () => {
|
||||
const { config, callbacks } = createConfig({ autoApproveSafe: true, autoApproveAll: false })
|
||||
sinon.stub(SubagentRunner.prototype, "run").resolves({
|
||||
status: "completed",
|
||||
result: "done",
|
||||
stats: {
|
||||
toolCalls: 1,
|
||||
inputTokens: 2,
|
||||
outputTokens: 3,
|
||||
cacheWriteTokens: 0,
|
||||
cacheReadTokens: 0,
|
||||
totalCost: 0.25,
|
||||
contextTokens: 5,
|
||||
contextWindow: 200000,
|
||||
contextUsagePercentage: 0.0025,
|
||||
},
|
||||
})
|
||||
|
||||
const handler = new UseSubagentsToolHandler()
|
||||
await handler.execute(config, {
|
||||
type: "tool_use",
|
||||
name: ClineDefaultTool.USE_SUBAGENTS,
|
||||
params: {
|
||||
prompt_1: "one",
|
||||
},
|
||||
partial: false,
|
||||
})
|
||||
|
||||
sinon.assert.notCalled(callbacks.ask)
|
||||
const subagentStatusCalls = callbacks.say.getCalls().filter((call) => call.args[0] === "subagent")
|
||||
assert.ok(subagentStatusCalls.length >= 1)
|
||||
})
|
||||
|
||||
it("fans out prompts in parallel and emits aggregated status", async () => {
|
||||
const { config, callbacks } = createConfig({ autoApproveSafe: true, autoApproveAll: true })
|
||||
let activeRuns = 0
|
||||
let maxActiveRuns = 0
|
||||
|
||||
sinon.stub(SubagentRunner.prototype, "run").callsFake(async (_prompt: string, onProgress) => {
|
||||
activeRuns++
|
||||
maxActiveRuns = Math.max(maxActiveRuns, activeRuns)
|
||||
onProgress({
|
||||
status: "running",
|
||||
stats: {
|
||||
toolCalls: 0,
|
||||
inputTokens: 0,
|
||||
outputTokens: 0,
|
||||
cacheWriteTokens: 0,
|
||||
cacheReadTokens: 0,
|
||||
totalCost: 0,
|
||||
contextTokens: 0,
|
||||
contextWindow: 200000,
|
||||
contextUsagePercentage: 0,
|
||||
},
|
||||
})
|
||||
await delay(10)
|
||||
activeRuns--
|
||||
return {
|
||||
status: "completed",
|
||||
result: "done",
|
||||
stats: {
|
||||
toolCalls: 1,
|
||||
inputTokens: 2,
|
||||
outputTokens: 3,
|
||||
cacheWriteTokens: 0,
|
||||
cacheReadTokens: 0,
|
||||
totalCost: 0.25,
|
||||
contextTokens: 5,
|
||||
contextWindow: 200000,
|
||||
contextUsagePercentage: 0.0025,
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
const handler = new UseSubagentsToolHandler()
|
||||
const result = await handler.execute(config, {
|
||||
type: "tool_use",
|
||||
name: ClineDefaultTool.USE_SUBAGENTS,
|
||||
params: {
|
||||
prompt_1: "one",
|
||||
prompt_2: "two",
|
||||
prompt_3: "three",
|
||||
},
|
||||
partial: false,
|
||||
})
|
||||
|
||||
assert.equal(typeof result, "string")
|
||||
assert.ok((result as string).includes("Total: 3"))
|
||||
assert.ok(maxActiveRuns > 1)
|
||||
|
||||
const subagentStatusCalls = callbacks.say.getCalls().filter((call) => call.args[0] === "subagent")
|
||||
assert.ok(subagentStatusCalls.length >= 2)
|
||||
const finalCall = subagentStatusCalls[subagentStatusCalls.length - 1]
|
||||
assert.equal(finalCall.args[4], false)
|
||||
|
||||
const usageCalls = callbacks.say.getCalls().filter((call) => call.args[0] === "subagent_usage")
|
||||
assert.equal(usageCalls.length, 1)
|
||||
const usagePayload = JSON.parse(usageCalls[0].args[1]) as ClineSubagentUsageInfo
|
||||
assert.equal(usagePayload.source, "subagents")
|
||||
assert.equal(usagePayload.tokensIn, 6)
|
||||
assert.equal(usagePayload.tokensOut, 9)
|
||||
assert.equal(usagePayload.cacheWrites, 0)
|
||||
assert.equal(usagePayload.cacheReads, 0)
|
||||
assert.equal(usagePayload.cost, 0.75)
|
||||
})
|
||||
|
||||
it("continues after per-subagent failures and reports both outcomes", async () => {
|
||||
const { config } = createConfig({ autoApproveSafe: true, autoApproveAll: true })
|
||||
|
||||
sinon.stub(SubagentRunner.prototype, "run").callsFake(async (prompt: string) => {
|
||||
if (prompt.includes("fail")) {
|
||||
return {
|
||||
status: "failed",
|
||||
error: "boom",
|
||||
stats: {
|
||||
toolCalls: 1,
|
||||
inputTokens: 0,
|
||||
outputTokens: 0,
|
||||
cacheWriteTokens: 0,
|
||||
cacheReadTokens: 0,
|
||||
totalCost: 0,
|
||||
contextTokens: 0,
|
||||
contextWindow: 200000,
|
||||
contextUsagePercentage: 0,
|
||||
},
|
||||
}
|
||||
}
|
||||
return {
|
||||
status: "completed",
|
||||
result: "ok",
|
||||
stats: {
|
||||
toolCalls: 2,
|
||||
inputTokens: 0,
|
||||
outputTokens: 0,
|
||||
cacheWriteTokens: 0,
|
||||
cacheReadTokens: 0,
|
||||
totalCost: 0,
|
||||
contextTokens: 0,
|
||||
contextWindow: 200000,
|
||||
contextUsagePercentage: 0,
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
const handler = new UseSubagentsToolHandler()
|
||||
const result = await handler.execute(config, {
|
||||
type: "tool_use",
|
||||
name: ClineDefaultTool.USE_SUBAGENTS,
|
||||
params: {
|
||||
prompt_1: "succeed",
|
||||
prompt_2: "fail",
|
||||
},
|
||||
partial: false,
|
||||
})
|
||||
|
||||
assert.equal(typeof result, "string")
|
||||
assert.ok((result as string).includes("Succeeded: 1"))
|
||||
assert.ok((result as string).includes("Failed: 1"))
|
||||
assert.ok((result as string).includes("boom"))
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,592 @@
|
||||
import { setTimeout as delay } from "node:timers/promises"
|
||||
import { buildApiHandler } from "@core/api"
|
||||
import { parseAssistantMessageV2, ToolUse } from "@core/assistant-message"
|
||||
import { discoverSkills, getAvailableSkills } from "@core/context/instructions/user-instructions/skills"
|
||||
import { formatResponse } from "@core/prompts/responses"
|
||||
import { PromptRegistry } from "@core/prompts/system-prompt"
|
||||
import { ClineToolSet } from "@core/prompts/system-prompt/registry/ClineToolSet"
|
||||
import type { SystemPromptContext } from "@core/prompts/system-prompt/types"
|
||||
import { StreamResponseHandler } from "@core/task/StreamResponseHandler"
|
||||
import { ClineAssistantToolUseBlock, ClineStorageMessage, ClineTextContentBlock } from "@shared/messages"
|
||||
import { Logger } from "@shared/services/Logger"
|
||||
import { ClineDefaultTool } from "@shared/tools"
|
||||
import * as path from "path"
|
||||
import { HostProvider } from "@/hosts/host-provider"
|
||||
import { ApiFormat } from "@/shared/proto/cline/models"
|
||||
import { calculateApiCostAnthropic } from "@/utils/cost"
|
||||
import { TaskState } from "../../TaskState"
|
||||
import type { TaskConfig } from "../types/TaskConfig"
|
||||
|
||||
const SUBAGENT_ALLOWED_TOOLS: ClineDefaultTool[] = [
|
||||
ClineDefaultTool.FILE_READ,
|
||||
ClineDefaultTool.LIST_FILES,
|
||||
ClineDefaultTool.SEARCH,
|
||||
ClineDefaultTool.LIST_CODE_DEF,
|
||||
ClineDefaultTool.BASH,
|
||||
ClineDefaultTool.USE_SKILL,
|
||||
]
|
||||
|
||||
export type SubagentRunStatus = "completed" | "failed"
|
||||
|
||||
export interface SubagentRunResult {
|
||||
status: SubagentRunStatus
|
||||
result?: string
|
||||
error?: string
|
||||
stats: SubagentRunStats
|
||||
}
|
||||
|
||||
interface SubagentProgressUpdate {
|
||||
stats?: SubagentRunStats
|
||||
status?: "running" | "completed" | "failed"
|
||||
result?: string
|
||||
error?: string
|
||||
}
|
||||
|
||||
interface SubagentRunStats {
|
||||
toolCalls: number
|
||||
inputTokens: number
|
||||
outputTokens: number
|
||||
cacheWriteTokens: number
|
||||
cacheReadTokens: number
|
||||
totalCost: number
|
||||
contextTokens: number
|
||||
contextWindow: number
|
||||
contextUsagePercentage: number
|
||||
}
|
||||
|
||||
interface SubagentToolCall {
|
||||
toolUseId: string
|
||||
id?: string
|
||||
call_id?: string
|
||||
signature?: string
|
||||
name: string
|
||||
input: unknown
|
||||
isNativeToolCall: boolean
|
||||
}
|
||||
|
||||
const SUBAGENT_SYSTEM_SUFFIX = `\n\n# Subagent Execution Mode
|
||||
You are running as a research subagent. Your job is to thoroughly explore the codebase and gather comprehensive information to answer the question.
|
||||
Explore broadly, read related files, trace through call chains, and build a complete picture before reporting back.
|
||||
You can read files, list directories, search for patterns, list code definitions, and run commands.
|
||||
Only use execute_command for readonly operations like ls, grep, git log, git diff, gh, etc.
|
||||
Do not run commands that modify files or system state.
|
||||
When you have a comprehensive answer, respond with your findings including file paths and line numbers.
|
||||
Also include a section titled "Recommended files for main agent" with a concise list of the highest-value files the main agent should read next, and a one-line reason for each file.`
|
||||
|
||||
function serializeToolResult(result: unknown): string {
|
||||
if (typeof result === "string") {
|
||||
return result
|
||||
}
|
||||
|
||||
if (Array.isArray(result)) {
|
||||
return result
|
||||
.map((item) => {
|
||||
if (!item || typeof item !== "object") {
|
||||
return String(item)
|
||||
}
|
||||
|
||||
const maybeText = (item as { text?: string }).text
|
||||
if (typeof maybeText === "string") {
|
||||
return maybeText
|
||||
}
|
||||
|
||||
return JSON.stringify(item)
|
||||
})
|
||||
.join("\n")
|
||||
}
|
||||
|
||||
return JSON.stringify(result, null, 2)
|
||||
}
|
||||
|
||||
function toToolUseParams(input: unknown): Partial<Record<string, string>> {
|
||||
if (!input || typeof input !== "object") {
|
||||
return {}
|
||||
}
|
||||
|
||||
const params: Record<string, string> = {}
|
||||
for (const [key, value] of Object.entries(input)) {
|
||||
params[key] = typeof value === "string" ? value : JSON.stringify(value)
|
||||
}
|
||||
|
||||
return params
|
||||
}
|
||||
|
||||
function normalizeToolCallArguments(argumentsPayload: unknown): string {
|
||||
if (typeof argumentsPayload === "string") {
|
||||
return argumentsPayload
|
||||
}
|
||||
|
||||
try {
|
||||
return JSON.stringify(argumentsPayload ?? {})
|
||||
} catch {
|
||||
return "{}"
|
||||
}
|
||||
}
|
||||
|
||||
function resolveToolUseId(call: { id?: string; call_id?: string; name?: string }, index: number): string {
|
||||
const id = call.id?.trim()
|
||||
if (id) {
|
||||
return id
|
||||
}
|
||||
|
||||
const callId = call.call_id?.trim()
|
||||
if (callId) {
|
||||
return callId
|
||||
}
|
||||
|
||||
const fallbackId = `subagent_tool_${Date.now()}_${index + 1}`
|
||||
Logger.warn(`[SubagentRunner] Missing tool call id for '${call.name || "unknown"}'; using fallback '${fallbackId}'`)
|
||||
return fallbackId
|
||||
}
|
||||
|
||||
function toAssistantToolUseBlock(call: SubagentToolCall): ClineAssistantToolUseBlock {
|
||||
return {
|
||||
type: "tool_use",
|
||||
id: call.toolUseId,
|
||||
name: call.name,
|
||||
input: call.input,
|
||||
call_id: call.call_id,
|
||||
signature: call.signature,
|
||||
}
|
||||
}
|
||||
|
||||
function parseNonNativeToolCalls(assistantText: string): SubagentToolCall[] {
|
||||
const parsedBlocks = parseAssistantMessageV2(assistantText)
|
||||
|
||||
return parsedBlocks
|
||||
.filter((block): block is ToolUse => block.type === "tool_use")
|
||||
.filter((block) => !block.partial)
|
||||
.map((block, index) => ({
|
||||
toolUseId: resolveToolUseId({ call_id: block.call_id, name: block.name }, index),
|
||||
name: block.name,
|
||||
input: block.params,
|
||||
call_id: block.call_id,
|
||||
signature: block.signature,
|
||||
isNativeToolCall: false,
|
||||
}))
|
||||
}
|
||||
|
||||
export class SubagentRunner {
|
||||
private activeApiAbort: (() => void) | undefined
|
||||
private abortRequested = false
|
||||
private activeCommandExecutions = 0
|
||||
private abortingCommands = false
|
||||
|
||||
constructor(private baseConfig: TaskConfig) {}
|
||||
|
||||
async abort(): Promise<void> {
|
||||
this.abortRequested = true
|
||||
|
||||
try {
|
||||
this.activeApiAbort?.()
|
||||
} catch (error) {
|
||||
Logger.error("[SubagentRunner] failed to abort active API stream", error)
|
||||
}
|
||||
|
||||
if (this.activeCommandExecutions > 0 && !this.abortingCommands && this.baseConfig.callbacks.cancelRunningCommandTool) {
|
||||
this.abortingCommands = true
|
||||
try {
|
||||
await this.baseConfig.callbacks.cancelRunningCommandTool()
|
||||
} catch (error) {
|
||||
Logger.error("[SubagentRunner] failed to cancel running command execution", error)
|
||||
} finally {
|
||||
this.abortingCommands = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private shouldAbort(): boolean {
|
||||
return this.abortRequested || this.baseConfig.taskState.abort
|
||||
}
|
||||
|
||||
private async getWorkspaceMetadataEnvironmentBlock(): Promise<string | null> {
|
||||
try {
|
||||
const workspacesJson =
|
||||
(await this.baseConfig.workspaceManager?.buildWorkspacesJson()) ??
|
||||
JSON.stringify(
|
||||
{
|
||||
workspaces: {
|
||||
[this.baseConfig.cwd]: {
|
||||
hint: path.basename(this.baseConfig.cwd) || this.baseConfig.cwd,
|
||||
},
|
||||
},
|
||||
},
|
||||
null,
|
||||
2,
|
||||
)
|
||||
|
||||
return `<environment_details>\n# Workspace Configuration\n${workspacesJson}\n</environment_details>`
|
||||
} catch (error) {
|
||||
Logger.warn("[SubagentRunner] Failed to build workspace metadata block", error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
async run(prompt: string, onProgress: (update: SubagentProgressUpdate) => void): Promise<SubagentRunResult> {
|
||||
this.abortRequested = false
|
||||
const state = new TaskState()
|
||||
const stats: SubagentRunStats = {
|
||||
toolCalls: 0,
|
||||
inputTokens: 0,
|
||||
outputTokens: 0,
|
||||
cacheWriteTokens: 0,
|
||||
cacheReadTokens: 0,
|
||||
totalCost: 0,
|
||||
contextTokens: 0,
|
||||
contextWindow: 0,
|
||||
contextUsagePercentage: 0,
|
||||
}
|
||||
|
||||
onProgress({ status: "running", stats })
|
||||
|
||||
try {
|
||||
const mode = this.baseConfig.services.stateManager.getGlobalSettingsKey("mode")
|
||||
const apiConfiguration = this.baseConfig.services.stateManager.getApiConfiguration()
|
||||
const effectiveApiConfiguration = {
|
||||
...apiConfiguration,
|
||||
ulid: this.baseConfig.ulid,
|
||||
}
|
||||
const api = buildApiHandler(effectiveApiConfiguration, mode)
|
||||
this.activeApiAbort = api.abort?.bind(api)
|
||||
|
||||
const providerId = (
|
||||
mode === "plan" ? apiConfiguration.planModeApiProvider : apiConfiguration.actModeApiProvider
|
||||
) as string
|
||||
const providerInfo = {
|
||||
providerId,
|
||||
model: api.getModel(),
|
||||
mode,
|
||||
customPrompt: this.baseConfig.services.stateManager.getGlobalSettingsKey("customPrompt"),
|
||||
}
|
||||
stats.contextWindow = providerInfo.model.info.contextWindow || 0
|
||||
const useNativeToolCalls =
|
||||
providerInfo.model.info.apiFormat === ApiFormat.OPENAI_RESPONSES ||
|
||||
!!this.baseConfig.services.stateManager.getGlobalStateKey("nativeToolCallEnabled")
|
||||
|
||||
const host = await HostProvider.env.getHostVersion({})
|
||||
const discoveredSkills = await discoverSkills(this.baseConfig.cwd)
|
||||
const skills = getAvailableSkills(discoveredSkills)
|
||||
|
||||
const context: SystemPromptContext = {
|
||||
providerInfo,
|
||||
cwd: this.baseConfig.cwd,
|
||||
ide: host?.platform || "Unknown",
|
||||
skills,
|
||||
focusChainSettings: this.baseConfig.focusChainSettings,
|
||||
browserSettings: this.baseConfig.browserSettings,
|
||||
yoloModeToggled: false,
|
||||
enableNativeToolCalls: useNativeToolCalls,
|
||||
enableParallelToolCalling: false,
|
||||
isSubagentRun: true,
|
||||
}
|
||||
|
||||
const promptRegistry = PromptRegistry.getInstance()
|
||||
const systemPrompt = (await promptRegistry.get(context)) + SUBAGENT_SYSTEM_SUFFIX
|
||||
const nativeTools = useNativeToolCalls ? this.buildNativeTools(context) : undefined
|
||||
const workspaceMetadataEnvironmentBlock = await this.getWorkspaceMetadataEnvironmentBlock()
|
||||
|
||||
if (useNativeToolCalls && (!nativeTools || nativeTools.length === 0)) {
|
||||
const error = "Subagent tool requires native tool calling support."
|
||||
onProgress({ status: "failed", error, stats })
|
||||
return { status: "failed", error, stats }
|
||||
}
|
||||
|
||||
if (this.shouldAbort()) {
|
||||
await this.abort()
|
||||
const error = "Subagent run cancelled."
|
||||
onProgress({ status: "failed", error, stats: { ...stats } })
|
||||
return { status: "failed", error, stats }
|
||||
}
|
||||
|
||||
const conversation: ClineStorageMessage[] = [
|
||||
{
|
||||
role: "user",
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: prompt,
|
||||
} as ClineTextContentBlock,
|
||||
// Server-side task loop checks require workspace metadata to be present in the
|
||||
// initial user message of subagent runs.
|
||||
...(workspaceMetadataEnvironmentBlock
|
||||
? [
|
||||
{
|
||||
type: "text",
|
||||
text: workspaceMetadataEnvironmentBlock,
|
||||
} as ClineTextContentBlock,
|
||||
]
|
||||
: []),
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
while (true) {
|
||||
const streamHandler = new StreamResponseHandler()
|
||||
const { toolUseHandler } = streamHandler.getHandlers()
|
||||
let requestInputTokens = 0
|
||||
let requestOutputTokens = 0
|
||||
let requestCacheWriteTokens = 0
|
||||
let requestCacheReadTokens = 0
|
||||
let requestTotalCost: number | undefined
|
||||
|
||||
let assistantText = ""
|
||||
let assistantTextSignature: string | undefined
|
||||
let requestId: string | undefined
|
||||
|
||||
const stream = api.createMessage(systemPrompt, conversation, nativeTools)
|
||||
|
||||
for await (const chunk of stream) {
|
||||
switch (chunk.type) {
|
||||
case "usage":
|
||||
requestId = requestId ?? chunk.id
|
||||
stats.inputTokens += chunk.inputTokens || 0
|
||||
stats.outputTokens += chunk.outputTokens || 0
|
||||
stats.cacheWriteTokens += chunk.cacheWriteTokens || 0
|
||||
stats.cacheReadTokens += chunk.cacheReadTokens || 0
|
||||
requestInputTokens += chunk.inputTokens || 0
|
||||
requestOutputTokens += chunk.outputTokens || 0
|
||||
requestCacheWriteTokens += chunk.cacheWriteTokens || 0
|
||||
requestCacheReadTokens += chunk.cacheReadTokens || 0
|
||||
requestTotalCost = chunk.totalCost ?? requestTotalCost
|
||||
stats.contextTokens =
|
||||
requestInputTokens + requestOutputTokens + requestCacheWriteTokens + requestCacheReadTokens
|
||||
stats.contextUsagePercentage =
|
||||
stats.contextWindow > 0 ? (stats.contextTokens / stats.contextWindow) * 100 : 0
|
||||
onProgress({ stats: { ...stats } })
|
||||
break
|
||||
case "text":
|
||||
requestId = requestId ?? chunk.id
|
||||
assistantText += chunk.text || ""
|
||||
assistantTextSignature = chunk.signature || assistantTextSignature
|
||||
break
|
||||
case "tool_calls":
|
||||
requestId = requestId ?? chunk.id
|
||||
toolUseHandler.processToolUseDelta(
|
||||
{
|
||||
id: chunk.tool_call.function?.id,
|
||||
type: "tool_use",
|
||||
name: chunk.tool_call.function?.name,
|
||||
input: normalizeToolCallArguments(chunk.tool_call.function?.arguments),
|
||||
signature: chunk.signature,
|
||||
},
|
||||
chunk.tool_call.call_id,
|
||||
)
|
||||
break
|
||||
case "reasoning":
|
||||
requestId = requestId ?? chunk.id
|
||||
break
|
||||
}
|
||||
|
||||
if (this.shouldAbort()) {
|
||||
await this.abort()
|
||||
const error = "Subagent run cancelled."
|
||||
onProgress({ status: "failed", error, stats: { ...stats } })
|
||||
return { status: "failed", error, stats }
|
||||
}
|
||||
}
|
||||
|
||||
const calculatedRequestCost =
|
||||
requestTotalCost ??
|
||||
calculateApiCostAnthropic(
|
||||
providerInfo.model.info,
|
||||
requestInputTokens,
|
||||
requestOutputTokens,
|
||||
requestCacheWriteTokens,
|
||||
requestCacheReadTokens,
|
||||
)
|
||||
stats.totalCost += calculatedRequestCost || 0
|
||||
|
||||
const nativeFinalizedToolCalls = toolUseHandler.getAllFinalizedToolUses().map((toolCall, index) => ({
|
||||
toolUseId: resolveToolUseId(toolCall, index),
|
||||
id: toolCall.id,
|
||||
call_id: toolCall.call_id,
|
||||
signature: toolCall.signature,
|
||||
name: toolCall.name,
|
||||
input: toolCall.input,
|
||||
isNativeToolCall: true,
|
||||
}))
|
||||
const parsedNonNativeToolCalls = parseNonNativeToolCalls(assistantText)
|
||||
const fallbackNonNativeToolCalls = nativeFinalizedToolCalls.map((toolCall) => ({
|
||||
...toolCall,
|
||||
isNativeToolCall: false,
|
||||
}))
|
||||
|
||||
let finalizedToolCalls: SubagentToolCall[] = []
|
||||
if (useNativeToolCalls) {
|
||||
finalizedToolCalls = nativeFinalizedToolCalls
|
||||
} else if (parsedNonNativeToolCalls.length > 0) {
|
||||
finalizedToolCalls = parsedNonNativeToolCalls
|
||||
} else if (fallbackNonNativeToolCalls.length > 0) {
|
||||
// Defensive fallback: if non-native mode receives structured tool call chunks,
|
||||
// execute them but serialize results as plain text to avoid tool_result pairing mismatches.
|
||||
Logger.warn(
|
||||
"[SubagentRunner] Received structured tool_calls while native tool calling is disabled; falling back to non-native result serialization.",
|
||||
)
|
||||
finalizedToolCalls = fallbackNonNativeToolCalls
|
||||
}
|
||||
const assistantContent = [] as any[]
|
||||
if (assistantText.trim().length > 0) {
|
||||
assistantContent.push({
|
||||
type: "text",
|
||||
text: assistantText,
|
||||
signature: assistantTextSignature,
|
||||
})
|
||||
}
|
||||
if (useNativeToolCalls) {
|
||||
assistantContent.push(...finalizedToolCalls.map(toAssistantToolUseBlock))
|
||||
}
|
||||
|
||||
if (assistantContent.length > 0) {
|
||||
conversation.push({
|
||||
role: "assistant",
|
||||
content: assistantContent,
|
||||
id: requestId,
|
||||
})
|
||||
}
|
||||
|
||||
if (finalizedToolCalls.length === 0) {
|
||||
if (assistantText.trim().length > 0) {
|
||||
onProgress({ status: "completed", result: assistantText.trim(), stats: { ...stats } })
|
||||
return { status: "completed", result: assistantText.trim(), stats }
|
||||
}
|
||||
|
||||
const error = "Subagent ended without a final text response."
|
||||
onProgress({ status: "failed", error, stats: { ...stats } })
|
||||
return { status: "failed", error, stats }
|
||||
}
|
||||
|
||||
const toolResultBlocks = [] as any[]
|
||||
for (const call of finalizedToolCalls) {
|
||||
const toolName = call.name as ClineDefaultTool
|
||||
|
||||
if (!SUBAGENT_ALLOWED_TOOLS.includes(toolName)) {
|
||||
const deniedResult = formatResponse.toolError(`Tool '${toolName}' is not available inside subagent runs.`)
|
||||
if (call.isNativeToolCall) {
|
||||
toolResultBlocks.push({
|
||||
type: "tool_result",
|
||||
tool_use_id: call.toolUseId,
|
||||
call_id: call.call_id,
|
||||
content: deniedResult,
|
||||
})
|
||||
} else {
|
||||
toolResultBlocks.push({
|
||||
type: "text",
|
||||
text: `${toolName} Result:\n${deniedResult}`,
|
||||
})
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
const toolCallParams = toToolUseParams(call.input)
|
||||
|
||||
const toolCallBlock: ToolUse = {
|
||||
type: "tool_use",
|
||||
name: toolName,
|
||||
params: toolCallParams,
|
||||
partial: false,
|
||||
isNativeToolCall: call.isNativeToolCall,
|
||||
call_id: call.call_id || call.toolUseId,
|
||||
signature: call.signature,
|
||||
}
|
||||
|
||||
if (call.call_id) {
|
||||
state.toolUseIdMap.set(call.call_id, call.toolUseId)
|
||||
}
|
||||
|
||||
const subagentConfig = this.createSubagentTaskConfig(state)
|
||||
const handler = this.baseConfig.coordinator.getHandler(toolName)
|
||||
let toolResult: unknown
|
||||
|
||||
if (!handler) {
|
||||
toolResult = formatResponse.toolError(`No handler registered for tool '${toolName}'.`)
|
||||
} else {
|
||||
try {
|
||||
toolResult = await handler.execute(subagentConfig, toolCallBlock)
|
||||
} catch (error) {
|
||||
toolResult = formatResponse.toolError((error as Error).message)
|
||||
}
|
||||
}
|
||||
|
||||
stats.toolCalls += 1
|
||||
onProgress({ stats: { ...stats } })
|
||||
|
||||
const serializedToolResult = serializeToolResult(toolResult)
|
||||
if (call.isNativeToolCall) {
|
||||
toolResultBlocks.push({
|
||||
type: "tool_result",
|
||||
tool_use_id: call.toolUseId,
|
||||
call_id: call.call_id,
|
||||
content: serializedToolResult,
|
||||
})
|
||||
} else {
|
||||
const toolDescription = handler?.getDescription(toolCallBlock) || `[${toolName}]`
|
||||
toolResultBlocks.push({
|
||||
type: "text",
|
||||
text: `${toolDescription} Result:\n${serializedToolResult}`,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
conversation.push({
|
||||
role: "user",
|
||||
content: toolResultBlocks,
|
||||
})
|
||||
|
||||
await delay(0)
|
||||
}
|
||||
} catch (error) {
|
||||
if (this.shouldAbort()) {
|
||||
const cancelledError = "Subagent run cancelled."
|
||||
onProgress({ status: "failed", error: cancelledError, stats: { ...stats } })
|
||||
return { status: "failed", error: cancelledError, stats }
|
||||
}
|
||||
|
||||
const errorText = (error as Error).message || "Subagent execution failed."
|
||||
Logger.error("[SubagentRunner] run failed", error)
|
||||
onProgress({ status: "failed", error: errorText, stats: { ...stats } })
|
||||
return { status: "failed", error: errorText, stats }
|
||||
} finally {
|
||||
this.activeApiAbort = undefined
|
||||
}
|
||||
}
|
||||
|
||||
private createSubagentTaskConfig(state: TaskState): TaskConfig {
|
||||
const baseCallbacks = this.baseConfig.callbacks
|
||||
|
||||
return {
|
||||
...this.baseConfig,
|
||||
taskState: state,
|
||||
isSubagentExecution: true,
|
||||
vscodeTerminalExecutionMode: "backgroundExec",
|
||||
callbacks: {
|
||||
...baseCallbacks,
|
||||
say: async () => undefined,
|
||||
sayAndCreateMissingParamError: async (_toolName, paramName) =>
|
||||
formatResponse.toolError(formatResponse.missingToolParameterError(paramName)),
|
||||
executeCommandTool: async (command: string, timeoutSeconds: number | undefined) => {
|
||||
this.activeCommandExecutions += 1
|
||||
try {
|
||||
return await baseCallbacks.executeCommandTool(command, timeoutSeconds, {
|
||||
useBackgroundExecution: true,
|
||||
suppressUserInteraction: true,
|
||||
})
|
||||
} finally {
|
||||
this.activeCommandExecutions = Math.max(0, this.activeCommandExecutions - 1)
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
private buildNativeTools(context: SystemPromptContext) {
|
||||
const family = PromptRegistry.getInstance().getModelFamily(context)
|
||||
const toolSets = ClineToolSet.getToolsForVariantWithFallback(family, SUBAGENT_ALLOWED_TOOLS)
|
||||
const filteredToolSpecs = toolSets
|
||||
.map((toolSet) => toolSet.config)
|
||||
.filter((toolSpec) => !toolSpec.contextRequirements || toolSpec.contextRequirements(context))
|
||||
|
||||
const converter = ClineToolSet.getNativeConverter(context.providerInfo.providerId, context.providerInfo.model.id)
|
||||
|
||||
return filteredToolSpecs.map((tool) => converter(tool, context))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,373 @@
|
||||
import { strict as assert } from "node:assert"
|
||||
import * as coreApi from "@core/api"
|
||||
import * as skills from "@core/context/instructions/user-instructions/skills"
|
||||
import { PromptRegistry } from "@core/prompts/system-prompt"
|
||||
import type { TaskConfig } from "@core/task/tools/types/TaskConfig"
|
||||
import { afterEach, describe, it } from "mocha"
|
||||
import sinon from "sinon"
|
||||
import { HostProvider } from "@/hosts/host-provider"
|
||||
import { ApiFormat } from "@/shared/proto/cline/models"
|
||||
import { ClineDefaultTool } from "@/shared/tools"
|
||||
import { TaskState } from "../../../TaskState"
|
||||
import { SubagentRunner } from "../SubagentRunner"
|
||||
|
||||
function initializeHostProvider() {
|
||||
HostProvider.reset()
|
||||
HostProvider.initialize(
|
||||
() => ({}) as never,
|
||||
() => ({}) as never,
|
||||
() => ({}) as never,
|
||||
() => ({}) as never,
|
||||
{
|
||||
workspaceClient: {},
|
||||
envClient: {
|
||||
getHostVersion: async () => ({ platform: "test" }),
|
||||
},
|
||||
windowClient: {},
|
||||
diffClient: {},
|
||||
} as never,
|
||||
() => undefined,
|
||||
async () => "",
|
||||
async () => "",
|
||||
"",
|
||||
"",
|
||||
)
|
||||
}
|
||||
|
||||
function createTaskConfig(nativeToolCallEnabled: boolean): TaskConfig {
|
||||
return {
|
||||
taskId: "task-1",
|
||||
ulid: "ulid-1",
|
||||
cwd: "/tmp",
|
||||
mode: "act",
|
||||
strictPlanModeEnabled: false,
|
||||
yoloModeToggled: false,
|
||||
vscodeTerminalExecutionMode: "backgroundExec",
|
||||
enableParallelToolCalling: false,
|
||||
isSubagentExecution: false,
|
||||
context: {},
|
||||
taskState: new TaskState(),
|
||||
messageState: {},
|
||||
api: {
|
||||
getModel: () => ({
|
||||
id: "anthropic/claude-sonnet-4.5",
|
||||
info: {
|
||||
contextWindow: 200_000,
|
||||
apiFormat: ApiFormat.ANTHROPIC_CHAT,
|
||||
supportsPromptCache: true,
|
||||
},
|
||||
}),
|
||||
},
|
||||
services: {
|
||||
stateManager: {
|
||||
getGlobalSettingsKey: (key: string) => {
|
||||
if (key === "mode") {
|
||||
return "act"
|
||||
}
|
||||
if (key === "customPrompt") {
|
||||
return undefined
|
||||
}
|
||||
return undefined
|
||||
},
|
||||
getGlobalStateKey: (key: string) => (key === "nativeToolCallEnabled" ? nativeToolCallEnabled : undefined),
|
||||
getApiConfiguration: () => ({
|
||||
actModeApiProvider: "anthropic",
|
||||
planModeApiProvider: "anthropic",
|
||||
}),
|
||||
},
|
||||
},
|
||||
browserSettings: {},
|
||||
focusChainSettings: {},
|
||||
autoApprovalSettings: {
|
||||
enableNotifications: false,
|
||||
actions: { executeSafeCommands: false, executeAllCommands: false },
|
||||
},
|
||||
autoApprover: { shouldAutoApproveTool: sinon.stub().returns([false, false]) },
|
||||
callbacks: {
|
||||
say: sinon.stub().resolves(undefined),
|
||||
ask: sinon.stub().resolves({ response: "yesButtonClicked" }),
|
||||
saveCheckpoint: sinon.stub().resolves(),
|
||||
sayAndCreateMissingParamError: sinon.stub().resolves("missing"),
|
||||
removeLastPartialMessageIfExistsWithType: sinon.stub().resolves(),
|
||||
executeCommandTool: sinon.stub().resolves([false, "ok"]),
|
||||
cancelRunningCommandTool: sinon.stub().resolves(false),
|
||||
doesLatestTaskCompletionHaveNewChanges: sinon.stub().resolves(false),
|
||||
updateFCListFromToolResponse: sinon.stub().resolves(),
|
||||
shouldAutoApproveTool: sinon.stub().returns([true, true]),
|
||||
shouldAutoApproveToolWithPath: sinon.stub().resolves(false),
|
||||
postStateToWebview: sinon.stub().resolves(),
|
||||
reinitExistingTaskFromId: sinon.stub().resolves(),
|
||||
cancelTask: sinon.stub().resolves(),
|
||||
updateTaskHistory: sinon.stub().resolves([]),
|
||||
applyLatestBrowserSettings: sinon.stub().resolves(undefined),
|
||||
switchToActMode: sinon.stub().resolves(false),
|
||||
setActiveHookExecution: sinon.stub().resolves(),
|
||||
clearActiveHookExecution: sinon.stub().resolves(),
|
||||
getActiveHookExecution: sinon.stub().resolves(undefined),
|
||||
runUserPromptSubmitHook: sinon.stub().resolves({}),
|
||||
},
|
||||
coordinator: {
|
||||
getHandler: sinon.stub().callsFake((toolName: ClineDefaultTool) => {
|
||||
if (toolName === ClineDefaultTool.LIST_FILES) {
|
||||
return {
|
||||
execute: sinon.stub().resolves("ok"),
|
||||
getDescription: sinon.stub().returns("list_files"),
|
||||
}
|
||||
}
|
||||
|
||||
return undefined
|
||||
}),
|
||||
},
|
||||
} as unknown as TaskConfig
|
||||
}
|
||||
|
||||
describe("SubagentRunner", () => {
|
||||
afterEach(() => {
|
||||
sinon.restore()
|
||||
HostProvider.reset()
|
||||
})
|
||||
|
||||
it("emits native tool_use blocks with matching tool_result tool_use_id across turns", async () => {
|
||||
const createMessage = sinon.stub()
|
||||
createMessage.onFirstCall().callsFake(async function* () {
|
||||
yield {
|
||||
type: "tool_calls",
|
||||
tool_call: {
|
||||
function: {
|
||||
id: "toolu_subagent_1",
|
||||
name: ClineDefaultTool.LIST_FILES,
|
||||
arguments: JSON.stringify({ path: ".", recursive: false }),
|
||||
},
|
||||
},
|
||||
}
|
||||
})
|
||||
createMessage.onSecondCall().callsFake(async function* (_systemPrompt: string, conversation: unknown[]) {
|
||||
const assistantMessage = conversation[1] as {
|
||||
role: string
|
||||
content: Array<{ type?: string; [key: string]: unknown }>
|
||||
}
|
||||
assert.equal(assistantMessage.role, "assistant")
|
||||
assert.ok(Array.isArray(assistantMessage.content))
|
||||
|
||||
const toolUse = assistantMessage.content.find((block) => block.type === "tool_use")
|
||||
assert.ok(toolUse, "assistant message should include tool_use block")
|
||||
assert.equal(toolUse.id, "toolu_subagent_1")
|
||||
assert.equal(toolUse.name, ClineDefaultTool.LIST_FILES)
|
||||
|
||||
const userMessage = conversation[2] as { role: string; content: Array<{ type?: string; [key: string]: unknown }> }
|
||||
assert.equal(userMessage.role, "user")
|
||||
assert.ok(Array.isArray(userMessage.content))
|
||||
|
||||
const toolResult = userMessage.content.find((block) => block.type === "tool_result")
|
||||
assert.ok(toolResult, "user message should include tool_result block")
|
||||
assert.equal(toolResult.tool_use_id, "toolu_subagent_1")
|
||||
|
||||
yield {
|
||||
type: "text",
|
||||
text: "done",
|
||||
}
|
||||
})
|
||||
|
||||
sinon.stub(PromptRegistry.getInstance(), "get").resolves("system prompt")
|
||||
sinon.stub(coreApi, "buildApiHandler").returns({
|
||||
abort: sinon.stub(),
|
||||
getModel: () => ({
|
||||
id: "anthropic/claude-sonnet-4.5",
|
||||
info: {
|
||||
contextWindow: 200_000,
|
||||
apiFormat: ApiFormat.ANTHROPIC_CHAT,
|
||||
supportsPromptCache: true,
|
||||
},
|
||||
}),
|
||||
createMessage,
|
||||
})
|
||||
sinon.stub(skills, "discoverSkills").resolves([])
|
||||
sinon.stub(skills, "getAvailableSkills").returns([])
|
||||
initializeHostProvider()
|
||||
|
||||
const config = createTaskConfig(true)
|
||||
|
||||
const runner = new SubagentRunner(config)
|
||||
sinon
|
||||
.stub(runner as unknown as { buildNativeTools: () => unknown[] }, "buildNativeTools")
|
||||
.returns([{ name: "list_files" }])
|
||||
|
||||
const result = await runner.run("List files", () => {})
|
||||
|
||||
assert.equal(result.status, "completed")
|
||||
assert.equal(result.result, "done")
|
||||
assert.equal(createMessage.callCount, 2)
|
||||
})
|
||||
|
||||
it("falls back to non-native result blocks if structured tool calls appear while native mode is disabled", async () => {
|
||||
const createMessage = sinon.stub()
|
||||
createMessage.onFirstCall().callsFake(async function* () {
|
||||
yield {
|
||||
type: "tool_calls",
|
||||
tool_call: {
|
||||
function: {
|
||||
id: "toolu_subagent_2",
|
||||
name: ClineDefaultTool.LIST_FILES,
|
||||
arguments: JSON.stringify({ path: ".", recursive: false }),
|
||||
},
|
||||
},
|
||||
}
|
||||
})
|
||||
createMessage.onSecondCall().callsFake(async function* (_systemPrompt: string, conversation: unknown[]) {
|
||||
const lastMessage = conversation[conversation.length - 1] as {
|
||||
role: string
|
||||
content: Array<{ type?: string; [key: string]: unknown }>
|
||||
}
|
||||
|
||||
assert.equal(lastMessage.role, "user")
|
||||
assert.ok(Array.isArray(lastMessage.content))
|
||||
assert.ok(lastMessage.content.every((block) => block.type === "text"))
|
||||
assert.equal(
|
||||
lastMessage.content.some((block) => block.type === "tool_result"),
|
||||
false,
|
||||
)
|
||||
|
||||
yield {
|
||||
type: "text",
|
||||
text: "done",
|
||||
}
|
||||
})
|
||||
|
||||
sinon.stub(PromptRegistry.getInstance(), "get").resolves("system prompt")
|
||||
sinon.stub(coreApi, "buildApiHandler").returns({
|
||||
abort: sinon.stub(),
|
||||
getModel: () => ({
|
||||
id: "anthropic/claude-sonnet-4.5",
|
||||
info: {
|
||||
contextWindow: 200_000,
|
||||
apiFormat: ApiFormat.ANTHROPIC_CHAT,
|
||||
supportsPromptCache: true,
|
||||
},
|
||||
}),
|
||||
createMessage,
|
||||
})
|
||||
sinon.stub(skills, "discoverSkills").resolves([])
|
||||
sinon.stub(skills, "getAvailableSkills").returns([])
|
||||
initializeHostProvider()
|
||||
|
||||
const config = createTaskConfig(false)
|
||||
const runner = new SubagentRunner(config)
|
||||
|
||||
const result = await runner.run("List files", () => {})
|
||||
|
||||
assert.equal(result.status, "completed")
|
||||
assert.equal(result.result, "done")
|
||||
assert.equal(createMessage.callCount, 2)
|
||||
})
|
||||
|
||||
it("builds subagent api handler with the parent task ulid", async () => {
|
||||
const createMessage = sinon.stub().callsFake(async function* () {
|
||||
yield {
|
||||
type: "text",
|
||||
text: "done",
|
||||
}
|
||||
})
|
||||
|
||||
sinon.stub(PromptRegistry.getInstance(), "get").resolves("system prompt")
|
||||
const buildApiHandlerStub = sinon.stub(coreApi, "buildApiHandler").returns({
|
||||
abort: sinon.stub(),
|
||||
getModel: () => ({
|
||||
id: "anthropic/claude-sonnet-4.5",
|
||||
info: {
|
||||
contextWindow: 200_000,
|
||||
apiFormat: ApiFormat.ANTHROPIC_CHAT,
|
||||
supportsPromptCache: true,
|
||||
},
|
||||
}),
|
||||
createMessage,
|
||||
})
|
||||
sinon.stub(skills, "discoverSkills").resolves([])
|
||||
sinon.stub(skills, "getAvailableSkills").returns([])
|
||||
initializeHostProvider()
|
||||
|
||||
const config = createTaskConfig(true)
|
||||
const runner = new SubagentRunner(config)
|
||||
sinon
|
||||
.stub(runner as unknown as { buildNativeTools: () => unknown[] }, "buildNativeTools")
|
||||
.returns([{ name: "list_files" }])
|
||||
|
||||
const result = await runner.run("List files", () => {})
|
||||
|
||||
assert.equal(result.status, "completed")
|
||||
assert.equal(buildApiHandlerStub.called, true)
|
||||
sinon.assert.calledWithMatch(buildApiHandlerStub, sinon.match({ ulid: "ulid-1" }), "act")
|
||||
})
|
||||
|
||||
it("includes workspace metadata only in the initial user message", async () => {
|
||||
const createMessage = sinon.stub()
|
||||
createMessage.onFirstCall().callsFake(async function* (_systemPrompt: string, conversation: unknown[]) {
|
||||
const initialUser = conversation[0] as {
|
||||
role: string
|
||||
content: Array<{ type?: string; text?: string }>
|
||||
}
|
||||
assert.equal(initialUser.role, "user")
|
||||
const initialTexts = initialUser.content
|
||||
.filter((block) => block.type === "text")
|
||||
.map((block) => block.text || "")
|
||||
.join("\n")
|
||||
assert.match(initialTexts, /# Workspace Configuration/)
|
||||
|
||||
yield {
|
||||
type: "tool_calls",
|
||||
tool_call: {
|
||||
function: {
|
||||
id: "toolu_subagent_workspace_1",
|
||||
name: ClineDefaultTool.LIST_FILES,
|
||||
arguments: JSON.stringify({ path: ".", recursive: false }),
|
||||
},
|
||||
},
|
||||
}
|
||||
})
|
||||
createMessage.onSecondCall().callsFake(async function* (_systemPrompt: string, conversation: unknown[]) {
|
||||
const followUpUser = conversation[2] as {
|
||||
role: string
|
||||
content: Array<{ type?: string; text?: string }>
|
||||
}
|
||||
assert.equal(followUpUser.role, "user")
|
||||
const followUpTexts = followUpUser.content
|
||||
.filter((block) => block.type === "text")
|
||||
.map((block) => block.text || "")
|
||||
.join("\n")
|
||||
assert.equal(followUpTexts.includes("# Workspace Configuration"), false)
|
||||
|
||||
yield {
|
||||
type: "text",
|
||||
text: "done",
|
||||
}
|
||||
})
|
||||
|
||||
sinon.stub(PromptRegistry.getInstance(), "get").resolves("system prompt")
|
||||
sinon.stub(coreApi, "buildApiHandler").returns({
|
||||
abort: sinon.stub(),
|
||||
getModel: () => ({
|
||||
id: "anthropic/claude-sonnet-4.5",
|
||||
info: {
|
||||
contextWindow: 200_000,
|
||||
apiFormat: ApiFormat.ANTHROPIC_CHAT,
|
||||
supportsPromptCache: true,
|
||||
},
|
||||
}),
|
||||
createMessage,
|
||||
})
|
||||
sinon.stub(skills, "discoverSkills").resolves([])
|
||||
sinon.stub(skills, "getAvailableSkills").returns([])
|
||||
initializeHostProvider()
|
||||
|
||||
const config = createTaskConfig(true)
|
||||
const runner = new SubagentRunner(config)
|
||||
sinon
|
||||
.stub(runner as unknown as { buildNativeTools: () => unknown[] }, "buildNativeTools")
|
||||
.returns([{ name: "list_files" }])
|
||||
|
||||
const result = await runner.run("List files", () => {})
|
||||
|
||||
assert.equal(result.status, "completed")
|
||||
assert.equal(result.result, "done")
|
||||
assert.equal(createMessage.callCount, 2)
|
||||
})
|
||||
})
|
||||
@@ -3,6 +3,7 @@ import type { FileContextTracker } from "@core/context/context-tracking/FileCont
|
||||
import type { ClineIgnoreController } from "@core/ignore/ClineIgnoreController"
|
||||
import type { CommandPermissionController } from "@core/permissions"
|
||||
import type { DiffViewProvider } from "@integrations/editor/DiffViewProvider"
|
||||
import type { CommandExecutionOptions } from "@integrations/terminal"
|
||||
import type { BrowserSession } from "@services/browser/BrowserSession"
|
||||
import type { UrlContentFetcher } from "@services/browser/UrlContentFetcher"
|
||||
import type { McpHub } from "@services/mcp/McpHub"
|
||||
@@ -39,6 +40,7 @@ export interface TaskConfig {
|
||||
doubleCheckCompletionEnabled: boolean
|
||||
vscodeTerminalExecutionMode: "vscodeTerminal" | "backgroundExec"
|
||||
enableParallelToolCalling: boolean
|
||||
isSubagentExecution: boolean
|
||||
context: vscode.ExtensionContext
|
||||
|
||||
// Multi-workspace support (optional for backward compatibility)
|
||||
@@ -104,7 +106,12 @@ export interface TaskCallbacks {
|
||||
|
||||
removeLastPartialMessageIfExistsWithType: (type: "ask" | "say", askOrSay: ClineAsk | ClineSay) => Promise<void>
|
||||
|
||||
executeCommandTool: (command: string, timeoutSeconds: number | undefined) => Promise<[boolean, any]>
|
||||
executeCommandTool: (
|
||||
command: string,
|
||||
timeoutSeconds: number | undefined,
|
||||
options?: CommandExecutionOptions,
|
||||
) => Promise<[boolean, any]>
|
||||
cancelRunningCommandTool?: () => Promise<boolean>
|
||||
|
||||
doesLatestTaskCompletionHaveNewChanges: () => Promise<boolean>
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ export const TASK_CONFIG_KEYS = [
|
||||
"doubleCheckCompletionEnabled",
|
||||
"vscodeTerminalExecutionMode",
|
||||
"enableParallelToolCalling",
|
||||
"isSubagentExecution",
|
||||
"context",
|
||||
"taskState",
|
||||
"messageState",
|
||||
|
||||
@@ -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,20 +9,17 @@
|
||||
* - VscodeTerminalManager → VscodeTerminalProcess (shell integration)
|
||||
* - StandaloneTerminalManager → StandaloneTerminalProcess (child_process)
|
||||
*
|
||||
* IMPORTANT: Subagent commands (cline CLI) are ALWAYS routed to use
|
||||
* StandaloneTerminalManager regardless of the configured mode. This ensures
|
||||
* subagents run in hidden/background terminals rather than cluttering the
|
||||
* user's visible VSCode terminal.
|
||||
* IMPORTANT: Background execution mode uses StandaloneTerminalManager to run
|
||||
* commands in hidden terminals without cluttering the visible terminal.
|
||||
*/
|
||||
|
||||
import { isSubagentCommand, transformClineCommand } from "@integrations/cli-subagents/subagent_command"
|
||||
import { telemetryService } from "@services/telemetry"
|
||||
import { findLastIndex } from "@shared/array"
|
||||
import { ClineToolResponseContent } from "@shared/messages"
|
||||
import { Logger } from "@/shared/services/Logger"
|
||||
import { orchestrateCommandExecution } from "./CommandOrchestrator"
|
||||
import { StandaloneTerminalManager } from "./standalone/StandaloneTerminalManager"
|
||||
import type {
|
||||
CommandExecutionOptions,
|
||||
CommandExecutorCallbacks,
|
||||
CommandExecutorConfig,
|
||||
ITerminalManager,
|
||||
@@ -73,10 +70,9 @@ export class CommandExecutor {
|
||||
this.standaloneManager = config.terminalManager
|
||||
Logger.info(`[CommandExecutor] Reusing Task's StandaloneTerminalManager for backgroundExec mode`)
|
||||
} else {
|
||||
// Create new StandaloneTerminalManager for subagents (even in VSCode mode)
|
||||
// This ensures subagents run in hidden terminals, not cluttering the user's VSCode terminal
|
||||
// Create a standalone manager for background execution support.
|
||||
this.standaloneManager = new StandaloneTerminalManager()
|
||||
Logger.info(`[CommandExecutor] Created new StandaloneTerminalManager for subagents`)
|
||||
Logger.info(`[CommandExecutor] Created new StandaloneTerminalManager`)
|
||||
|
||||
// Copy settings from the provided terminalManager to ensure consistency
|
||||
if ("shellIntegrationTimeout" in config.terminalManager) {
|
||||
@@ -84,7 +80,6 @@ export class CommandExecutor {
|
||||
this.standaloneManager.setShellIntegrationTimeout(tm.shellIntegrationTimeout || 4000)
|
||||
this.standaloneManager.setTerminalReuseEnabled(tm.terminalReuseEnabled ?? true)
|
||||
this.standaloneManager.setTerminalOutputLineLimit(tm.terminalOutputLineLimit || 500)
|
||||
this.standaloneManager.setSubagentTerminalOutputLineLimit(tm.subagentTerminalOutputLineLimit || 2000)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -93,32 +88,26 @@ export class CommandExecutor {
|
||||
* Execute a command in the terminal.
|
||||
*
|
||||
* Routing logic:
|
||||
* 1. Subagent commands (cline CLI) → Always use StandaloneTerminalManager
|
||||
* This ensures subagents run in hidden terminals, not cluttering the user's VSCode terminal
|
||||
* 2. Regular commands → Use the configured terminal manager based on terminalExecutionMode
|
||||
* 1. Background mode commands use StandaloneTerminalManager
|
||||
* 2. Regular commands use the configured terminal manager
|
||||
*
|
||||
* @param command The command to execute
|
||||
* @param timeoutSeconds Optional timeout in seconds
|
||||
* @returns [userRejected, result] tuple
|
||||
*/
|
||||
async execute(command: string, timeoutSeconds: number | undefined): Promise<[boolean, ClineToolResponseContent]> {
|
||||
// Transform subagent commands to ensure flags are correct
|
||||
const isSubagent = isSubagentCommand(command)
|
||||
if (isSubagent) {
|
||||
command = transformClineCommand(command)
|
||||
}
|
||||
|
||||
async execute(
|
||||
command: string,
|
||||
timeoutSeconds: number | undefined,
|
||||
options?: CommandExecutionOptions,
|
||||
): Promise<[boolean, ClineToolResponseContent]> {
|
||||
// Strip leading `cd` to workspace from command
|
||||
const workspaceCdPrefix = `cd ${this.cwd} && `
|
||||
if (command.startsWith(workspaceCdPrefix)) {
|
||||
command = command.substring(workspaceCdPrefix.length)
|
||||
}
|
||||
|
||||
const subAgentStartTime = isSubagent ? performance.now() : 0
|
||||
|
||||
// Select the appropriate terminal manager
|
||||
// Subagents always use standalone manager (hidden terminal)
|
||||
const useStandalone = isSubagent || this.terminalExecutionMode === "backgroundExec"
|
||||
const useStandalone = options?.useBackgroundExecution || this.terminalExecutionMode === "backgroundExec"
|
||||
const manager = useStandalone ? this.standaloneManager : this.terminalManager
|
||||
Logger.info(`Executing command in ${useStandalone ? "standalone" : "VSCode"} terminal: ${command}`)
|
||||
|
||||
@@ -141,6 +130,7 @@ export class CommandExecutor {
|
||||
const result = await orchestrateCommandExecution(process, manager, this.callbacks, {
|
||||
command,
|
||||
timeoutSeconds,
|
||||
suppressUserInteraction: options?.suppressUserInteraction,
|
||||
// When "Proceed While Running" is triggered, track the command in the manager
|
||||
// Returns the log file path so the orchestrator can send it to the UI
|
||||
// existingOutput contains all output lines captured so far
|
||||
@@ -154,12 +144,6 @@ export class CommandExecutor {
|
||||
terminalType: useStandalone ? "standalone" : "vscode",
|
||||
})
|
||||
|
||||
// Capture subagent telemetry
|
||||
if (isSubagent && subAgentStartTime > 0) {
|
||||
const durationMs = Math.round(performance.now() - subAgentStartTime)
|
||||
telemetryService.captureSubagentExecution(this.ulid, durationMs, result.outputLines.length, result.completed)
|
||||
}
|
||||
|
||||
// If the command was cancelled externally (via cancel button), return a clear cancellation message
|
||||
// This ensures the AI agent knows the command was cancelled by the user
|
||||
if (this.wasCancelledExternally) {
|
||||
|
||||
@@ -61,8 +61,35 @@ export async function orchestrateCommandExecution(
|
||||
showShellIntegrationSuggestion,
|
||||
onProceedWhileRunning,
|
||||
terminalType = "vscode",
|
||||
suppressUserInteraction = false,
|
||||
} = options
|
||||
|
||||
const say = async (
|
||||
type: Parameters<CommandExecutorCallbacks["say"]>[0],
|
||||
text?: Parameters<CommandExecutorCallbacks["say"]>[1],
|
||||
images?: Parameters<CommandExecutorCallbacks["say"]>[2],
|
||||
files?: Parameters<CommandExecutorCallbacks["say"]>[3],
|
||||
partial?: Parameters<CommandExecutorCallbacks["say"]>[4],
|
||||
): Promise<Awaited<ReturnType<CommandExecutorCallbacks["say"]>>> => {
|
||||
if (suppressUserInteraction) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
return callbacks.say(type, text, images, files, partial)
|
||||
}
|
||||
|
||||
const ask = async (
|
||||
type: Parameters<CommandExecutorCallbacks["ask"]>[0],
|
||||
text?: Parameters<CommandExecutorCallbacks["ask"]>[1],
|
||||
partial?: Parameters<CommandExecutorCallbacks["ask"]>[2],
|
||||
): Promise<Awaited<ReturnType<CommandExecutorCallbacks["ask"]>> | undefined> => {
|
||||
if (suppressUserInteraction) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
return callbacks.ask(type, text, partial)
|
||||
}
|
||||
|
||||
// Track command execution state
|
||||
callbacks.updateBackgroundCommandState(true)
|
||||
|
||||
@@ -121,7 +148,11 @@ export async function orchestrateCommandExecution(
|
||||
try {
|
||||
// Use ask() to present output and wait for user response
|
||||
// This enables "Proceed While Running" button functionality
|
||||
const { response, text, images, files } = await callbacks.ask("command_output", chunk)
|
||||
const interaction = await ask("command_output", chunk)
|
||||
if (!interaction) {
|
||||
return
|
||||
}
|
||||
const { response, text, images, files } = interaction
|
||||
|
||||
if (response === "yesButtonClicked") {
|
||||
// Track when user clicks "Proceed While Running"
|
||||
@@ -167,7 +198,7 @@ export async function orchestrateCommandExecution(
|
||||
// Send log file message to UI BEFORE resuming the process
|
||||
// This ensures the message appears before any new output lines
|
||||
if (trackingResult?.logFilePath) {
|
||||
await callbacks.say("command_output", `\n📋 Output is being logged to: ${trackingResult.logFilePath}`)
|
||||
await say("command_output", `\n📋 Output is being logged to: ${trackingResult.logFilePath}`)
|
||||
}
|
||||
|
||||
// Now resume the process - any new lines will be handled by the background tracker
|
||||
@@ -186,7 +217,7 @@ export async function orchestrateCommandExecution(
|
||||
outputBufferSize = 0
|
||||
// Send cancellation message BEFORE resuming the process
|
||||
// This ensures the message appears before any new output lines
|
||||
await callbacks.say("command_output", "Command cancelled")
|
||||
await say("command_output", "Command cancelled")
|
||||
// Now resume the process
|
||||
process.continue()
|
||||
} else {
|
||||
@@ -209,7 +240,7 @@ export async function orchestrateCommandExecution(
|
||||
}
|
||||
} else {
|
||||
// After "Proceed While Running": stream output directly to UI
|
||||
await callbacks.say("command_output", chunk)
|
||||
await say("command_output", chunk)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -245,7 +276,7 @@ export async function orchestrateCommandExecution(
|
||||
outputBufferSize = 0
|
||||
if (!didContinue) {
|
||||
// Use say() instead of ask() since we're transitioning to file mode
|
||||
await callbacks.say("command_output", chunk)
|
||||
await say("command_output", chunk)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -271,7 +302,7 @@ export async function orchestrateCommandExecution(
|
||||
lastLines = outputLines.slice(-SUMMARY_LINES_TO_KEEP)
|
||||
|
||||
// FINALLY: Notify user (now this will appear at the end after all buffered output)
|
||||
await callbacks.say(
|
||||
await say(
|
||||
"command_output",
|
||||
`\n📋 Output is large (${outputLines.length} lines, ${Math.round(totalOutputBytes / 1024)}KB). Writing to: ${largeOutputLogPath}`,
|
||||
)
|
||||
@@ -346,7 +377,7 @@ export async function orchestrateCommandExecution(
|
||||
// After "Proceed While Running" (without background tracking): stream output directly to UI
|
||||
// But throttle if we're in file mode to avoid flooding UI
|
||||
if (!isWritingToFile) {
|
||||
await callbacks.say("command_output", line)
|
||||
await say("command_output", line)
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -383,9 +414,9 @@ export async function orchestrateCommandExecution(
|
||||
|
||||
process.once("no_shell_integration", async () => {
|
||||
if (showShellIntegrationSuggestion) {
|
||||
await callbacks.say("shell_integration_warning_with_suggestion")
|
||||
await say("shell_integration_warning_with_suggestion")
|
||||
} else {
|
||||
await callbacks.say("shell_integration_warning")
|
||||
await say("shell_integration_warning")
|
||||
}
|
||||
})
|
||||
|
||||
@@ -435,7 +466,7 @@ export async function orchestrateCommandExecution(
|
||||
|
||||
// Send log file message to UI BEFORE resuming the process
|
||||
if (trackingResult?.logFilePath) {
|
||||
await callbacks.say(
|
||||
await say(
|
||||
"command_output",
|
||||
`\n⏱️ Command timed out. Output is being logged to: ${trackingResult.logFilePath}`,
|
||||
)
|
||||
@@ -523,7 +554,7 @@ export async function orchestrateCommandExecution(
|
||||
}
|
||||
|
||||
if (userFeedback) {
|
||||
await callbacks.say("user_feedback", userFeedback.text, userFeedback.images, userFeedback.files)
|
||||
await say("user_feedback", userFeedback.text, userFeedback.images, userFeedback.files)
|
||||
|
||||
let fileContentString = ""
|
||||
if (userFeedback.files && userFeedback.files.length > 0) {
|
||||
|
||||
@@ -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
|
||||
// =============================================================================
|
||||
|
||||
@@ -36,6 +36,7 @@ export type {
|
||||
// Command Executor types
|
||||
ActiveBackgroundCommand,
|
||||
AskResponse,
|
||||
CommandExecutionOptions,
|
||||
CommandExecutorCallbacks,
|
||||
CommandExecutorConfig,
|
||||
FullCommandExecutorConfig,
|
||||
|
||||
@@ -14,11 +14,7 @@
|
||||
|
||||
import { ClineTempManager } from "@services/temp"
|
||||
import * as fs from "fs"
|
||||
import {
|
||||
BACKGROUND_COMMAND_TIMEOUT_MS,
|
||||
DEFAULT_SUBAGENT_TERMINAL_OUTPUT_LINE_LIMIT,
|
||||
DEFAULT_TERMINAL_OUTPUT_LINE_LIMIT,
|
||||
} from "../constants"
|
||||
import { BACKGROUND_COMMAND_TIMEOUT_MS, DEFAULT_TERMINAL_OUTPUT_LINE_LIMIT } from "../constants"
|
||||
import type { BackgroundCommand, ITerminalManager, TerminalInfo, TerminalProcessResultPromise } from "../types"
|
||||
import { StandaloneTerminalProcess } from "./StandaloneTerminalProcess"
|
||||
import { StandaloneTerminalRegistry } from "./StandaloneTerminalRegistry"
|
||||
@@ -81,9 +77,6 @@ export class StandaloneTerminalManager implements ITerminalManager {
|
||||
/** Maximum output lines to keep */
|
||||
private terminalOutputLineLimit: number = DEFAULT_TERMINAL_OUTPUT_LINE_LIMIT
|
||||
|
||||
/** Maximum output lines for subagent commands */
|
||||
private subagentTerminalOutputLineLimit: number = DEFAULT_SUBAGENT_TERMINAL_OUTPUT_LINE_LIMIT
|
||||
|
||||
/** Default terminal profile */
|
||||
private defaultTerminalProfile = "default"
|
||||
|
||||
@@ -227,15 +220,10 @@ export class StandaloneTerminalManager implements ITerminalManager {
|
||||
* Process output lines, potentially truncating if over limit.
|
||||
* @param outputLines Array of output lines
|
||||
* @param overrideLimit Optional limit override
|
||||
* @param isSubagentCommand Whether this is a subagent command
|
||||
* @returns Processed output string
|
||||
*/
|
||||
processOutput(outputLines: string[], overrideLimit?: number, isSubagentCommand?: boolean): string {
|
||||
const limit = isSubagentCommand
|
||||
? overrideLimit !== undefined
|
||||
? overrideLimit
|
||||
: this.subagentTerminalOutputLineLimit
|
||||
: this.terminalOutputLineLimit
|
||||
processOutput(outputLines: string[], overrideLimit?: number): string {
|
||||
const limit = overrideLimit !== undefined ? overrideLimit : this.terminalOutputLineLimit
|
||||
if (outputLines.length > limit) {
|
||||
const halfLimit = Math.floor(limit / 2)
|
||||
const start = outputLines.slice(0, halfLimit)
|
||||
@@ -295,14 +283,6 @@ export class StandaloneTerminalManager implements ITerminalManager {
|
||||
this.terminalOutputLineLimit = limit
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the maximum number of output lines for subagent commands.
|
||||
* @param limit Maximum number of lines
|
||||
*/
|
||||
setSubagentTerminalOutputLineLimit(limit: number): void {
|
||||
this.subagentTerminalOutputLineLimit = limit
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the default terminal profile.
|
||||
* @param profile The profile identifier
|
||||
|
||||
@@ -223,12 +223,6 @@ export interface ITerminalManager {
|
||||
*/
|
||||
setTerminalOutputLineLimit(limit: number): void
|
||||
|
||||
/**
|
||||
* Set the maximum number of output lines for subagent commands.
|
||||
* @param limit Maximum number of lines
|
||||
*/
|
||||
setSubagentTerminalOutputLineLimit(limit: number): void
|
||||
|
||||
/**
|
||||
* Set the default terminal profile.
|
||||
* @param profile The profile identifier
|
||||
@@ -239,10 +233,9 @@ export interface ITerminalManager {
|
||||
* Process output lines, potentially truncating if over limit.
|
||||
* @param outputLines Array of output lines
|
||||
* @param overrideLimit Optional limit override
|
||||
* @param isSubagentCommand Whether this is a subagent command
|
||||
* @returns Processed output string
|
||||
*/
|
||||
processOutput(outputLines: string[], overrideLimit?: number, isSubagentCommand?: boolean): string
|
||||
processOutput(outputLines: string[], overrideLimit?: number): string
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -348,6 +341,22 @@ export interface CommandExecutorCallbacks {
|
||||
addToUserMessageContent: (content: { type: string; text: string }) => void
|
||||
}
|
||||
|
||||
/**
|
||||
* Optional per-command execution behavior overrides.
|
||||
*/
|
||||
export interface CommandExecutionOptions {
|
||||
/**
|
||||
* Force command execution in standalone/background terminal mode for this command.
|
||||
* This is useful for subagent runs and headless-style execution flows.
|
||||
*/
|
||||
useBackgroundExecution?: boolean
|
||||
/**
|
||||
* Suppress command interaction/output UI messages (ask/say) for this command execution.
|
||||
* Command output is still captured and returned as the tool result.
|
||||
*/
|
||||
suppressUserInteraction?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Configuration for CommandExecutor
|
||||
*/
|
||||
@@ -395,6 +404,11 @@ export interface OrchestrationOptions {
|
||||
* Defaults to "vscode" for backward compatibility.
|
||||
*/
|
||||
terminalType?: "vscode" | "standalone"
|
||||
/**
|
||||
* If true, suppresses command-output ask/say UI interactions.
|
||||
* Output is still collected and included in the final result.
|
||||
*/
|
||||
suppressUserInteraction?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -37,7 +37,6 @@ export type Platform = "aix" | "darwin" | "freebsd" | "linux" | "openbsd" | "sun
|
||||
export const DEFAULT_PLATFORM = "unknown"
|
||||
|
||||
export const COMMAND_CANCEL_TOKEN = "__cline_command_cancel__"
|
||||
|
||||
export interface ExtensionState {
|
||||
isNewUser: boolean
|
||||
welcomeViewCompleted: boolean
|
||||
@@ -65,7 +64,6 @@ export interface ExtensionState {
|
||||
terminalReuseEnabled?: boolean
|
||||
terminalOutputLineLimit: number
|
||||
maxConsecutiveMistakes: number
|
||||
subagentTerminalOutputLineLimit: number
|
||||
defaultTerminalProfile?: string
|
||||
vscodeTerminalExecutionMode: string
|
||||
backgroundCommandRunning?: boolean
|
||||
@@ -87,6 +85,7 @@ export interface ExtensionState {
|
||||
strictPlanModeEnabled?: boolean
|
||||
yoloModeToggled?: boolean
|
||||
useAutoCondense?: boolean
|
||||
subagentsEnabled?: boolean
|
||||
clineWebToolsEnabled?: ClineFeatureSetting
|
||||
worktreesEnabled?: ClineFeatureSetting
|
||||
focusChainSettings: FocusChainSettings
|
||||
@@ -105,7 +104,6 @@ export interface ExtensionState {
|
||||
dismissedBanners?: Array<{ bannerId: string; dismissedAt: number }>
|
||||
hooksEnabled?: boolean
|
||||
remoteConfigSettings?: Partial<RemoteConfigFields>
|
||||
subagentsEnabled?: boolean
|
||||
globalSkillsToggles?: Record<string, boolean>
|
||||
localSkillsToggles?: Record<string, boolean>
|
||||
nativeToolCallSetting?: boolean
|
||||
@@ -154,6 +152,7 @@ export type ClineAsk =
|
||||
| "condense"
|
||||
| "summarize_task"
|
||||
| "report_bug"
|
||||
| "use_subagents"
|
||||
|
||||
export type ClineSay =
|
||||
| "task"
|
||||
@@ -190,6 +189,9 @@ export type ClineSay =
|
||||
| "task_progress"
|
||||
| "hook_status"
|
||||
| "hook_output_stream"
|
||||
| "subagent"
|
||||
| "use_subagents"
|
||||
| "subagent_usage"
|
||||
| "conditional_rules_applied"
|
||||
|
||||
export interface ClineSayTool {
|
||||
@@ -269,6 +271,38 @@ export interface ClineSayGenerateExplanation {
|
||||
error?: string
|
||||
}
|
||||
|
||||
export type SubagentExecutionStatus = "pending" | "running" | "completed" | "failed"
|
||||
|
||||
export interface SubagentStatusItem {
|
||||
index: number
|
||||
prompt: string
|
||||
status: SubagentExecutionStatus
|
||||
toolCalls: number
|
||||
inputTokens: number
|
||||
outputTokens: number
|
||||
totalCost: number
|
||||
contextTokens: number
|
||||
contextWindow: number
|
||||
contextUsagePercentage: number
|
||||
result?: string
|
||||
error?: string
|
||||
}
|
||||
|
||||
export interface ClineSaySubagentStatus {
|
||||
status: "running" | "completed" | "failed"
|
||||
total: number
|
||||
completed: number
|
||||
successes: number
|
||||
failures: number
|
||||
toolCalls: number
|
||||
inputTokens: number
|
||||
outputTokens: number
|
||||
contextWindow: number
|
||||
maxContextTokens: number
|
||||
maxContextUsagePercentage: number
|
||||
items: SubagentStatusItem[]
|
||||
}
|
||||
|
||||
export type BrowserActionResult = {
|
||||
screenshot?: string
|
||||
logs?: string
|
||||
@@ -284,6 +318,10 @@ export interface ClineAskUseMcpServer {
|
||||
uri?: string
|
||||
}
|
||||
|
||||
export interface ClineAskUseSubagents {
|
||||
prompts: string[]
|
||||
}
|
||||
|
||||
export interface ClinePlanModeResponse {
|
||||
response: string
|
||||
options?: string[]
|
||||
@@ -317,6 +355,15 @@ export interface ClineApiReqInfo {
|
||||
}
|
||||
}
|
||||
|
||||
export interface ClineSubagentUsageInfo {
|
||||
source: "subagents"
|
||||
tokensIn: number
|
||||
tokensOut: number
|
||||
cacheWrites: number
|
||||
cacheReads: number
|
||||
cost: number
|
||||
}
|
||||
|
||||
export type ClineApiReqCancelReason = "streaming_failed" | "user_cancelled" | "retries_exhausted"
|
||||
|
||||
export const COMPLETION_RESULT_CHANGES_FLAG = "HAS_CHANGES"
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user