Skip to content

Commit 21b883b

Browse files
authored
Merge pull request RooCodeInc#1236 from RooVetGit/cte/fix-terminal-output-parsing
Fix TerminalProcess output parsing
2 parents 390932e + 4b66ce8 commit 21b883b

File tree

7 files changed

+585
-251
lines changed

7 files changed

+585
-251
lines changed

src/core/Cline.ts

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ import {
2222
everyLineHasLineNumbers,
2323
truncateOutput,
2424
} from "../integrations/misc/extract-text"
25-
import { TerminalManager } from "../integrations/terminal/TerminalManager"
25+
import { TerminalManager, ExitCodeDetails } from "../integrations/terminal/TerminalManager"
2626
import { UrlContentFetcher } from "../services/browser/UrlContentFetcher"
2727
import { listFiles } from "../services/glob/list-files"
2828
import { regexSearchFiles } from "../services/ripgrep"
@@ -834,10 +834,21 @@ export class Cline {
834834
})
835835

836836
let completed = false
837-
process.once("completed", () => {
837+
let exitDetails: ExitCodeDetails | undefined
838+
process.once("completed", (output?: string) => {
839+
// Use provided output if available, otherwise keep existing result.
840+
if (output) {
841+
lines = output.split("\n")
842+
}
838843
completed = true
839844
})
840845

846+
process.once("shell_execution_complete", (id: number, details: ExitCodeDetails) => {
847+
if (id === terminalInfo.id) {
848+
exitDetails = details
849+
}
850+
})
851+
841852
process.once("no_shell_integration", async () => {
842853
await this.say("shell_integration_warning")
843854
})
@@ -869,7 +880,18 @@ export class Cline {
869880
}
870881

871882
if (completed) {
872-
return [false, `Command executed.${result.length > 0 ? `\nOutput:\n${result}` : ""}`]
883+
let exitStatus = "No exit code available"
884+
if (exitDetails !== undefined) {
885+
if (exitDetails.signal) {
886+
exitStatus = `Process terminated by signal ${exitDetails.signal} (${exitDetails.signalName})`
887+
if (exitDetails.coreDumpPossible) {
888+
exitStatus += " - core dump possible"
889+
}
890+
} else {
891+
exitStatus = `Exit code: ${exitDetails.exitCode}`
892+
}
893+
}
894+
return [false, `Command executed. ${exitStatus}${result.length > 0 ? `\nOutput:\n${result}` : ""}`]
873895
} else {
874896
return [
875897
false,

src/integrations/terminal/TerminalManager.ts

Lines changed: 169 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -70,43 +70,192 @@ Interestingly, some environments like Cursor enable these APIs even without the
7070
This approach allows us to leverage advanced features when available while ensuring broad compatibility.
7171
*/
7272
declare module "vscode" {
73+
// https://github.com/microsoft/vscode/blob/f0417069c62e20f3667506f4b7e53ca0004b4e3e/src/vscode-dts/vscode.d.ts#L7442
74+
// interface Terminal {
75+
// shellIntegration?: {
76+
// cwd?: vscode.Uri
77+
// executeCommand?: (command: string) => {
78+
// read: () => AsyncIterable<string>
79+
// }
80+
// }
81+
// }
7382
// https://github.com/microsoft/vscode/blob/f0417069c62e20f3667506f4b7e53ca0004b4e3e/src/vscode-dts/vscode.d.ts#L10794
7483
interface Window {
7584
onDidStartTerminalShellExecution?: (
7685
listener: (e: any) => any,
7786
thisArgs?: any,
7887
disposables?: vscode.Disposable[],
7988
) => vscode.Disposable
89+
onDidEndTerminalShellExecution?: (
90+
listener: (e: { terminal: vscode.Terminal; exitCode?: number; shellType?: string }) => any,
91+
thisArgs?: any,
92+
disposables?: vscode.Disposable[],
93+
) => vscode.Disposable
8094
}
8195
}
8296

83-
// Extend the Terminal type to include our custom properties
84-
type ExtendedTerminal = vscode.Terminal & {
85-
shellIntegration?: {
86-
cwd?: vscode.Uri
87-
executeCommand?: (command: string) => {
88-
read: () => AsyncIterable<string>
89-
}
90-
}
97+
export interface ExitCodeDetails {
98+
exitCode: number | undefined
99+
signal?: number | undefined
100+
signalName?: string
101+
coreDumpPossible?: boolean
91102
}
92103

93104
export class TerminalManager {
94105
private terminalIds: Set<number> = new Set()
95106
private processes: Map<number, TerminalProcess> = new Map()
96107
private disposables: vscode.Disposable[] = []
97108

109+
private interpretExitCode(exitCode: number | undefined): ExitCodeDetails {
110+
if (exitCode === undefined) {
111+
return { exitCode }
112+
}
113+
114+
if (exitCode <= 128) {
115+
return { exitCode }
116+
}
117+
118+
const signal = exitCode - 128
119+
const signals: Record<number, string> = {
120+
// Standard signals
121+
1: "SIGHUP",
122+
2: "SIGINT",
123+
3: "SIGQUIT",
124+
4: "SIGILL",
125+
5: "SIGTRAP",
126+
6: "SIGABRT",
127+
7: "SIGBUS",
128+
8: "SIGFPE",
129+
9: "SIGKILL",
130+
10: "SIGUSR1",
131+
11: "SIGSEGV",
132+
12: "SIGUSR2",
133+
13: "SIGPIPE",
134+
14: "SIGALRM",
135+
15: "SIGTERM",
136+
16: "SIGSTKFLT",
137+
17: "SIGCHLD",
138+
18: "SIGCONT",
139+
19: "SIGSTOP",
140+
20: "SIGTSTP",
141+
21: "SIGTTIN",
142+
22: "SIGTTOU",
143+
23: "SIGURG",
144+
24: "SIGXCPU",
145+
25: "SIGXFSZ",
146+
26: "SIGVTALRM",
147+
27: "SIGPROF",
148+
28: "SIGWINCH",
149+
29: "SIGIO",
150+
30: "SIGPWR",
151+
31: "SIGSYS",
152+
153+
// Real-time signals base
154+
34: "SIGRTMIN",
155+
156+
// SIGRTMIN+n signals
157+
35: "SIGRTMIN+1",
158+
36: "SIGRTMIN+2",
159+
37: "SIGRTMIN+3",
160+
38: "SIGRTMIN+4",
161+
39: "SIGRTMIN+5",
162+
40: "SIGRTMIN+6",
163+
41: "SIGRTMIN+7",
164+
42: "SIGRTMIN+8",
165+
43: "SIGRTMIN+9",
166+
44: "SIGRTMIN+10",
167+
45: "SIGRTMIN+11",
168+
46: "SIGRTMIN+12",
169+
47: "SIGRTMIN+13",
170+
48: "SIGRTMIN+14",
171+
49: "SIGRTMIN+15",
172+
173+
// SIGRTMAX-n signals
174+
50: "SIGRTMAX-14",
175+
51: "SIGRTMAX-13",
176+
52: "SIGRTMAX-12",
177+
53: "SIGRTMAX-11",
178+
54: "SIGRTMAX-10",
179+
55: "SIGRTMAX-9",
180+
56: "SIGRTMAX-8",
181+
57: "SIGRTMAX-7",
182+
58: "SIGRTMAX-6",
183+
59: "SIGRTMAX-5",
184+
60: "SIGRTMAX-4",
185+
61: "SIGRTMAX-3",
186+
62: "SIGRTMAX-2",
187+
63: "SIGRTMAX-1",
188+
64: "SIGRTMAX",
189+
}
190+
191+
// These signals may produce core dumps:
192+
// SIGQUIT, SIGILL, SIGABRT, SIGBUS, SIGFPE, SIGSEGV
193+
const coreDumpPossible = new Set([3, 4, 6, 7, 8, 11])
194+
195+
return {
196+
exitCode,
197+
signal,
198+
signalName: signals[signal] || `Unknown Signal (${signal})`,
199+
coreDumpPossible: coreDumpPossible.has(signal),
200+
}
201+
}
202+
98203
constructor() {
99-
let disposable: vscode.Disposable | undefined
204+
let startDisposable: vscode.Disposable | undefined
205+
let endDisposable: vscode.Disposable | undefined
100206
try {
101-
disposable = (vscode.window as vscode.Window).onDidStartTerminalShellExecution?.(async (e) => {
102-
// Creating a read stream here results in a more consistent output. This is most obvious when running the `date` command.
103-
e?.execution?.read()
207+
// onDidStartTerminalShellExecution
208+
startDisposable = (vscode.window as vscode.Window).onDidStartTerminalShellExecution?.(async (e) => {
209+
// Get a handle to the stream as early as possible:
210+
const stream = e?.execution.read()
211+
const terminalInfo = TerminalRegistry.getTerminalInfoByTerminal(e.terminal)
212+
if (stream && terminalInfo) {
213+
const process = this.processes.get(terminalInfo.id)
214+
if (process) {
215+
terminalInfo.stream = stream
216+
terminalInfo.running = true
217+
terminalInfo.streamClosed = false
218+
process.emit("stream_available", terminalInfo.id, stream)
219+
}
220+
} else {
221+
console.error("[TerminalManager] Stream failed, not registered for terminal")
222+
}
223+
224+
console.info("[TerminalManager] Shell execution started:", {
225+
hasExecution: !!e?.execution,
226+
command: e?.execution?.commandLine?.value,
227+
terminalId: terminalInfo?.id,
228+
})
229+
})
230+
231+
// onDidEndTerminalShellExecution
232+
endDisposable = (vscode.window as vscode.Window).onDidEndTerminalShellExecution?.(async (e) => {
233+
const exitDetails = this.interpretExitCode(e?.exitCode)
234+
console.info("[TerminalManager] Shell execution ended:", {
235+
...exitDetails,
236+
})
237+
238+
// Signal completion to any waiting processes
239+
for (const id of this.terminalIds) {
240+
const info = TerminalRegistry.getTerminal(id)
241+
if (info && info.terminal === e.terminal) {
242+
info.running = false
243+
const process = this.processes.get(id)
244+
if (process) {
245+
process.emit("shell_execution_complete", id, exitDetails)
246+
}
247+
break
248+
}
249+
}
104250
})
105251
} catch (error) {
106-
// console.error("Error setting up onDidEndTerminalShellExecution", error)
252+
console.error("[TerminalManager] Error setting up shell execution handlers:", error)
253+
}
254+
if (startDisposable) {
255+
this.disposables.push(startDisposable)
107256
}
108-
if (disposable) {
109-
this.disposables.push(disposable)
257+
if (endDisposable) {
258+
this.disposables.push(endDisposable)
110259
}
111260
}
112261

@@ -140,19 +289,16 @@ export class TerminalManager {
140289
})
141290

142291
// if shell integration is already active, run the command immediately
143-
const terminal = terminalInfo.terminal as ExtendedTerminal
144-
if (terminal.shellIntegration) {
292+
if (terminalInfo.terminal.shellIntegration) {
145293
process.waitForShellIntegration = false
146-
process.run(terminal, command)
294+
process.run(terminalInfo.terminal, command)
147295
} else {
148296
// docs recommend waiting 3s for shell integration to activate
149-
pWaitFor(() => (terminalInfo.terminal as ExtendedTerminal).shellIntegration !== undefined, {
150-
timeout: 4000,
151-
}).finally(() => {
297+
pWaitFor(() => terminalInfo.terminal.shellIntegration !== undefined, { timeout: 4000 }).finally(() => {
152298
const existingProcess = this.processes.get(terminalInfo.id)
153299
if (existingProcess && existingProcess.waitForShellIntegration) {
154300
existingProcess.waitForShellIntegration = false
155-
existingProcess.run(terminal, command)
301+
existingProcess.run(terminalInfo.terminal, command)
156302
}
157303
})
158304
}
@@ -168,8 +314,7 @@ export class TerminalManager {
168314
if (t.busy) {
169315
return false
170316
}
171-
const terminal = t.terminal as ExtendedTerminal
172-
const terminalCwd = terminal.shellIntegration?.cwd // one of cline's commands could have changed the cwd of the terminal
317+
const terminalCwd = t.terminal.shellIntegration?.cwd // one of cline's commands could have changed the cwd of the terminal
173318
if (!terminalCwd) {
174319
return false
175320
}

0 commit comments

Comments
 (0)