diff --git a/web/src/catalog.ts b/web/src/catalog.ts index f7e8f0b..485b9d9 100644 --- a/web/src/catalog.ts +++ b/web/src/catalog.ts @@ -663,8 +663,9 @@ export function renderSidebar(store: CatalogStore, opts: SidebarOptions = {}): S applyNav(); }; - // Hover previews the row under the pointer (docs/PREVIEW.md). Touch is left - // out: there is no hover there, and a tap already opens the session. + // Pointer hover previews only while Ctrl is held — plain hover was too + // disruptive when sweeping the sidebar. Keyboard walk (j/k) still previews + // unconditionally via applyNav. let hovered: string | null = null; const setHovered = (id: string | null) => { if (hovered === id) return; @@ -672,7 +673,7 @@ export function renderSidebar(store: CatalogStore, opts: SidebarOptions = {}): S opts.onPreview?.(id); }; treeHost.addEventListener("pointerover", (e) => { - if (e.pointerType === "touch") return; + if (e.pointerType === "touch" || !e.ctrlKey) return; const row = (e.target as HTMLElement | null)?.closest(".tree-leaf"); setHovered(row?.dataset.sessionId ?? null); }); diff --git a/web/src/sidebarNav.test.ts b/web/src/sidebarNav.test.ts index 4e836ac..71c93b8 100644 --- a/web/src/sidebarNav.test.ts +++ b/web/src/sidebarNav.test.ts @@ -182,26 +182,32 @@ describe("sidebar preview requests (docs/PREVIEW.md)", () => { expect(seen.at(-1)).toBe(null); }); - it("asks for a preview of the hovered card, and clears it on the way out", () => { + it("ctrl+hover previews a card, plain hover does not", () => { const seen: (string | null)[] = []; const { el } = twoFolders({ onPreview: (id) => seen.push(id) }); const card = el.querySelectorAll(".tree-leaf")[0]; + // Plain hover — no preview. card.dispatchEvent(new PointerEvent("pointerover", { bubbles: true, pointerType: "mouse" })); + expect(seen).toEqual([]); + + // Ctrl+hover — preview fires. + card.dispatchEvent(new PointerEvent("pointerover", { bubbles: true, pointerType: "mouse", ctrlKey: true })); expect(seen).toEqual([A]); - // Moving within the same card is not a new preview. - card.dispatchEvent(new PointerEvent("pointerover", { bubbles: true, pointerType: "mouse" })); + + // Deduplicated while still on the same card. + card.dispatchEvent(new PointerEvent("pointerover", { bubbles: true, pointerType: "mouse", ctrlKey: true })); expect(seen).toHaveLength(1); tree(el).dispatchEvent(new PointerEvent("pointerleave", { pointerType: "mouse" })); expect(seen.at(-1)).toBe(null); }); - it("ignores touch, where a tap already opens the session", () => { + it("ignores touch even with ctrl", () => { const seen: (string | null)[] = []; const { el } = twoFolders({ onPreview: (id) => seen.push(id) }); const card = el.querySelectorAll(".tree-leaf")[0]; - card.dispatchEvent(new PointerEvent("pointerover", { bubbles: true, pointerType: "touch" })); + card.dispatchEvent(new PointerEvent("pointerover", { bubbles: true, pointerType: "touch", ctrlKey: true })); expect(seen).toEqual([]); }); });