Prevent terminal replay from sending input

This commit is contained in:
Federico Jaramillo Martinez
2026-05-11 15:20:59 +02:00
parent 6a21d6768e
commit 3be4489ee7
3 changed files with 33 additions and 8 deletions
+29 -4
View File
@@ -21,6 +21,7 @@ export class TerminalPanel extends LitElement {
private socket: WebSocket | undefined;
private resizeObserver: ResizeObserver | undefined;
private intersectionObserver: IntersectionObserver | undefined;
private suppressTerminalInput = false;
private observedCwd: string | undefined;
private loadedCwd: string | undefined;
@@ -128,7 +129,11 @@ export class TerminalPanel extends LitElement {
this.fitAddon = fitAddon;
this.resizeObserver = new ResizeObserver(() => { this.fitAndNotify(); });
this.resizeObserver.observe(this.terminalHost);
terminal.onData((data) => { this.send({ type: "input", data }); });
terminal.onData((data) => {
if (this.suppressTerminalInput) return;
const filtered = filterTerminalInput(data);
if (filtered !== "") this.send({ type: "input", data: filtered });
});
this.connectSocket(workspace.projectId, workspace.id, this.selectedId, terminal);
requestAnimationFrame(() => { this.fitAndNotify(); });
terminal.focus();
@@ -150,7 +155,9 @@ export class TerminalPanel extends LitElement {
private async handleSocketMessage(data: unknown, terminalId: string, terminal: Terminal): Promise<void> {
try {
const message = parseServerMessage(await socketDataToString(data));
if (message.type === "output") terminal.write(message.data);
if (message.type === "output") {
this.writeTerminalOutput(terminal, message.data, message.replay === true);
}
if (message.type === "exit") {
terminal.writeln(`\r\n[process exited${message.exitCode === undefined ? "" : ` with code ${String(message.exitCode)}`}]`);
this.terminals = this.terminals.map((item) => item.id === terminalId ? { ...item, exited: true, ...(message.exitCode === undefined ? {} : { exitCode: message.exitCode }) } : item);
@@ -161,6 +168,17 @@ export class TerminalPanel extends LitElement {
}
}
private writeTerminalOutput(terminal: Terminal, data: string, replay: boolean): void {
if (!replay) {
terminal.write(data);
return;
}
this.suppressTerminalInput = true;
terminal.write(data, () => {
this.suppressTerminalInput = false;
});
}
private fitAndNotify(): void {
if (this.fitAddon === undefined || this.terminal === undefined) return;
this.fitAddon.fit();
@@ -232,7 +250,7 @@ export class TerminalPanel extends LitElement {
}
type ServerTerminalMessage =
| { type: "output"; data: string }
| { type: "output"; data: string; replay?: boolean }
| { type: "exit"; exitCode?: number }
| { type: "error"; message: string };
@@ -240,12 +258,19 @@ function parseServerMessage(data: string): ServerTerminalMessage {
const value: unknown = JSON.parse(data);
if (!isRecord(value)) return { type: "error", message: "Invalid terminal message" };
const record = value;
if (record["type"] === "output" && typeof record["data"] === "string") return { type: "output", data: record["data"] };
if (record["type"] === "output" && typeof record["data"] === "string") return { type: "output", data: record["data"], ...(typeof record["replay"] === "boolean" ? { replay: record["replay"] } : {}) };
if (record["type"] === "exit") return { type: "exit", ...(typeof record["exitCode"] === "number" ? { exitCode: record["exitCode"] } : {}) };
if (record["type"] === "error" && typeof record["message"] === "string") return { type: "error", message: record["message"] };
return { type: "error", message: "Invalid terminal message" };
}
export function filterTerminalInput(data: string): string {
// Xterm can emit focus-in/focus-out sequences when replayed output leaves focus
// tracking enabled. Bash/readline treats those sequences as typed text, which
// leaves stray characters on the prompt after reconnecting to an active shell.
return data.replaceAll("\x1b[I", "").replaceAll("\x1b[O", "");
}
async function socketDataToString(data: unknown): Promise<string> {
if (typeof data === "string") return data;
if (data instanceof ArrayBuffer) return new TextDecoder().decode(data);
+1 -1
View File
@@ -25,7 +25,7 @@ export function registerTerminalRoutes(app: FastifyInstance, terminals: Terminal
let detach: (() => void) | undefined;
try {
detach = terminals.attach(request.params.terminalId, {
output: (data) => { socket.send(JSON.stringify({ type: "output", data })); },
output: (data, replay) => { socket.send(JSON.stringify({ type: "output", data, replay })); },
exit: (exitCode) => { socket.send(JSON.stringify({ type: "exit", exitCode })); },
});
} catch (error) {
+3 -3
View File
@@ -76,11 +76,11 @@ export class TerminalService {
return terminal === undefined ? undefined : toInfo(terminal);
}
attach(id: string, handlers: { output: (data: string) => void; exit: (exitCode: number | undefined) => void }): () => void {
attach(id: string, handlers: { output: (data: string, replay: boolean) => void; exit: (exitCode: number | undefined) => void }): () => void {
const terminal = this.require(id);
if (terminal.buffer !== "") handlers.output(terminal.buffer);
if (terminal.buffer !== "") handlers.output(terminal.buffer, true);
if (terminal.exited) handlers.exit(terminal.exitCode);
const onOutput = (data: string) => { handlers.output(data); };
const onOutput = (data: string) => { handlers.output(data, false); };
const onExit = (exitCode: number | undefined) => { handlers.exit(exitCode); };
terminal.events.on("output", onOutput);
terminal.events.on("exit", onExit);