<!--
Go to Worktree… (⌘K / Ctrl+K): every worktree on the machine, matched by a
few letters of `project/worktree`.
From every level, because the machine home and a project view have no
workbench, and getting into one is exactly what they are for. A worktree
that is not open is opened on the way in; one that cannot be is said here,
and the page stays where it was.
-->
<script lang="ts">
import { onMount, tick } from "svelte";
import { ui } from "../../stores/ui.svelte.js";
import { levels } from "../../stores/levels.svelte.js";
import { context } from "../../stores/context.svelte.js";
import { rankWorktrees, worktreeLabel } from "../../utils/fuzzy.js";
import type { Worktree } from "../../api/types/levels.js";
import AppIcon from "../layout/AppIcon.svelte";
let inputRef: HTMLInputElement | undefined = $state(undefined);
let query = $state("");
let selectedIndex = $state(0);
const matches = $derived(rankWorktrees(query, levels.worktrees));
$effect(() => {
// A new query starts at its best match.
void matches;
selectedIndex = 0;
});
$effect(() => {
const _index = selectedIndex;
void tick().then(() => {
document
.querySelector(".switcher-results .switcher-item.selected")
?.scrollIntoView({ block: "nearest" });
});
});
onMount(() => {
inputRef?.focus();
void levels.ensureLoaded();
});
function close() {
ui.activeModal = null;
}
async function go(worktree: Worktree) {
if (await context.enter(worktree.id)) close();
}
function handleKeydown(e: KeyboardEvent) {
if (e.key === "ArrowDown") {
e.preventDefault();
selectedIndex = Math.min(selectedIndex + 1, matches.length - 1);
} else if (e.key === "ArrowUp") {
e.preventDefault();
selectedIndex = Math.max(selectedIndex - 1, 0);
} else if (e.key === "Enter") {
e.preventDefault();
const worktree = matches[selectedIndex];
if (worktree) void go(worktree);
} else if (e.key === "Escape") {
e.preventDefault();
close();
}
}
function handleOverlayClick(e: MouseEvent) {
if ((e.target as HTMLElement).classList.contains("switcher-overlay")) close();
}
</script>
<!-- svelte-ignore a11y_no_static_element_interactions -->
<div class="switcher-overlay" onclick={handleOverlayClick} onkeydown={handleKeydown}>
<div class="switcher" role="dialog" aria-label="Go to worktree">
<div class="switcher-input-wrap">
<span class="switcher-prefix">
<AppIcon name="folder" size={14} />
</span>
<input
bind:this={inputRef}
class="switcher-input"
type="text"
placeholder="Go to worktree — project/worktree"
aria-label="Worktree"
bind:value={query}
/>
<kbd class="esc-hint">Esc</kbd>
</div>
{#if levels.openFailure}
<p class="switcher-error" role="alert">
{levels.openFailure.worktree} could not be opened: {levels.openFailure.message}
</p>
{/if}
<div class="switcher-results">
{#if !levels.loaded && levels.loading}
<div class="switcher-empty">Reading this machine's worktrees…</div>
{:else if levels.error && !levels.loaded}
<div class="switcher-empty">{levels.error}</div>
{:else if matches.length === 0}
<div class="switcher-empty">No worktree matches “{query}”</div>
{:else}
{#each matches as worktree, i (worktree.id)}
<button
class="switcher-item"
class:selected={i === selectedIndex}
class:current={worktree.id === context.worktree}
onclick={() => void go(worktree)}
onmouseenter={() => (selectedIndex = i)}
>
<span class="switcher-label">{worktreeLabel(worktree)}</span>
<span class="switcher-desc">
{#if levels.opening === worktree.id}
opening…
{:else}
{worktree.branch ?? (worktree.head ? `detached at ${worktree.head.slice(0, 8)}` : "unknown")}{worktree.open ? "" : " · not open"}
{/if}
</span>
{#if worktree.id === context.worktree}
<span class="switcher-check" title="The worktree on screen">
<AppIcon name="check" size={14} />
</span>
{/if}
</button>
{/each}
{/if}
</div>
</div>
</div>
<style>
.switcher-overlay {
position: fixed;
inset: 0;
background: var(--vscode-widget-shadow);
display: flex;
justify-content: center;
padding-top: 20vh;
z-index: 100;
}
.switcher {
width: 520px;
max-width: calc(100vw - var(--wb-spacing-32));
max-height: 420px;
background: var(--vscode-sideBar-background);
border: 1px solid var(--vscode-panel-border);
border-radius: var(--wsb-radius-lg);
box-shadow: var(--wb-shadow-md);
display: flex;
flex-direction: column;
overflow: hidden;
}
.switcher-input-wrap {
display: flex;
align-items: center;
gap: var(--wb-spacing-8);
padding: var(--wb-spacing-10) var(--wb-spacing-14);
border-bottom: 1px solid var(--vscode-panel-border);
}
.switcher-prefix {
color: var(--vscode-focusBorder);
flex-shrink: 0;
display: flex;
}
.switcher-input {
flex: 1;
background: none;
border: none;
font-size: var(--wb-font-size-14);
color: var(--vscode-editor-foreground);
outline: none;
}
.switcher-input::placeholder {
color: var(--vscode-disabledForeground);
}
.esc-hint {
font-size: var(--wb-font-size-10);
padding: var(--wb-spacing-1) var(--wb-spacing-5);
border: 1px solid var(--vscode-panel-border);
border-radius: var(--wsb-radius-sm);
color: var(--vscode-disabledForeground);
background: var(--vscode-input-background);
font-family: var(--wsb-font-sans);
flex-shrink: 0;
}
.switcher-error {
margin: 0;
padding: var(--wb-spacing-8) var(--wb-spacing-14);
font-size: var(--wb-font-size-12);
color: var(--wb-error);
border-bottom: 1px solid var(--vscode-panel-border);
}
.switcher-results {
overflow-y: auto;
flex: 1;
padding: var(--wb-spacing-4) 0;
}
.switcher-item {
display: flex;
align-items: center;
gap: var(--wb-spacing-10);
width: 100%;
padding: var(--wb-spacing-7) var(--wb-spacing-14);
text-align: left;
font-size: var(--wb-font-size-13);
color: var(--vscode-editor-foreground);
}
.switcher-item:hover,
.switcher-item.selected {
background: var(--vscode-list-hoverBackground);
}
.switcher-item.current {
color: var(--vscode-focusBorder);
}
.switcher-label {
flex: 1;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
font-family: var(--vscode-editor-font-family, monospace);
}
.switcher-desc {
font-size: var(--wb-font-size-11);
color: var(--vscode-disabledForeground);
white-space: nowrap;
flex-shrink: 0;
}
.switcher-check {
flex-shrink: 0;
display: flex;
color: var(--vscode-focusBorder);
}
.switcher-empty {
padding: var(--wb-spacing-16);
text-align: center;
color: var(--vscode-disabledForeground);
font-size: var(--wb-font-size-13);
}
</style>