refactor: move terminal functionality to Terminal class

Move terminal lifecycle management to improve organization:

1. Move runCommand to Terminal class
2. Move getOrCreateTerminal to TerminalRegistry

Signed-off-by: Eric Wheeler <roo-code@z.ewheeler.org>
This commit is contained in:
Eric Wheeler 2025-03-05 18:35:00 -08:00
parent 1973f87c6b
commit bf2ce7e1ee
4 changed files with 81 additions and 69 deletions

View file

@ -923,7 +923,7 @@ export class Cline {
async executeCommandTool(command: string): Promise<[boolean, ToolResponse]> {
const terminalInfo = await this.terminalManager.getOrCreateTerminal(cwd)
terminalInfo.terminal.show() // weird visual bug when creating new terminals (even manually) where there's an empty space at the top.
const process = this.terminalManager.runCommand(terminalInfo, command)
const process = terminalInfo.runCommand(command)
let userFeedback: { text?: string; images?: string[] } | undefined
let didContinue = false

View file

@ -1,5 +1,6 @@
import * as vscode from "vscode"
import { ExitCodeDetails, TerminalProcess } from "./TerminalProcess"
import pWaitFor from "p-wait-for"
import { ExitCodeDetails, mergePromise, TerminalProcess, TerminalProcessResultPromise } from "./TerminalProcess"
export class Terminal {
public terminal: vscode.Terminal
@ -71,6 +72,39 @@ export class Terminal {
}
}
public runCommand(command: string): TerminalProcessResultPromise {
this.busy = true
this.lastCommand = command
// Create process immediately
const process = new TerminalProcess(this)
// Set process on terminal
this.process = process
// Create a promise for command completion
const promise = new Promise<void>((resolve, reject) => {
// Set up event handlers
process.once("continue", () => resolve())
process.once("error", (error) => {
console.error(`Error in terminal ${this.id}:`, error)
reject(error)
})
// Wait for shell integration before executing the command
pWaitFor(() => this.terminal.shellIntegration !== undefined, { timeout: 4000 })
.then(() => {
process.run(command)
})
.catch(() => {
console.log("[Terminal] Shell integration not available. Command execution aborted.")
process.emit("no_shell_integration")
})
})
return mergePromise(process, promise)
}
/**
* Gets the terminal contents based on the number of commands to include
* @param commands Number of previous commands to include (-1 for all)

View file

@ -34,10 +34,13 @@ Windows: pwsh
Example:
const terminalManager = new TerminalManager(context);
const terminalManager = new TerminalManager();
// Get a terminal for the project directory
const terminal = await terminalManager.getTerminal('/path/to/project');
// Run a command
const process = terminalManager.runCommand('npm install', '/path/to/project');
const process = terminal.runCommand('npm install');
process.on('line', (line) => {
console.log(line);
@ -50,7 +53,7 @@ await process;
process.continue();
// Later, if you need to get the unretrieved output:
const unretrievedOutput = terminalManager.getUnretrievedOutput(terminalId);
const unretrievedOutput = TerminalRegistry.getUnretrievedOutput(terminal.id);
console.log('Unretrieved output:', unretrievedOutput);
Resources:
@ -98,71 +101,10 @@ declare module "vscode" {
export class TerminalManager {
private terminalIds: Set<number> = new Set()
runCommand(terminalInfo: Terminal, command: string): TerminalProcessResultPromise {
terminalInfo.busy = true
terminalInfo.lastCommand = command
// Create process immediately
const process = new TerminalProcess(terminalInfo)
// Set process on terminal
terminalInfo.process = process
// Create a promise for command completion
const promise = new Promise<void>((resolve, reject) => {
// Set up event handlers
process.once("continue", () => resolve())
process.once("error", (error) => {
console.error(`Error in terminal ${terminalInfo.id}:`, error)
reject(error)
})
// Wait for shell integration before executing the command
pWaitFor(() => terminalInfo.terminal.shellIntegration !== undefined, { timeout: 4000 })
.then(() => {
process.run(command)
})
.catch(() => {
console.log("[TerminalManager] Shell integration not available. Command execution aborted.")
process.emit("no_shell_integration")
})
})
return mergePromise(process, promise)
}
async getOrCreateTerminal(cwd: string): Promise<Terminal> {
const terminals = TerminalRegistry.getAllTerminals()
// Find available terminal from our pool first (created for this task)
const matchingTerminal = terminals.find((t) => {
if (t.busy) {
return false
}
const terminalCwd = t.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 (matchingTerminal) {
this.terminalIds.add(matchingTerminal.id)
return matchingTerminal
}
// If no matching terminal exists, try to find any non-busy terminal
const availableTerminal = terminals.find((t) => !t.busy)
if (availableTerminal) {
// Navigate back to the desired directory
await this.runCommand(availableTerminal, `cd "${cwd}"`)
this.terminalIds.add(availableTerminal.id)
return availableTerminal
}
// If all terminals are busy, create a new one
const newTerminalInfo = TerminalRegistry.createTerminal(cwd)
this.terminalIds.add(newTerminalInfo.id)
return newTerminalInfo
const terminal = await TerminalRegistry.getOrCreateTerminal(cwd)
this.terminalIds.add(terminal.id)
return terminal
}
disposeAll() {

View file

@ -1,4 +1,5 @@
import * as vscode from "vscode"
import { arePathsEqual } from "../../utils/path"
import { Terminal } from "./Terminal"
import { TerminalProcess } from "./TerminalProcess"
@ -174,4 +175,39 @@ export class TerminalRegistry {
this.disposables.forEach((disposable) => disposable.dispose())
this.disposables = []
}
/**
* Gets an existing terminal or creates a new one for the given working directory
* @param cwd The working directory path
* @returns A Terminal instance
*/
static async getOrCreateTerminal(cwd: string): Promise<Terminal> {
const terminals = this.getAllTerminals()
// Find available terminal from our pool first (created for this task)
const matchingTerminal = terminals.find((t) => {
if (t.busy) {
return false
}
const terminalCwd = t.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 (matchingTerminal) {
return matchingTerminal
}
// If no matching terminal exists, try to find any non-busy terminal
const availableTerminal = terminals.find((t) => !t.busy)
if (availableTerminal) {
// Navigate back to the desired directory
await availableTerminal.runCommand(`cd "${cwd}"`)
return availableTerminal
}
// If all terminals are busy, create a new one
return this.createTerminal(cwd)
}
}