History

Handle optional terminal dimensions and preserve active terminal size

- make `cols`/`rows` optional in terminal open contracts - default new/restarted sessions to 120x30 when size hints are missing - reuse existing terminal dimensions unless a new size is explicitly requested - only send script terminal size hints when creating a new terminal

Julius Marminge committed 8 months ago

Commit b033c601 parent 2a5525a4 files · +92 −16Browse files

Unified · Split

4 files
  1. apps/server/src/terminalManager.test.ts +59 −0
  2. apps/server/src/terminalManager.ts +25 −12
  3. apps/web/src/components/ChatView.tsx +6 −2
  4. packages/contracts/src/terminal.ts +2 −2
apps/server/src/terminalManager.test.ts+59−0
@@ -196,6 +196,65 @@ describe("TerminalManager", () => { · 195 unmodified lines
196196 manager.dispose();
197197 });
198198
199+ it("resizes running terminal on open when a different size is requested", async () => {
200+ const { manager, ptyAdapter } = makeManager();
201+ await manager.open(openInput({ cols: 100, rows: 24 }));
202+ const process = ptyAdapter.processes[0];
203+ expect(process).toBeDefined();
204+ if (!process) return;
205+
206+ await manager.open(openInput({ cols: 140, rows: 40 }));
207+
208+ expect(process.resizeCalls).toEqual([{ cols: 140, rows: 40 }]);
209+
210+ manager.dispose();
211+ });
212+
213+ it("preserves existing terminal size on open when size is omitted", async () => {
214+ const { manager, ptyAdapter } = makeManager();
215+ await manager.open(openInput({ cols: 100, rows: 24 }));
216+ const ptyProcess = ptyAdapter.processes[0];
217+ expect(ptyProcess).toBeDefined();
218+ if (!ptyProcess) return;
219+
220+ await manager.open({
221+ threadId: "thread-1",
222+ cwd: globalThis.process.cwd(),
223+ });
224+
225+ expect(ptyProcess.resizeCalls).toEqual([]);
226+
227+ ptyProcess.emitExit({ exitCode: 0, signal: 0 });
228+ await manager.open({
229+ threadId: "thread-1",
230+ cwd: globalThis.process.cwd(),
231+ });
232+
233+ const resumedSpawn = ptyAdapter.spawnInputs[1];
234+ expect(resumedSpawn).toBeDefined();
235+ if (!resumedSpawn) return;
236+ expect(resumedSpawn.cols).toBe(100);
237+ expect(resumedSpawn.rows).toBe(24);
238+
239+ manager.dispose();
240+ });
241+
242+ it("uses default dimensions when opening a new terminal without size hints", async () => {
243+ const { manager, ptyAdapter } = makeManager();
244+ await manager.open({
245+ threadId: "thread-1",
246+ cwd: process.cwd(),
247+ });
248+
249+ const spawned = ptyAdapter.spawnInputs[0];
250+ expect(spawned).toBeDefined();
251+ if (!spawned) return;
252+ expect(spawned.cols).toBe(120);
253+ expect(spawned.rows).toBe(30);
254+
255+ manager.dispose();
256+ });
257+
199258 it("supports multiple terminals per thread with isolated sessions", async () => {
200259 const { manager, ptyAdapter } = makeManager();
201260 await manager.open(openInput({ terminalId: "default" }));
apps/server/src/terminalManager.ts+25−12
@@ -26,6 +26,8 @@ import { runProcess } from "./processRunner"; · 25 unmodified lines
2626 const DEFAULT_HISTORY_LINE_LIMIT = 5_000;
2727 const DEFAULT_PERSIST_DEBOUNCE_MS = 40;
2828 const DEFAULT_SUBPROCESS_POLL_INTERVAL_MS = 1_000;
29+const DEFAULT_OPEN_COLS = 120;
30+const DEFAULT_OPEN_ROWS = 30;
2931 const TERMINAL_ENV_BLOCKLIST = new Set(["PORT", "ELECTRON_RENDERER_PORT", "ELECTRON_RUN_AS_NODE"]);
3032
3133 type TerminalSubprocessChecker = (terminalPid: number) => Promise<boolean>;
@@ -67,6 +69,8 @@ interface ShellCandidate { · 35 unmodified lines
6769 args?: string[];
6870 }
6971
72+type TerminalStartInput = TerminalOpenInput & { cols: number; rows: number };
73+
7074 function defaultShellResolver(): string {
7175 if (process.platform === "win32") {
7276 return process.env.ComSpec ?? "cmd.exe";
@@ -334,6 +338,8 @@ export class TerminalManager extends EventEmitter<TerminalManagerEvents> { · 261 unmodified lines
334338 if (!existing) {
335339 await this.flushPersistQueue(input.threadId, input.terminalId);
336340 const history = await this.readHistory(input.threadId, input.terminalId);
341+ const cols = input.cols ?? DEFAULT_OPEN_COLS;
342+ const rows = input.rows ?? DEFAULT_OPEN_ROWS;
337343 const session: TerminalSessionState = {
338344 threadId: input.threadId,
339345 terminalId: input.terminalId,
@@ -344,8 +350,8 @@ export class TerminalManager extends EventEmitter<TerminalManagerEvents> { · 4 unmodified lines
344350 exitCode: null,
345351 exitSignal: null,
346352 updatedAt: new Date().toISOString(),
347− cols: input.cols,
348− rows: input.rows,
353+ cols,
354+ rows,
349355 process: null,
350356 unsubscribeData: null,
351357 unsubscribeExit: null,
@@ -353,12 +359,14 @@ export class TerminalManager extends EventEmitter<TerminalManagerEvents> { · 1 unmodified line
353359 runtimeEnv: normalizedRuntimeEnv(input.env),
354360 };
355361 this.sessions.set(sessionKey, session);
356− this.startSession(session, input, "started");
362+ this.startSession(session, { ...input, cols, rows }, "started");
357363 return this.snapshot(session);
358364 }
359365
360366 const nextRuntimeEnv = normalizedRuntimeEnv(input.env);
361367 const currentRuntimeEnv = existing.runtimeEnv;
368+ const targetCols = input.cols ?? existing.cols;
369+ const targetRows = input.rows ?? existing.rows;
362370 const runtimeEnvChanged =
363371 JSON.stringify(currentRuntimeEnv) !== JSON.stringify(nextRuntimeEnv);
364372
@@ -377,14 +385,14 @@ export class TerminalManager extends EventEmitter<TerminalManagerEvents> { · 12 unmodified lines
377385 }
378386
379387 if (!existing.process) {
380− this.startSession(existing, input, "started");
388+ this.startSession(existing, { ...input, cols: targetCols, rows: targetRows }, "started");
381389 return this.snapshot(existing);
382390 }
383391
384− if (existing.cols !== input.cols || existing.rows !== input.rows) {
385− existing.cols = input.cols;
386− existing.rows = input.rows;
387− existing.process.resize(input.cols, input.rows);
392+ if (existing.cols !== targetCols || existing.rows !== targetRows) {
393+ existing.cols = targetCols;
394+ existing.rows = targetRows;
395+ existing.process.resize(targetCols, targetRows);
388396 existing.updatedAt = new Date().toISOString();
389397 }
390398
@@ -441,6 +449,8 @@ export class TerminalManager extends EventEmitter<TerminalManagerEvents> { · 50 unmodified lines
441449 const sessionKey = toSessionKey(input.threadId, input.terminalId);
442450 let session = this.sessions.get(sessionKey);
443451 if (!session) {
452+ const cols = input.cols ?? DEFAULT_OPEN_COLS;
453+ const rows = input.rows ?? DEFAULT_OPEN_ROWS;
444454 session = {
445455 threadId: input.threadId,
446456 terminalId: input.terminalId,
@@ -451,8 +461,8 @@ export class TerminalManager extends EventEmitter<TerminalManagerEvents> { · 4 unmodified lines
451461 exitCode: null,
452462 exitSignal: null,
453463 updatedAt: new Date().toISOString(),
454− cols: input.cols,
455− rows: input.rows,
464+ cols,
465+ rows,
456466 process: null,
457467 unsubscribeData: null,
458468 unsubscribeExit: null,
@@ -466,9 +476,12 @@ export class TerminalManager extends EventEmitter<TerminalManagerEvents> { · 7 unmodified lines
466476 session.runtimeEnv = normalizedRuntimeEnv(input.env);
467477 }
468478
479+ const cols = input.cols ?? session.cols;
480+ const rows = input.rows ?? session.rows;
481+
469482 session.history = "";
470483 await this.persistHistory(input.threadId, input.terminalId, session.history);
471− this.startSession(session, input, "restarted");
484+ this.startSession(session, { ...input, cols, rows }, "restarted");
472485 return this.snapshot(session);
473486 });
474487 }
@@ -517,7 +530,7 @@ export class TerminalManager extends EventEmitter<TerminalManagerEvents> { · 42 unmodified lines
517530
518531 private startSession(
519532 session: TerminalSessionState,
520− input: TerminalOpenInput,
533+ input: TerminalStartInput,
521534 eventType: "started" | "restarted",
522535 ): void {
523536 this.stopProcess(session);
apps/web/src/components/ChatView.tsx+6−2
@@ -725,9 +725,13 @@ export default function ChatView() { · 724 unmodified lines
725725 threadId: activeThreadId,
726726 terminalId: targetTerminalId,
727727 cwd: targetCwd,
728− cols: SCRIPT_TERMINAL_COLS,
729− rows: SCRIPT_TERMINAL_ROWS,
730728 env: runtimeEnv,
729+ ...(shouldCreateNewTerminal
730+ ? {
731+ cols: SCRIPT_TERMINAL_COLS,
732+ rows: SCRIPT_TERMINAL_ROWS,
733+ }
734+ : {}),
731735 });
732736 await api.terminal.write({
733737 threadId: activeThreadId,
packages/contracts/src/terminal.ts+2−2
@@ -21,8 +21,8 @@ export const terminalSessionInputSchema = terminalThreadInputSchema.extend({ · 20 unmodified lines
2121
2222 export const terminalOpenInputSchema = terminalSessionInputSchema.extend({
2323 cwd: z.string().trim().min(1),
24− cols: terminalColsSchema,
25− rows: terminalRowsSchema,
24+ cols: terminalColsSchema.optional(),
25+ rows: terminalRowsSchema.optional(),
2626 env: terminalEnvSchema.optional(),
2727 });
2828