From bedba3b75e77a274bd8929ba64e8a6c285c8c0f5 Mon Sep 17 00:00:00 2001 From: centdix Date: Mon, 23 Feb 2026 00:41:23 +0000 Subject: [PATCH] fix: move pane zoom into attach cmd chain for reliable initial zoom - Pass initialPane to attach() so zoom runs inside the shell command chain where tmux is guaranteed to exist (no external race) - Send initialPane in the first resize WS message (atomic, single msg) - Remove pendingPane from WsData (dead code from iterative patching) - Fix unzoom: use shell conditional instead of broken tmux if-shell Co-Authored-By: Claude Opus 4.6 --- dev-dashboard/backend/src/server.ts | 28 ++++++------------- dev-dashboard/backend/src/terminal.ts | 12 +++++--- .../frontend/src/lib/Terminal.svelte | 5 ++-- 3 files changed, 20 insertions(+), 25 deletions(-) diff --git a/dev-dashboard/backend/src/server.ts b/dev-dashboard/backend/src/server.ts index f15f1427d9..e241b5c7ea 100644 --- a/dev-dashboard/backend/src/server.ts +++ b/dev-dashboard/backend/src/server.ts @@ -73,7 +73,6 @@ function errorResponse(message: string, status = 500): Response { interface WsData { worktree: string; attached: boolean; - pendingPane: number | null; } function makeCallbacks(ws: { send: (data: string) => void; readyState: number }) { @@ -101,7 +100,7 @@ Bun.serve({ const wsMatch = url.pathname.match(/^\/ws\/(.+)$/); if (wsMatch) { const worktree = decodeURIComponent(wsMatch[1]); - const upgraded = server.upgrade(req, { data: { worktree, attached: false, pendingPane: null } }); + const upgraded = server.upgrade(req, { data: { worktree, attached: false } }); if (upgraded) return undefined as unknown as Response; return new Response("WebSocket upgrade failed", { status: 400 }); } @@ -128,16 +127,9 @@ Bun.serve({ write(worktree, msg.data); break; case "selectPane": - if (typeof msg.pane === "number") { - if (ws.data.attached) { - // Attach already completed — apply directly - console.log(`[ws:${ts()}] selectPane direct pane=${msg.pane} worktree=${worktree}`); - selectPane(worktree, msg.pane); - } else { - // Attach still in progress — queue for post-attach - ws.data.pendingPane = msg.pane; - console.log(`[ws:${ts()}] selectPane queued pane=${msg.pane} worktree=${worktree}`); - } + if (ws.data.attached && typeof msg.pane === "number") { + console.log(`[ws:${ts()}] selectPane pane=${msg.pane} worktree=${worktree}`); + selectPane(worktree, msg.pane); } break; case "resize": @@ -146,7 +138,11 @@ Bun.serve({ ws.data.attached = true; console.log(`[ws:${ts()}] first resize (attaching) worktree=${worktree} cols=${msg.cols} rows=${msg.rows}`); try { - await attach(worktree, msg.cols, msg.rows); + const initialPane = typeof msg.initialPane === "number" ? msg.initialPane : undefined; + if (initialPane !== undefined) { + console.log(`[ws:${ts()}] initialPane=${initialPane} worktree=${worktree}`); + } + await attach(worktree, msg.cols, msg.rows, initialPane); const { onData, onExit } = makeCallbacks(ws); setCallbacks(worktree, onData, onExit); const scrollback = getScrollback(worktree); @@ -154,12 +150,6 @@ Bun.serve({ if (scrollback) { ws.send(JSON.stringify({ type: "scrollback", data: scrollback })); } - // Apply any pane zoom queued while attach was in progress - if (ws.data.pendingPane !== null) { - console.log(`[ws:${ts()}] applying pendingPane=${ws.data.pendingPane} worktree=${worktree}`); - selectPane(worktree, ws.data.pendingPane); - ws.data.pendingPane = null; - } } catch (err: unknown) { const errMsg = err instanceof Error ? err.message : String(err); console.log(`[ws:${ts()}] attach failed worktree=${worktree}: ${errMsg}`); diff --git a/dev-dashboard/backend/src/terminal.ts b/dev-dashboard/backend/src/terminal.ts index eb9a89345f..bc76758598 100644 --- a/dev-dashboard/backend/src/terminal.ts +++ b/dev-dashboard/backend/src/terminal.ts @@ -51,7 +51,8 @@ function killTmuxSession(name: string): void { export async function attach( worktreeName: string, cols: number, - rows: number + rows: number, + initialPane?: number ): Promise { console.log(`[term:${ts()}] attach(${worktreeName}) cols=${cols} rows=${rows} existing=${sessions.has(worktreeName)}`); if (sessions.has(worktreeName)) { @@ -68,14 +69,17 @@ export async function attach( // Kill stale session with same name if it exists (leftover from previous server run) killTmuxSession(gName); + const paneTarget = `${gName}:${windowTarget}.${initialPane ?? 0}`; const cmd = [ `tmux new-session -d -s "${gName}" -t "${tmuxSession}"`, `tmux set-option -t "${gName}" mouse on`, `tmux set-option -t "${gName}" set-clipboard on`, `tmux select-window -t "${gName}:${windowTarget}"`, - // Unzoom if a previous session left a pane zoomed (zoom state is shared) - `tmux if-shell -t "${gName}:${windowTarget}" "#{window_zoomed_flag}" "resize-pane -Z -t ${gName}:${windowTarget}"`, - `tmux select-pane -t "${gName}:${windowTarget}.0"`, + // Unzoom if a previous session left a pane zoomed (zoom state is shared across grouped sessions) + `if [ "$(tmux display-message -t '${gName}:${windowTarget}' -p '#{window_zoomed_flag}')" = "1" ]; then tmux resize-pane -Z -t '${gName}:${windowTarget}'; fi`, + `tmux select-pane -t "${paneTarget}"`, + // On mobile, zoom the selected pane to fill the window + ...(initialPane !== undefined ? [`tmux resize-pane -Z -t "${paneTarget}"`] : []), `stty rows ${rows} cols ${cols}`, `exec tmux attach-session -t "${gName}"`, ].join(" && "); diff --git a/dev-dashboard/frontend/src/lib/Terminal.svelte b/dev-dashboard/frontend/src/lib/Terminal.svelte index a9c263c039..88ad907230 100644 --- a/dev-dashboard/frontend/src/lib/Terminal.svelte +++ b/dev-dashboard/frontend/src/lib/Terminal.svelte @@ -107,10 +107,11 @@ ws.onopen = () => { fitAddon.fit(); - ws.send(JSON.stringify({ type: "resize", cols: term.cols, rows: term.rows })); + const msg: Record = { type: "resize", cols: term.cols, rows: term.rows }; if (isMobile && initialPane !== undefined) { - sendSelectPane(initialPane); + msg.initialPane = initialPane; } + ws.send(JSON.stringify(msg)); }; ws.onclose = () => {