90 lines
2.4 KiB
TypeScript
90 lines
2.4 KiB
TypeScript
import * as vscode from "vscode";
|
|
import type { AgentEvent, SfClient } from "./sf-client.js";
|
|
|
|
/**
|
|
* Routes the SF agent's Bash tool output to a dedicated VS Code terminal panel.
|
|
* Shows streaming output from tool_execution_update events in real time.
|
|
*/
|
|
export class SfBashTerminal implements vscode.Disposable {
|
|
private terminal: vscode.Terminal | undefined;
|
|
private writeEmitter: vscode.EventEmitter<string> | undefined;
|
|
private disposables: vscode.Disposable[] = [];
|
|
|
|
constructor(client: SfClient) {
|
|
this.disposables.push(
|
|
client.onEvent((evt: AgentEvent) => this.handleEvent(evt)),
|
|
client.onConnectionChange((connected) => {
|
|
if (!connected) {
|
|
this.close();
|
|
}
|
|
}),
|
|
);
|
|
}
|
|
|
|
private getOrCreateTerminal(): {
|
|
terminal: vscode.Terminal;
|
|
writeEmitter: vscode.EventEmitter<string>;
|
|
} {
|
|
if (!this.terminal || this.terminal.exitStatus !== undefined) {
|
|
this.writeEmitter?.dispose();
|
|
this.writeEmitter = new vscode.EventEmitter<string>();
|
|
const emitter = this.writeEmitter;
|
|
const pty: vscode.Pseudoterminal = {
|
|
onDidWrite: emitter.event,
|
|
open: () => {},
|
|
close: () => {
|
|
this.terminal = undefined;
|
|
},
|
|
};
|
|
this.terminal = vscode.window.createTerminal({ name: "SF Agent", pty });
|
|
}
|
|
return { terminal: this.terminal, writeEmitter: this.writeEmitter! };
|
|
}
|
|
|
|
private handleEvent(evt: AgentEvent): void {
|
|
switch (evt.type) {
|
|
case "tool_execution_start": {
|
|
if (evt.toolName !== "Bash") {
|
|
break;
|
|
}
|
|
const cmd = (evt.toolInput as Record<string, unknown> | undefined)
|
|
?.command as string | undefined;
|
|
const { terminal, writeEmitter } = this.getOrCreateTerminal();
|
|
terminal.show(true); // preserve editor focus
|
|
writeEmitter.fire(`\x1b[90m$ ${cmd ?? ""}\x1b[0m\r\n`);
|
|
break;
|
|
}
|
|
case "tool_execution_update": {
|
|
if (evt.toolName !== "Bash" || !this.writeEmitter) {
|
|
break;
|
|
}
|
|
const partial = evt.partialResult as string | undefined;
|
|
if (partial) {
|
|
this.writeEmitter.fire(partial.replace(/\n/g, "\r\n"));
|
|
}
|
|
break;
|
|
}
|
|
case "tool_execution_end": {
|
|
if (evt.toolName !== "Bash" || !this.writeEmitter) {
|
|
break;
|
|
}
|
|
this.writeEmitter.fire("\r\n");
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
close(): void {
|
|
this.terminal?.dispose();
|
|
this.terminal = undefined;
|
|
this.writeEmitter?.dispose();
|
|
this.writeEmitter = undefined;
|
|
}
|
|
|
|
dispose(): void {
|
|
this.close();
|
|
for (const d of this.disposables) {
|
|
d.dispose();
|
|
}
|
|
}
|
|
}
|