feat: add keyboard shortcuts for worktree navigation in dev-dashboard

Cmd+Up/Down to switch worktrees, Cmd+K to create new, Cmd+D to remove.
xterm.js passthrough ensures shortcuts bubble up from terminal. Shortcut
hints displayed in a fixed panel at the bottom of the sidebar.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
centdix
2026-02-21 20:01:45 +00:00
co-authored by Claude Opus 4.6
parent 3df8964fc6
commit 9de0060884
2 changed files with 60 additions and 2 deletions
+49 -2
View File
@@ -80,10 +80,52 @@
}
}
function selectNeighborWorktree(direction: -1 | 1) {
const selectable = visibleWorktrees.filter(
(w) => w.path !== "(here)" && w.branch !== "main" && !removingBranches.has(w.branch)
);
if (selectable.length === 0) return;
if (!selectedBranch) {
selectedBranch = selectable[direction === 1 ? 0 : selectable.length - 1].branch;
return;
}
const idx = selectable.findIndex((w) => w.branch === selectedBranch);
const next = idx + direction;
if (next >= 0 && next < selectable.length) {
selectedBranch = selectable[next].branch;
}
}
function handleKeydown(e: KeyboardEvent) {
// Ignore shortcuts when a dialog is open (let dialog handle its own keys)
if (showCreateDialog || removeBranch) return;
const mod = e.metaKey || e.ctrlKey;
if (!mod) return;
if (e.key === "ArrowUp") {
e.preventDefault();
selectNeighborWorktree(-1);
} else if (e.key === "ArrowDown") {
e.preventDefault();
selectNeighborWorktree(1);
} else if (e.key === "k" || e.key === "K") {
e.preventDefault();
if (!creating) showCreateDialog = true;
} else if (e.key === "d" || e.key === "D") {
e.preventDefault();
if (selectedBranch && !isMain) removeBranch = selectedBranch;
}
}
onMount(() => {
refresh();
const interval = setInterval(refresh, 5000);
return () => clearInterval(interval);
window.addEventListener("keydown", handleKeydown);
return () => {
clearInterval(interval);
window.removeEventListener("keydown", handleKeydown);
};
});
</script>
@@ -95,10 +137,15 @@
class="h-8 px-2 gap-1.5 rounded-md border border-edge bg-surface text-accent text-xs flex items-center justify-center cursor-pointer hover:bg-hover disabled:opacity-50 disabled:cursor-not-allowed"
onclick={() => (showCreateDialog = true)}
disabled={creating}
title="New Worktree"
title="New Worktree (Cmd+K)"
><span class="text-lg leading-none">+</span> New</button>
</div>
<WorktreeList worktrees={visibleWorktrees} selected={selectedBranch} removing={removingBranches} onselect={(b) => (selectedBranch = b)} onremove={(b) => (removeBranch = b)} />
<div class="shrink-0 border-t border-edge px-4 py-3 text-[11px] text-muted flex flex-col gap-1">
<div class="flex justify-between"><span>Navigate</span><kbd class="opacity-60">Cmd+Up/Down</kbd></div>
<div class="flex justify-between"><span>New worktree</span><kbd class="opacity-60">Cmd+K</kbd></div>
<div class="flex justify-between"><span>Remove</span><kbd class="opacity-60">Cmd+D</kbd></div>
</div>
</aside>
<main class="flex-1 min-w-0 flex flex-col overflow-hidden">
@@ -56,6 +56,17 @@
}
});
// Let app-level shortcuts (Cmd+Arrow, Cmd+N, Cmd+D) bubble up instead of
// being consumed by xterm. Return false → xterm ignores the event.
term.attachCustomKeyEventHandler((e: KeyboardEvent) => {
if (e.type !== "keydown") return true;
const mod = e.metaKey || e.ctrlKey;
if (mod && (e.key === "ArrowUp" || e.key === "ArrowDown")) return false;
if (mod && (e.key === "k" || e.key === "K")) return false;
if (mod && (e.key === "d" || e.key === "D")) return false;
return true;
});
requestAnimationFrame(() => fitAddon.fit());
const protocol = location.protocol === "https:" ? "wss:" : "ws:";