mirror of
https://github.com/pacnpal/Roo-Code.git
synced 2025-12-20 04:11:10 -05:00
214 lines
7.4 KiB
TypeScript
214 lines
7.4 KiB
TypeScript
import pWaitFor from "p-wait-for"
|
|
import * as vscode from "vscode"
|
|
import { arePathsEqual } from "../../utils/path"
|
|
import { mergePromise, TerminalProcess, TerminalProcessResultPromise } from "./TerminalProcess"
|
|
import { TerminalInfo, TerminalRegistry } from "./TerminalRegistry"
|
|
|
|
/*
|
|
TerminalManager:
|
|
- Creates/reuses terminals
|
|
- Runs commands via runCommand(), returning a TerminalProcess
|
|
- Handles shell integration events
|
|
|
|
TerminalProcess extends EventEmitter and implements Promise:
|
|
- Emits 'line' events with output while promise is pending
|
|
- process.continue() resolves promise and stops event emission
|
|
- Allows real-time output handling or background execution
|
|
|
|
getUnretrievedOutput() fetches latest output for ongoing commands
|
|
|
|
Enables flexible command execution:
|
|
- Await for completion
|
|
- Listen to real-time events
|
|
- Continue execution in background
|
|
- Retrieve missed output later
|
|
|
|
Notes:
|
|
- it turns out some shellIntegration APIs are available on cursor, although not on older versions of vscode
|
|
- "By default, the shell integration script should automatically activate on supported shells launched from VS Code."
|
|
Supported shells:
|
|
Linux/macOS: bash, fish, pwsh, zsh
|
|
Windows: pwsh
|
|
|
|
|
|
Example:
|
|
|
|
const terminalManager = new TerminalManager(context);
|
|
|
|
// Run a command
|
|
const process = terminalManager.runCommand('npm install', '/path/to/project');
|
|
|
|
process.on('line', (line) => {
|
|
console.log(line);
|
|
});
|
|
|
|
// To wait for the process to complete naturally:
|
|
await process;
|
|
|
|
// Or to continue execution even if the command is still running:
|
|
process.continue();
|
|
|
|
// Later, if you need to get the unretrieved output:
|
|
const unretrievedOutput = terminalManager.getUnretrievedOutput(terminalId);
|
|
console.log('Unretrieved output:', unretrievedOutput);
|
|
|
|
Resources:
|
|
- https://github.com/microsoft/vscode/issues/226655
|
|
- https://code.visualstudio.com/updates/v1_93#_terminal-shell-integration-api
|
|
- https://code.visualstudio.com/docs/terminal/shell-integration
|
|
- https://code.visualstudio.com/api/references/vscode-api#Terminal
|
|
- https://github.com/microsoft/vscode-extension-samples/blob/main/terminal-sample/src/extension.ts
|
|
- https://github.com/microsoft/vscode-extension-samples/blob/main/shell-integration-sample/src/extension.ts
|
|
*/
|
|
|
|
/*
|
|
The new shellIntegration API gives us access to terminal command execution output handling.
|
|
However, we don't update our VSCode type definitions or engine requirements to maintain compatibility
|
|
with older VSCode versions. Users on older versions will automatically fall back to using sendText
|
|
for terminal command execution.
|
|
Interestingly, some environments like Cursor enable these APIs even without the latest VSCode engine.
|
|
This approach allows us to leverage advanced features when available while ensuring broad compatibility.
|
|
*/
|
|
declare module "vscode" {
|
|
// https://github.com/microsoft/vscode/blob/f0417069c62e20f3667506f4b7e53ca0004b4e3e/src/vscode-dts/vscode.d.ts#L10794
|
|
interface Window {
|
|
onDidStartTerminalShellExecution?: (
|
|
listener: (e: any) => any,
|
|
thisArgs?: any,
|
|
disposables?: vscode.Disposable[],
|
|
) => vscode.Disposable
|
|
}
|
|
}
|
|
|
|
// Extend the Terminal type to include our custom properties
|
|
type ExtendedTerminal = vscode.Terminal & {
|
|
shellIntegration?: {
|
|
cwd?: vscode.Uri
|
|
executeCommand?: (command: string) => {
|
|
read: () => AsyncIterable<string>
|
|
}
|
|
}
|
|
}
|
|
|
|
export class TerminalManager {
|
|
private terminalIds: Set<number> = new Set()
|
|
private processes: Map<number, TerminalProcess> = new Map()
|
|
private disposables: vscode.Disposable[] = []
|
|
|
|
constructor() {
|
|
let disposable: vscode.Disposable | undefined
|
|
try {
|
|
disposable = (vscode.window as vscode.Window).onDidStartTerminalShellExecution?.(async (e) => {
|
|
// Creating a read stream here results in a more consistent output. This is most obvious when running the `date` command.
|
|
e?.execution?.read()
|
|
})
|
|
} catch (error) {
|
|
// console.error("Error setting up onDidEndTerminalShellExecution", error)
|
|
}
|
|
if (disposable) {
|
|
this.disposables.push(disposable)
|
|
}
|
|
}
|
|
|
|
runCommand(terminalInfo: TerminalInfo, command: string): TerminalProcessResultPromise {
|
|
terminalInfo.busy = true
|
|
terminalInfo.lastCommand = command
|
|
const process = new TerminalProcess()
|
|
this.processes.set(terminalInfo.id, process)
|
|
|
|
process.once("completed", () => {
|
|
terminalInfo.busy = false
|
|
})
|
|
|
|
// if shell integration is not available, remove terminal so it does not get reused as it may be running a long-running process
|
|
process.once("no_shell_integration", () => {
|
|
console.log(`no_shell_integration received for terminal ${terminalInfo.id}`)
|
|
// Remove the terminal so we can't reuse it (in case it's running a long-running process)
|
|
TerminalRegistry.removeTerminal(terminalInfo.id)
|
|
this.terminalIds.delete(terminalInfo.id)
|
|
this.processes.delete(terminalInfo.id)
|
|
})
|
|
|
|
const promise = new Promise<void>((resolve, reject) => {
|
|
process.once("continue", () => {
|
|
resolve()
|
|
})
|
|
process.once("error", (error) => {
|
|
console.error(`Error in terminal ${terminalInfo.id}:`, error)
|
|
reject(error)
|
|
})
|
|
})
|
|
|
|
// if shell integration is already active, run the command immediately
|
|
const terminal = terminalInfo.terminal as ExtendedTerminal
|
|
if (terminal.shellIntegration) {
|
|
process.waitForShellIntegration = false
|
|
process.run(terminal, command)
|
|
} else {
|
|
// docs recommend waiting 3s for shell integration to activate
|
|
pWaitFor(() => (terminalInfo.terminal as ExtendedTerminal).shellIntegration !== undefined, { timeout: 4000 }).finally(() => {
|
|
const existingProcess = this.processes.get(terminalInfo.id)
|
|
if (existingProcess && existingProcess.waitForShellIntegration) {
|
|
existingProcess.waitForShellIntegration = false
|
|
existingProcess.run(terminal, command)
|
|
}
|
|
})
|
|
}
|
|
|
|
return mergePromise(process, promise)
|
|
}
|
|
|
|
async getOrCreateTerminal(cwd: string): Promise<TerminalInfo> {
|
|
// Find available terminal from our pool first (created for this task)
|
|
const availableTerminal = TerminalRegistry.getAllTerminals().find((t) => {
|
|
if (t.busy) {
|
|
return false
|
|
}
|
|
const terminal = t.terminal as ExtendedTerminal
|
|
const terminalCwd = terminal.shellIntegration?.cwd // one of cline's commands could have changed the cwd of the terminal
|
|
if (!terminalCwd) {
|
|
return false
|
|
}
|
|
return arePathsEqual(vscode.Uri.file(cwd).fsPath, terminalCwd.fsPath)
|
|
})
|
|
if (availableTerminal) {
|
|
this.terminalIds.add(availableTerminal.id)
|
|
return availableTerminal
|
|
}
|
|
|
|
const newTerminalInfo = TerminalRegistry.createTerminal(cwd)
|
|
this.terminalIds.add(newTerminalInfo.id)
|
|
return newTerminalInfo
|
|
}
|
|
|
|
getTerminals(busy: boolean): { id: number; lastCommand: string }[] {
|
|
return Array.from(this.terminalIds)
|
|
.map((id) => TerminalRegistry.getTerminal(id))
|
|
.filter((t): t is TerminalInfo => t !== undefined && t.busy === busy)
|
|
.map((t) => ({ id: t.id, lastCommand: t.lastCommand }))
|
|
}
|
|
|
|
getUnretrievedOutput(terminalId: number): string {
|
|
if (!this.terminalIds.has(terminalId)) {
|
|
return ""
|
|
}
|
|
const process = this.processes.get(terminalId)
|
|
return process ? process.getUnretrievedOutput() : ""
|
|
}
|
|
|
|
isProcessHot(terminalId: number): boolean {
|
|
const process = this.processes.get(terminalId)
|
|
return process ? process.isHot : false
|
|
}
|
|
|
|
disposeAll() {
|
|
// for (const info of this.terminals) {
|
|
// //info.terminal.dispose() // dont want to dispose terminals when task is aborted
|
|
// }
|
|
this.terminalIds.clear()
|
|
this.processes.clear()
|
|
this.disposables.forEach((disposable) => disposable.dispose())
|
|
this.disposables = []
|
|
}
|
|
}
|