Hold back the Disk panel and OS drag-out from the ship branch

This is a scope reduction, not an abandonment. Both subsystems are
preserved in full on `hold/disk-and-dragout` and are intended to come
back once they have been hardened separately. Nothing here is a
judgement that the features are unwanted — three successive
audit-and-fix cycles each closed a critical defect in these two areas
and each opened a new one, so the rest of the round ships now and these
two get their own cycle rather than holding it up.

Removed: the Disk settings panel and its whole reclaim / destroy /
compaction surface — `DiskSettings`, `DiskProjectTable`, `useDiskUsage`,
`docker/disk.rs`, `disk_tests.rs`, the disk commands in
`docker_commands.rs`, and their `generate_handler!` entries. Dropping
the IPC entries is the point: a UI-only removal would have left five
commands callable by a compromised webview, one of them a verified
arbitrary-DELETE primitive. `sweep_orphaned_snapshots`'s *command* goes
with them (the panel was its only caller); the sweep itself stays.

Removed: OS drag-out from the Files tab — `stage_container_file_for_drag`
and its host staging lifecycle, the pointer gesture and `dragPreview`,
`stageForDrag` / `isStagedHostPath`, the `tauri-plugin-drag` and
`@crabnebula/tauri-plugin-drag` dependencies, and the
`drag:allow-start-drag` capability grant, which could not be scoped.
The capability test's expected list is updated; its `*:default` and
`store:*` assertions are untouched.

Kept, deliberately: drag-and-drop *into* the app (Files pane and
terminal) and "Save to host…", which is now the only route out of a
container. The prevention work is untouched — the pre-commit scrub and
`SNAPSHOT_SCRUB_PATHS`, capped container logs, the `triple-c.base` /
`triple-c.managed` labels, `sweep_orphaned_snapshots` and the startup
housekeeping, the migration pin/probe reapers, scheduler log pruning,
`formatBytes.ts`, and `project_lock.rs` in full with every acquisition
site outside `disk.rs`.

Entanglements, resolved rather than deleted blind:
* `container.rs`'s `a_compaction_runs_this_module_s_scrub_script_byte_for_byte`
  pinned the compaction Dockerfile against `snapshot_scrub_script()`.
  Dropped — it existed only for compaction. `snapshot_scrub_script` and
  its containment tests are untouched.
* `lib.rs`'s startup reap of `:compacting` tags and `triple-c-compact-*`
  containers is dropped: nothing on this branch creates them.
* `project_lock`'s `Compaction` / `CacheClear` variants and
  `any_held_excluding`, `migration_commands::is_migrating`, and
  `formatBytes{Delta,Ceiling}` lose their last production caller but are
  kept and still tested, annotated with why.
* `projects_store::corrupt_since` and `migration_store::peek_ownerless_since`
  were read only by the disk survey and are removed. The corrupt-load
  marker and `.bak` are still written.

