fix(terminal): create PTYs at the client viewport size, not 80x24 (#2023)
New terminals were always spawned by the daemon at the hardcoded 80x24 default and only resized once the client's first resize arrived. On a slow first mount that leaves a window -- or a stuck state -- where a new terminal renders far smaller than its pane (e.g. vim in a corner). Seed the PTY with the client's measured viewport size at creation time so it is born at the right size and the race window is gone. - protocol: optional `size` on create_terminal_request. Old daemons ignore it and keep 80x24; old clients send nothing. Fully backward compatible, no capability gate. - client: a small last-measured-size cache keyed by serverId+cwd (every terminal in a workspace shares the pane) with a most-recent fallback; written on fit, read at create. - server: thread rows/cols through the controller, terminal manager, and the worker-thread boundary into the existing size-aware createTerminal. Tests: cache logic, protocol back-compat parse, controller forwarding. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
ec93ca866e
commit
2658132384
@@ -0,0 +1,56 @@
|
||||
import { beforeEach, describe, expect, it } from "vitest";
|
||||
import {
|
||||
estimateTerminalViewportSize,
|
||||
rememberTerminalViewportSize,
|
||||
resetTerminalViewportSizeCacheForTests,
|
||||
} from "./terminal-size-cache";
|
||||
|
||||
describe("terminal-size-cache", () => {
|
||||
beforeEach(() => {
|
||||
resetTerminalViewportSizeCacheForTests();
|
||||
});
|
||||
|
||||
it("returns null before any size has been measured", () => {
|
||||
expect(estimateTerminalViewportSize({ serverId: "s1", cwd: "/repo" })).toBeNull();
|
||||
});
|
||||
|
||||
it("returns the last measured size for the same workspace", () => {
|
||||
rememberTerminalViewportSize({ serverId: "s1", cwd: "/repo", size: { rows: 55, cols: 136 } });
|
||||
expect(estimateTerminalViewportSize({ serverId: "s1", cwd: "/repo" })).toEqual({
|
||||
rows: 55,
|
||||
cols: 136,
|
||||
});
|
||||
});
|
||||
|
||||
it("falls back to the most recent size from another workspace", () => {
|
||||
rememberTerminalViewportSize({ serverId: "s1", cwd: "/repo-a", size: { rows: 40, cols: 100 } });
|
||||
// No terminal has been measured in /repo-b yet — the estimate uses the global most-recent size.
|
||||
expect(estimateTerminalViewportSize({ serverId: "s1", cwd: "/repo-b" })).toEqual({
|
||||
rows: 40,
|
||||
cols: 100,
|
||||
});
|
||||
});
|
||||
|
||||
it("prefers the same-workspace size over the global most-recent", () => {
|
||||
rememberTerminalViewportSize({ serverId: "s1", cwd: "/repo-a", size: { rows: 40, cols: 100 } });
|
||||
rememberTerminalViewportSize({ serverId: "s1", cwd: "/repo-b", size: { rows: 55, cols: 136 } });
|
||||
// /repo-a keeps its own measured size even though /repo-b was measured more recently.
|
||||
expect(estimateTerminalViewportSize({ serverId: "s1", cwd: "/repo-a" })).toEqual({
|
||||
rows: 40,
|
||||
cols: 100,
|
||||
});
|
||||
});
|
||||
|
||||
it("keys by server + cwd so different hosts do not collide", () => {
|
||||
rememberTerminalViewportSize({ serverId: "s1", cwd: "/repo", size: { rows: 40, cols: 100 } });
|
||||
rememberTerminalViewportSize({ serverId: "s2", cwd: "/repo", size: { rows: 55, cols: 136 } });
|
||||
expect(estimateTerminalViewportSize({ serverId: "s1", cwd: "/repo" })).toEqual({
|
||||
rows: 40,
|
||||
cols: 100,
|
||||
});
|
||||
expect(estimateTerminalViewportSize({ serverId: "s2", cwd: "/repo" })).toEqual({
|
||||
rows: 55,
|
||||
cols: 136,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,50 @@
|
||||
export interface TerminalViewportSize {
|
||||
rows: number;
|
||||
cols: number;
|
||||
}
|
||||
|
||||
function cacheKey(input: { serverId: string; cwd: string }): string {
|
||||
// JSON-encode the pair so a `:` inside serverId or cwd (e.g. a Windows `C:\` path) can't
|
||||
// make two different (serverId, cwd) pairs collide onto the same key.
|
||||
return JSON.stringify([input.serverId, input.cwd]);
|
||||
}
|
||||
|
||||
const sizeByWorkspace = new Map<string, TerminalViewportSize>();
|
||||
let mostRecentSize: TerminalViewportSize | null = null;
|
||||
|
||||
/**
|
||||
* Remember the latest measured terminal viewport size for a workspace. Every terminal in a
|
||||
* workspace renders into the same pane, so this is the best estimate of the size a *new*
|
||||
* terminal in that workspace will render at — used to seed the PTY at creation time instead
|
||||
* of the daemon's 80x24 default (which otherwise shows briefly, or sticks, until the first
|
||||
* resize lands).
|
||||
*/
|
||||
export function rememberTerminalViewportSize(input: {
|
||||
serverId: string;
|
||||
cwd: string;
|
||||
size: TerminalViewportSize;
|
||||
}): void {
|
||||
const size: TerminalViewportSize = { rows: input.size.rows, cols: input.size.cols };
|
||||
sizeByWorkspace.set(cacheKey(input), size);
|
||||
mostRecentSize = size;
|
||||
}
|
||||
|
||||
/**
|
||||
* Best estimate of the viewport size a new terminal in this workspace will render at:
|
||||
* the last measured size for the same workspace, else the most recently measured size
|
||||
* anywhere (panes are usually the same size across workspaces on one device), else null
|
||||
* when nothing has been measured yet this session — in which case the daemon keeps its
|
||||
* 80x24 default and the first resize corrects it as before.
|
||||
*/
|
||||
export function estimateTerminalViewportSize(input: {
|
||||
serverId: string;
|
||||
cwd: string;
|
||||
}): TerminalViewportSize | null {
|
||||
return sizeByWorkspace.get(cacheKey(input)) ?? mostRecentSize;
|
||||
}
|
||||
|
||||
/** Test-only: clear all remembered sizes so cases don't leak into each other. */
|
||||
export function resetTerminalViewportSizeCacheForTests(): void {
|
||||
sizeByWorkspace.clear();
|
||||
mostRecentSize = null;
|
||||
}
|
||||
Reference in New Issue
Block a user