mirror of
https://github.com/cline/cline.git
synced 2026-09-04 11:44:01 +08:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| fd9bdfd4f9 | |||
| 0a4400f07f |
+9
-314
@@ -43,7 +43,6 @@ import { TerminalManager } from "@integrations/terminal/TerminalManager"
|
||||
import { BrowserSession } from "@services/browser/BrowserSession"
|
||||
import { UrlContentFetcher } from "@services/browser/UrlContentFetcher"
|
||||
import { listFiles } from "@services/glob/list-files"
|
||||
import { Logger } from "@services/logging/Logger"
|
||||
import { McpHub } from "@services/mcp/McpHub"
|
||||
import { ApiConfiguration } from "@shared/api"
|
||||
import { findLast, findLastIndex } from "@shared/array"
|
||||
@@ -59,7 +58,6 @@ import { getGitRemoteUrls, getLatestGitCommitHash } from "@utils/git"
|
||||
import { isLocalModel, isNextGenModelFamily } from "@utils/model-utils"
|
||||
import { arePathsEqual, getDesktopDir } from "@utils/path"
|
||||
import cloneDeep from "clone-deep"
|
||||
import { execa } from "execa"
|
||||
import pWaitFor from "p-wait-for"
|
||||
import * as path from "path"
|
||||
import { ulid } from "ulid"
|
||||
@@ -69,20 +67,22 @@ import { getSystemPrompt } from "@/core/prompts/system-prompt"
|
||||
import { HostProvider } from "@/hosts/host-provider"
|
||||
import { ErrorService } from "@/services/error"
|
||||
import { featureFlagsService } from "@/services/feature-flags"
|
||||
import { TerminalHangStage, TerminalUserInterventionAction, telemetryService } from "@/services/telemetry"
|
||||
import { telemetryService } from "@/services/telemetry"
|
||||
import { ShowMessageType } from "@/shared/proto/index.host"
|
||||
import { isInTestMode } from "../../services/test/TestMode"
|
||||
import { ensureLocalClineDirExists } from "../context/instructions/user-instructions/rule-helpers"
|
||||
import { refreshWorkflowToggles } from "../context/instructions/user-instructions/workflows"
|
||||
import { Controller } from "../controller"
|
||||
import { StateManager } from "../storage/StateManager"
|
||||
import { FocusChainManager } from "./focus-chain"
|
||||
import { MessageStateHandler } from "./message-state"
|
||||
import { CommandRunner } from "./runtime/CommandRunner"
|
||||
import { TaskState } from "./TaskState"
|
||||
import { ToolExecutor } from "./ToolExecutor"
|
||||
|
||||
export type { ToolResponse } from "./types"
|
||||
|
||||
import { detectAvailableCliTools, updateApiReqMsg } from "./utils"
|
||||
|
||||
export type ToolResponse = string | Array<Anthropic.TextBlockParam | Anthropic.ImageBlockParam>
|
||||
type UserContent = Array<Anthropic.ContentBlockParam>
|
||||
|
||||
type TaskParams = {
|
||||
@@ -129,6 +129,7 @@ export class Task {
|
||||
public checkpointManager?: ICheckpointManager
|
||||
private clineIgnoreController: ClineIgnoreController
|
||||
private toolExecutor: ToolExecutor
|
||||
private commandRunner: CommandRunner
|
||||
|
||||
// Metadata tracking
|
||||
private fileContextTracker: FileContextTracker
|
||||
@@ -377,6 +378,8 @@ export class Task {
|
||||
telemetryService.captureTaskCreated(this.ulid, currentProvider)
|
||||
}
|
||||
|
||||
this.commandRunner = new CommandRunner(this.cwd, this.terminalManager, this.ask.bind(this), this.say.bind(this))
|
||||
|
||||
this.toolExecutor = new ToolExecutor(
|
||||
this.controller.context,
|
||||
this.taskState,
|
||||
@@ -400,7 +403,7 @@ export class Task {
|
||||
this.saveCheckpointCallback.bind(this),
|
||||
this.sayAndCreateMissingParamError.bind(this),
|
||||
this.removeLastPartialMessageIfExistsWithType.bind(this),
|
||||
this.executeCommandTool.bind(this),
|
||||
this.commandRunner.execute.bind(this.commandRunner),
|
||||
() => this.checkpointManager?.doesLatestTaskCompletionHaveNewChanges() ?? Promise.resolve(false),
|
||||
this.FocusChainManager?.updateFCListFromToolResponse.bind(this.FocusChainManager) || (async () => {}),
|
||||
this.switchToActModeCallback.bind(this),
|
||||
@@ -941,314 +944,6 @@ export class Task {
|
||||
}
|
||||
}
|
||||
|
||||
// Tools
|
||||
|
||||
/**
|
||||
* Executes a command directly in Node.js using execa
|
||||
* This is used in test mode to capture the full output without using the VS Code terminal
|
||||
* Commands are automatically terminated after 30 seconds using Promise.race
|
||||
*/
|
||||
private async executeCommandInNode(command: string): Promise<[boolean, ToolResponse]> {
|
||||
try {
|
||||
// Create a child process
|
||||
const childProcess = execa(command, {
|
||||
shell: true,
|
||||
cwd: this.cwd,
|
||||
reject: false,
|
||||
all: true, // Merge stdout and stderr
|
||||
})
|
||||
|
||||
// Set up variables to collect output
|
||||
let output = ""
|
||||
|
||||
// Collect output in real-time
|
||||
if (childProcess.all) {
|
||||
childProcess.all.on("data", (data) => {
|
||||
output += data.toString()
|
||||
})
|
||||
}
|
||||
|
||||
// Create a timeout promise that rejects after 30 seconds
|
||||
const timeoutPromise = new Promise<never>((_, reject) => {
|
||||
setTimeout(() => {
|
||||
if (childProcess.pid) {
|
||||
childProcess.kill("SIGKILL") // Use SIGKILL for more forceful termination
|
||||
}
|
||||
reject(new Error("Command timeout after 30s"))
|
||||
}, 30000)
|
||||
})
|
||||
|
||||
// Race between command completion and timeout
|
||||
const result = await Promise.race([childProcess, timeoutPromise]).catch((_error) => {
|
||||
// If we get here due to timeout, return a partial result with timeout flag
|
||||
Logger.info(`Command timed out after 30s: ${command}`)
|
||||
return {
|
||||
stdout: "",
|
||||
stderr: "",
|
||||
exitCode: 124, // Standard timeout exit code
|
||||
timedOut: true,
|
||||
}
|
||||
})
|
||||
|
||||
// Check if timeout occurred
|
||||
const wasTerminated = result.timedOut === true
|
||||
|
||||
// Use collected output or result output
|
||||
if (!output) {
|
||||
output = result.stdout || result.stderr || ""
|
||||
}
|
||||
|
||||
Logger.info(`Command executed in Node: ${command}\nOutput:\n${output}`)
|
||||
|
||||
// Add termination message if the command was terminated
|
||||
if (wasTerminated) {
|
||||
output += "\nCommand was taking a while to run so it was auto terminated after 30s"
|
||||
}
|
||||
|
||||
// Format the result similar to terminal output
|
||||
return [
|
||||
false,
|
||||
`Command executed${wasTerminated ? " (terminated after 30s)" : ""} with exit code ${
|
||||
result.exitCode
|
||||
}.${output.length > 0 ? `\nOutput:\n${output}` : ""}`,
|
||||
]
|
||||
} catch (error) {
|
||||
// Handle any errors that might occur
|
||||
const errorMessage = error instanceof Error ? error.message : String(error)
|
||||
return [false, `Error executing command: ${errorMessage}`]
|
||||
}
|
||||
}
|
||||
|
||||
async executeCommandTool(command: string, timeoutSeconds: number | undefined): Promise<[boolean, ToolResponse]> {
|
||||
Logger.info("IS_TEST: " + isInTestMode())
|
||||
|
||||
// Check if we're in test mode
|
||||
if (isInTestMode()) {
|
||||
// In test mode, execute the command directly in Node
|
||||
Logger.info("Executing command in Node: " + command)
|
||||
return this.executeCommandInNode(command)
|
||||
}
|
||||
Logger.info("Executing command in terminal: " + command)
|
||||
|
||||
const terminalInfo = await this.terminalManager.getOrCreateTerminal(this.cwd)
|
||||
terminalInfo.terminal.show() // weird visual bug when creating new terminals (even manually) where there's an empty space at the top.
|
||||
const process = this.terminalManager.runCommand(terminalInfo, command)
|
||||
|
||||
let userFeedback: { text?: string; images?: string[]; files?: string[] } | undefined
|
||||
let didContinue = false
|
||||
|
||||
// Chunked terminal output buffering
|
||||
const CHUNK_LINE_COUNT = 20
|
||||
const CHUNK_BYTE_SIZE = 2048 // 2KB
|
||||
const CHUNK_DEBOUNCE_MS = 100
|
||||
|
||||
let outputBuffer: string[] = []
|
||||
let outputBufferSize: number = 0
|
||||
let chunkTimer: NodeJS.Timeout | null = null
|
||||
|
||||
// Track if buffer gets stuck (correlated with PROCESS_WHILE_RUNNING to indicate genuine technical issues)
|
||||
let bufferStuckTimer: NodeJS.Timeout | null = null
|
||||
const BUFFER_STUCK_TIMEOUT_MS = 6000 // 6 seconds
|
||||
|
||||
const flushBuffer = async (force = false) => {
|
||||
if (outputBuffer.length === 0) {
|
||||
if (force) {
|
||||
// If force is true, flush anyway
|
||||
} else {
|
||||
return
|
||||
}
|
||||
}
|
||||
const chunk = outputBuffer.join("\n")
|
||||
outputBuffer = []
|
||||
outputBufferSize = 0
|
||||
|
||||
// Start timer to detect if buffer gets stuck
|
||||
bufferStuckTimer = setTimeout(() => {
|
||||
telemetryService.captureTerminalHang(TerminalHangStage.BUFFER_STUCK)
|
||||
bufferStuckTimer = null
|
||||
}, BUFFER_STUCK_TIMEOUT_MS)
|
||||
|
||||
try {
|
||||
const { response, text, images, files } = await this.ask("command_output", chunk)
|
||||
if (response === "yesButtonClicked") {
|
||||
// Track when user clicks "Process while Running"
|
||||
telemetryService.captureTerminalUserIntervention(TerminalUserInterventionAction.PROCESS_WHILE_RUNNING)
|
||||
// proceed while running - but still capture user feedback if provided
|
||||
if (text || (images && images.length > 0) || (files && files.length > 0)) {
|
||||
userFeedback = { text, images, files }
|
||||
}
|
||||
} else {
|
||||
userFeedback = { text, images, files }
|
||||
}
|
||||
didContinue = true
|
||||
process.continue()
|
||||
|
||||
// If more output accumulated, flush again
|
||||
if (outputBuffer.length > 0) {
|
||||
await flushBuffer()
|
||||
}
|
||||
} catch {
|
||||
Logger.error("Error while asking for command output")
|
||||
} finally {
|
||||
// If the command finishes execution before the 'command_output' ask promise resolves (in other words before the user responded to the ask, which is expected when the command finishes execution first), this block is reached. This is expected and safe to ignore, as no further handling is required.
|
||||
|
||||
// Clear the stuck timer
|
||||
if (bufferStuckTimer) {
|
||||
clearTimeout(bufferStuckTimer)
|
||||
bufferStuckTimer = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const scheduleFlush = () => {
|
||||
if (chunkTimer) {
|
||||
clearTimeout(chunkTimer)
|
||||
}
|
||||
chunkTimer = setTimeout(async () => await flushBuffer(), CHUNK_DEBOUNCE_MS)
|
||||
}
|
||||
|
||||
const outputLines: string[] = []
|
||||
process.on("line", async (line) => {
|
||||
outputLines.push(line)
|
||||
|
||||
if (!didContinue) {
|
||||
outputBuffer.push(line)
|
||||
outputBufferSize += Buffer.byteLength(line, "utf8")
|
||||
// Flush if buffer is large enough
|
||||
if (outputBuffer.length >= CHUNK_LINE_COUNT || outputBufferSize >= CHUNK_BYTE_SIZE) {
|
||||
await flushBuffer()
|
||||
} else {
|
||||
scheduleFlush()
|
||||
}
|
||||
} else {
|
||||
this.say("command_output", line)
|
||||
}
|
||||
})
|
||||
|
||||
let completed = false
|
||||
let completionTimer: NodeJS.Timeout | null = null
|
||||
const COMPLETION_TIMEOUT_MS = 6000 // 6 seconds
|
||||
|
||||
// Start timer to detect if waiting for completion takes too long
|
||||
completionTimer = setTimeout(() => {
|
||||
if (!completed) {
|
||||
telemetryService.captureTerminalHang(TerminalHangStage.WAITING_FOR_COMPLETION)
|
||||
completionTimer = null
|
||||
}
|
||||
}, COMPLETION_TIMEOUT_MS)
|
||||
|
||||
process.once("completed", async () => {
|
||||
completed = true
|
||||
// Clear the completion timer
|
||||
if (completionTimer) {
|
||||
clearTimeout(completionTimer)
|
||||
completionTimer = null
|
||||
}
|
||||
// Flush any remaining buffered output
|
||||
if (!didContinue && outputBuffer.length > 0) {
|
||||
if (chunkTimer) {
|
||||
clearTimeout(chunkTimer)
|
||||
chunkTimer = null
|
||||
}
|
||||
await flushBuffer(true)
|
||||
}
|
||||
})
|
||||
|
||||
process.once("no_shell_integration", async () => {
|
||||
await this.say("shell_integration_warning")
|
||||
})
|
||||
|
||||
//await process
|
||||
|
||||
if (timeoutSeconds) {
|
||||
const timeoutPromise = new Promise<never>((_, reject) => {
|
||||
setTimeout(() => {
|
||||
reject(new Error("COMMAND_TIMEOUT"))
|
||||
}, timeoutSeconds * 1000)
|
||||
})
|
||||
|
||||
try {
|
||||
await Promise.race([process, timeoutPromise])
|
||||
} catch (error) {
|
||||
// This will continue running the command in the background
|
||||
didContinue = true
|
||||
process.continue()
|
||||
|
||||
// Clear all our timers
|
||||
if (chunkTimer) {
|
||||
clearTimeout(chunkTimer)
|
||||
chunkTimer = null
|
||||
}
|
||||
if (completionTimer) {
|
||||
clearTimeout(completionTimer)
|
||||
completionTimer = null
|
||||
}
|
||||
|
||||
// Process any output we captured before timeout
|
||||
await setTimeoutPromise(50)
|
||||
const result = this.terminalManager.processOutput(outputLines)
|
||||
|
||||
if (error.message === "COMMAND_TIMEOUT") {
|
||||
return [
|
||||
false,
|
||||
`Command execution timed out after ${timeoutSeconds} seconds. The command may still be running in the terminal.${result.length > 0 ? `\nOutput so far:\n${result}` : ""}`,
|
||||
]
|
||||
}
|
||||
|
||||
// Re-throw other errors
|
||||
throw error
|
||||
}
|
||||
} else {
|
||||
await process
|
||||
}
|
||||
|
||||
// Clear timer if process completes normally
|
||||
if (completionTimer) {
|
||||
clearTimeout(completionTimer)
|
||||
completionTimer = null
|
||||
}
|
||||
|
||||
// Wait for a short delay to ensure all messages are sent to the webview
|
||||
// This delay allows time for non-awaited promises to be created and
|
||||
// for their associated messages to be sent to the webview, maintaining
|
||||
// the correct order of messages (although the webview is smart about
|
||||
// grouping command_output messages despite any gaps anyways)
|
||||
await setTimeoutPromise(50)
|
||||
|
||||
const result = this.terminalManager.processOutput(outputLines)
|
||||
|
||||
if (userFeedback) {
|
||||
await this.say("user_feedback", userFeedback.text, userFeedback.images, userFeedback.files)
|
||||
|
||||
let fileContentString = ""
|
||||
if (userFeedback.files && userFeedback.files.length > 0) {
|
||||
fileContentString = await processFilesIntoText(userFeedback.files)
|
||||
}
|
||||
|
||||
return [
|
||||
true,
|
||||
formatResponse.toolResult(
|
||||
`Command is still running in the user's terminal.${
|
||||
result.length > 0 ? `\nHere's the output so far:\n${result}` : ""
|
||||
}\n\nThe user provided the following feedback:\n<feedback>\n${userFeedback.text}\n</feedback>`,
|
||||
userFeedback.images,
|
||||
fileContentString,
|
||||
),
|
||||
]
|
||||
}
|
||||
|
||||
if (completed) {
|
||||
return [false, `Command executed.${result.length > 0 ? `\nOutput:\n${result}` : ""}`]
|
||||
} else {
|
||||
return [
|
||||
false,
|
||||
`Command is still running in the user's terminal.${
|
||||
result.length > 0 ? `\nHere's the output so far:\n${result}` : ""
|
||||
}\n\nYou will be updated on the terminal status and new output in the future.`,
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Migrates the disableBrowserTool setting from VSCode configuration to browserSettings
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,289 @@
|
||||
import { setTimeout as setTimeoutPromise } from "node:timers/promises"
|
||||
|
||||
import { formatResponse } from "@core/prompts/responses"
|
||||
import { processFilesIntoText } from "@integrations/misc/extract-text"
|
||||
import { TerminalManager } from "@integrations/terminal/TerminalManager"
|
||||
import { Logger } from "@services/logging/Logger"
|
||||
import { execa } from "execa"
|
||||
import { TerminalHangStage, TerminalUserInterventionAction, telemetryService } from "@/services/telemetry"
|
||||
import { isInTestMode } from "@/services/test/TestMode"
|
||||
|
||||
import type { AskFn, CommandRunnerResult, SayFn } from "../types"
|
||||
|
||||
export class CommandRunner {
|
||||
constructor(
|
||||
private readonly cwd: string,
|
||||
private readonly terminalManager: TerminalManager,
|
||||
private readonly ask: AskFn,
|
||||
private readonly say: SayFn,
|
||||
) {}
|
||||
|
||||
private async executeInNode(command: string): Promise<CommandRunnerResult> {
|
||||
try {
|
||||
const childProcess = execa(command, {
|
||||
shell: true,
|
||||
cwd: this.cwd,
|
||||
reject: false,
|
||||
all: true,
|
||||
})
|
||||
|
||||
let output = ""
|
||||
|
||||
if (childProcess.all) {
|
||||
childProcess.all.on("data", (data) => {
|
||||
output += data.toString()
|
||||
})
|
||||
}
|
||||
|
||||
const timeoutPromise = new Promise<never>((_, reject) => {
|
||||
setTimeout(() => {
|
||||
if (childProcess.pid) {
|
||||
childProcess.kill("SIGKILL")
|
||||
}
|
||||
reject(new Error("Command timeout after 30s"))
|
||||
}, 30000)
|
||||
})
|
||||
|
||||
const result = await Promise.race([childProcess, timeoutPromise]).catch((_error) => {
|
||||
Logger.info(`Command timed out after 30s: ${command}`)
|
||||
return {
|
||||
stdout: "",
|
||||
stderr: "",
|
||||
exitCode: 124,
|
||||
timedOut: true,
|
||||
}
|
||||
})
|
||||
|
||||
const wasTerminated = result.timedOut === true
|
||||
|
||||
if (!output) {
|
||||
output = result.stdout || result.stderr || ""
|
||||
}
|
||||
|
||||
Logger.info(`Command executed in Node: ${command}\nOutput:\n${output}`)
|
||||
|
||||
if (wasTerminated) {
|
||||
output += "\nCommand was taking a while to run so it was auto terminated after 30s"
|
||||
}
|
||||
|
||||
return [
|
||||
false,
|
||||
`Command executed${wasTerminated ? " (terminated after 30s)" : ""} with exit code ${
|
||||
result.exitCode
|
||||
}.${output.length > 0 ? `\nOutput:\n${output}` : ""}`,
|
||||
]
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error)
|
||||
return [false, `Error executing command: ${errorMessage}`]
|
||||
}
|
||||
}
|
||||
|
||||
async execute(command: string, timeoutSeconds: number | undefined): Promise<CommandRunnerResult> {
|
||||
Logger.info("IS_TEST: " + isInTestMode())
|
||||
|
||||
if (isInTestMode()) {
|
||||
Logger.info("Executing command in Node: " + command)
|
||||
return this.executeInNode(command)
|
||||
}
|
||||
|
||||
Logger.info("Executing command in terminal: " + command)
|
||||
|
||||
const terminalInfo = await this.terminalManager.getOrCreateTerminal(this.cwd)
|
||||
terminalInfo.terminal.show()
|
||||
const terminalProcess = this.terminalManager.runCommand(terminalInfo, command)
|
||||
|
||||
let userFeedback: { text?: string; images?: string[]; files?: string[] } | undefined
|
||||
let didContinue = false
|
||||
|
||||
const CHUNK_LINE_COUNT = 20
|
||||
const CHUNK_BYTE_SIZE = 2048
|
||||
const CHUNK_DEBOUNCE_MS = 100
|
||||
|
||||
let outputBuffer: string[] = []
|
||||
let outputBufferSize = 0
|
||||
let chunkTimer: NodeJS.Timeout | null = null
|
||||
let chunkEnroute = false
|
||||
|
||||
let bufferStuckTimer: NodeJS.Timeout | null = null
|
||||
const BUFFER_STUCK_TIMEOUT_MS = 6000
|
||||
|
||||
const flushBuffer = async (force = false): Promise<void> => {
|
||||
if (chunkEnroute || outputBuffer.length === 0) {
|
||||
if (force && !chunkEnroute && outputBuffer.length > 0) {
|
||||
// fallthrough to flush
|
||||
} else {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
const chunk = outputBuffer.join("\n")
|
||||
outputBuffer = []
|
||||
outputBufferSize = 0
|
||||
chunkEnroute = true
|
||||
|
||||
bufferStuckTimer = setTimeout(() => {
|
||||
telemetryService.captureTerminalHang(TerminalHangStage.BUFFER_STUCK)
|
||||
bufferStuckTimer = null
|
||||
}, BUFFER_STUCK_TIMEOUT_MS)
|
||||
|
||||
try {
|
||||
const { response, text, images, files } = await this.ask("command_output", chunk)
|
||||
if (response === "yesButtonClicked") {
|
||||
telemetryService.captureTerminalUserIntervention(TerminalUserInterventionAction.PROCESS_WHILE_RUNNING)
|
||||
if (text || (images && images.length > 0) || (files && files.length > 0)) {
|
||||
userFeedback = { text, images, files }
|
||||
}
|
||||
} else {
|
||||
userFeedback = { text, images, files }
|
||||
}
|
||||
didContinue = true
|
||||
terminalProcess.continue()
|
||||
} catch {
|
||||
Logger.error("Error while asking for command output")
|
||||
} finally {
|
||||
if (bufferStuckTimer) {
|
||||
clearTimeout(bufferStuckTimer)
|
||||
bufferStuckTimer = null
|
||||
}
|
||||
chunkEnroute = false
|
||||
if (outputBuffer.length > 0) {
|
||||
await flushBuffer()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const scheduleFlush = () => {
|
||||
if (chunkTimer) {
|
||||
clearTimeout(chunkTimer)
|
||||
}
|
||||
chunkTimer = setTimeout(async () => await flushBuffer(), CHUNK_DEBOUNCE_MS)
|
||||
}
|
||||
|
||||
const outputLines: string[] = []
|
||||
terminalProcess.on("line", async (line) => {
|
||||
outputLines.push(line)
|
||||
|
||||
if (!didContinue) {
|
||||
outputBuffer.push(line)
|
||||
outputBufferSize += Buffer.byteLength(line, "utf8")
|
||||
if (outputBuffer.length >= CHUNK_LINE_COUNT || outputBufferSize >= CHUNK_BYTE_SIZE) {
|
||||
await flushBuffer()
|
||||
} else {
|
||||
scheduleFlush()
|
||||
}
|
||||
} else {
|
||||
void this.say("command_output", line)
|
||||
}
|
||||
})
|
||||
|
||||
let completed = false
|
||||
let completionTimer: NodeJS.Timeout | null = null
|
||||
const COMPLETION_TIMEOUT_MS = 6000
|
||||
|
||||
completionTimer = setTimeout(() => {
|
||||
if (!completed) {
|
||||
telemetryService.captureTerminalHang(TerminalHangStage.WAITING_FOR_COMPLETION)
|
||||
completionTimer = null
|
||||
}
|
||||
}, COMPLETION_TIMEOUT_MS)
|
||||
|
||||
terminalProcess.once("completed", async () => {
|
||||
completed = true
|
||||
if (completionTimer) {
|
||||
clearTimeout(completionTimer)
|
||||
completionTimer = null
|
||||
}
|
||||
if (!didContinue && outputBuffer.length > 0) {
|
||||
if (chunkTimer) {
|
||||
clearTimeout(chunkTimer)
|
||||
chunkTimer = null
|
||||
}
|
||||
await flushBuffer(true)
|
||||
}
|
||||
})
|
||||
|
||||
terminalProcess.once("no_shell_integration", async () => {
|
||||
await this.say("shell_integration_warning")
|
||||
})
|
||||
|
||||
if (timeoutSeconds) {
|
||||
const timeoutPromise = new Promise<never>((_, reject) => {
|
||||
setTimeout(() => {
|
||||
reject(new Error("COMMAND_TIMEOUT"))
|
||||
}, timeoutSeconds * 1000)
|
||||
})
|
||||
|
||||
try {
|
||||
await Promise.race([terminalProcess, timeoutPromise])
|
||||
} catch (error) {
|
||||
didContinue = true
|
||||
terminalProcess.continue()
|
||||
|
||||
if (chunkTimer) {
|
||||
clearTimeout(chunkTimer)
|
||||
chunkTimer = null
|
||||
}
|
||||
if (completionTimer) {
|
||||
clearTimeout(completionTimer)
|
||||
completionTimer = null
|
||||
}
|
||||
|
||||
await setTimeoutPromise(50)
|
||||
const result = this.terminalManager.processOutput(outputLines)
|
||||
|
||||
if (error instanceof Error && error.message === "COMMAND_TIMEOUT") {
|
||||
return [
|
||||
false,
|
||||
`Command execution timed out after ${timeoutSeconds} seconds. The command may still be running in the terminal.${
|
||||
result.length > 0 ? `\nOutput so far:\n${result}` : ""
|
||||
}`,
|
||||
]
|
||||
}
|
||||
|
||||
throw error
|
||||
}
|
||||
} else {
|
||||
await terminalProcess
|
||||
}
|
||||
|
||||
if (completionTimer) {
|
||||
clearTimeout(completionTimer)
|
||||
completionTimer = null
|
||||
}
|
||||
|
||||
await setTimeoutPromise(50)
|
||||
|
||||
const result = this.terminalManager.processOutput(outputLines)
|
||||
|
||||
if (userFeedback) {
|
||||
await this.say("user_feedback", userFeedback.text, userFeedback.images, userFeedback.files)
|
||||
|
||||
let fileContentString = ""
|
||||
if (userFeedback.files && userFeedback.files.length > 0) {
|
||||
fileContentString = await processFilesIntoText(userFeedback.files)
|
||||
}
|
||||
|
||||
return [
|
||||
true,
|
||||
formatResponse.toolResult(
|
||||
`Command is still running in the user's terminal.${
|
||||
result.length > 0 ? `\nHere's the output so far:\n${result}` : ""
|
||||
}\n\nThe user provided the following feedback:\n<feedback>\n${userFeedback.text}\n</feedback>`,
|
||||
userFeedback.images,
|
||||
fileContentString,
|
||||
),
|
||||
]
|
||||
}
|
||||
|
||||
if (completed) {
|
||||
return [false, `Command executed.${result.length > 0 ? `\nOutput:\n${result}` : ""}`]
|
||||
}
|
||||
|
||||
return [
|
||||
false,
|
||||
`Command is still running in the user's terminal.${
|
||||
result.length > 0 ? `\nHere's the output so far:\n${result}` : ""
|
||||
}\n\nYou will be updated on the terminal status and new output in the future.`,
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import { Anthropic } from "@anthropic-ai/sdk"
|
||||
import { ClineAsk, ClineSay } from "@shared/ExtensionMessage"
|
||||
import { ClineAskResponse } from "@shared/WebviewMessage"
|
||||
|
||||
export type ToolResponse = string | Array<Anthropic.TextBlockParam | Anthropic.ImageBlockParam>
|
||||
|
||||
export type CommandRunnerResult = [boolean, ToolResponse]
|
||||
|
||||
export type AskFn = (
|
||||
type: ClineAsk,
|
||||
text?: string,
|
||||
partial?: boolean,
|
||||
) => Promise<{
|
||||
response: ClineAskResponse
|
||||
text?: string
|
||||
images?: string[]
|
||||
files?: string[]
|
||||
}>
|
||||
|
||||
export type SayFn = (
|
||||
type: ClineSay,
|
||||
text?: string,
|
||||
images?: string[],
|
||||
files?: string[],
|
||||
partial?: boolean,
|
||||
) => Promise<number | undefined>
|
||||
Reference in New Issue
Block a user