<!--
One worktree, as the machine home and the project view list it: what is
checked out, how it stands against its upstream, whether its services run,
and its prj session slots.
The name is the way in. A worktree that is not open is opened on the way, so
following the link never lands on a workbench that answers 404.
-->
<script lang="ts">
import type { Worktree } from "../../api/types/levels.js";
import { actionKey, levels } from "../../stores/levels.svelte.js";
import { context } from "../../stores/context.svelte.js";
import { router } from "../../stores/router.svelte.js";
import { workbench } from "../../stores/workbench.svelte.js";
import { opensInWorkbench } from "../../utils/open-link.js";
import { formatUnixRelative } from "../../utils/format.js";
import SlotRow from "./SlotRow.svelte";
interface Props {
worktree: Worktree;
/** The project view's fuller row: path, commit and upstream. */
detailed?: boolean;
}
let { worktree, detailed = false }: Props = $props();
let confirmingRemove = $state(false);
const href = $derived(router.href({ level: "worktree", worktree: worktree.id }));
const startOutcome = $derived(levels.outcome(actionKey.start(worktree.id)));
const removeOutcome = $derived(levels.outcome(actionKey.remove(worktree.id)));
const openFailure = $derived(
levels.openFailure?.worktree === worktree.id ? levels.openFailure.message : null,
);
/** The branch, or the commit a detached head is at, or nothing git could say. */
const checkedOut = $derived(
worktree.branch ??
(worktree.head !== null ? `detached at ${worktree.head.slice(0, 8)}` : "unknown"),
);
/** main-worktree holds the project's own repositories; prj never removes it. */
const removable = $derived(worktree.project !== null && worktree.name !== "main");
/** Work in it, with its overview open: opened first when it is closed. */
const enter = opensInWorkbench(async (beside) => {
if (await context.enter(worktree.id)) workbench.openOverview(worktree.id, { beside });
});
async function remove() {
confirmingRemove = false;
await levels.removeWorktree(worktree.id);
}
</script>
<div class="worktree" class:current={worktree.id === context.worktree} data-worktree={worktree.id}>
<div class="worktree-head">
<a class="worktree-name" {href} onclick={enter}>{worktree.name}</a>
{#if levels.opening === worktree.id}
<span class="chip">opening…</span>
{:else if worktree.open}
<span class="chip chip--open" title="Its services are running in this daemon">open</span>
{:else}
<span class="chip" title="Opened when you go into it">not open</span>
{/if}
<span class="worktree-branch" title={worktree.head ?? undefined}>
{checkedOut}
</span>
</div>
{#if worktree.error !== null}
<p class="worktree-failure" role="alert">Its git state could not be read: {worktree.error}</p>
{:else}
<div class="worktree-facts subtle">
<span class:dirty={worktree.dirty > 0}>
{worktree.dirty === 0 ? "clean" : `${worktree.dirty} changed`}
</span>
{#if worktree.upstream === null}
<span>no upstream</span>
{:else}
<span title="Against {worktree.upstream}">
{worktree.ahead ?? 0} ahead · {worktree.behind ?? 0} behind
</span>
{/if}
<span title="The checked-out commit">{formatUnixRelative(worktree.last_commit_at)}</span>
</div>
{/if}
{#if detailed}
<dl class="worktree-detail">
<dt>Path</dt>
<dd>{worktree.path}</dd>
{#if worktree.head !== null}
<dt>Commit</dt>
<dd>{worktree.head}</dd>
{/if}
{#if worktree.upstream !== null}
<dt>Upstream</dt>
<dd>{worktree.upstream}</dd>
{/if}
</dl>
{/if}
{#if openFailure}
<p class="worktree-failure" role="alert">Could not open it: {openFailure}</p>
{/if}
{#if worktree.slots.length > 0}
<ul class="worktree-slots" aria-label="Session slots">
{#each worktree.slots as slot (slot.key)}
<SlotRow {slot} />
{/each}
</ul>
{:else if detailed}
<p class="empty">No session slots.</p>
{/if}
<!-- prj keeps slots and worktrees for its own projects only: a worktree
outside `$PRJ` has neither to offer. -->
{#if worktree.project !== null}
<div class="button-row worktree-actions">
<button
class="secondary tiny"
disabled={startOutcome?.running}
onclick={() => void levels.startSession(worktree.id)}
>
{startOutcome?.running ? "Starting…" : "New session"}
</button>
{#if removable}
{#if confirmingRemove}
<span class="subtle">Remove {worktree.name}-worktree? prj refuses unless it is clean and pushed.</span>
<button class="danger tiny" onclick={() => void remove()}>Remove</button>
<button class="secondary tiny" onclick={() => (confirmingRemove = false)}>Cancel</button>
{:else}
<button
class="danger-outline tiny"
disabled={removeOutcome?.running}
onclick={() => (confirmingRemove = true)}
>
{removeOutcome?.running ? "Removing…" : "Remove"}
</button>
{/if}
{/if}
</div>
{/if}
{#each [{ key: actionKey.start(worktree.id), outcome: startOutcome }, { key: actionKey.remove(worktree.id), outcome: removeOutcome }] as entry (entry.key)}
{#if entry.outcome && !entry.outcome.running}
<div class="worktree-outcome" class:failed={entry.outcome.error !== null}>
{#if entry.outcome.error !== null}
<span role="alert">{entry.outcome.error}</span>
{:else if entry.outcome.output}
<pre>{entry.outcome.output}</pre>
{/if}
<button class="quiet" onclick={() => levels.dismiss(entry.key)}>Dismiss</button>
</div>
{/if}
{/each}
</div>
<style>
.worktree {
padding: var(--wb-spacing-8) var(--wb-spacing-10);
border-top: 1px solid var(--vscode-panel-border);
}
.worktree.current {
box-shadow: inset 2px 0 0 var(--vscode-focusBorder);
}
.worktree-head {
display: flex;
align-items: center;
gap: var(--wb-spacing-8);
min-width: 0;
}
.worktree-name {
font-weight: 600;
font-size: var(--wb-font-size-13);
color: var(--vscode-textLink-foreground);
text-decoration: none;
}
.worktree-name:hover {
text-decoration: underline;
}
.worktree-branch {
margin-left: auto;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-family: var(--vscode-editor-font-family, monospace);
font-size: var(--wb-font-size-11);
color: var(--vscode-descriptionForeground);
}
.worktree-facts {
display: flex;
flex-wrap: wrap;
gap: var(--wb-spacing-10);
margin-top: var(--wb-spacing-4);
font-size: var(--wb-font-size-11);
}
.worktree-facts .dirty {
color: var(--vscode-gitDecoration-modifiedResourceForeground, var(--vscode-editor-foreground));
}
.worktree-detail {
display: grid;
grid-template-columns: max-content 1fr;
gap: var(--wb-spacing-2) var(--wb-spacing-10);
margin: var(--wb-spacing-6) 0 0;
font-size: var(--wb-font-size-11);
}
.worktree-detail dt {
color: var(--vscode-descriptionForeground);
}
.worktree-detail dd {
margin: 0;
min-width: 0;
overflow-wrap: anywhere;
font-family: var(--vscode-editor-font-family, monospace);
}
.worktree-failure {
margin: var(--wb-spacing-6) 0 0;
font-size: var(--wb-font-size-11);
color: var(--wb-error);
}
.worktree-slots {
margin: var(--wb-spacing-6) 0 0;
padding: 0;
}
.worktree-actions {
margin-top: var(--wb-spacing-6);
align-items: center;
}
.worktree-outcome {
display: flex;
align-items: flex-start;
gap: var(--wb-spacing-8);
margin-top: var(--wb-spacing-4);
font-size: var(--wb-font-size-11);
color: var(--vscode-descriptionForeground);
}
.worktree-outcome.failed {
color: var(--wb-error);
}
.worktree-outcome pre {
margin: 0;
flex: 1;
min-width: 0;
white-space: pre-wrap;
font-family: var(--vscode-editor-font-family, monospace);
}
.worktree-outcome span {
flex: 1;
}
</style>