Fix Shift+Enter in terminal input modes (#999)

* Fix Shift+Enter terminal input modes

* Advertise terminal input capabilities
This commit is contained in:
Mohamed Boudra
2026-05-14 17:48:22 +08:00
committed by GitHub
parent eb7fa02ac8
commit e1ab779fe0
16 changed files with 702 additions and 21 deletions
@@ -681,7 +681,13 @@ export class TerminalSessionController {
return;
}
if (!this.terminalManager?.getTerminal(activeStream.terminalId)) {
const terminalManager = this.terminalManager;
if (!terminalManager) {
this.detachStream(activeStream.terminalId, { emitExit: true });
return;
}
const terminal = terminalManager.getTerminal(activeStream.terminalId);
if (!terminal) {
this.detachStream(activeStream.terminalId, { emitExit: true });
return;
}
@@ -689,7 +695,7 @@ export class TerminalSessionController {
activeStream.outputCoalescer.flush();
activeStream.snapshotInFlight = true;
try {
const snapshot = await this.terminalManager.getTerminalState(activeStream.terminalId);
const snapshot = await terminalManager.getTerminalState(activeStream.terminalId);
if (this.activeStreams.get(activeStream.slot) !== activeStream) {
return;
}
@@ -706,6 +712,11 @@ export class TerminalSessionController {
}),
);
const replayPreamble = terminal.getReplayPreamble();
if (replayPreamble.length > 0) {
activeStream.outputCoalescer.handle(replayPreamble);
}
const bufferedOutputs = activeStream.bufferedOutputs.splice(
0,
activeStream.bufferedOutputs.length,
@@ -234,6 +234,7 @@ describe.skipIf(isPlatform("win32"))("terminal POSIX-only", () => {
});
expect(resolvedEnv.TERM).toBe("xterm-256color");
expect(resolvedEnv.TERM_PROGRAM).toBe("kitty");
expect(resolvedEnv.PASEO_ZSH_ZDOTDIR).toBe("/tmp/paseo-zdotdir");
expect(resolvedEnv.ZDOTDIR).not.toBe("/tmp/paseo-zdotdir");
expect(existsSync(join(resolvedEnv.ZDOTDIR, ".zshenv"))).toBe(true);
+14
View File
@@ -9,6 +9,7 @@ import { fileURLToPath } from "node:url";
import { createExternalProcessEnv } from "../server/paseo-env.js";
import { writePrivateFileAtomicSync } from "../server/private-files.js";
import type { TerminalCell, TerminalState } from "../shared/messages.js";
import { TerminalInputModeTracker } from "../shared/terminal-input-mode.js";
const { Terminal } = xterm;
const require = createRequire(import.meta.url);
@@ -54,6 +55,7 @@ export interface TerminalSession {
getSize(): { rows: number; cols: number };
getState(): TerminalState;
getStateSnapshot(): TerminalStateSnapshot;
getReplayPreamble(): string;
getTitle(): string | undefined;
getExitInfo(): TerminalExitInfo | null;
kill(): void;
@@ -219,6 +221,7 @@ export function buildTerminalEnvironment(
): Record<string, string> {
const baseEnv: Record<string, string> = createExternalProcessEnv(process.env, input.env, {
TERM: "xterm-256color",
TERM_PROGRAM: "kitty",
});
if (basename(input.shell) !== "zsh") {
@@ -551,6 +554,7 @@ export async function createTerminal(options: CreateTerminalOptions): Promise<Te
let pendingInput = "";
let inputFlushImmediate: ReturnType<typeof setImmediate> | null = null;
let stateRevision = 0;
const inputModeTracker = new TerminalInputModeTracker();
// Create xterm.js headless terminal
const terminal = new Terminal({
@@ -703,6 +707,7 @@ export async function createTerminal(options: CreateTerminalOptions): Promise<Te
}
disposed = true;
pendingInput = "";
inputModeTracker.reset();
if (inputFlushImmediate) {
clearImmediate(inputFlushImmediate);
inputFlushImmediate = null;
@@ -735,6 +740,10 @@ export async function createTerminal(options: CreateTerminalOptions): Promise<Te
// Pipe PTY output to terminal emulator
ptyProcess.onData((data) => {
if (killed) return;
const inputModeUpdate = inputModeTracker.feed(data);
for (const response of inputModeUpdate.responses) {
ptyProcess.write(response);
}
recentOutputText = `${recentOutputText}${data}`;
if (recentOutputText.length > TERMINAL_EXIT_OUTPUT_CHAR_LIMIT) {
recentOutputText = recentOutputText.slice(-TERMINAL_EXIT_OUTPUT_CHAR_LIMIT);
@@ -803,6 +812,10 @@ export async function createTerminal(options: CreateTerminalOptions): Promise<Te
};
}
function getReplayPreamble(): string {
return inputModeTracker.getPreamble();
}
function writeInputToPty(data: string): void {
ptyProcess.write(data);
}
@@ -1034,6 +1047,7 @@ export async function createTerminal(options: CreateTerminalOptions): Promise<Te
getSize,
getState,
getStateSnapshot,
getReplayPreamble,
getTitle,
getExitInfo,
kill,
@@ -2,6 +2,7 @@ import { fork } from "node:child_process";
import { fileURLToPath } from "node:url";
import { randomUUID } from "node:crypto";
import type { TerminalState } from "../shared/messages.js";
import { TerminalInputModeTracker } from "../shared/terminal-input-mode.js";
import type {
ClientMessage,
ServerMessage,
@@ -43,6 +44,7 @@ interface PendingRequest {
interface WorkerTerminalRecord {
info: WorkerTerminalInfo;
state: TerminalState;
inputModeTracker: TerminalInputModeTracker;
exitInfo: TerminalExitInfo | null;
messageListeners: Set<(msg: ServerMessage) => void>;
exitListeners: Set<(info: TerminalExitInfo) => void>;
@@ -169,6 +171,7 @@ export function createWorkerTerminalManager(
const record: WorkerTerminalRecord = {
info: cloneTerminalInfo(input.info),
state: input.state,
inputModeTracker: new TerminalInputModeTracker(),
exitInfo: null,
messageListeners: new Set(),
exitListeners: new Set(),
@@ -247,6 +250,9 @@ export function createWorkerTerminalManager(
revision: 0,
};
},
getReplayPreamble(): string {
return record.inputModeTracker.getPreamble();
},
getTitle(): string | undefined {
return record.info.title;
},
@@ -302,6 +308,9 @@ export function createWorkerTerminalManager(
if (message.message.type === "snapshot") {
record.state = message.message.state;
}
if (message.message.type === "output") {
record.inputModeTracker.feed(message.message.data);
}
for (const listener of Array.from(record.messageListeners)) {
listener(message.message);
}