diff --git a/web/src/catalog.ts b/web/src/catalog.ts index 0a0a77c..00fb71f 100644 --- a/web/src/catalog.ts +++ b/web/src/catalog.ts @@ -44,6 +44,14 @@ export const FOCUS_PANE_EVENT = "catalog-focus-pane"; * ("Spawn similar"). Detail is the session being cloned. */ export const SPAWN_SIMILAR_EVENT = "catalog-spawn-similar"; +/** A session row was clicked (issue #133). The row's `href` drives normal + * hash navigation, but a split/drag can move focus without ever touching + * `location.hash` — so a click back onto a session whose hash still + * matches fires no `hashchange` and the app never hears about it. This + * event is the click-driven path that doesn't depend on the hash actually + * changing. */ +export const SESSION_SELECT_EVENT = "catalog-session-select"; + /** The rows a keyboard walk stops on, in visual (document) order. Expanded * folder headers are skipped: a directory is not something to focus, and the * cards under it are one row away anyway. A *collapsed* folder still counts — @@ -194,6 +202,9 @@ function renderNode(node: TreeNode, depth: number): HTMLElement { ev.stopPropagation(); void requestKill(s, name); }); + row.addEventListener("click", () => { + document.dispatchEvent(new CustomEvent(SESSION_SELECT_EVENT, { detail: s.id })); + }); row.addEventListener("contextmenu", (ev) => { ev.preventDefault(); ev.stopPropagation(); diff --git a/web/src/main.ts b/web/src/main.ts index 23ac046..aff03e3 100644 --- a/web/src/main.ts +++ b/web/src/main.ts @@ -29,7 +29,12 @@ import { mountHud, type Hud } from "./hud.js"; import { emptyLayout, type Layout, openSessions } from "./layout.js"; import { createMultiView, type MultiView } from "./multiview.js"; import { loadLayout, saveLayout } from "./persistence.js"; -import { FOCUS_PANE_EVENT, OPEN_SPAWN_EVENT, SPAWN_SIMILAR_EVENT } from "./catalog.js"; +import { + FOCUS_PANE_EVENT, + OPEN_SPAWN_EVENT, + SESSION_SELECT_EVENT, + SPAWN_SIMILAR_EVENT, +} from "./catalog.js"; import { initTheme } from "./theme.js"; import { sessionName, type Project, type Session, type SpawnPreset } from "./types.js"; import { installViewportSizing } from "./viewport.js"; @@ -355,6 +360,20 @@ window.addEventListener("keydown", (e) => { // Escape in the session list hands the keyboard back to the pane. document.addEventListener(FOCUS_PANE_EVENT, () => pool.focusTerminal()); +/** Open/focus a session and reflect it in the shell chrome. Shared by the + * hash router and the sidebar's click-driven path (#133), which can't rely + * on `hashchange` alone — a split or drag can move focus without the hash + * changing, leaving a click back onto that session's row silently ignored. */ +function focusSession(sessionId: string): void { + showMultiView(); + mv.openSession(sessionId); + updateShellRoute(); +} + +document.addEventListener(SESSION_SELECT_EVENT, (e) => { + focusSession((e as CustomEvent).detail); +}); + type Route = | { kind: "catalog" } | { kind: "attach"; sessionId: string } @@ -373,9 +392,7 @@ function dispatch(): void { switch (route.kind) { case "attach": // Deep-link: open/focus the session, then show the layout. - showMultiView(); - mv.openSession(route.sessionId); - updateShellRoute(); + focusSession(route.sessionId); return; case "not-found": shellEl.dataset.route = "notfound";