From ca27d03c60b7f2e1254aa5f7de1da7eda5c962f0 Mon Sep 17 00:00:00 2001 From: "russell@unturf.com" Date: Mon, 15 Jun 2026 11:32:22 -0400 Subject: [PATCH] repl: route newTab through setActiveTab so toolbar buttons sync MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit newTab() used to mutate state.activeTabId directly, skipping setActiveTab's auto-pause hook AND the toolbar button-state sync. Opening a new tab during a running eval left the send button disabled on the brand-new (idle) tab — fox would land on a tab he couldn't type into. Now newTab pushes the tab onto state.tabs, then awaits setActiveTab which handles both the outgoing-tab auto-pause (if applicable) and the incoming-tab button sync. Verified end-to-end headless: with COOP/COEP enabled, switching from a c-tier eval to a new tab now correctly snapshots the source tab's state, leaves the send button enabled on tab 2, and on switch-back the resumed eval re-fires from the snapshot. --- wasm/dist-repl/repl.js | 23 ++++++++++++++++++----- wasm/repl/repl.js | 23 ++++++++++++++++++----- www/repl/repl.js | 23 ++++++++++++++++++----- 3 files changed, 54 insertions(+), 15 deletions(-) diff --git a/wasm/dist-repl/repl.js b/wasm/dist-repl/repl.js index 51141e7..ab00233 100644 --- a/wasm/dist-repl/repl.js +++ b/wasm/dist-repl/repl.js @@ -146,13 +146,15 @@ function relock() { } // ─── Tabs ─────────────────────────────────────────────────────────── -function newTab(name) { +async function newTab(name) { const id = state.nextTabId++; name = name || `session ${id}`; state.tabs.push({ id, name, tier: "c", transcript: [], inputDraft: "" }); - state.activeTabId = id; - renderAll(); - saveSoon(); + // Route through setActiveTab so the outgoing tab's auto-pause + // fires and the toolbar buttons sync to the freshly-created + // (idle) tab. Without this, opening a new tab during an eval + // leaves the send button disabled and the eval orphaned. + await setActiveTab(id); } function closeTab(id) { @@ -206,8 +208,19 @@ async function setActiveTab(id) { } state.activeTabId = id; renderAll(); - saveSoon(); + // Sync the global toolbar button states to the incoming tab — + // send/cancel/pause are not tab-scoped DOM elements, so a + // switch-away during one tab's eval would otherwise leave the + // newly-active (idle) tab with send disabled. The active tab + // is idle iff nothing in state.pending matches its id. const incoming = activeTab(); + if (incoming) { + const evalRunning = isEvalInFlight(incoming); + sendBtn.disabled = evalRunning; + cancelBtn.disabled = !evalRunning; + pauseBtn.disabled = !evalRunning || !pauseFlagView; + } + saveSoon(); if (incoming && incoming.autoPause) { await autoResumeTab(incoming); } diff --git a/wasm/repl/repl.js b/wasm/repl/repl.js index 51141e7..ab00233 100644 --- a/wasm/repl/repl.js +++ b/wasm/repl/repl.js @@ -146,13 +146,15 @@ function relock() { } // ─── Tabs ─────────────────────────────────────────────────────────── -function newTab(name) { +async function newTab(name) { const id = state.nextTabId++; name = name || `session ${id}`; state.tabs.push({ id, name, tier: "c", transcript: [], inputDraft: "" }); - state.activeTabId = id; - renderAll(); - saveSoon(); + // Route through setActiveTab so the outgoing tab's auto-pause + // fires and the toolbar buttons sync to the freshly-created + // (idle) tab. Without this, opening a new tab during an eval + // leaves the send button disabled and the eval orphaned. + await setActiveTab(id); } function closeTab(id) { @@ -206,8 +208,19 @@ async function setActiveTab(id) { } state.activeTabId = id; renderAll(); - saveSoon(); + // Sync the global toolbar button states to the incoming tab — + // send/cancel/pause are not tab-scoped DOM elements, so a + // switch-away during one tab's eval would otherwise leave the + // newly-active (idle) tab with send disabled. The active tab + // is idle iff nothing in state.pending matches its id. const incoming = activeTab(); + if (incoming) { + const evalRunning = isEvalInFlight(incoming); + sendBtn.disabled = evalRunning; + cancelBtn.disabled = !evalRunning; + pauseBtn.disabled = !evalRunning || !pauseFlagView; + } + saveSoon(); if (incoming && incoming.autoPause) { await autoResumeTab(incoming); } diff --git a/www/repl/repl.js b/www/repl/repl.js index 51141e7..ab00233 100644 --- a/www/repl/repl.js +++ b/www/repl/repl.js @@ -146,13 +146,15 @@ function relock() { } // ─── Tabs ─────────────────────────────────────────────────────────── -function newTab(name) { +async function newTab(name) { const id = state.nextTabId++; name = name || `session ${id}`; state.tabs.push({ id, name, tier: "c", transcript: [], inputDraft: "" }); - state.activeTabId = id; - renderAll(); - saveSoon(); + // Route through setActiveTab so the outgoing tab's auto-pause + // fires and the toolbar buttons sync to the freshly-created + // (idle) tab. Without this, opening a new tab during an eval + // leaves the send button disabled and the eval orphaned. + await setActiveTab(id); } function closeTab(id) { @@ -206,8 +208,19 @@ async function setActiveTab(id) { } state.activeTabId = id; renderAll(); - saveSoon(); + // Sync the global toolbar button states to the incoming tab — + // send/cancel/pause are not tab-scoped DOM elements, so a + // switch-away during one tab's eval would otherwise leave the + // newly-active (idle) tab with send disabled. The active tab + // is idle iff nothing in state.pending matches its id. const incoming = activeTab(); + if (incoming) { + const evalRunning = isEvalInFlight(incoming); + sendBtn.disabled = evalRunning; + cancelBtn.disabled = !evalRunning; + pauseBtn.disabled = !evalRunning || !pauseFlagView; + } + saveSoon(); if (incoming && incoming.autoPause) { await autoResumeTab(incoming); }