feat(cli): add paseo terminal command group for managing workspace terminals
New CLI commands: ls, create, kill, capture (tmux capture-pane style with line range slicing and ANSI stripping), and send-keys (with special token interpretation). Server-side adds capture_terminal_request RPC and list-all-terminals support (optional CWD). Includes e2e tests and skill docs.
This commit is contained in:
@@ -4,6 +4,7 @@ import { randomUUID } from "crypto";
|
||||
import { chmodSync, existsSync, statSync } from "node:fs";
|
||||
import { dirname, join } from "node:path";
|
||||
import { createRequire } from "node:module";
|
||||
import stripAnsi from "strip-ansi";
|
||||
import type { TerminalCell, TerminalState } from "../shared/messages.js";
|
||||
|
||||
const { Terminal } = xterm;
|
||||
@@ -40,6 +41,17 @@ export interface CreateTerminalOptions {
|
||||
name?: string;
|
||||
}
|
||||
|
||||
export interface CaptureTerminalLinesOptions {
|
||||
start?: number;
|
||||
end?: number;
|
||||
stripAnsi?: boolean;
|
||||
}
|
||||
|
||||
export interface CaptureTerminalLinesResult {
|
||||
lines: string[];
|
||||
totalLines: number;
|
||||
}
|
||||
|
||||
type EnsureNodePtySpawnHelperExecutableOptions = {
|
||||
packageRoot?: string;
|
||||
platform?: NodeJS.Platform;
|
||||
@@ -230,6 +242,60 @@ function extractCursorState(terminal: TerminalType): TerminalState["cursor"] {
|
||||
};
|
||||
}
|
||||
|
||||
function cellsToPlainText(cells: TerminalCell[], options: { stripAnsi: boolean }): string {
|
||||
const text = cells.map((cell) => cell.char).join("").trimEnd();
|
||||
return options.stripAnsi ? stripAnsi(text) : text;
|
||||
}
|
||||
|
||||
function resolveCaptureLineIndex(
|
||||
lineNumber: number | undefined,
|
||||
totalLines: number,
|
||||
fallback: "start" | "end",
|
||||
): number {
|
||||
if (totalLines === 0) {
|
||||
return fallback === "start" ? 0 : -1;
|
||||
}
|
||||
|
||||
const defaultIndex = fallback === "start" ? 0 : totalLines - 1;
|
||||
if (typeof lineNumber !== "number") {
|
||||
return defaultIndex;
|
||||
}
|
||||
|
||||
const resolvedIndex = lineNumber < 0 ? totalLines + lineNumber : lineNumber;
|
||||
if (resolvedIndex < 0) {
|
||||
return 0;
|
||||
}
|
||||
if (resolvedIndex >= totalLines) {
|
||||
return totalLines - 1;
|
||||
}
|
||||
return resolvedIndex;
|
||||
}
|
||||
|
||||
export function captureTerminalLines(
|
||||
terminal: TerminalSession,
|
||||
options: CaptureTerminalLinesOptions = {},
|
||||
): CaptureTerminalLinesResult {
|
||||
const state = terminal.getState();
|
||||
const allLines = [...state.scrollback, ...state.grid].map((cells) =>
|
||||
cellsToPlainText(cells, { stripAnsi: options.stripAnsi ?? true }),
|
||||
);
|
||||
const totalLines = allLines.length;
|
||||
const startIndex = resolveCaptureLineIndex(options.start, totalLines, "start");
|
||||
const endIndex = resolveCaptureLineIndex(options.end, totalLines, "end");
|
||||
|
||||
if (totalLines === 0 || startIndex > endIndex) {
|
||||
return {
|
||||
lines: [],
|
||||
totalLines,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
lines: allLines.slice(startIndex, endIndex + 1),
|
||||
totalLines,
|
||||
};
|
||||
}
|
||||
|
||||
export async function createTerminal(options: CreateTerminalOptions): Promise<TerminalSession> {
|
||||
const {
|
||||
cwd,
|
||||
|
||||
Reference in New Issue
Block a user