wsb-daemon 0.275.7622

wsb's daemon: the workbench, the GitHub mirror's schedule and peer continuity, for every workspace on the machine
<!--
  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>