wsb-daemon 0.275.7622

wsb's daemon: the workbench, the GitHub mirror's schedule and peer continuity, for every workspace on the machine
<!--
  Throwing away local changes, after showing exactly what would go.

  This is `wsb discard`, and its first half is the point: the list comes from
  the same code that then acts, so what somebody approves IS what goes. A
  dialog that said "discard changes?" and let the operator find out afterwards
  would be one they could only audit by reading what it had already destroyed.

  Tracked changes are irreversible — the content was never committed, so git
  holds no copy to go back to. Untracked files are shown but are only removed
  when they are asked for separately: they are the ones git has no copy of at
  all, and the likeliest to be something nobody meant to be disposable.
-->
<script lang="ts">
  import { onMount } from "svelte";
  import { git } from "../../../stores/git.svelte.js";
  import type { DiscardAnswer } from "../../../api/client.js";
  import AppIcon from "../../layout/AppIcon.svelte";

  let {
    repoPath,
    label,
    onclose,
  }: {
    repoPath?: string;
    /** What to call this repository in the heading. */
    label: string;
    onclose: () => void;
  } = $props();

  let preview = $state<DiscardAnswer | null>(null);
  let alsoUntracked = $state(false);
  let working = $state(false);
  let error = $state<string | null>(null);

  onMount(async () => {
    try {
      preview = await git.previewDiscard(repoPath);
    } catch (failure) {
      error = failure instanceof Error ? failure.message : String(failure);
    }
  });

  const nothingToLose = $derived(
    preview !== null && preview.tracked.length === 0 && preview.untracked.length === 0,
  );
  const goingCount = $derived(
    preview === null
      ? 0
      : preview.tracked.length + (alsoUntracked ? preview.untracked.length : 0),
  );

  async function discard() {
    working = true;
    try {
      const done = await git.discard(repoPath, alsoUntracked);
      if (done === null) {
        error = "another git operation is in progress";
        return;
      }
      onclose();
    } catch (failure) {
      error = failure instanceof Error ? failure.message : String(failure);
    } finally {
      working = false;
    }
  }

  function handleKeydown(event: KeyboardEvent) {
    if (event.key === "Escape") onclose();
  }
</script>

<div class="modal-overlay" onclick={onclose} onkeydown={handleKeydown} role="presentation">
  <!-- svelte-ignore a11y_no_noninteractive_tabindex -->
  <div
    class="modal-panel"
    onclick={(event) => event.stopPropagation()}
    onkeydown={handleKeydown}
    role="dialog"
    aria-label="Discard changes"
    tabindex="0"
  >
    <div class="modal-header">
      <h2 class="modal-title">Discard changes in {label}</h2>
      <button class="modal-close" onclick={onclose} aria-label="Close">
        <AppIcon name="x" size={14} />
      </button>
    </div>
    <div class="modal-body">
      {#if error}
        <div class="modal-error">{error}</div>
      {/if}

      {#if preview === null && !error}
        <p class="empty">Reading what is there…</p>
      {:else if nothingToLose}
        <p class="empty">Nothing to discard — this working tree is clean.</p>
      {:else if preview}
        {#if preview.tracked.length > 0}
          <p class="what">
            {preview.tracked.length} tracked file{preview.tracked.length === 1 ? "" : "s"} —
            <strong>this cannot be undone</strong>, the changes were never committed.
          </p>
          <ul class="files">
            {#each preview.tracked as path (path)}<li>{path}</li>{/each}
          </ul>
        {/if}

        {#if preview.untracked.length > 0}
          <label class="check">
            <input type="checkbox" bind:checked={alsoUntracked} />
            also remove {preview.untracked.length} untracked file{preview.untracked.length === 1
              ? ""
              : "s"}
          </label>
          <ul class="files" class:dimmed={!alsoUntracked}>
            {#each preview.untracked as path (path)}<li>{path}</li>{/each}
          </ul>
        {/if}

        <p class="empty">
          Ignored files are never touched — build output and <code>.wsb/</code> stay.
        </p>
      {/if}
    </div>
    <div class="modal-footer">
      <button class="secondary" onclick={onclose}>Cancel</button>
      <button
        class="danger"
        disabled={working || nothingToLose || goingCount === 0}
        onclick={() => void discard()}
      >
        {working ? "Discarding…" : `Discard ${goingCount} file${goingCount === 1 ? "" : "s"}`}
      </button>
    </div>
  </div>
</div>

<style>
  .modal-body {
    display: flex;
    flex-direction: column;
    gap: var(--wb-spacing-8);
    max-height: 50vh;
    overflow-y: auto;
  }

  .what {
    margin: 0;
    font-size: var(--wb-font-size-12);
    color: var(--vscode-editor-foreground);
  }

  .files {
    margin: 0;
    padding: 0 0 0 var(--wb-spacing-16);
    font-family: var(--wsb-font-mono);
    font-size: var(--wb-font-size-11);
    color: var(--vscode-descriptionForeground);
    max-height: 180px;
    overflow-y: auto;
  }

  .files.dimmed {
    opacity: 0.5;
  }

  .modal-footer {
    display: flex;
    justify-content: flex-end;
    gap: var(--wb-spacing-6);
    padding: var(--wb-spacing-10) var(--wb-spacing-14) var(--wb-spacing-12);
  }

  code {
    font-family: var(--wsb-font-mono);
  }
</style>