Skip to content

Commit b188693

Browse files
committed
More progress
1 parent 33bf54e commit b188693

File tree

6 files changed

+78
-78
lines changed

6 files changed

+78
-78
lines changed

src/core/tools/executeCommandTool.ts

Lines changed: 51 additions & 70 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" = "vscode",
76+
terminalProvider: "vscode" | "execa" = "execa",
7777
): Promise<[boolean, ToolResponse]> {
7878
let workingDir: string
7979

@@ -98,69 +98,50 @@ export async function executeCommand(
9898
// workingDir = terminalInfo.getCurrentWorkingDirectory()
9999
const workingDirInfo = workingDir ? ` from '${workingDir.toPosix()}'` : ""
100100

101-
let userFeedback: { text?: string; images?: string[] } | undefined
102-
let runInBackground: boolean | undefined = undefined
101+
let message: { text?: string; images?: string[] } | undefined
102+
let runInBackground = false
103103
let completed = false
104104
let result: string = ""
105105
let exitDetails: ExitCodeDetails | undefined
106106
const { terminalOutputLineLimit = 500 } = (await cline.providerRef.deref()?.getState()) ?? {}
107107

108-
const debounceLineLimit = 100 // Flush after this many lines.
109-
const debounceTimeoutMs = 200 // Flush after this much time inactivity (ms).
110-
let buffer: string[] = []
111-
let debounceTimer: NodeJS.Timeout | null = null
112-
113-
async function flush(process?: RooTerminalProcess) {
114-
if (debounceTimer) {
115-
clearTimeout(debounceTimer)
116-
debounceTimer = null
117-
}
118-
119-
if (buffer.length === 0) {
120-
return
121-
}
122-
123-
const output = buffer.join("\n")
124-
buffer = []
125-
126-
result = Terminal.compressTerminalOutput(result + output, terminalOutputLineLimit)
127-
const compressed = Terminal.compressTerminalOutput(output, terminalOutputLineLimit)
128-
cline.say("command_output", compressed)
129-
130-
if (typeof runInBackground !== "undefined") {
131-
return
132-
}
133-
134-
console.log(`ask command_output: waiting for response`)
135-
const { response, text, images } = await cline.ask("command_output", compressed)
136-
console.log(`ask command_output =>`, response)
137-
138-
if (response === "yesButtonClicked") {
139-
runInBackground = false
140-
} else {
141-
runInBackground = true
142-
userFeedback = { text, images }
143-
}
144-
145-
process?.continue()
146-
}
147-
148108
const callbacks = {
149-
onLine: async (line: string, process: RooTerminalProcess) => {
150-
buffer.push(line)
151-
152-
if (buffer.length >= debounceLineLimit) {
153-
await flush(process)
154-
} else {
155-
if (debounceTimer) {
156-
clearTimeout(debounceTimer)
157-
}
109+
onLine: async (output: string, process: RooTerminalProcess) => {
110+
const compressed = Terminal.compressTerminalOutput(output, terminalOutputLineLimit)
111+
cline.say("command_output", compressed)
158112

159-
debounceTimer = setTimeout(() => flush(process), debounceTimeoutMs)
113+
if (runInBackground) {
114+
return
160115
}
116+
117+
try {
118+
const { response, text, images } = await cline.ask("command_output", compressed)
119+
console.log(`ask command_output =>`, response)
120+
runInBackground = true
121+
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.
131+
message = { text, images }
132+
process.continue()
133+
}
134+
} catch (_error) {}
135+
},
136+
onCompleted: (output: string | undefined) => {
137+
console.log(`onCompleted =>`, output)
138+
result = Terminal.compressTerminalOutput(output ?? "", terminalOutputLineLimit)
139+
completed = true
140+
},
141+
onShellExecutionComplete: (details: ExitCodeDetails) => {
142+
console.log(`onShellExecutionComplete =>`, details)
143+
exitDetails = details
161144
},
162-
onCompleted: () => (completed = true),
163-
onShellExecutionComplete: (details: ExitCodeDetails) => (exitDetails = details),
164145
onNoShellIntegration: async (message: string) => {
165146
telemetryService.captureShellIntegrationError(cline.taskId)
166147
await cline.say("shell_integration_warning", message)
@@ -178,39 +159,32 @@ export async function executeCommand(
178159

179160
await terminal.runCommand(command, callbacks)
180161

181-
if (debounceTimer) {
182-
clearTimeout(debounceTimer)
183-
debounceTimer = null
184-
}
185-
186-
// If there are any lines in the buffer, flush them to `result`.
187-
await flush()
188-
189162
// Wait for a short delay to ensure all messages are sent to the webview.
190163
// This delay allows time for non-awaited promises to be created and
191164
// for their associated messages to be sent to the webview, maintaining
192165
// the correct order of messages (although the webview is smart about
193166
// grouping command_output messages despite any gaps anyways).
194167
await delay(50)
195168

196-
if (userFeedback) {
197-
await cline.say("user_feedback", userFeedback.text, userFeedback.images)
169+
if (message) {
170+
const { text, images } = message
171+
await cline.say("user_feedback", text, images)
198172

199173
return [
200174
true,
201175
formatResponse.toolResult(
202176
`Command is still running in terminal ${workingDirInfo}.${
203177
result.length > 0 ? `\nHere's the output so far:\n${result}` : ""
204-
}\n\nThe user provided the following feedback:\n<feedback>\n${userFeedback.text}\n</feedback>`,
205-
userFeedback.images,
178+
}\n\nThe user provided the following feedback:\n<feedback>\n${text}\n</feedback>`,
179+
images,
206180
),
207181
]
208-
} else if (completed) {
182+
} else if (completed || exitDetails) {
209183
let exitStatus: string = ""
210184

211185
if (exitDetails !== undefined) {
212-
if (exitDetails.signal) {
213-
exitStatus = `Process terminated by signal ${exitDetails.signal} (${exitDetails.signalName})`
186+
if (exitDetails.signalName) {
187+
exitStatus = `Process terminated by signal ${exitDetails.signalName}`
214188

215189
if (exitDetails.coreDumpPossible) {
216190
exitStatus += " - core dump possible"
@@ -238,8 +212,15 @@ export async function executeCommand(
238212
// }
239213

240214
const outputInfo = `\nOutput:\n${result}`
215+
console.log(`Command executed in terminal ${workingDirInfo}. ${exitStatus}${outputInfo}`)
241216
return [false, `Command executed in terminal ${workingDirInfo}. ${exitStatus}${outputInfo}`]
242217
} else {
218+
console.log(
219+
`Command is still running in terminal ${workingDirInfo}.${
220+
result.length > 0 ? `\nHere's the output so far:\n${result}` : ""
221+
}\n\nYou will be updated on the terminal status and new output in the future.`,
222+
)
223+
243224
return [
244225
false,
245226
`Command is still running in terminal ${workingDirInfo}.${

src/integrations/terminal/ExecaTerminalProcess.ts

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,13 +32,15 @@ export class ExecaTerminalProcess extends EventEmitter<RooTerminalProcessEvents>
3232
cancelSignal: this.controller.signal,
3333
})`${command}`
3434

35+
this.emit("line", "")
36+
3537
for await (const line of stream) {
3638
this.fullOutput += line
3739
this.fullOutput += "\n"
3840

3941
const now = Date.now()
4042

41-
if (this.isListening && (now - this.lastEmitTime_ms > 100 || this.lastEmitTime_ms === 0)) {
43+
if (this.isListening && (now - this.lastEmitTime_ms > 250 || this.lastEmitTime_ms === 0)) {
4244
this.emitRemainingBufferIfListening()
4345
this.lastEmitTime_ms = now
4446
}
@@ -51,7 +53,10 @@ export class ExecaTerminalProcess extends EventEmitter<RooTerminalProcessEvents>
5153
} catch (error) {
5254
if (error instanceof ExecaError) {
5355
console.error(`[ExecaTerminalProcess] shell execution error: ${error.message}`)
54-
this.emit("shell_execution_complete", { exitCode: error.exitCode, signalName: error.signal })
56+
this.emit("shell_execution_complete", {
57+
exitCode: error.exitCode ?? 1,
58+
signalName: error.signal,
59+
})
5560
} else {
5661
this.emit("shell_execution_complete", { exitCode: 1 })
5762
}
@@ -67,6 +72,10 @@ export class ExecaTerminalProcess extends EventEmitter<RooTerminalProcessEvents>
6772
this.emit("continue")
6873
}
6974

75+
public abort() {
76+
this.controller?.abort()
77+
}
78+
7079
public hasUnretrievedOutput() {
7180
return this.lastRetrievedIndex < this.fullOutput.length
7281
}
@@ -78,7 +87,7 @@ export class ExecaTerminalProcess extends EventEmitter<RooTerminalProcessEvents>
7887
if (this.isStreamClosed) {
7988
endIndex = outputToProcess.length
8089
} else {
81-
let endIndex = outputToProcess.lastIndexOf("\n")
90+
endIndex = outputToProcess.lastIndexOf("\n")
8291

8392
if (endIndex === -1) {
8493
return ""

src/integrations/terminal/TerminalProcess.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -500,6 +500,10 @@ export class TerminalProcess extends EventEmitter<RooTerminalProcessEvents> {
500500
this.emit("continue")
501501
}
502502

503+
public abort() {
504+
// TODO
505+
}
506+
503507
/**
504508
* Checks if this process has unretrieved output
505509
* @returns true if there is output that hasn't been fully retrieved yet

src/integrations/terminal/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ export interface RooTerminalCallbacks {
1212

1313
export interface RooTerminalProcess {
1414
continue: () => void
15+
abort: () => void
1516
}
1617

1718
export interface RooTerminalProcessEvents {

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

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -207,7 +207,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
207207
setClineAsk("command_output")
208208
setEnableButtons(true)
209209
setPrimaryButtonText(t("chat:proceedWhileRunning.title"))
210-
setSecondaryButtonText(undefined)
210+
setSecondaryButtonText(t("chat:killCommand.title"))
211211
break
212212
case "use_mcp_server":
213213
setTextAreaDisabled(isPartial)
@@ -443,6 +443,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
443443
const handleSecondaryButtonClick = useCallback(
444444
(text?: string, images?: string[]) => {
445445
const trimmedInput = text?.trim()
446+
446447
if (isStreaming) {
447448
vscode.postMessage({ type: "cancelTask" })
448449
setDidClickCancel(true)
@@ -469,15 +470,15 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
469470
})
470471
} else {
471472
// responds to the API with a "This operation failed" and lets it try again
472-
vscode.postMessage({
473-
type: "askResponse",
474-
askResponse: "noButtonClicked",
475-
})
473+
vscode.postMessage({ type: "askResponse", askResponse: "noButtonClicked" })
476474
}
477475
// Clear input state after sending
478476
setInputValue("")
479477
setSelectedImages([])
480478
break
479+
case "command_output":
480+
vscode.postMessage({ type: "askResponse", askResponse: "noButtonClicked" })
481+
break
481482
}
482483
setTextAreaDisabled(true)
483484
setClineAsk(undefined)

webview-ui/src/i18n/locales/en/chat.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,10 @@
5252
"title": "Proceed While Running",
5353
"tooltip": "Continue despite warnings"
5454
},
55+
"killCommand": {
56+
"title": "Kill Command",
57+
"tooltip": "Kill the current command"
58+
},
5559
"resumeTask": {
5660
"title": "Resume Task",
5761
"tooltip": "Continue the current task"

0 commit comments

Comments
 (0)