From c4dec213dee6b61ea936bd5b1be68beb3b0c6af1 Mon Sep 17 00:00:00 2001 From: Patrick Buckley Date: Sun, 7 Jun 2026 22:50:53 -0700 Subject: [PATCH] fix(ui): address /review of the workstream-lifecycle change MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Multi-stage review (find → verify → sanity) of b8914854 found one critical bug plus four minor + one nit; all confirmed against source and fixed: - CRITICAL — the interactive launcher's "Specific node" pick was unusable: selecting a node fired the composer `change` event → onChange → _applyLauncherFields → _populateLauncherNodes → setOptionChoices, which rebuilds the ). The selection MUST be captured + restored across the + # rebuild, else a Specific-node session can never be launched. + assert 'const previous = _homeCoordComposer.getOptionValue("node_id")' in app, ( + "_populateLauncherNodes must snapshot the current node pick before rebuild" + ) + assert 'if (previous) _homeCoordComposer.setOptionValue("node_id", previous)' in app, ( + "_populateLauncherNodes must restore the node pick after rebuild (bug-1)" + ) def test_pane_persists_meta_for_rehydrate() -> None: @@ -364,7 +374,7 @@ def test_step5_interactive_pane_registered_and_wired() -> None: "interactive is ESM — the shell imports it (as it now does the coordinator)" ) assert 'registerType("interactive"' in shell, "shell must register the interactive pane type" - assert "createInteractivePane(this.bodyEl, id, {" in shell, ( + assert "createInteractivePane(pane.bodyEl, id, {" in shell, ( "first activate must build the controller into the pane body" ) # Rehydrate-safety: the node is RESOLVED + the session (re)opened before the @@ -377,6 +387,12 @@ def test_step5_interactive_pane_registered_and_wired() -> None: assert "pm.setPaneMeta(pane.id" in shell, ( "the resolved node must be persisted so a reload restores the same node" ) + # Hot-path optimisation (review perf-1): a LIVE session (Tier-1 already names + # its node) connects directly — only the dormant/reload case pays the + # resolve+open round-trip. + assert "const liveNode = caps.cluster ? nodeForWs(id)" in shell, ( + "a live ws (Tier-1 names its node) must connect directly, skipping POST /open" + ) # Focus-tracking lifecycle: connect/deactivate/destroy + login re-arm. for hook in ("this._ctl.connect()", "this._ctl.deactivate()", "this._ctl.destroy()"): assert hook in shell, f"interactive pane missing lifecycle {hook!r}" diff --git a/turnstone/console/static/app.js b/turnstone/console/static/app.js index f647db03..e30f79a3 100644 --- a/turnstone/console/static/app.js +++ b/turnstone/console/static/app.js @@ -1064,6 +1064,13 @@ function _applyLauncherFields() { // nodes. Re-read on each reveal so a node that just (dis)appeared is current. function _populateLauncherNodes() { if (!_homeCoordComposer) return; + // Preserve the operator's current pick across the rebuild: selecting a node + // fires the composer `change` event → onChange → _applyLauncherFields → here, + // and setOptionChoices() resets the