Verified: `npm run test` 611 passing, `npx tsc --noEmit` clean,
`npm run build` green; `cargo test` 419 passed / 2 ignored,
`cargo build` 0 warnings. Every test removed belongs to a removed
feature — no kept-behaviour test was weakened or deleted.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBq2rGum6GX7xXgsas1fDc
This commit is contained in:
2026-08-23 15:20:22 -07:00
co-authored by Claude Opus 5
parent 6a8972980d
commit ed91423666
41 changed files with 126 additions and 11404 deletions
@@ -1,133 +0,0 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
import { render, screen, fireEvent } from "@testing-library/react";
import TypedConfirmModal from "./TypedConfirmModal";
const onConfirm = vi.fn();
const onCancel = vi.fn();
function renderModal(props: Partial<React.ComponentProps<typeof TypedConfirmModal>> = {}) {
render(
<TypedConfirmModal
title="Delete claude config volume"
expected="whp"
confirmLabel="Delete config volume"
onConfirm={onConfirm}
onCancel={onCancel}
{...props}
>
<p>Everything goes.</p>
</TypedConfirmModal>,
);
return {
input: screen.getByLabelText(/Type/),
confirm: screen.getByRole("button", { name: "Delete config volume" }),
};
}
beforeEach(() => vi.clearAllMocks());
describe("TypedConfirmModal", () => {
it("is a real dialog, from the Modal primitive", () => {
renderModal();
const dialog = screen.getByRole("dialog");
expect(dialog).toHaveAttribute("aria-modal", "true");
});
it("keeps the confirm button shut until the name is typed exactly", () => {
const { input, confirm } = renderModal();
expect(confirm).toBeDisabled();
fireEvent.change(input, { target: { value: "wh" } });
expect(confirm).toBeDisabled();
fireEvent.change(input, { target: { value: "whp" } });
expect(confirm).toBeEnabled();
fireEvent.click(confirm);
expect(onConfirm).toHaveBeenCalledWith("whp");
});
it("is case-sensitive, because Api and api are different projects", () => {
// This gate is the only thing between a misclick on a sorted table of
// numbers and a project's transcripts, so a near-miss is a miss.
const { input, confirm } = renderModal({ expected: "Api" });
fireEvent.change(input, { target: { value: "api" } });
expect(confirm).toBeDisabled();
fireEvent.change(input, { target: { value: "Api" } });
expect(confirm).toBeEnabled();
});
it("forgives surrounding whitespace from a paste", () => {
const { input, confirm } = renderModal();
fireEvent.change(input, { target: { value: " whp " } });
expect(confirm).toBeEnabled();
});
it("announces the gate's state in words rather than only by the button fill", () => {
const { input } = renderModal();
expect(screen.getByRole("status")).toHaveTextContent(
"Waiting for the exact project name.",
);
fireEvent.change(input, { target: { value: "whp" } });
expect(screen.getByRole("status")).toHaveTextContent("Name matches.");
});
it("names what it is waiting for, when that is not a project", () => {
// An orphaned volume has no project — its id matches nothing in the store,
// which is the definition of the variant — so the gate takes the volume's
// own name and must not ask for a string that does not exist.
renderModal({ expected: "triple-c-claude-config-gone", subject: "volume name" });
expect(screen.getByRole("status")).toHaveTextContent(
"Waiting for the exact volume name.",
);
});
it("spells out what is lost, from the caller's copy", () => {
renderModal();
expect(screen.getByText("Everything goes.")).toBeInTheDocument();
});
it("locks itself while the deletion is running", () => {
render(
<TypedConfirmModal
title="Delete claude config volume"
expected="whp"
confirmLabel="Delete config volume"
onConfirm={onConfirm}
onCancel={onCancel}
busy
>
<p>Everything goes.</p>
</TypedConfirmModal>,
);
// The confirm button reports the work in a word rather than only going
// grey, so it is found by its busy label, not its idle one.
expect(screen.getByLabelText(/Type/)).toBeDisabled();
expect(screen.getByRole("button", { name: "Working…" })).toBeDisabled();
expect(screen.getByRole("button", { name: "Cancel" })).toBeDisabled();
});
it("cancels without confirming", () => {
renderModal();
fireEvent.click(screen.getByRole("button", { name: "Cancel" }));
expect(onCancel).toHaveBeenCalled();
expect(onConfirm).not.toHaveBeenCalled();
});
it("carries a failed attempt inside the dialog, as an alert", () => {
// The caller keeps this dialog open when the deletion fails, because the
// panel behind it is several screens long and its error line sits at the
// top — nowhere near the row this was opened from.
renderModal({ error: "volume triple-c-home-p-whp is in use by a running container" });
expect(screen.getByRole("alert")).toHaveTextContent(/in use by a running container/);
});
it("says nothing about failure when there has been none", () => {
renderModal();
expect(screen.queryByRole("alert")).not.toBeInTheDocument();
});
it("cannot be satisfied by an empty box when there is no name to type", () => {
const { confirm } = renderModal({ expected: "" });
expect(confirm).toBeDisabled();
});
});
-145
View File
@@ -1,145 +0,0 @@
import { useId, useRef, useState, type ReactNode } from "react";
import Modal from "./Modal";
import Button from "./Button";
import { inputClass } from "./Field";
interface Props {
title: string;
/** What must be typed, verbatim, before the confirm button enables. */
expected: string;
/**
* What `expected` *is*, for the waiting message — "project name" unless the
* caller says otherwise.
*
* An orphaned volume has no project by definition, so its gate takes the
* volume's own name (that is what `disk.rs`'s `destroy` compares against),
* and telling that user we are "waiting for the exact project name" would be
* asking for a string that does not exist.
*/
subject?: string;
/** The verb on the confirm button. Repeat the action — never "OK". */
confirmLabel: string;
/** What is about to be lost, in full. */
children: ReactNode;
onConfirm: (typed: string) => void;
onCancel: () => void;
busy?: boolean;
/**
* Why the last attempt did not happen. The caller keeps the dialog open when
* its action fails, so the failure has to be readable *here* — the panel
* behind this one is several screens long and its error line is at the top
* of it, which is not where the user is looking.
*/
error?: string | null;
}
/**
* The confirmation gate for something that has no other copy.
*
* ## Why this exists when `ConfirmResetModal` already did
*
* Reset and Remove are reached from a project's own overflow menu, one project
* at a time, by a user who went looking for them. The Disk panel lists every
* project's volumes side by side in a table of numbers, sorted by size — which
* is exactly the layout that invites a misclick on the wrong row. A two-button
* dialog does not survive that, because the thing being confirmed (*which*
* project) is the thing the user got wrong.
*
* Typing the name fixes the failure mode rather than adding friction to it: the
* gate is not "are you sure", it is "name the project you mean".
*
* The comparison is `expected.trim() === typed.trim()` and **case-sensitive** —
* mirroring `confirmation_matches` in `docker/disk.rs`, which is the check that
* actually holds, since this one is only a UI affordance. The backend refuses a
* mismatch on its own.
*/
export default function TypedConfirmModal({
title,
expected,
subject = "project name",
confirmLabel,
children,
onConfirm,
onCancel,
busy = false,
error = null,
}: Props) {
const [typed, setTyped] = useState("");
const inputRef = useRef<HTMLInputElement>(null);
// Every other `ui/` component uses `useId`; a hardcoded id breaks the
// label association as soon as two of these are mounted at once.
const inputId = useId();
const matches = expected.trim().length > 0 && typed.trim() === expected.trim();
return (
<Modal
title={title}
onClose={onCancel}
widthClassName="w-[30rem]"
initialFocusRef={inputRef}
dismissible={!busy}
footer={
<>
<Button size="md" variant="ghost" onClick={onCancel} disabled={busy}>
Cancel
</Button>
<Button
size="md"
onClick={() => onConfirm(typed)}
disabled={!matches || busy}
className={
matches && !busy
? "bg-[var(--error-emphasis)] text-white border border-transparent hover:opacity-90"
: "bg-[var(--bg-tertiary)] text-[var(--text-disabled)] border border-[var(--border-color)]"
}
>
{busy ? "Working…" : confirmLabel}
</Button>
</>
}
>
<div className="space-y-3 text-[13px] text-[var(--text-secondary)]">
{children}
<div>
<label
htmlFor={inputId}
className="block text-[13px] text-[var(--text-primary)] mb-1.5"
>
Type <strong className="font-mono">{expected}</strong> to confirm
</label>
<input
id={inputId}
ref={inputRef}
value={typed}
onChange={(e) => setTyped(e.target.value)}
disabled={busy}
autoComplete="off"
spellCheck={false}
className={`${inputClass} font-mono`}
/>
{/* Announced rather than only coloured — the gate's state has to be
readable without relying on the button's fill. */}
<p role="status" aria-live="polite" className="mt-1.5 text-xs">
{matches ? (
<span className="text-[var(--text-secondary)]">Name matches.</span>
) : (
// Not disabled content — the gate is live and waiting on the
// user. `--text-disabled` is ~4.1:1 and fails AA at 12px.
<span className="text-[var(--text-secondary)]">
Waiting for the exact {subject}.
</span>
)}
</p>
</div>
{error && (
// Rendered last, next to the button that was just pressed, and as an
// `alert` so it is announced on arrival rather than waiting to be
// found.
<p role="alert" className="text-[var(--error)]">
{error}
</p>
)}
</div>
</Modal>
);
}