diff --git a/src/extension.ts b/src/extension.ts index df18f9a22b..60ae65c9a4 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -18,6 +18,7 @@ import { CodeActionProvider } from "./core/CodeActionProvider" import { DIFF_VIEW_URI_SCHEME } from "./integrations/editor/DiffViewProvider" import { McpServerManager } from "./services/mcp/McpServerManager" import { telemetryService } from "./services/telemetry/TelemetryService" +import { TerminalRegistry } from "./integrations/terminal/TerminalRegistry" import { handleUri, registerCommands, registerCodeActions, createRooCodeAPI, registerTerminalActions } from "./activate" @@ -42,6 +43,8 @@ export function activate(context: vscode.ExtensionContext) { // Initialize telemetry service after environment variables are loaded telemetryService.initialize() + // Initialize terminal shell execution handlers + TerminalRegistry.initialize() // Get default commands from configuration. const defaultCommands = vscode.workspace.getConfiguration("roo-cline").get("allowedCommands") || [] @@ -108,4 +111,7 @@ export async function deactivate() { // Clean up MCP server manager await McpServerManager.cleanup(extensionContext) telemetryService.shutdown() + + // Clean up terminal handlers + TerminalRegistry.cleanup() } diff --git a/src/integrations/terminal/TerminalManager.ts b/src/integrations/terminal/TerminalManager.ts index 1c860001ce..4b2a78db6d 100644 --- a/src/integrations/terminal/TerminalManager.ts +++ b/src/integrations/terminal/TerminalManager.ts @@ -97,54 +97,6 @@ declare module "vscode" { export class TerminalManager { private terminalIds: Set = new Set() - private disposables: vscode.Disposable[] = [] - - constructor() { - let startDisposable: vscode.Disposable | undefined - let endDisposable: vscode.Disposable | undefined - try { - // onDidStartTerminalShellExecution - startDisposable = (vscode.window as vscode.Window).onDidStartTerminalShellExecution?.(async (e) => { - // Get a handle to the stream as early as possible: - const stream = e?.execution.read() - const terminalInfo = TerminalRegistry.getTerminalByVSCETerminal(e.terminal) - if (terminalInfo) { - terminalInfo.setActiveStream(stream) - } else { - console.error("[TerminalManager] Stream failed, not registered for terminal") - } - - console.info("[TerminalManager] Shell execution started:", { - hasExecution: !!e?.execution, - command: e?.execution?.commandLine?.value, - terminalId: terminalInfo?.id, - }) - }) - - // onDidEndTerminalShellExecution - endDisposable = (vscode.window as vscode.Window).onDidEndTerminalShellExecution?.(async (e) => { - const terminalInfo = TerminalRegistry.getTerminalByVSCETerminal(e.terminal) - const process = terminalInfo?.process - const exitDetails = process ? TerminalProcess.interpretExitCode(e?.exitCode) : { exitCode: e?.exitCode } - console.info("[TerminalManager] Shell execution ended:", { - ...exitDetails, - }) - - // Signal completion to any waiting processes - if (terminalInfo && this.terminalIds.has(terminalInfo.id)) { - terminalInfo.shellExecutionComplete(exitDetails) - } - }) - } catch (error) { - console.error("[TerminalManager] Error setting up shell execution handlers:", error) - } - if (startDisposable) { - this.disposables.push(startDisposable) - } - if (endDisposable) { - this.disposables.push(endDisposable) - } - } runCommand(terminalInfo: Terminal, command: string): TerminalProcessResultPromise { terminalInfo.busy = true @@ -214,11 +166,6 @@ export class TerminalManager { } disposeAll() { - // for (const info of this.terminals) { - // //info.terminal.dispose() // dont want to dispose terminals when task is aborted - // } this.terminalIds.clear() - this.disposables.forEach((disposable) => disposable.dispose()) - this.disposables = [] } } diff --git a/src/integrations/terminal/TerminalRegistry.ts b/src/integrations/terminal/TerminalRegistry.ts index ef72ae428b..50d89de9c3 100644 --- a/src/integrations/terminal/TerminalRegistry.ts +++ b/src/integrations/terminal/TerminalRegistry.ts @@ -1,11 +1,65 @@ import * as vscode from "vscode" import { Terminal } from "./Terminal" +import { TerminalProcess } from "./TerminalProcess" // Although vscode.window.terminals provides a list of all open terminals, there's no way to know whether they're busy or not (exitStatus does not provide useful information for most commands). In order to prevent creating too many terminals, we need to keep track of terminals through the life of the extension, as well as session specific terminals for the life of a task (to get latest unretrieved output). // Since we have promises keeping track of terminal processes, we get the added benefit of keep track of busy terminals even after a task is closed. export class TerminalRegistry { private static terminals: Terminal[] = [] private static nextTerminalId = 1 + private static disposables: vscode.Disposable[] = [] + private static isInitialized = false + + static initialize() { + if (this.isInitialized) { + throw new Error("TerminalRegistry.initialize() should only be called once") + } + this.isInitialized = true + + try { + // onDidStartTerminalShellExecution + const startDisposable = (vscode.window as vscode.Window).onDidStartTerminalShellExecution?.(async (e) => { + // Get a handle to the stream as early as possible: + const stream = e?.execution.read() + const terminalInfo = this.getTerminalByVSCETerminal(e.terminal) + if (terminalInfo) { + terminalInfo.setActiveStream(stream) + } else { + console.error("[TerminalRegistry] Stream failed, not registered for terminal") + } + + console.info("[TerminalRegistry] Shell execution started:", { + hasExecution: !!e?.execution, + command: e?.execution?.commandLine?.value, + terminalId: terminalInfo?.id, + }) + }) + + // onDidEndTerminalShellExecution + const endDisposable = (vscode.window as vscode.Window).onDidEndTerminalShellExecution?.(async (e) => { + const terminalInfo = this.getTerminalByVSCETerminal(e.terminal) + const process = terminalInfo?.process + const exitDetails = process ? TerminalProcess.interpretExitCode(e?.exitCode) : { exitCode: e?.exitCode } + console.info("[TerminalRegistry] Shell execution ended:", { + ...exitDetails, + }) + + // Signal completion to any waiting processes + if (terminalInfo) { + terminalInfo.shellExecutionComplete(exitDetails) + } + }) + + if (startDisposable) { + this.disposables.push(startDisposable) + } + if (endDisposable) { + this.disposables.push(endDisposable) + } + } catch (error) { + console.error("[TerminalRegistry] Error setting up shell execution handlers:", error) + } + } static createTerminal(cwd?: string | vscode.Uri | undefined): Terminal { const terminal = vscode.window.createTerminal({ @@ -115,4 +169,9 @@ export class TerminalRegistry { static getTerminals(busy: boolean): Terminal[] { return this.getAllTerminals().filter((t) => t.busy === busy) } + + static cleanup() { + this.disposables.forEach((disposable) => disposable.dispose()) + this.disposables = [] + } } diff --git a/src/integrations/terminal/__tests__/TerminalProcessExec.test.ts b/src/integrations/terminal/__tests__/TerminalProcessExec.test.ts index a696b70d6b..dddbcca921 100644 --- a/src/integrations/terminal/__tests__/TerminalProcessExec.test.ts +++ b/src/integrations/terminal/__tests__/TerminalProcessExec.test.ts @@ -241,6 +241,11 @@ async function testTerminalCommand( } describe("TerminalProcess with Real Command Output", () => { + beforeAll(() => { + // Initialize TerminalRegistry event handlers once globally + TerminalRegistry.initialize() + }) + beforeEach(() => { // Reset the terminals array before each test TerminalRegistry["terminals"] = []