Archived
Prevent terminal replay from sending input
This commit is contained in:
@@ -21,6 +21,7 @@ export class TerminalPanel extends LitElement {
|
|||||||
private socket: WebSocket | undefined;
|
private socket: WebSocket | undefined;
|
||||||
private resizeObserver: ResizeObserver | undefined;
|
private resizeObserver: ResizeObserver | undefined;
|
||||||
private intersectionObserver: IntersectionObserver | undefined;
|
private intersectionObserver: IntersectionObserver | undefined;
|
||||||
|
private suppressTerminalInput = false;
|
||||||
private observedCwd: string | undefined;
|
private observedCwd: string | undefined;
|
||||||
private loadedCwd: string | undefined;
|
private loadedCwd: string | undefined;
|
||||||
|
|
||||||
@@ -128,7 +129,11 @@ export class TerminalPanel extends LitElement {
|
|||||||
this.fitAddon = fitAddon;
|
this.fitAddon = fitAddon;
|
||||||
this.resizeObserver = new ResizeObserver(() => { this.fitAndNotify(); });
|
this.resizeObserver = new ResizeObserver(() => { this.fitAndNotify(); });
|
||||||
this.resizeObserver.observe(this.terminalHost);
|
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);
|
this.connectSocket(workspace.projectId, workspace.id, this.selectedId, terminal);
|
||||||
requestAnimationFrame(() => { this.fitAndNotify(); });
|
requestAnimationFrame(() => { this.fitAndNotify(); });
|
||||||
terminal.focus();
|
terminal.focus();
|
||||||
@@ -150,7 +155,9 @@ export class TerminalPanel extends LitElement {
|
|||||||
private async handleSocketMessage(data: unknown, terminalId: string, terminal: Terminal): Promise<void> {
|
private async handleSocketMessage(data: unknown, terminalId: string, terminal: Terminal): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const message = parseServerMessage(await socketDataToString(data));
|
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") {
|
if (message.type === "exit") {
|
||||||
terminal.writeln(`\r\n[process exited${message.exitCode === undefined ? "" : ` with code ${String(message.exitCode)}`}]`);
|
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);
|
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 {
|
private fitAndNotify(): void {
|
||||||
if (this.fitAddon === undefined || this.terminal === undefined) return;
|
if (this.fitAddon === undefined || this.terminal === undefined) return;
|
||||||
this.fitAddon.fit();
|
this.fitAddon.fit();
|
||||||
@@ -232,7 +250,7 @@ export class TerminalPanel extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type ServerTerminalMessage =
|
type ServerTerminalMessage =
|
||||||
| { type: "output"; data: string }
|
| { type: "output"; data: string; replay?: boolean }
|
||||||
| { type: "exit"; exitCode?: number }
|
| { type: "exit"; exitCode?: number }
|
||||||
| { type: "error"; message: string };
|
| { type: "error"; message: string };
|
||||||
|
|
||||||
@@ -240,12 +258,19 @@ function parseServerMessage(data: string): ServerTerminalMessage {
|
|||||||
const value: unknown = JSON.parse(data);
|
const value: unknown = JSON.parse(data);
|
||||||
if (!isRecord(value)) return { type: "error", message: "Invalid terminal message" };
|
if (!isRecord(value)) return { type: "error", message: "Invalid terminal message" };
|
||||||
const record = value;
|
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"] === "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"] };
|
if (record["type"] === "error" && typeof record["message"] === "string") return { type: "error", message: record["message"] };
|
||||||
return { type: "error", message: "Invalid terminal 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> {
|
async function socketDataToString(data: unknown): Promise<string> {
|
||||||
if (typeof data === "string") return data;
|
if (typeof data === "string") return data;
|
||||||
if (data instanceof ArrayBuffer) return new TextDecoder().decode(data);
|
if (data instanceof ArrayBuffer) return new TextDecoder().decode(data);
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ export function registerTerminalRoutes(app: FastifyInstance, terminals: Terminal
|
|||||||
let detach: (() => void) | undefined;
|
let detach: (() => void) | undefined;
|
||||||
try {
|
try {
|
||||||
detach = terminals.attach(request.params.terminalId, {
|
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 })); },
|
exit: (exitCode) => { socket.send(JSON.stringify({ type: "exit", exitCode })); },
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -76,11 +76,11 @@ export class TerminalService {
|
|||||||
return terminal === undefined ? undefined : toInfo(terminal);
|
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);
|
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);
|
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); };
|
const onExit = (exitCode: number | undefined) => { handlers.exit(exitCode); };
|
||||||
terminal.events.on("output", onOutput);
|
terminal.events.on("output", onOutput);
|
||||||
terminal.events.on("exit", onExit);
|
terminal.events.on("exit", onExit);
|
||||||
|
|||||||
Reference in New Issue
Block a user