<!--
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>