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 <noreply@anthropic.com>
This commit is contained in:
centdix
2026-02-23 00:41:23 +00:00
parent 771d67c849
commit bedba3b75e
3 changed files with 20 additions and 25 deletions
+9 -19
View File
@@ -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<WsData>({
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<WsData>({
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<WsData>({
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<WsData>({
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}`);
+8 -4
View File
@@ -51,7 +51,8 @@ function killTmuxSession(name: string): void {
export async function attach(
worktreeName: string,
cols: number,
rows: number
rows: number,
initialPane?: number
): Promise<string> {
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(" && ");
@@ -107,10 +107,11 @@
ws.onopen = () => {
fitAddon.fit();
ws.send(JSON.stringify({ type: "resize", cols: term.cols, rows: term.rows }));
const msg: Record<string, unknown> = { type: "resize", cols: term.cols, rows: term.rows };
if (isMobile && initialPane !== undefined) {
sendSelectPane(initialPane);
msg.initialPane = initialPane;
}
ws.send(JSON.stringify(msg));
};
ws.onclose = () => {