fix(maestro): re-focus a sidebar session already open in a split

Sidebar rows only navigated via location.hash + hashchange. Moving
pane focus by split, drag, or direct pane click never touched the
hash, so a later click back onto a session whose row href matched the
stale hash fired no hashchange and was silently ignored — the pane
had to be clicked directly to regain focus.

Row clicks now also dispatch a session-select event handled the same
way as a hash deep-link, independent of whether the hash text changes.
This commit is contained in:
Miguel Palhas
2026-09-02 17:37:15 +01:00
parent 21fc20b30a
commit b5feb3c39a
2 changed files with 32 additions and 4 deletions
+11
View File
@@ -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();
+21 -4
View File
@@ -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<string>).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";