<!--
One prj session slot: its name, its state as prj reports it, and the action
that state offers.
prj owns slots — wsb only reads them and asks prj to act — so the state is
prj's own word, shown as it is, and what an action printed or why it failed
stays beside the button that ran it until it is dismissed.
-->
<script lang="ts">
import type { Slot } from "../../api/types/levels.js";
import { actionKey, levels } from "../../stores/levels.svelte.js";
import { formatUnixRelative } from "../../utils/format.js";
interface Props {
slot: Slot;
}
let { slot }: Props = $props();
/** A running slot can be stopped; any other can be resumed. */
const running = $derived(slot.status === "running");
const key = $derived(running ? actionKey.stop(slot) : actionKey.resume(slot));
const outcome = $derived(levels.outcome(key));
</script>
<li class="slot" data-slot={slot.key}>
<div class="slot-line">
<span class="slot-name">{slot.name}</span>
<span class="chip" class:chip--open={running} data-testid="slot-status">{slot.status}</span>
{#if slot.detail}
<span class="subtle slot-detail" title={slot.detail}>{slot.detail}</span>
{/if}
{#if slot.activity_at !== null}
<span class="subtle slot-activity">{formatUnixRelative(slot.activity_at)}</span>
{/if}
<span class="slot-actions">
{#if running}
<button
class="secondary tiny"
disabled={outcome?.running}
onclick={() => void levels.stopSlot(slot)}
>
{outcome?.running ? "Stopping…" : "Stop"}
</button>
{:else}
<button
class="primary tiny"
disabled={outcome?.running}
onclick={() => void levels.resumeSlot(slot)}
>
{outcome?.running ? "Resuming…" : "Resume"}
</button>
{/if}
</span>
</div>
{#if outcome && !outcome.running}
<div class="slot-outcome" class:failed={outcome.error !== null}>
{#if outcome.error !== null}
<span role="alert">{outcome.error}</span>
{:else if outcome.output}
<pre>{outcome.output}</pre>
{/if}
<button class="quiet" onclick={() => levels.dismiss(key)} aria-label="Dismiss">Dismiss</button>
</div>
{/if}
</li>
<style>
.slot {
list-style: none;
padding: var(--wb-spacing-4) 0;
}
.slot-line {
display: flex;
align-items: center;
gap: var(--wb-spacing-8);
min-width: 0;
font-size: var(--wb-font-size-12);
}
.slot-name {
font-family: var(--vscode-editor-font-family, monospace);
color: var(--vscode-editor-foreground);
}
.slot-detail {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.slot-activity {
flex-shrink: 0;
}
.slot-actions {
margin-left: auto;
flex-shrink: 0;
}
.slot-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);
}
.slot-outcome.failed {
color: var(--wb-error);
}
.slot-outcome pre {
margin: 0;
flex: 1;
min-width: 0;
white-space: pre-wrap;
font-family: var(--vscode-editor-font-family, monospace);
}
.slot-outcome span {
flex: 1;
}
</style>