Skip to content

Commit b038a09

Browse files
committed
Implement "Kill Command"
1 parent b188693 commit b038a09

File tree

7 files changed

+37
-44
lines changed

7 files changed

+37
-44
lines changed

src/core/Cline.ts

Lines changed: 12 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ import { CheckpointServiceOptions, RepoPerTaskCheckpointService } from "../servi
5252
// integrations
5353
import { DIFF_VIEW_URI_SCHEME, DiffViewProvider } from "../integrations/editor/DiffViewProvider"
5454
import { findToolName, formatContentBlockToMarkdown } from "../integrations/misc/export-markdown"
55+
import { RooTerminalProcess } from "../integrations/terminal/types"
5556
import { Terminal } from "../integrations/terminal/Terminal"
5657
import { TerminalRegistry } from "../integrations/terminal/TerminalRegistry"
5758

@@ -192,6 +193,9 @@ export class Cline extends EventEmitter<ClineEvents> {
192193
// metrics
193194
private toolUsage: ToolUsage = {}
194195

196+
// terminal
197+
public terminalProcess?: RooTerminalProcess
198+
195199
constructor({
196200
provider,
197201
apiConfiguration,
@@ -203,7 +207,6 @@ export class Cline extends EventEmitter<ClineEvents> {
203207
task,
204208
images,
205209
historyItem,
206-
experiments,
207210
startTask = true,
208211
rootTask,
209212
parentTask,
@@ -521,6 +524,14 @@ export class Cline extends EventEmitter<ClineEvents> {
521524
this.askResponseImages = images
522525
}
523526

527+
async handleTerminalOperation(terminalOperation: "continue" | "abort") {
528+
if (terminalOperation === "continue") {
529+
this.terminalProcess?.continue()
530+
} else if (terminalOperation === "abort") {
531+
this.terminalProcess?.abort()
532+
}
533+
}
534+
524535
async say(
525536
type: ClineSay,
526537
text?: string,
@@ -2055,23 +2066,6 @@ export class Cline extends EventEmitter<ClineEvents> {
20552066
}).catch(() => {})
20562067
}
20572068

2058-
// we want to get diagnostics AFTER terminal cools down for a few reasons: terminal could be scaffolding a project, dev servers (compilers like webpack) will first re-compile and then send diagnostics, etc
2059-
/*
2060-
let diagnosticsDetails = ""
2061-
const diagnostics = await this.diagnosticsMonitor.getCurrentDiagnostics(this.didEditFile || terminalWasBusy) // if cline ran a command (ie npm install) or edited the workspace then wait a bit for updated diagnostics
2062-
for (const [uri, fileDiagnostics] of diagnostics) {
2063-
const problems = fileDiagnostics.filter((d) => d.severity === vscode.DiagnosticSeverity.Error)
2064-
if (problems.length > 0) {
2065-
diagnosticsDetails += `\n## ${path.relative(this.cwd, uri.fsPath)}`
2066-
for (const diagnostic of problems) {
2067-
// let severity = diagnostic.severity === vscode.DiagnosticSeverity.Error ? "Error" : "Warning"
2068-
const line = diagnostic.range.start.line + 1 // VSCode lines are 0-indexed
2069-
const source = diagnostic.source ? `[${diagnostic.source}] ` : ""
2070-
diagnosticsDetails += `\n- ${source}Line ${line}: ${diagnostic.message}`
2071-
}
2072-
}
2073-
}
2074-
*/
20752069
this.didEditFile = false // reset, this lets us know when to wait for saved files to update terminals
20762070

20772071
// waiting for updated diagnostics lets terminal output be the most up-to-date possible

src/core/tools/executeCommandTool.ts

Lines changed: 6 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ export async function executeCommand(
7373
cline: Cline,
7474
command: string,
7575
customCwd?: string,
76-
terminalProvider: "vscode" | "execa" = "execa",
76+
terminalProvider: "vscode" | "execa" = "vscode",
7777
): Promise<[boolean, ToolResponse]> {
7878
let workingDir: string
7979

@@ -116,30 +116,19 @@ export async function executeCommand(
116116

117117
try {
118118
const { response, text, images } = await cline.ask("command_output", compressed)
119-
console.log(`ask command_output =>`, response)
120119
runInBackground = true
121120

122-
if (response === "yesButtonClicked") {
123-
// Continue running the command in the background.
124-
process.continue()
125-
} else if (response === "noButtonClicked") {
126-
// Abort the command with a SIGINT.
127-
process.abort()
128-
} else {
129-
// Continue running the command in the background, but inject
130-
// the message into the context.
121+
if (response === "messageResponse") {
131122
message = { text, images }
132123
process.continue()
133124
}
134125
} catch (_error) {}
135126
},
136127
onCompleted: (output: string | undefined) => {
137-
console.log(`onCompleted =>`, output)
138128
result = Terminal.compressTerminalOutput(output ?? "", terminalOutputLineLimit)
139129
completed = true
140130
},
141131
onShellExecutionComplete: (details: ExitCodeDetails) => {
142-
console.log(`onShellExecutionComplete =>`, details)
143132
exitDetails = details
144133
},
145134
onNoShellIntegration: async (message: string) => {
@@ -157,7 +146,10 @@ export async function executeCommand(
157146
terminal = new ExecaTerminal(workingDir)
158147
}
159148

160-
await terminal.runCommand(command, callbacks)
149+
const process = terminal.runCommand(command, callbacks)
150+
cline.terminalProcess = process
151+
await process
152+
cline.terminalProcess = undefined
161153

162154
// Wait for a short delay to ensure all messages are sent to the webview.
163155
// This delay allows time for non-awaited promises to be created and

src/core/webview/webviewMessageHandler.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import { GlobalFileNames } from "../../shared/globalFileNames"
1212

1313
import { checkoutDiffPayloadSchema, checkoutRestorePayloadSchema, WebviewMessage } from "../../shared/WebviewMessage"
1414
import { checkExistKey } from "../../shared/checkExistApiConfig"
15-
import { EXPERIMENT_IDS, experimentDefault, ExperimentId } from "../../shared/experiments"
15+
import { experimentDefault } from "../../shared/experiments"
1616
import { Terminal } from "../../integrations/terminal/Terminal"
1717
import { openFile, openImage } from "../../integrations/misc/open-file"
1818
import { selectImages } from "../../integrations/misc/process-images"
@@ -274,6 +274,9 @@ export const webviewMessageHandler = async (provider: ClineProvider, message: We
274274
case "askResponse":
275275
provider.getCurrentCline()?.handleWebviewAskResponse(message.askResponse!, message.text, message.images)
276276
break
277+
case "terminalOperation":
278+
provider.getCurrentCline()?.handleTerminalOperation(message.terminalOperation)
279+
break
277280
case "clearTask":
278281
// clear task resets the current session and allows for a new task to be started, if this session is a subtask - it allows the parent task to be resumed
279282
await provider.finishSubTask(t("common:tasks.canceled"))

src/integrations/terminal/ExecaTerminalProcess.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,15 +26,15 @@ export class ExecaTerminalProcess extends EventEmitter<RooTerminalProcessEvents>
2626
this.controller = new AbortController()
2727

2828
try {
29-
const stream = execa({
29+
const subprocess = execa({
3030
shell: true,
3131
cwd: this.terminalRef.deref()?.getCurrentWorkingDirectory(),
3232
cancelSignal: this.controller.signal,
3333
})`${command}`
3434

3535
this.emit("line", "")
3636

37-
for await (const line of stream) {
37+
for await (const line of subprocess) {
3838
this.fullOutput += line
3939
this.fullOutput += "\n"
4040

src/integrations/terminal/TerminalProcess.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -501,7 +501,10 @@ export class TerminalProcess extends EventEmitter<RooTerminalProcessEvents> {
501501
}
502502

503503
public abort() {
504-
// TODO
504+
if (this.isListening) {
505+
// Send SIGINT using CTRL+C
506+
this.terminalInfo.terminal.sendText("\x03")
507+
}
505508
}
506509

507510
/**

src/shared/WebviewMessage.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ export interface WebviewMessage {
3030
| "webviewDidLaunch"
3131
| "newTask"
3232
| "askResponse"
33+
| "terminalOperation"
3334
| "clearTask"
3435
| "didShowAnnouncement"
3536
| "selectImages"
@@ -151,6 +152,7 @@ export interface WebviewMessage {
151152
requestId?: string
152153
ids?: string[]
153154
hasSystemPromptOverride?: boolean
155+
terminalOperation?: "continue" | "abort"
154156
}
155157

156158
export const checkoutDiffPayloadSchema = z.object({

webview-ui/src/components/chat/ChatView.tsx

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -403,7 +403,6 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
403403
switch (clineAsk) {
404404
case "api_req_failed":
405405
case "command":
406-
case "command_output":
407406
case "tool":
408407
case "browser_action_launch":
409408
case "use_mcp_server":
@@ -418,20 +417,20 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
418417
images: images,
419418
})
420419
} else {
421-
vscode.postMessage({
422-
type: "askResponse",
423-
askResponse: "yesButtonClicked",
424-
})
420+
vscode.postMessage({ type: "askResponse", askResponse: "yesButtonClicked" })
425421
}
426422
// Clear input state after sending
427423
setInputValue("")
428424
setSelectedImages([])
429425
break
430426
case "completion_result":
431427
case "resume_completed_task":
432-
// extension waiting for feedback. but we can just present a new task button
428+
// Waiting for feedback, but we can just present a new task button
433429
startNewTask()
434430
break
431+
case "command_output":
432+
vscode.postMessage({ type: "terminalOperation", terminalOperation: "continue" })
433+
break
435434
}
436435
setTextAreaDisabled(true)
437436
setClineAsk(undefined)
@@ -469,15 +468,15 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
469468
images: images,
470469
})
471470
} else {
472-
// responds to the API with a "This operation failed" and lets it try again
471+
// Responds to the API with a "This operation failed" and lets it try again
473472
vscode.postMessage({ type: "askResponse", askResponse: "noButtonClicked" })
474473
}
475474
// Clear input state after sending
476475
setInputValue("")
477476
setSelectedImages([])
478477
break
479478
case "command_output":
480-
vscode.postMessage({ type: "askResponse", askResponse: "noButtonClicked" })
479+
vscode.postMessage({ type: "terminalOperation", terminalOperation: "abort" })
481480
break
482481
}
483482
setTextAreaDisabled(true)

0 commit comments

Comments
 (0)