Skip to content

Commit 6301e90

Browse files
KJ7LNWEric Wheelercte
authored
Fix shell integration race condition (and other minor fixup/cleanup) (#1660)
* fix: clarify PowerShell command completion workaround The command completion detection approach in PowerShell requires an output string to allow duplicate commands to execute in some versions of code. Update the string to explicitly indicate it is a Roo PowerShell workaround, making it clear in terminal output that this is intentional behavior rather than a side effect. Signed-off-by: Eric Wheeler <[email protected]> * cleanup: improve terminal logging and error handling No functional changes - purely improves error handling and logging clarity. Terminal.ts: - Handle undefined process state in setActiveStream without throwing - Add terminal IDs to all log messages for better traceability - Improve error message clarity in shell integration timeout TerminalRegistry.ts: - Reorganize shell execution event handlers for better flow - Log shell execution events before processing for reliable debugging - Add detailed context to terminal not found scenarios - Include command and execution state in error messages Signed-off-by: Eric Wheeler <[email protected]> * feat: make terminal shell integration timeout configurable Users with long shell startup times were encountering "Shell Integration Unavailable" errors due to the hard-coded 4s timeout. The timeout is now configurable through Advanced Settings (1-60s). Thanks @FILTHY for troubleshooting and @kiwina for suggesting making the timeout configurable. Fixes #1654 Signed-off-by: Eric Wheeler <[email protected]> * critical fix: race condition that prevents command completion Terminal running state is now managed in TerminalRegistry instead of Terminal to prevent race between stream close and shell completion. While this race may not trigger on current VSCode versions, newer releases with additional terminal fixes may expose the issue. This proactively prevents "Shell execution end event received, but process is not running" errors. Signed-off-by: Eric Wheeler <[email protected]> * fix: improve command execution path reporting Enhance clarity of command execution context and error reporting: - Check to see if the directory changed because of the command - Clarify execution path message - Add explicit message when command exits with non-zero code Signed-off-by: Eric Wheeler <[email protected]> * system instructions: clarify terminal directory operations Clear guidance for the AI system on: - Working directory constraints - Path handling requirements - Tool vs terminal directory behavior Signed-off-by: Eric Wheeler <[email protected]> * test: update snapshots for system prompt working directory instructions Signed-off-by: Eric Wheeler <[email protected]> --------- Signed-off-by: Eric Wheeler <[email protected]> Co-authored-by: Eric Wheeler <[email protected]> Co-authored-by: Chris Estreich <[email protected]>
1 parent d6d6e35 commit 6301e90

File tree

14 files changed

+149
-50
lines changed

14 files changed

+149
-50
lines changed

src/core/Cline.ts

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1033,7 +1033,7 @@ export class Cline extends EventEmitter<ClineEvents> {
10331033
),
10341034
]
10351035
} else if (completed) {
1036-
let exitStatus: string
1036+
let exitStatus: string = ""
10371037
if (exitDetails !== undefined) {
10381038
if (exitDetails.signal) {
10391039
exitStatus = `Process terminated by signal ${exitDetails.signal} (${exitDetails.signalName})`
@@ -1044,13 +1044,22 @@ export class Cline extends EventEmitter<ClineEvents> {
10441044
result += "<VSCE exit code is undefined: terminal output and command execution status is unknown.>"
10451045
exitStatus = `Exit code: <undefined, notify user>`
10461046
} else {
1047-
exitStatus = `Exit code: ${exitDetails.exitCode}`
1047+
if (exitDetails.exitCode !== 0) {
1048+
exitStatus += "Command execution was not successful, inspect the cause and adjust as needed.\n"
1049+
}
1050+
exitStatus += `Exit code: ${exitDetails.exitCode}`
10481051
}
10491052
} else {
10501053
result += "<VSCE exitDetails == undefined: terminal output and command execution status is unknown.>"
10511054
exitStatus = `Exit code: <undefined, notify user>`
10521055
}
1053-
const workingDirInfo = workingDir ? ` from '${workingDir.toPosix()}'` : ""
1056+
1057+
let workingDirInfo: string = workingDir ? ` within working directory '${workingDir.toPosix()}'` : ""
1058+
const newWorkingDir = terminalInfo.getCurrentWorkingDirectory()
1059+
1060+
if (newWorkingDir !== workingDir) {
1061+
workingDirInfo += `; command changed working directory for this terminal to '${newWorkingDir.toPosix()} so be aware that future commands will be executed from this directory`
1062+
}
10541063

10551064
const outputInfo = `\nOutput:\n${result}`
10561065
return [

src/core/prompts/__tests__/__snapshots__/system.test.ts.snap

Lines changed: 30 additions & 15 deletions
Large diffs are not rendered by default.

src/core/prompts/sections/rules.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,8 @@ export function getRulesSection(
6464
6565
RULES
6666
67-
- Your current working directory is: ${cwd.toPosix()}
67+
- The project base directory is: ${cwd.toPosix()}
68+
- All all file paths must be relative to this directory. However, commands may change directories in terminals, so respect working directory specified by the response to <execute_command>.
6869
- You cannot \`cd\` into a different directory to complete a task. You are stuck operating from '${cwd.toPosix()}', so be sure to pass in the correct 'path' parameter when using tools that require a path.
6970
- Do not use the ~ character or $HOME to refer to the home directory.
7071
- Before using the execute_command tool, you must first think about the SYSTEM INFORMATION context provided to understand the user's environment and tailor your commands to ensure they are compatible with their system. You must also consider if the command you need to run should be executed in a specific directory outside of the current working directory '${cwd.toPosix()}', and if so prepend with \`cd\`'ing into that directory && then executing the command (as one command since you are stuck operating from '${cwd.toPosix()}'). For example, if you needed to run \`npm install\` in a project outside of '${cwd.toPosix()}', you would need to prepend with a \`cd\` i.e. pseudocode for this would be \`cd (path to project) && (command, in this case npm install)\`.

src/core/webview/ClineProvider.ts

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import fs from "fs/promises"
66
import os from "os"
77
import pWaitFor from "p-wait-for"
88
import * as path from "path"
9+
import { Terminal } from "../../integrations/terminal/Terminal"
910
import * as vscode from "vscode"
1011

1112
import { setPanel } from "../../activate/registerCommands"
@@ -355,9 +356,10 @@ export class ClineProvider extends EventEmitter<ClineProviderEvents> implements
355356
setPanel(webviewView, "sidebar")
356357
}
357358

358-
// Initialize sound enabled state
359-
this.getState().then(({ soundEnabled }) => {
359+
// Initialize out-of-scope variables that need to recieve persistent global state values
360+
this.getState().then(({ soundEnabled, terminalShellIntegrationTimeout }) => {
360361
setSoundEnabled(soundEnabled ?? false)
362+
Terminal.setShellIntegrationTimeout(terminalShellIntegrationTimeout ?? 4000)
361363
})
362364

363365
// Initialize tts enabled state
@@ -1403,6 +1405,13 @@ export class ClineProvider extends EventEmitter<ClineProviderEvents> implements
14031405
await this.updateGlobalState("terminalOutputLineLimit", message.value)
14041406
await this.postStateToWebview()
14051407
break
1408+
case "terminalShellIntegrationTimeout":
1409+
await this.updateGlobalState("terminalShellIntegrationTimeout", message.value)
1410+
await this.postStateToWebview()
1411+
if (message.value !== undefined) {
1412+
Terminal.setShellIntegrationTimeout(message.value)
1413+
}
1414+
break
14061415
case "mode":
14071416
await this.handleModeSwitch(message.text as Mode)
14081417
break
@@ -2369,6 +2378,7 @@ export class ClineProvider extends EventEmitter<ClineProviderEvents> implements
23692378
remoteBrowserEnabled,
23702379
writeDelayMs,
23712380
terminalOutputLineLimit,
2381+
terminalShellIntegrationTimeout,
23722382
fuzzyMatchThreshold,
23732383
mcpEnabled,
23742384
enableMcpServerCreation,
@@ -2432,6 +2442,7 @@ export class ClineProvider extends EventEmitter<ClineProviderEvents> implements
24322442
remoteBrowserEnabled: remoteBrowserEnabled ?? false,
24332443
writeDelayMs: writeDelayMs ?? 1000,
24342444
terminalOutputLineLimit: terminalOutputLineLimit ?? 500,
2445+
terminalShellIntegrationTimeout: terminalShellIntegrationTimeout ?? 4000,
24352446
fuzzyMatchThreshold: fuzzyMatchThreshold ?? 1.0,
24362447
mcpEnabled: mcpEnabled ?? true,
24372448
enableMcpServerCreation: enableMcpServerCreation ?? true,
@@ -2591,6 +2602,7 @@ export class ClineProvider extends EventEmitter<ClineProviderEvents> implements
25912602
fuzzyMatchThreshold: stateValues.fuzzyMatchThreshold ?? 1.0,
25922603
writeDelayMs: stateValues.writeDelayMs ?? 1000,
25932604
terminalOutputLineLimit: stateValues.terminalOutputLineLimit ?? 500,
2605+
terminalShellIntegrationTimeout: stateValues.terminalShellIntegrationTimeout ?? 4000,
25942606
mode: stateValues.mode ?? defaultModeSlug,
25952607
language: stateValues.language ?? formatLanguage(vscode.env.language),
25962608
mcpEnabled: stateValues.mcpEnabled ?? true,

src/exports/roo-code.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,7 @@ export type GlobalStateKey =
220220
| "fuzzyMatchThreshold"
221221
| "writeDelayMs"
222222
| "terminalOutputLineLimit"
223+
| "terminalShellIntegrationTimeout"
223224
| "mcpEnabled"
224225
| "enableMcpServerCreation"
225226
| "alwaysApproveResubmit"

src/integrations/terminal/Terminal.ts

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import { ExitCodeDetails, mergePromise, TerminalProcess, TerminalProcessResultPr
44
import { truncateOutput, applyRunLengthEncoding } from "../misc/extract-text"
55

66
export class Terminal {
7+
private static shellIntegrationTimeout: number = 4000
8+
79
public terminal: vscode.Terminal
810
public busy: boolean
911
public id: number
@@ -57,16 +59,18 @@ export class Terminal {
5759
if (stream) {
5860
// New stream is available
5961
if (!this.process) {
60-
throw new Error(`Cannot set active stream on terminal ${this.id} because process is undefined`)
62+
this.running = false
63+
console.warn(
64+
`[Terminal ${this.id}] process is undefined, so cannot set terminal stream (probably user-initiated non-Roo command)`,
65+
)
66+
return
6167
}
6268

6369
this.streamClosed = false
64-
this.running = true
6570
this.process.emit("stream_available", stream)
6671
} else {
6772
// Stream is being closed
6873
this.streamClosed = true
69-
this.running = false
7074
}
7175
}
7276

@@ -75,7 +79,6 @@ export class Terminal {
7579
* @param exitDetails The exit details of the shell execution
7680
*/
7781
public shellExecutionComplete(exitDetails: ExitCodeDetails): void {
78-
this.running = false
7982
this.busy = false
8083

8184
if (this.process) {
@@ -149,6 +152,9 @@ export class Terminal {
149152
}
150153

151154
public runCommand(command: string): TerminalProcessResultPromise {
155+
// We set busy before the command is running because the terminal may be waiting
156+
// on terminal integration, and we must prevent another instance from selecting
157+
// the terminal for use during that time.
152158
this.busy = true
153159

154160
// Create process immediately
@@ -165,20 +171,20 @@ export class Terminal {
165171
// Set up event handlers
166172
process.once("continue", () => resolve())
167173
process.once("error", (error) => {
168-
console.error(`Error in terminal ${this.id}:`, error)
174+
console.error(`[Terminal ${this.id}] error:`, error)
169175
reject(error)
170176
})
171177

172178
// Wait for shell integration before executing the command
173-
pWaitFor(() => this.terminal.shellIntegration !== undefined, { timeout: 4000 })
179+
pWaitFor(() => this.terminal.shellIntegration !== undefined, { timeout: Terminal.shellIntegrationTimeout })
174180
.then(() => {
175181
process.run(command)
176182
})
177183
.catch(() => {
178-
console.log("[Terminal] Shell integration not available. Command execution aborted.")
184+
console.log(`[Terminal ${this.id}] Shell integration not available. Command execution aborted.`)
179185
process.emit(
180186
"no_shell_integration",
181-
"Shell integration initialization sequence '\\x1b]633;A' was not received within 4 seconds. Shell integration has been disabled for this terminal instance.",
187+
"Shell integration initialization sequence '\\x1b]633;A' was not received within 4 seconds. Shell integration has been disabled for this terminal instance. Increase the timeout in the settings if necessary.",
182188
)
183189
})
184190
})
@@ -244,6 +250,10 @@ export class Terminal {
244250
* @param input The terminal output to compress
245251
* @returns The compressed terminal output
246252
*/
253+
public static setShellIntegrationTimeout(timeoutMs: number): void {
254+
Terminal.shellIntegrationTimeout = timeoutMs
255+
}
256+
247257
public static compressTerminalOutput(input: string, lineLimit: number): string {
248258
return truncateOutput(applyRunLengthEncoding(input), lineLimit)
249259
}

src/integrations/terminal/TerminalProcess.ts

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -285,7 +285,7 @@ export class TerminalProcess extends EventEmitter<TerminalProcessEvents> {
285285
(defaultWindowsShellProfile as string)?.toLowerCase().includes("powershell"))
286286
if (isPowerShell) {
287287
terminal.shellIntegration.executeCommand(
288-
`${command} ; ${this.terminalInfo.cmdCounter++} > $null; start-sleep -milliseconds 150`,
288+
`${command} ; "(Roo/PS Workaround: ${this.terminalInfo.cmdCounter++})" > $null; start-sleep -milliseconds 150`,
289289
)
290290
} else {
291291
terminal.shellIntegration.executeCommand(command)
@@ -306,10 +306,7 @@ export class TerminalProcess extends EventEmitter<TerminalProcessEvents> {
306306
"<VSCE shell integration stream did not start: terminal output and command execution status is unknown>",
307307
)
308308

309-
// Ensure terminal is marked as not busy
310-
if (this.terminalInfo) {
311-
this.terminalInfo.busy = false
312-
}
309+
this.terminalInfo.busy = false
313310

314311
// Emit continue event to allow execution to proceed
315312
this.emit("continue")

src/integrations/terminal/TerminalRegistry.ts

Lines changed: 24 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -24,17 +24,22 @@ export class TerminalRegistry {
2424
// Get a handle to the stream as early as possible:
2525
const stream = e?.execution.read()
2626
const terminalInfo = this.getTerminalByVSCETerminal(e.terminal)
27-
if (terminalInfo) {
28-
terminalInfo.setActiveStream(stream)
29-
} else {
30-
console.error("[TerminalRegistry] Stream failed, not registered for terminal")
31-
}
3227

3328
console.info("[TerminalRegistry] Shell execution started:", {
3429
hasExecution: !!e?.execution,
3530
command: e?.execution?.commandLine?.value,
3631
terminalId: terminalInfo?.id,
3732
})
33+
34+
if (terminalInfo) {
35+
terminalInfo.running = true
36+
terminalInfo.setActiveStream(stream)
37+
} else {
38+
console.error(
39+
"[TerminalRegistry] Shell execution started, but not from a Roo-registered terminal:",
40+
e,
41+
)
42+
}
3843
},
3944
)
4045

@@ -44,10 +49,20 @@ export class TerminalRegistry {
4449
const terminalInfo = this.getTerminalByVSCETerminal(e.terminal)
4550
const process = terminalInfo?.process
4651

52+
const exitDetails = TerminalProcess.interpretExitCode(e?.exitCode)
53+
54+
console.info("[TerminalRegistry] Shell execution ended:", {
55+
hasExecution: !!e?.execution,
56+
command: e?.execution?.commandLine?.value,
57+
terminalId: terminalInfo?.id,
58+
...exitDetails,
59+
})
60+
4761
if (!terminalInfo) {
48-
console.error("[TerminalRegistry] Shell execution ended but terminal not found:", {
49-
exitCode: e?.exitCode,
50-
})
62+
console.error(
63+
"[TerminalRegistry] Shell execution ended, but not from a Roo-registered terminal:",
64+
e,
65+
)
5166
return
5267
}
5368

@@ -74,15 +89,9 @@ export class TerminalRegistry {
7489
return
7590
}
7691

77-
const exitDetails = TerminalProcess.interpretExitCode(e?.exitCode)
78-
console.info("[TerminalRegistry] Shell execution ended:", {
79-
...exitDetails,
80-
terminalId: terminalInfo.id,
81-
command: process?.command ?? "<unknown>",
82-
})
83-
8492
// Signal completion to any waiting processes
8593
if (terminalInfo) {
94+
terminalInfo.running = false
8695
terminalInfo.shellExecutionComplete(exitDetails)
8796
}
8897
},

src/shared/ExtensionMessage.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,7 @@ export interface ExtensionState {
138138
language?: string
139139
writeDelayMs: number
140140
terminalOutputLineLimit?: number
141+
terminalShellIntegrationTimeout?: number
141142
mcpEnabled: boolean
142143
enableMcpServerCreation: boolean
143144
enableCustomModeCreation?: boolean

src/shared/WebviewMessage.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ export interface WebviewMessage {
7474
| "draggedImages"
7575
| "deleteMessage"
7676
| "terminalOutputLineLimit"
77+
| "terminalShellIntegrationTimeout"
7778
| "mcpEnabled"
7879
| "enableMcpServerCreation"
7980
| "enableCustomModeCreation"

0 commit comments

Comments
 (0)