fix(maestro): keep the keyboard in the list while previewing

Two mounts were taking focus out of the sidebar mid-walk: the render that
put the multi-view on screen before the preview applied, and the render
that restores the previous pane when a preview ends. Both now mount
without focusing, so j/k keeps walking. Also marks the preview with a
badge on the canvas and drops the close button from its tab.
This commit is contained in:
Miguel Palhas
2026-09-03 10:52:43 +01:00
parent 6635cff46e
commit 2319b4e47b
3 changed files with 45 additions and 7 deletions
+10 -2
View File
@@ -114,8 +114,9 @@ let previewTimer: number | undefined;
function schedulePreview(sessionId: string | null, source: "key" | "hover"): void {
clearTimeout(previewTimer);
previewTimer = window.setTimeout(() => {
showMultiView();
ensureMultiView();
mv?.previewSession(sessionId);
updateShellRoute();
}, PREVIEW_DELAY[source]);
}
@@ -262,11 +263,18 @@ mv = createMultiView({
onLayoutChange: scheduleSave,
});
let contentShowsMv = false;
function showMultiView(): void {
/** Put the multi-view in the content pane, without rendering it. A preview
* renders once, on its own terms (`docs/PREVIEW.md`); a render here would
* first re-mount the pane it is about to replace, and that mount takes the
* keyboard away from the list being walked. */
function ensureMultiView(): void {
if (!contentShowsMv) {
content.replaceChildren(mv.el);
contentShowsMv = true;
}
}
function showMultiView(): void {
ensureMultiView();
mv.render();
updateShellRoute();
}
+20 -5
View File
@@ -110,6 +110,20 @@ export function createMultiView(opts: MultiViewOptions): MultiView {
tempTabId: string | null;
}
let preview: Preview | null = null;
/// Set while a preview-driven render runs: ending a preview re-mounts the
/// pane that was there, and that mount must not pull the keyboard out of the
/// sidebar the operator is still walking.
let quietRender = false;
/** Render without any mount taking the keyboard. */
function renderQuietly(): void {
quietRender = true;
try {
render();
} finally {
quietRender = false;
}
}
const isTempTab = (tabId: string) => preview?.tempTabId === tabId;
@@ -152,7 +166,7 @@ export function createMultiView(opts: MultiViewOptions): MultiView {
function previewSession(sessionId: string | null): void {
if (preview?.sessionId === sessionId) return;
if (sessionId === null) {
if (restorePreview()) render();
if (restorePreview()) renderQuietly();
return;
}
restorePreview();
@@ -176,7 +190,7 @@ export function createMultiView(opts: MultiViewOptions): MultiView {
layout.activeTabId = tab.id;
preview = { sessionId, prevActiveTabId, prevFocus: null, tempTabId: tab.id };
}
render();
renderQuietly();
}
/** Any user action that changes the layout for real ends the preview first,
@@ -385,7 +399,8 @@ export function createMultiView(opts: MultiViewOptions): MultiView {
}
render();
});
if (layout.tabs.length > 1) {
// A preview tab is not the operator's to close: it goes on its own.
if (layout.tabs.length > 1 && !isTempTab(t.id)) {
const close = document.createElement("span");
close.className = "mv-tab-close";
close.textContent = "✕";
@@ -555,7 +570,7 @@ export function createMultiView(opts: MultiViewOptions): MultiView {
// terminal, or j/k would stop walking the list after the first step.
pool.setVisible(
hosts.map((h) => ({ id: h.id, pane: h.host, info: h.info })),
{ focus: !preview },
{ focus: !preview && !quietRender },
);
syncFocus();
persist();
@@ -579,7 +594,7 @@ export function createMultiView(opts: MultiViewOptions): MultiView {
// Only the focused leaf's session is live on mobile; the rest park.
pool.setVisible(
focused.sessionId ? [{ id: focused.sessionId, pane: pane.host, info: infoFor(focused.sessionId) }] : [],
{ focus: !preview },
{ focus: !preview && !quietRender },
);
if (!preview) pool.setFocus(focused.sessionId ?? null);
sidebar.setHighlights({ open: realOpenSessions(), focused: focused.sessionId ?? null });
+15
View File
@@ -2890,6 +2890,21 @@ input.leaf-rename:focus {
border-style: dashed;
font-style: italic;
}
.mv.mv-previewing .mv-canvas::before {
content: "preview";
position: absolute;
top: var(--sp-2);
right: var(--sp-3);
z-index: var(--z-sticky);
padding: 1px var(--sp-2);
border-radius: var(--r-1);
font: 500 var(--fs-11) / 1.5 var(--font-mono);
letter-spacing: 0.04em;
color: var(--accent-ink);
background: var(--accent);
opacity: 0.85;
pointer-events: none;
}
.mv.mv-previewing .mv-canvas::after {
content: "";
position: absolute;