From 34b31c4caf84a9a6023fe329d3706584b4df594e Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 31 Jul 2026 21:39:02 +0200 Subject: [PATCH] Stop shell terminal width growth --- web/src/ShellTerminal.tsx | 31 ++++++++++++++++++++++++------- web/src/styles.css | 11 ++++++----- 2 files changed, 30 insertions(+), 12 deletions(-) diff --git a/web/src/ShellTerminal.tsx b/web/src/ShellTerminal.tsx index 801ac72..540e19b 100644 --- a/web/src/ShellTerminal.tsx +++ b/web/src/ShellTerminal.tsx @@ -16,6 +16,7 @@ export function ShellTerminal({ csrfToken }: { csrfToken: string }) { useEffect(() => { if (!host.current) return; + const hostElement = host.current; const terminal = new Terminal({ cursorBlink: true, cursorStyle: "bar", @@ -36,8 +37,7 @@ export function ShellTerminal({ csrfToken }: { csrfToken: string }) { }); const fit = new FitAddon(); terminal.loadAddon(fit); - terminal.open(host.current); - fit.fit(); + terminal.open(hostElement); terminal.writeln("\x1b[38;2;185;231;105mPi Car Companion shell\x1b[0m"); terminal.writeln("Restricted local session running as pi-companion.\r\n"); @@ -48,6 +48,24 @@ export function ShellTerminal({ csrfToken }: { csrfToken: string }) { socket.send(JSON.stringify({ type: "resize", cols: terminal.cols, rows: terminal.rows })); } }; + let resizeFrame: number | null = null; + let fittedWidth = -1; + let fittedHeight = -1; + const fitToHost = () => { + resizeFrame = null; + const width = hostElement.clientWidth; + const height = hostElement.clientHeight; + if (width <= 0 || height <= 0 || (width === fittedWidth && height === fittedHeight)) return; + fittedWidth = width; + fittedHeight = height; + fit.fit(); + sendSize(); + }; + const scheduleFit = () => { + if (resizeFrame !== null) window.cancelAnimationFrame(resizeFrame); + resizeFrame = window.requestAnimationFrame(fitToHost); + }; + fitToHost(); socket.addEventListener("open", () => { socket.send(JSON.stringify({ type: "authenticate", csrfToken })); }); @@ -56,6 +74,7 @@ export function ShellTerminal({ csrfToken }: { csrfToken: string }) { const message = JSON.parse(String(event.data)) as ServerMessage; if (message.type === "ready") { setState("ready"); + fitToHost(); sendSize(); terminal.focus(); } else if (message.type === "output") terminal.write(message.data); @@ -80,14 +99,12 @@ export function ShellTerminal({ csrfToken }: { csrfToken: string }) { const input = terminal.onData((data) => { if (socket.readyState === WebSocket.OPEN) socket.send(JSON.stringify({ type: "input", data })); }); - const resize = new ResizeObserver(() => { - fit.fit(); - sendSize(); - }); - resize.observe(host.current); + const resize = new ResizeObserver(scheduleFit); + resize.observe(hostElement); return () => { resize.disconnect(); + if (resizeFrame !== null) window.cancelAnimationFrame(resizeFrame); input.dispose(); socket.close(1000, "Terminal tab closed"); terminal.dispose(); diff --git a/web/src/styles.css b/web/src/styles.css index c4f987f..8636990 100644 --- a/web/src/styles.css +++ b/web/src/styles.css @@ -137,7 +137,8 @@ button:disabled { opacity: 0.55; cursor: not-allowed; } .activity-empty { min-height: 74px; display: grid; place-items: center; padding: 18px; color: var(--muted); font-size: 0.8rem; text-align: center; } .activity-privacy { margin: 0; color: var(--muted); font-size: 0.72rem; text-align: right; } -.settings-page { display: grid; gap: 18px; } +.settings-page { min-width: 0; width: 100%; display: grid; gap: 18px; } +.settings-page > [role="tabpanel"] { min-width: 0; width: 100%; max-width: 100%; } .settings-tabs { width: fit-content; display: flex; gap: 5px; padding: 5px; background: #121610; border: 1px solid var(--line); border-radius: 12px; } .settings-tabs button { min-height: 48px; display: inline-flex; align-items: center; gap: 9px; padding: 0 19px; color: var(--muted); background: transparent; border: 0; border-radius: 8px; font-weight: 700; cursor: pointer; transition: color 160ms ease, background 160ms ease, transform 120ms ease; } .settings-tabs button:hover { color: var(--text); background: var(--surface-raised); } @@ -189,8 +190,8 @@ button:disabled { opacity: 0.55; cursor: not-allowed; } .behavior-list { display: grid; gap: 24px; margin: 26px 0 0; } .behavior-list div { display: grid; gap: 7px; } .behavior-list dd { color: #dce4d6; } -.shell-panel { overflow: hidden; background: var(--surface); border: 1px solid var(--line); border-radius: var(--radius); } -.shell-loading { min-height: 420px; display: grid; place-items: center; color: var(--muted); background: var(--surface); border: 1px solid var(--line); border-radius: var(--radius); } +.shell-panel { min-width: 0; width: 100%; max-width: 100%; overflow: hidden; background: var(--surface); border: 1px solid var(--line); border-radius: var(--radius); } +.shell-loading { min-width: 0; width: 100%; max-width: 100%; min-height: 420px; display: grid; place-items: center; color: var(--muted); background: var(--surface); border: 1px solid var(--line); border-radius: var(--radius); } .shell-toolbar { min-height: 68px; display: flex; align-items: center; justify-content: space-between; gap: 18px; padding: 10px 14px 10px 20px; border-bottom: 1px solid var(--line); } .shell-toolbar > div { display: grid; grid-template-columns: auto auto; align-items: center; gap: 3px 9px; } .shell-toolbar strong { font-size: 0.87rem; } @@ -198,8 +199,8 @@ button:disabled { opacity: 0.55; cursor: not-allowed; } .shell-state { grid-row: 1 / 3; width: 9px; height: 9px; border-radius: 50%; background: #7c8477; } .shell-state.ready { background: var(--accent); box-shadow: 0 0 0 5px rgba(185, 231, 105, 0.08); } .shell-state.error { background: var(--danger); } -.terminal-host { height: min(590px, calc(100dvh - 290px)); min-height: 360px; padding: 16px 12px; background: #0b0e0a; } -.terminal-host .xterm { height: 100%; } +.terminal-host { contain: inline-size; min-width: 0; width: 100%; max-width: 100%; height: min(590px, calc(100dvh - 290px)); min-height: 360px; overflow: hidden; padding: 16px 12px; background: #0b0e0a; } +.terminal-host .xterm { width: 100%; max-width: 100%; height: 100%; } .shell-panel > .settings-note { margin: 0; padding: 14px 20px 16px; border-top: 1px solid var(--line); } .shell-panel code { color: #dce4d6; } .modal-backdrop { position: fixed; inset: 0; z-index: 20; display: grid; place-items: center; padding: 24px; background: rgba(8, 10, 8, 0.78); }