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);