fix(terminal): a pane on a hidden tab never shrinks its tmux window
A terminal mounted on a hidden tab sits in a 0x0 box; its first fit shrank xterm to 12x6 and the attach carried that size to tmux, so every background pane was squeezed (Claude's TUI redrawn at 12 columns). It now neither fits nor reports a size until shown, attaches with no size, and the server spawns such an attach at the window's own size (tmux.windowSize). A shown tab reports its size even when the fit did not change xterm's. Settings > Agents shows the host's login profiles to staff only: a developer got two 403s from reads that were never theirs.
This commit is contained in:
@@ -465,7 +465,11 @@ export const TerminalView = memo(function TerminalView({
|
||||
gpu = null;
|
||||
}
|
||||
|
||||
fit.fit();
|
||||
// A terminal mounted on a hidden tab sits in a 0×0 box (`display:none`):
|
||||
// fitting it there shrinks xterm to its 12×6 minimum, and that size would
|
||||
// reach tmux with the attach. It stays unfitted until it is shown.
|
||||
const hasBox = () => !!containerRef.current?.clientWidth && !!containerRef.current.clientHeight;
|
||||
if (hasBox()) fit.fit();
|
||||
// Take the keyboard as soon as the pane is on screen: the terminal IS the
|
||||
// page here, and having to click it first meant the first thing typed went
|
||||
// nowhere. A click elsewhere still moves focus away, as in any editor.
|
||||
@@ -605,7 +609,7 @@ export const TerminalView = memo(function TerminalView({
|
||||
*/
|
||||
const sendSizeNow = (drive = false) => {
|
||||
if (!ws || ws.readyState !== WebSocket.OPEN) return;
|
||||
if (term.cols < 2 || term.rows < 2) return;
|
||||
if (!hasBox() || term.cols < 2 || term.rows < 2) return;
|
||||
ws.send(JSON.stringify({ type: "resize", cols: term.cols, rows: term.rows, drive }));
|
||||
};
|
||||
// This browser is the one in use now: back to its own size, and the
|
||||
@@ -642,7 +646,10 @@ export const TerminalView = memo(function TerminalView({
|
||||
// scrollback as torn fragments along the left margin, visible as soon as
|
||||
// you scroll back (issue #33). Happens on every attach: page load,
|
||||
// lane switch, and each reconnect.
|
||||
const size = term.cols > 1 && term.rows > 1 ? `?cols=${term.cols}&rows=${term.rows}` : "";
|
||||
// A hidden terminal sends none: the server attaches it at the window's
|
||||
// own size, so a pane on a background tab is never resized by it.
|
||||
const size =
|
||||
hasBox() && term.cols > 1 && term.rows > 1 ? `?cols=${term.cols}&rows=${term.rows}` : "";
|
||||
const sock = new WebSocket(`${wsBaseUrl}/ws-pty/${encodeURIComponent(runId)}${size}`);
|
||||
ws = sock;
|
||||
wsRef.current = sock;
|
||||
@@ -737,6 +744,9 @@ export const TerminalView = memo(function TerminalView({
|
||||
if (!el || !el.clientWidth || !el.clientHeight) return;
|
||||
if (follow) return scaleToFit(); // the driver's grid, not ours
|
||||
fit.fit(); // a real change fires onResize, which reports it
|
||||
// A tab shown for the first time may fit to the very size xterm already
|
||||
// had (no onResize), yet tmux has never heard it: report it anyway.
|
||||
sendSize();
|
||||
};
|
||||
const resizeObserver = new ResizeObserver(refit);
|
||||
if (containerRef.current) resizeObserver.observe(containerRef.current);
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
* Alt+arrow word moves.
|
||||
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
|
||||
*/
|
||||
import { describe, it, expect, vi, beforeAll, beforeEach, afterEach } from "vitest";
|
||||
import { describe, it, expect, vi, beforeAll, beforeEach, afterEach, onTestFinished } from "vitest";
|
||||
import {
|
||||
render,
|
||||
cleanup,
|
||||
@@ -154,6 +154,17 @@ class MockWebSocket {
|
||||
// @ts-expect-error test override
|
||||
global.WebSocket = MockWebSocket;
|
||||
|
||||
/** Give every element a real box (jsdom's are 0×0, like a terminal on a hidden
|
||||
* tab); returns the restore. */
|
||||
function withBox(width = 800, height = 600) {
|
||||
const w = vi.spyOn(HTMLElement.prototype, "clientWidth", "get").mockReturnValue(width);
|
||||
const h = vi.spyOn(HTMLElement.prototype, "clientHeight", "get").mockReturnValue(height);
|
||||
return () => {
|
||||
w.mockRestore();
|
||||
h.mockRestore();
|
||||
};
|
||||
}
|
||||
|
||||
describe("TerminalView", () => {
|
||||
// xterm is a lazy chunk; once loaded every terminal sets up synchronously.
|
||||
beforeAll(() => loadXterm());
|
||||
@@ -185,11 +196,51 @@ describe("TerminalView", () => {
|
||||
// it: tmux resizes the window to the newest client, so attaching at the
|
||||
// server's default 80x24 makes claude redraw its UI at 80 columns and
|
||||
// leaves that width in the scrollback (issue #33).
|
||||
render(<TerminalView runId="agenthub-lane-1" wsBaseUrl="ws://localhost:4820" />);
|
||||
expect(MockWebSocket.instances).toHaveLength(1);
|
||||
expect(MockWebSocket.instances[0]!.url).toBe(
|
||||
"ws://localhost:4820/ws-pty/agenthub-lane-1?cols=131&rows=28"
|
||||
);
|
||||
const restore = withBox();
|
||||
try {
|
||||
render(<TerminalView runId="agenthub-lane-1" wsBaseUrl="ws://localhost:4820" />);
|
||||
expect(MockWebSocket.instances).toHaveLength(1);
|
||||
expect(MockWebSocket.instances[0]!.url).toBe(
|
||||
"ws://localhost:4820/ws-pty/agenthub-lane-1?cols=131&rows=28"
|
||||
);
|
||||
} finally {
|
||||
restore();
|
||||
}
|
||||
});
|
||||
|
||||
it("a terminal on a hidden tab sends no size until it is shown", () => {
|
||||
// Its box is 0×0 (display:none): fitting there shrank xterm to 12×6 and
|
||||
// that size resized the real tmux pane under every other client.
|
||||
let observed: (() => void) | null = null;
|
||||
const RO = globalThis.ResizeObserver;
|
||||
// @ts-expect-error test override
|
||||
globalThis.ResizeObserver = class {
|
||||
constructor(cb: () => void) {
|
||||
observed = cb;
|
||||
}
|
||||
observe() {}
|
||||
disconnect() {}
|
||||
};
|
||||
vi.useFakeTimers();
|
||||
let restore = () => {};
|
||||
try {
|
||||
render(<TerminalView runId="agenthub-lane-1" wsBaseUrl="ws://localhost:4820" />);
|
||||
const ws = MockWebSocket.instances[0]!;
|
||||
expect(ws.url).toBe("ws://localhost:4820/ws-pty/agenthub-lane-1");
|
||||
ws.onopen?.();
|
||||
expect(ws.sent).toEqual([]);
|
||||
// The tab is shown: the box gets a size and its size reaches tmux.
|
||||
restore = withBox();
|
||||
observed!();
|
||||
vi.advanceTimersByTime(200);
|
||||
expect(ws.sent).toEqual([
|
||||
JSON.stringify({ type: "resize", cols: 131, rows: 28, drive: false }),
|
||||
]);
|
||||
} finally {
|
||||
restore();
|
||||
vi.useRealTimers();
|
||||
globalThis.ResizeObserver = RO;
|
||||
}
|
||||
});
|
||||
|
||||
it("omits the size when the terminal has none yet (a collapsed console is 0x0)", () => {
|
||||
@@ -466,17 +517,24 @@ describe("TerminalView", () => {
|
||||
// next row's left margin, and tmux never repaints a row it believes
|
||||
// unchanged — the stray characters glued to the left edge after scrolling.
|
||||
it("reports its size when the socket opens", () => {
|
||||
render(<TerminalView runId="agenthub-lane-1" wsBaseUrl="ws://localhost:4820" />);
|
||||
const ws = MockWebSocket.instances[0]!;
|
||||
ws.onopen?.();
|
||||
expect(ws.sent).toContain(
|
||||
JSON.stringify({ type: "resize", cols: 131, rows: 28, drive: false })
|
||||
);
|
||||
const restore = withBox();
|
||||
try {
|
||||
render(<TerminalView runId="agenthub-lane-1" wsBaseUrl="ws://localhost:4820" />);
|
||||
const ws = MockWebSocket.instances[0]!;
|
||||
ws.onopen?.();
|
||||
expect(ws.sent).toContain(
|
||||
JSON.stringify({ type: "resize", cols: 131, rows: 28, drive: false })
|
||||
);
|
||||
} finally {
|
||||
restore();
|
||||
}
|
||||
});
|
||||
|
||||
it("reports a size change xterm made on its own, with no container resize", async () => {
|
||||
// Webfont load and page zoom both move cols without resizing the element
|
||||
// ResizeObserver watches, which is why onResize is the hook.
|
||||
const restore = withBox();
|
||||
onTestFinished(restore);
|
||||
render(<TerminalView runId="agenthub-lane-1" wsBaseUrl="ws://localhost:4820" />);
|
||||
const ws = MockWebSocket.instances[0]!;
|
||||
ws.onopen?.();
|
||||
|
||||
@@ -252,7 +252,7 @@ export function SettingsHub({ section }: { section?: string } = {}) {
|
||||
};
|
||||
}, [location.key, location.hash]);
|
||||
|
||||
const { isAdmin, local } = usePermissions();
|
||||
const { isAdmin, local, seesAll } = usePermissions();
|
||||
const authState = useAuth().state;
|
||||
const signedIn = authState.mode === "server" && !!authState.user;
|
||||
const enabled = (id: string) => aiTools.byId(id)?.enabled !== false;
|
||||
@@ -307,8 +307,9 @@ export function SettingsHub({ section }: { section?: string } = {}) {
|
||||
<AgentInstructionsSettings />
|
||||
<SessionDefaultsSettings />
|
||||
<AiToolsSettings settingsHref={toolHref} />
|
||||
{/* Every AI tool's login profiles, linked to the account pool. */}
|
||||
<ToolProfilesSettings />
|
||||
{/* Every AI tool's login profiles, linked to the account pool: the
|
||||
host's logins, a staff-only read (a developer would get 403s). */}
|
||||
{seesAll && <ToolProfilesSettings />}
|
||||
{/* The Hub's install switch: server mode, an admin's (it hides itself). */}
|
||||
<ArtifactsHubSettings />
|
||||
{/* The host's omp login for members: server mode + S3, an admin's. */}
|
||||
|
||||
+1
-1
@@ -3453,7 +3453,7 @@ Sent when a notification is created.
|
||||
|
||||
#### /ws-pty/:runId — PTY frames
|
||||
|
||||
A dedicated binary WebSocket stream (not JSON-framed) for tmux-backed PTY transport. Established by the Workspace TerminalView component on workspace load; endpoint is `/ws-pty/:runId` where `runId` comes from `POST /api/run`. Frames are raw PTY output (stdin echoes, command output, prompt updates, terminal control sequences) as binary blobs; the client feeds each frame to xterm.js for live rendering. The same tmux session can have multiple simultaneous clients (browser Workspace, `agenthub workspace shell`, other tools), all receiving the same frames live-synced. **The attach URL carries the client's size** (`/ws-pty/:runId?cols=<n>&rows=<n>`, integers in `[2, 1000]`, else 80×24): tmux resizes the window to whichever client attached most recently, so spawning the attach at a default size and correcting it from the first `resize` message a beat later made the pane's program redraw its whole UI at 80 columns — and everything painted at that width stayed in the scrollback, surfacing as torn left-margin fragments the moment the user scrolled back (issue #33). Resize events after that: the client sends a `{"type":"resize","cols","rows"}` text frame and the server applies it as a `TIOCSWINSZ` ioctl down the pane's pty, so window-sensitive commands (e.g. pagers, text editors) adapt to the viewport size. The client reports its size from **xterm's own `onResize`**, not from the container's `ResizeObserver` — cols also move when the webfont finishes loading or the page zooms, neither of which resizes the observed element — and takes the lead whenever the terminal regains focus. **Several browsers on one pane share ONE size** (`server/lib/pty-attach.js`): tmux's `window-size latest` snapped the pane to whoever typed last, so two viewers on screens of different sizes kept resizing it under each other and one saw it cropped. The server names a DRIVER per pane — the first attach, then whichever sends `{"type":"resize","cols","rows","drive":true}` (the client does on focus and on a keystroke, never on a mouse report). Every other attach is a follower: its PTY is held at the driver's size, a follower's own `resize` without `drive` is remembered but not applied, and the server tells it `{"type":"size","cols","rows"}` — the client resizes xterm to that grid and scales it down with CSS to fit, so the browser in use decides the size and the others just shrink. A follower attaches at the driver's size, so joining never resizes the pane. When the driver's socket closes the rest get `{"type":"lead"}` and size themselves again. On Windows every attach is also sent `{"type":"wheel"}`: psmux never turns mouse tracking on for its client, so the browser sends the SGR wheel report (`ESC[<64;1;1M` up, `ESC[<65;1;1M` down) itself while xterm reports no tracking — psmux scrolls, or forwards it to a pane app that tracks the mouse (Claude Code fullscreen). Driver tracking is per server process: a real terminal (`agenthub workspace shell`) or a second dashboard on the same tmux still follows tmux's own `latest` rule. Keeping the two widths equal is not cosmetic: a tmux pane **wider** than xterm makes every over-long row autowrap its tail onto the next row's left margin, and tmux never repaints a row it believes unchanged — so stray characters stay glued to the left edge of blank lines, visible as soon as a scroll repaints history. A zero size (the console collapses with CSS, leaving the container 0×0) is never reported. The WebSocket connection inherits the same loopback same-origin guard and optional token auth as other `/api/*` routes. The client re-attaches automatically 1.5 s after an unexpected close (server restart, network blip) — tmux keeps the session alive, so the re-attach repaints the pane in full. It stops retrying only after the server's one-shot `{"type":"exit"}` frame, which means the pane process itself ended — the Workspace takes that frame as the signal to restore the run setup form, since there is nothing left to attach to. Without that retry a dropped socket left the pane frozen on its last painted frame: it still looked live, but no keystroke and no mouse report (wheel scroll included) reached tmux again.
|
||||
A dedicated binary WebSocket stream (not JSON-framed) for tmux-backed PTY transport. Established by the Workspace TerminalView component on workspace load; endpoint is `/ws-pty/:runId` where `runId` comes from `POST /api/run`. Frames are raw PTY output (stdin echoes, command output, prompt updates, terminal control sequences) as binary blobs; the client feeds each frame to xterm.js for live rendering. The same tmux session can have multiple simultaneous clients (browser Workspace, `agenthub workspace shell`, other tools), all receiving the same frames live-synced. **The attach URL carries the client's size** (`/ws-pty/:runId?cols=<n>&rows=<n>`, integers in `[2, 1000]`). A terminal with no size to give — one on a hidden tab, whose box is 0×0 — names none and reports none until it is shown, and its attach is spawned at the window's CURRENT size (`tmux.windowSize`, 80×24 only when tmux cannot answer), so a background tab never shrinks the pane (it used to fit to xterm's 12×6 minimum and resize tmux to that): tmux resizes the window to whichever client attached most recently, so spawning the attach at a default size and correcting it from the first `resize` message a beat later made the pane's program redraw its whole UI at 80 columns — and everything painted at that width stayed in the scrollback, surfacing as torn left-margin fragments the moment the user scrolled back (issue #33). Resize events after that: the client sends a `{"type":"resize","cols","rows"}` text frame and the server applies it as a `TIOCSWINSZ` ioctl down the pane's pty, so window-sensitive commands (e.g. pagers, text editors) adapt to the viewport size. The client reports its size from **xterm's own `onResize`**, not from the container's `ResizeObserver` — cols also move when the webfont finishes loading or the page zooms, neither of which resizes the observed element — and takes the lead whenever the terminal regains focus. **Several browsers on one pane share ONE size** (`server/lib/pty-attach.js`): tmux's `window-size latest` snapped the pane to whoever typed last, so two viewers on screens of different sizes kept resizing it under each other and one saw it cropped. The server names a DRIVER per pane — the first attach, then whichever sends `{"type":"resize","cols","rows","drive":true}` (the client does on focus and on a keystroke, never on a mouse report). Every other attach is a follower: its PTY is held at the driver's size, a follower's own `resize` without `drive` is remembered but not applied, and the server tells it `{"type":"size","cols","rows"}` — the client resizes xterm to that grid and scales it down with CSS to fit, so the browser in use decides the size and the others just shrink. A follower attaches at the driver's size, so joining never resizes the pane. When the driver's socket closes the rest get `{"type":"lead"}` and size themselves again. On Windows every attach is also sent `{"type":"wheel"}`: psmux never turns mouse tracking on for its client, so the browser sends the SGR wheel report (`ESC[<64;1;1M` up, `ESC[<65;1;1M` down) itself while xterm reports no tracking — psmux scrolls, or forwards it to a pane app that tracks the mouse (Claude Code fullscreen). Driver tracking is per server process: a real terminal (`agenthub workspace shell`) or a second dashboard on the same tmux still follows tmux's own `latest` rule. Keeping the two widths equal is not cosmetic: a tmux pane **wider** than xterm makes every over-long row autowrap its tail onto the next row's left margin, and tmux never repaints a row it believes unchanged — so stray characters stay glued to the left edge of blank lines, visible as soon as a scroll repaints history. A zero size (the console collapses with CSS, leaving the container 0×0) is never reported. The WebSocket connection inherits the same loopback same-origin guard and optional token auth as other `/api/*` routes. The client re-attaches automatically 1.5 s after an unexpected close (server restart, network blip) — tmux keeps the session alive, so the re-attach repaints the pane in full. It stops retrying only after the server's one-shot `{"type":"exit"}` frame, which means the pane process itself ended — the Workspace takes that frame as the signal to restore the run setup form, since there is nothing left to attach to. Without that retry a dropped socket left the pane frozen on its last painted frame: it still looked live, but no keystroke and no mouse report (wheel scroll included) reached tmux again.
|
||||
|
||||
#### cc_config_changed
|
||||
|
||||
|
||||
@@ -235,6 +235,26 @@ describe("pty-attach", () => {
|
||||
assert.deepEqual(JSON.parse(a.__sent.at(-1).data), { type: "lead" });
|
||||
});
|
||||
|
||||
it("attaches a terminal that names no size at the window's own size", () => {
|
||||
// A terminal on a hidden tab is 0×0 and sends no size; spawning its attach
|
||||
// at a default resized the pane under every other client.
|
||||
const opts = [];
|
||||
ptyAttach.__setSpawnImpl((_f, _a, o) => {
|
||||
opts.push(o);
|
||||
return makeFakePty();
|
||||
});
|
||||
tmux.__setExecImpl((args) => (args.includes("#{window_width} #{window_height}") ? "141 48\n" : ""));
|
||||
ptyAttach.attach(makeFakeWs(), "agenthub-lane-1", { cols: undefined, rows: undefined });
|
||||
assert.deepEqual([opts[0].cols, opts[0].rows], [141, 48]);
|
||||
// tmux cannot answer: the old default, never a crash.
|
||||
ptyAttach.__resetGroups();
|
||||
tmux.__setExecImpl(() => {
|
||||
throw new Error("no server");
|
||||
});
|
||||
ptyAttach.attach(makeFakeWs(), "agenthub-lane-2", { cols: undefined, rows: undefined });
|
||||
assert.deepEqual([opts[1].cols, opts[1].rows], [80, 24]);
|
||||
});
|
||||
|
||||
it("survives a stopped pane: pipe errors, typing and resizes after the attach exited", () => {
|
||||
// Windows node-pty re-throws a read-pipe error nobody listens for, and its
|
||||
// write pipe has no listener at all: Stop (kill-session) ended the attach
|
||||
|
||||
@@ -19,13 +19,17 @@ describe("sizeFromQuery — the attach carries the client's real size", () => {
|
||||
assert.deepEqual(sizeFromQuery(query("?cols=226&rows=29")), { cols: 226, rows: 29 });
|
||||
});
|
||||
|
||||
it("falls back to 80x24 when the client named no size", () => {
|
||||
assert.deepEqual(sizeFromQuery(query("")), { cols: 80, rows: 24 });
|
||||
// No size (a terminal on a hidden tab) stays no size: the attach then takes
|
||||
// the window's own, never a default that would resize the pane.
|
||||
const none = { cols: undefined, rows: undefined };
|
||||
|
||||
it("names no size when the client named none", () => {
|
||||
assert.deepEqual(sizeFromQuery(query("")), none);
|
||||
});
|
||||
|
||||
it("refuses anything out of bounds — this number reaches an ioctl", () => {
|
||||
assert.deepEqual(sizeFromQuery(query("?cols=0&rows=-5")), { cols: 80, rows: 24 });
|
||||
assert.deepEqual(sizeFromQuery(query("?cols=99999&rows=99999")), { cols: 80, rows: 24 });
|
||||
assert.deepEqual(sizeFromQuery(query("?cols=abc&rows=12.5")), { cols: 80, rows: 12 });
|
||||
assert.deepEqual(sizeFromQuery(query("?cols=0&rows=-5")), none);
|
||||
assert.deepEqual(sizeFromQuery(query("?cols=99999&rows=99999")), none);
|
||||
assert.deepEqual(sizeFromQuery(query("?cols=abc&rows=12")), none);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -162,12 +162,16 @@ function attach(ws, runId, { cols, rows, platform = process.platform }) {
|
||||
const [file, args] = require("./drop-caps").ptyArgv(
|
||||
...(tmux.memberAttach(runId) || [attachBinary(), ["attach-session", "-t", tmux.session(runId)]])
|
||||
);
|
||||
// An attach with no size of its own (a terminal on a hidden tab, 0×0) takes
|
||||
// the window's current size, so attaching it never shrinks the pane.
|
||||
const own =
|
||||
cols && rows ? { cols, rows } : (!lead && tmux.windowSize(runId)) || { cols: 80, rows: 24 };
|
||||
// A follower attaches at the driver's size: tmux sizes the window to the
|
||||
// newest client, so its own size would resize the driver's pane first.
|
||||
const pty = spawnImpl(file, args, {
|
||||
name: "xterm-256color",
|
||||
cols: lead ? lead.cols : cols || 80,
|
||||
rows: lead ? lead.rows : rows || 24,
|
||||
cols: lead ? lead.cols : own.cols,
|
||||
rows: lead ? lead.rows : own.rows,
|
||||
// Windows' inbox ConPTY re-renders psmux's output instead of passing it
|
||||
// through, and wherever its glyph widths disagree with xterm.js's (⏺, ✻,
|
||||
// Vietnamese) it leaves stray cells: a torn left edge, bars mid-screen.
|
||||
@@ -176,7 +180,7 @@ function attach(ws, runId, { cols, rows, platform = process.platform }) {
|
||||
...(platform === "win32" && { useConptyDll: true }),
|
||||
});
|
||||
swallowPipeErrors(pty);
|
||||
const me = { ws, pty, cols: cols || 80, rows: rows || 24, exited: false };
|
||||
const me = { ws, pty, cols: own.cols, rows: own.rows, exited: false };
|
||||
group.viewers.add(me);
|
||||
if (lead) tell(me, { type: "size", cols: lead.cols, rows: lead.rows });
|
||||
else group.driver = me;
|
||||
|
||||
@@ -723,6 +723,25 @@ function leaveCopyMode(name) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The session's window size as tmux has it now, `{cols, rows}`, or null when
|
||||
* tmux can't answer. An attach that brings no size of its own (a terminal on
|
||||
* a hidden tab) is spawned at this, so attaching it resizes nothing.
|
||||
*/
|
||||
function windowSize(name) {
|
||||
try {
|
||||
const [cols, rows] = String(
|
||||
onServerOf(name, ["display-message", "-p", "-t", pane(name), "#{window_width} #{window_height}"])
|
||||
)
|
||||
.trim()
|
||||
.split(" ")
|
||||
.map(Number);
|
||||
return cols >= 2 && rows >= 2 ? { cols, rows } : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The command currently running in the session's active pane (`bash`, `zsh`,
|
||||
* `claude`, …). Null when tmux can't answer — callers treat that as "unknown,
|
||||
@@ -1013,6 +1032,7 @@ module.exports = {
|
||||
enableMouse,
|
||||
leaveCopyMode,
|
||||
paneCommand,
|
||||
windowSize,
|
||||
sendCommand,
|
||||
sendText,
|
||||
pasteText,
|
||||
|
||||
+7
-3
@@ -115,13 +115,17 @@ const PTY_PATH_RE = /^\/ws-pty\/(agenthub-lane-\d+(?:-[1-9])?)$/;
|
||||
* `resize` message lands, leaving 80-column wreckage in the scrollback that
|
||||
* shows up as torn left-margin fragments when the user scrolls back
|
||||
* (issue #33). Bounded, because this number reaches a `TIOCSWINSZ` ioctl.
|
||||
* No size at all (a terminal on a hidden tab has none to give) stays
|
||||
* `undefined`: the attach then takes the window's own size and resizes nothing.
|
||||
*/
|
||||
function sizeFromQuery(params) {
|
||||
const read = (name, fallback) => {
|
||||
const read = (name) => {
|
||||
const parsed = Number.parseInt(params.get(name) ?? "", 10);
|
||||
return Number.isInteger(parsed) && parsed >= 2 && parsed <= 1000 ? parsed : fallback;
|
||||
return Number.isInteger(parsed) && parsed >= 2 && parsed <= 1000 ? parsed : undefined;
|
||||
};
|
||||
return { cols: read("cols", 80), rows: read("rows", 24) };
|
||||
const cols = read("cols");
|
||||
const rows = read("rows");
|
||||
return cols && rows ? { cols, rows } : { cols: undefined, rows: undefined };
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user