Skip to content

Commit afd0ca2

Browse files
committed
More cleanup
1 parent 502e16d commit afd0ca2

File tree

6 files changed

+312
-341
lines changed

6 files changed

+312
-341
lines changed

src/core/Cline.ts

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1986,6 +1986,7 @@ export class Cline extends EventEmitter<ClineEvents> {
19861986

19871987
// It could be useful for cline to know if the user went from one or no file to another between messages, so we always include this context
19881988
details += "\n\n# VSCode Visible Files"
1989+
19891990
const visibleFilePaths = vscode.window.visibleTextEditors
19901991
?.map((editor) => editor.document?.uri?.fsPath)
19911992
.filter(Boolean)
@@ -2039,28 +2040,31 @@ export class Cline extends EventEmitter<ClineEvents> {
20392040
}
20402041

20412042
if (busyTerminals.length > 0) {
2042-
// wait for terminals to cool down
2043+
// Wait for terminals to cool down.
20432044
await pWaitFor(() => busyTerminals.every((t) => !TerminalRegistry.isProcessHot(t.id)), {
20442045
interval: 100,
20452046
timeout: 15_000,
20462047
}).catch(() => {})
20472048
}
20482049

2049-
this.didEditFile = false // reset, this lets us know when to wait for saved files to update terminals
2050+
// Reset, this lets us know when to wait for saved files to update terminals.
2051+
this.didEditFile = false
20502052

2051-
// waiting for updated diagnostics lets terminal output be the most up-to-date possible
2053+
// Waiting for updated diagnostics lets terminal output be the most
2054+
// up-to-date possible.
20522055
let terminalDetails = ""
2056+
20532057
if (busyTerminals.length > 0) {
2054-
// terminals are cool, let's retrieve their output
2058+
// Terminals are cool, let's retrieve their output.
20552059
terminalDetails += "\n\n# Actively Running Terminals"
2060+
20562061
for (const busyTerminal of busyTerminals) {
20572062
terminalDetails += `\n## Original command: \`${busyTerminal.getLastCommand()}\``
20582063
let newOutput = TerminalRegistry.getUnretrievedOutput(busyTerminal.id)
2064+
20592065
if (newOutput) {
20602066
newOutput = Terminal.compressTerminalOutput(newOutput, terminalOutputLineLimit)
20612067
terminalDetails += `\n### New Output\n${newOutput}`
2062-
} else {
2063-
// details += `\n(Still running, no new output)` // don't want to show this right after running the command
20642068
}
20652069
}
20662070
}

src/integrations/terminal/BaseTerminalProcess.ts

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@ import type { RooTerminalProcess, RooTerminalProcessEvents, ExitCodeDetails } fr
44

55
export abstract class BaseTerminalProcess extends EventEmitter<RooTerminalProcessEvents> implements RooTerminalProcess {
66
public command: string = ""
7+
78
public isHot: boolean = false
9+
protected hotTimer: NodeJS.Timeout | null = null
810

911
protected isListening: boolean = true
1012
protected lastEmitTime_ms: number = 0
@@ -134,4 +136,51 @@ export abstract class BaseTerminalProcess extends EventEmitter<RooTerminalProces
134136
* @returns The unretrieved output
135137
*/
136138
abstract getUnretrievedOutput(): string
139+
140+
protected startHotTimer(data: string) {
141+
this.isHot = true
142+
143+
if (this.hotTimer) {
144+
clearTimeout(this.hotTimer)
145+
}
146+
147+
this.hotTimer = setTimeout(() => (this.isHot = false), BaseTerminalProcess.isCompiling(data) ? 15_000 : 2_000)
148+
}
149+
150+
protected stopHotTimer() {
151+
if (this.hotTimer) {
152+
clearTimeout(this.hotTimer)
153+
}
154+
155+
this.isHot = false
156+
}
157+
158+
// These markers indicate the command is some kind of local dev
159+
// server recompiling the app, which we want to wait for output
160+
// of before sending request to Roo Code.
161+
private static compilingMarkers = ["compiling", "building", "bundling", "transpiling", "generating", "starting"]
162+
163+
private static compilingMarkerNullifiers = [
164+
"compiled",
165+
"success",
166+
"finish",
167+
"complete",
168+
"succeed",
169+
"done",
170+
"end",
171+
"stop",
172+
"exit",
173+
"terminate",
174+
"error",
175+
"fail",
176+
]
177+
178+
private static isCompiling(data: string): boolean {
179+
return (
180+
BaseTerminalProcess.compilingMarkers.some((marker) => data.toLowerCase().includes(marker.toLowerCase())) &&
181+
!BaseTerminalProcess.compilingMarkerNullifiers.some((nullifier) =>
182+
data.toLowerCase().includes(nullifier.toLowerCase()),
183+
)
184+
)
185+
}
137186
}

src/integrations/terminal/ExecaTerminalProcess.ts

Lines changed: 12 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import { BaseTerminalProcess } from "./BaseTerminalProcess"
66
export class ExecaTerminalProcess extends BaseTerminalProcess {
77
private terminalRef: WeakRef<RooTerminal>
88
private controller?: AbortController
9-
private isStreamClosed: boolean = false
109

1110
constructor(terminal: RooTerminal) {
1211
super()
@@ -29,6 +28,8 @@ export class ExecaTerminalProcess extends BaseTerminalProcess {
2928
this.controller = new AbortController()
3029

3130
try {
31+
this.isHot = true
32+
3233
const subprocess = execa({
3334
shell: true,
3435
cwd: this.terminal.getCurrentWorkingDirectory(),
@@ -38,19 +39,17 @@ export class ExecaTerminalProcess extends BaseTerminalProcess {
3839
this.emit("line", "")
3940

4041
for await (const line of subprocess) {
41-
this.fullOutput += line
42-
this.fullOutput += "\n"
42+
this.fullOutput += `${line}\n`
4343

4444
const now = Date.now()
4545

4646
if (this.isListening && (now - this.lastEmitTime_ms > 250 || this.lastEmitTime_ms === 0)) {
4747
this.emitRemainingBufferIfListening()
4848
this.lastEmitTime_ms = now
4949
}
50-
}
5150

52-
this.isStreamClosed = true
53-
this.emitRemainingBufferIfListening()
51+
this.startHotTimer(line)
52+
}
5453

5554
this.emit("shell_execution_complete", { exitCode: 0 })
5655
} catch (error) {
@@ -65,6 +64,8 @@ export class ExecaTerminalProcess extends BaseTerminalProcess {
6564
}
6665
}
6766

67+
this.emitRemainingBufferIfListening()
68+
this.stopHotTimer()
6869
this.emit("completed", this.fullOutput)
6970
this.emit("continue")
7071
}
@@ -85,20 +86,14 @@ export class ExecaTerminalProcess extends BaseTerminalProcess {
8586

8687
public override getUnretrievedOutput() {
8788
let outputToProcess = this.fullOutput.slice(this.lastRetrievedIndex)
88-
let endIndex = -1
89-
90-
if (this.isStreamClosed) {
91-
endIndex = outputToProcess.length
92-
} else {
93-
endIndex = outputToProcess.lastIndexOf("\n")
89+
let endIndex = outputToProcess.lastIndexOf("\n")
9490

95-
if (endIndex === -1) {
96-
return ""
97-
}
98-
99-
endIndex++
91+
if (endIndex === -1) {
92+
return ""
10093
}
10194

95+
endIndex++
96+
10297
this.lastRetrievedIndex += endIndex
10398
return outputToProcess.slice(0, endIndex)
10499
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
NOTICE TO DEVELOPERS:
2+
3+
The Terminal classes are very sensitive to change, partially because of
4+
the complicated way that shell integration works with VSCE, and
5+
partially because of the way that Cline interacts with the Terminal\*
6+
class abstractions that make VSCE shell integration easier to work with.
7+
8+
At the point that PR #1365 is merged, it is unlikely that any Terminal\*
9+
classes will need to be modified substantially. Generally speaking, we
10+
should think of this as a stable interface and minimize changes.
11+
12+
`TerminalProcess` class is particularly critical because it
13+
provides all input handling and event notifications related to terminal
14+
output to send it to the rest of the program. User interfaces for working
15+
with data from terminals should only be as follows:
16+
17+
1. By listening to the events:
18+
19+
- this.on("completed", fullOutput) - provides full output upon completion
20+
- this.on("line") - provides new lines, probably more than one
21+
22+
2. By calling `this.getUnretrievedOutput()`
23+
24+
This implementation intentionally returns all terminal output to the user
25+
interfaces listed above. Any throttling or other stream modification _must_
26+
be implemented outside of this class.
27+
28+
All other interfaces are private.
29+
30+
Warning: Modifying the `TerminalProcess` class without fully understanding VSCE shell integration architecture may affect the reliability or performance of reading terminal output.
31+
32+
`TerminalProcess` was carefully designed for performance and accuracy:
33+
34+
Performance is obtained by: - Throttling event output on 100ms intervals - Using only indexes to access the output array - Maintaining a zero-copy implementation with a fullOutput string for storage - The fullOutput array is never split on carriage returns
35+
as this was found to be very slow - Allowing multi-line chunks - Minimizing regular expression calls, as they have been tested to be
36+
500x slower than the use of string parsing functions for large outputs
37+
in this implementation
38+
39+
Accuracy is obtained by: - Using only indexes against fullOutput - Paying close attention to off-by-one errors when indexing any content - Always returning exactly the content that was printed by the terminal,
40+
including all carriage returns which may (or may not) have been in the
41+
input stream
42+
43+
Additional resources:
44+
45+
- This implementation was rigorously tested using:
46+
47+
- https://github.com/KJ7LNW/vsce-test-terminal-integration
48+
49+
- There was a serious upstream bug that may not be fully solved,
50+
or that may resurface in future VSCE releases, simply due to
51+
the complexity of reliably handling terminal-provided escape
52+
sequences across multiple shell implementations. This implementation
53+
attempts to work around the problems and provide backwards
54+
compatibility for VSCE releases that may not have the fix in
55+
upstream bug #237208, but there still may be some unhandled
56+
corner cases. See this ticket for more detail:
57+
58+
- https://github.com/microsoft/vscode/issues/237208
59+
60+
- The original Cline PR has quite a bit of information:
61+
- https://github.com/cline/cline/pull/1089
62+
63+
Contact me if you have any questions: - GitHub: KJ7LNW - Discord: kj7lnw - [roo-cline at z.ewheeler.org]
64+
65+
Cheers,
66+
-Eric, KJ7LNW

0 commit comments

Comments
 (0)