singularity-forge/vscode-extension/src/bash-terminal.ts
Jeremy McSpadden c2cd8bcc0a feat(vscode): status bar, file decorations, bash terminal, session tree, conversation history, code lens [1/2] (#2651)
* feat(vscode): status bar, auto-retry, session name, copy response, keyboard shortcuts, full stats

* feat(vscode): file decorations, bash terminal, session tree view

* feat(vscode): conversation history webview, slash completion, code lens

- conversation-history.ts: GsdConversationHistoryPanel webview panel using
  getMessages() RPC; renders user/assistant turns with a Refresh button
- slash-completion.ts: GsdSlashCompletionProvider triggers on '/' at line
  start in md/plaintext/ts/js; fetches getCommands() RPC and caches results
- code-lens.ts: GsdCodeLensProvider adds 'Ask GSD' lens above named
  functions/classes in ts/js/py/go/rust; respects gsd.codeLens setting
- extension.ts: registers all three providers and new commands
  (gsd.showHistory, gsd.askAboutSymbol)
- package.json: declares new commands and gsd.codeLens config toggle
2026-03-26 16:18:37 -06:00

84 lines
2.4 KiB
TypeScript

import * as vscode from "vscode";
import type { AgentEvent, GsdClient } from "./gsd-client.js";
/**
* Routes the GSD 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 GsdBashTerminal implements vscode.Disposable {
private terminal: vscode.Terminal | undefined;
private writeEmitter: vscode.EventEmitter<string> | undefined;
private disposables: vscode.Disposable[] = [];
constructor(client: GsdClient) {
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: "GSD 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();
}
}
}