From ba74d3515b04306f7c7b86629c5453097d8cf36c Mon Sep 17 00:00:00 2001 From: Miguel Palhas Date: Fri, 4 Sep 2026 08:15:35 +0100 Subject: [PATCH] fix(maestro): keep sidebar accent on the picked session while previewing A preview moved the active tab's focus, and the sidebar took that as the focused session, so hovering another row dropped the accent from the row the operator had actually opened. Report the pre-preview focus instead. --- docs/PREVIEW.md | 1 + web/src/multiview.ts | 20 +++++++++++++++++--- web/src/preview.test.ts | 34 +++++++++++++++++++++++++++++++++- 3 files changed, 51 insertions(+), 4 deletions(-) diff --git a/docs/PREVIEW.md b/docs/PREVIEW.md index 5424960..bf98430 100644 --- a/docs/PREVIEW.md +++ b/docs/PREVIEW.md @@ -47,6 +47,7 @@ Web client (`--preview` and friends in `web/src/styles.css`): | The canvas | A `preview` chip, top right, and a dashed inset ring. | | A throwaway tab | Dashed border, italic label. | | The sidebar row being previewed | Filled with the preview hue, over whatever open state the row already had — but the row of the session already showing in the pane keeps its accent fill, since previewing it changes nothing on screen. | +| The sidebar row of the session the operator picked | Keeps its accent fill for the whole preview. A preview never moves the accent; only opening another session does. | | A hovered sidebar row, and the keyboard cursor on a session | A lighter wash of the same hue, because both are what previews a row. | Rows say it with fill, not with an edge — see the tier ladder in diff --git a/web/src/multiview.ts b/web/src/multiview.ts index 903f6ff..aad876e 100644 --- a/web/src/multiview.ts +++ b/web/src/multiview.ts @@ -215,6 +215,20 @@ export function createMultiView(opts: MultiViewOptions): MultiView { return fl?.sessionId ?? null; } + /** The session the operator actually picked — what `focusedLeafSession()` + * would return with the preview undone. A preview moves the active tab and + * its focus, but it is a view: the accent stays on the row the operator + * chose until they choose another (docs/PREVIEW.md). */ + function realFocusedSession(): string | null { + const p = preview; + if (!p) return focusedLeafSession(); + const tab = layout.tabs.find((t) => t.id === p.prevActiveTabId) ?? activeTab(layout); + const leafId = + p.prevFocus && p.prevFocus.tabId === tab.id ? p.prevFocus.leafId : tab.focusedLeafId; + const fl = leafId ? findLeaf(tab.root, leafId) : null; + return fl?.sessionId ?? null; + } + /** Update focus styling + pool focus + sidebar highlights without rebuilding * the DOM (cheap path for click-to-focus). */ function syncFocus(): void { @@ -233,7 +247,7 @@ export function createMultiView(opts: MultiViewOptions): MultiView { if (!preview) pool.setFocus(focusedLeafSession()); sidebar.setHighlights({ open: realOpenSessions(), - focused: focusedLeafSession(), + focused: realFocusedSession(), }); // The row the preview came from wears the same colour, so the sidebar and // the pane read as one thing rather than two unrelated highlights. @@ -608,7 +622,7 @@ export function createMultiView(opts: MultiViewOptions): MultiView { { focus: !preview && !quietRender }, ); if (!preview) pool.setFocus(focused.sessionId ?? null); - sidebar.setHighlights({ open: realOpenSessions(), focused: focused.sessionId ?? null }); + sidebar.setHighlights({ open: realOpenSessions(), focused: realFocusedSession() }); sidebar.setPreview(preview?.sessionId ?? null); } @@ -817,7 +831,7 @@ export function createMultiView(opts: MultiViewOptions): MultiView { openSessions: realOpenSessions, beginSessionDrag, render, - focusedSessionId: focusedLeafSession, + focusedSessionId: realFocusedSession, previewSession, previewedSessionId: () => preview?.sessionId ?? null, }; diff --git a/web/src/preview.test.ts b/web/src/preview.test.ts index cadc4b9..768a74a 100644 --- a/web/src/preview.test.ts +++ b/web/src/preview.test.ts @@ -47,12 +47,20 @@ function fakePool(): Spy { /** Last value the controller pushed to `SidebarHandle.setPreview`. */ const previewRow: { id: string | null } = { id: null }; +/** Last highlights the controller pushed to `SidebarHandle.setHighlights`. */ +const rowHighlights: { open: Set; focused: string | null } = { + open: new Set(), + focused: null, +}; function fakeSidebar(): SidebarHandle { return { el: document.createElement("aside"), setActive() {}, - setHighlights() {}, + setHighlights(h) { + rowHighlights.open = h.open; + rowHighlights.focused = h.focused; + }, setPreview(id: string | null) { previewRow.id = id; }, @@ -95,6 +103,8 @@ describe("sidebar preview", () => { stubFetch(); document.body.replaceChildren(); previewRow.id = null; + rowHighlights.open = new Set(); + rowHighlights.focused = null; }); afterEach(() => { @@ -134,6 +144,28 @@ describe("sidebar preview", () => { expect(mv.el.querySelectorAll(".mv-pane.is-preview")).toHaveLength(0); }); + it("leaves the accent on the session the pane actually holds", () => { + const layout = twoTabs(); + const spy = fakePool(); + const mv = makeMv(layout, spy.pool); + expect(rowHighlights.focused).toBe("s2"); + + // Walking past other rows never moves the accent off what the operator + // picked: only a real open does (docs/PREVIEW.md). + mv.previewSession("s1"); + expect(rowHighlights.focused).toBe("s2"); + expect(mv.focusedSessionId()).toBe("s2"); + + mv.previewSession("s9"); + expect(rowHighlights.focused).toBe("s2"); + + mv.previewSession(null); + expect(rowHighlights.focused).toBe("s2"); + + mv.openSession("s1"); + expect(rowHighlights.focused).toBe("s1"); + }); + it("shows a session no pane holds in a throwaway tab that leaves no trace", () => { const layout = twoTabs(); const before = serializeLayout(layout);