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
-96
View File
@@ -8,7 +8,6 @@ const downloadContainerFile = vi.fn();
const uploadFileToContainer = vi.fn();
const renameContainerPath = vi.fn();
const createContainerDirectory = vi.fn();
const stageContainerFileForDrag = vi.fn();
vi.mock("../lib/tauri-commands", () => ({
listContainerFiles: (p: string, path: string) => listContainerFiles(p, path),
@@ -18,7 +17,6 @@ vi.mock("../lib/tauri-commands", () => ({
createContainerDirectory: (p: string, parent: string, n: string) =>
createContainerDirectory(p, parent, n),
readContainerFile: vi.fn(),
stageContainerFileForDrag: (p: string, path: string) => stageContainerFileForDrag(p, path),
}));
/**
@@ -231,85 +229,6 @@ describe("useFileManager save to host", () => {
});
});
describe("useFileManager drag-out staging", () => {
it("copies the file onto the host and hands back the host path", async () => {
stageContainerFileForDrag.mockResolvedValue("/tmp/triple-c-drag-out/s1/a.txt");
const { result } = renderHook(() => useFileManager("p1"));
let staged: { hostPath: string; cached: boolean } | null = null;
await act(async () => {
staged = await result.current.stageForDrag(file("a.txt"));
});
expect(stageContainerFileForDrag).toHaveBeenCalledWith("p1", "/workspace/a.txt");
expect(staged).toEqual({ hostPath: "/tmp/triple-c-drag-out/s1/a.txt", cached: false });
// The note is transient — it must not still be sitting there afterwards.
expect(result.current.busy).toBeNull();
});
it("reuses the copy on a second drag of the same entry", async () => {
// The whole point of the cache: the copy is the slow half of the gesture,
// and a retry after a drag the OS missed has to be immediate.
stageContainerFileForDrag.mockResolvedValue("/tmp/triple-c-drag-out/s1/a.txt");
const { result } = renderHook(() => useFileManager("p1"));
let second: { hostPath: string; cached: boolean } | null = null;
await act(async () => {
await result.current.stageForDrag(file("a.txt"));
second = await result.current.stageForDrag(file("a.txt"));
});
expect(stageContainerFileForDrag).toHaveBeenCalledTimes(1);
expect(second).toEqual({ hostPath: "/tmp/triple-c-drag-out/s1/a.txt", cached: true });
});
it("re-stages once the entry has changed underneath it", async () => {
// Keyed on size and mtime, so a file edited in the container is copied
// again rather than dragged out at its old contents.
stageContainerFileForDrag.mockResolvedValue("/tmp/triple-c-drag-out/s1/a.txt");
const { result } = renderHook(() => useFileManager("p1"));
await act(async () => {
await result.current.stageForDrag(file("a.txt", { size: 10 }));
await result.current.stageForDrag(file("a.txt", { size: 4096 }));
});
expect(stageContainerFileForDrag).toHaveBeenCalledTimes(2);
});
it("surfaces a refused staging instead of returning a path that is not there", async () => {
stageContainerFileForDrag.mockRejectedValue(
'900 MB is too large to drag out (limit 256 MB) — use "Save to host…" instead.',
);
const { result } = renderHook(() => useFileManager("p1"));
let staged: { hostPath: string; cached: boolean } | null = null;
await act(async () => {
staged = await result.current.stageForDrag(file("huge.bin"));
});
expect(staged).toBeNull();
expect(toastText()).toContain("too large to drag out");
expect(toastText()).toContain("Save to host");
expect(result.current.busy).toBeNull();
});
it("does not cache a failure, so a retry actually retries", async () => {
stageContainerFileForDrag.mockRejectedValueOnce("Container not running");
stageContainerFileForDrag.mockResolvedValueOnce("/tmp/triple-c-drag-out/s1/a.txt");
const { result } = renderHook(() => useFileManager("p1"));
let staged: { hostPath: string; cached: boolean } | null = null;
await act(async () => {
await result.current.stageForDrag(file("a.txt"));
staged = await result.current.stageForDrag(file("a.txt"));
});
expect(stageContainerFileForDrag).toHaveBeenCalledTimes(2);
expect(staged).toEqual({ hostPath: "/tmp/triple-c-drag-out/s1/a.txt", cached: false });
});
});
describe("useFileManager stays where the user is", () => {
it("does not drag the pane back when the user navigates away mid-upload", async () => {
// The closure captured `/workspace`; the user is in `/workspace/src` by the
@@ -485,21 +404,6 @@ describe("useFileManager overwrite prompt", () => {
});
});
describe("useFileManager staged host paths", () => {
it("recognises a path it staged, and only that path", async () => {
stageContainerFileForDrag.mockResolvedValue("/tmp/triple-c-drag-out/s1/a.txt");
const { result } = renderHook(() => useFileManager("p1"));
expect(result.current.isStagedHostPath("/tmp/triple-c-drag-out/s1/a.txt")).toBe(false);
await act(async () => {
await result.current.stageForDrag(file("a.txt"));
});
expect(result.current.isStagedHostPath("/tmp/triple-c-drag-out/s1/a.txt")).toBe(true);
// Same basename, a real host file the user actually wants uploaded.
expect(result.current.isStagedHostPath("/home/me/a.txt")).toBe(false);
});
});
/**
* The loop, end to end. The prompt only earns its place if the *batch* survives
* it: one answer, given once, has to leave every other file in the drop exactly