Report and retry Docker resources remove_project could not delete
Secret Scan / scan (push) Successful in 10s
Build App (Preview) / compute-version (pull_request) Successful in 7s
Secret Scan / scan (pull_request) Successful in 8s
Build App (Preview) / create-release (pull_request) Successful in 5s
Build App (Preview) / build-linux (pull_request) Successful in 6m5s
Build App (Preview) / build-macos (pull_request) Successful in 2m45s
Build App (Preview) / build-windows (pull_request) Successful in 5m42s
Build App (Preview) / prune-previews (pull_request) Successful in 3s

remove_project_volumes always returned Ok(()) regardless of what actually
happened, making the `if let Err(e)` guarding it at every call site dead
code. remove_project then dropped the project record unconditionally, so a
volume, image or container that failed to delete became permanently
unreachable — confirmed against a real orphaned volume pair found in the
wild (fixes #31).

remove_project_volumes/remove_snapshot_image/remove_container now report
what they could not remove (treating "already gone" as success rather than
a leftover), remove_project surfaces this to the user via a toast, and
before dropping the project record it writes a pending-cleanup record that
startup housekeeping retries automatically on the next launch.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FGjXq6fqtAFHdbhk4f3PfZ
This commit is contained in:
2026-08-27 08:18:41 -07:00
co-authored by Claude Sonnet 5
parent 1a79852f65
commit 4827170715
10 changed files with 520 additions and 32 deletions
@@ -1,5 +1,6 @@
import { useEffect, useMemo, useState } from "react";
import { useShallow } from "zustand/react/shallow";
import type { ProjectRemovalReport } from "../../../lib/types";
import { useAppState } from "../../../store/appState";
import { useProjectActions } from "../../../hooks/useProjectActions";
import { useProjects } from "../../../hooks/useProjects";
@@ -30,6 +31,16 @@ const TABS = [
export type ProjectHomeTabId = (typeof TABS)[number]["id"];
/** Names what a `ProjectRemovalReport` says survived, for the leftover toast. */
function describeLeftovers(report: ProjectRemovalReport): string {
const parts: string[] = [];
if (report.container) parts.push("its container");
if (report.image) parts.push("its saved image");
if (report.volumes.length === 1) parts.push("a volume");
else if (report.volumes.length > 1) parts.push(`${report.volumes.length} volumes`);
return parts.join(", ");
}
interface Props {
projectId: string;
active: boolean;
@@ -282,7 +293,14 @@ export default function ProjectHome({ projectId, active }: Props) {
onConfirm={async () => {
setConfirmRemove(false);
try {
await remove(project.id);
const report = await remove(project.id);
if (report.container || report.image || report.volumes.length > 0) {
useAppState.getState().pushToast({
kind: "info",
message: `${project.name}” was removed, but some Docker resources are still on disk`,
detail: `Triple-C will retry removing ${describeLeftovers(report)} the next time it starts.`,
});
}
} catch (e) {
useAppState.getState().pushToast({
kind: "error",
+2 -1
View File
@@ -44,8 +44,9 @@ export function useProjects() {
const remove = useCallback(
async (id: string) => {
await commands.removeProject(id);
const report = await commands.removeProject(id);
removeProjectFromList(id);
return report;
},
[removeProjectFromList],
);
+2 -2
View File
@@ -1,5 +1,5 @@
import { invoke } from "@tauri-apps/api/core";
import type { Project, ProjectPath, ContainerInfo, AppSettings, UpdateInfo, ImageUpdateInfo, FileEntry, FileContents, WebTerminalInfo, SttStatus, GatewayStatus, InstallOptions, ClaudeSession, ContainerCapabilities, ScheduledTask, ScheduledTaskInput, SchedulerNotification, AuthBridgeStatus, BrowserViewStatus, BrowserViewPopoutState, BrowserPageState, PlaywrightDetection, BrowserSetupOutcome, BrowserInstallTarget, ContainerStaleness, MigrationOptions, MigrationReport, MigrationState, ClearTokenOutcome, CaCertInfo, UploadOutcome } from "./types";
import type { Project, ProjectPath, ProjectRemovalReport, ContainerInfo, AppSettings, UpdateInfo, ImageUpdateInfo, FileEntry, FileContents, WebTerminalInfo, SttStatus, GatewayStatus, InstallOptions, ClaudeSession, ContainerCapabilities, ScheduledTask, ScheduledTaskInput, SchedulerNotification, AuthBridgeStatus, BrowserViewStatus, BrowserViewPopoutState, BrowserPageState, PlaywrightDetection, BrowserSetupOutcome, BrowserInstallTarget, ContainerStaleness, MigrationOptions, MigrationReport, MigrationState, ClearTokenOutcome, CaCertInfo, UploadOutcome } from "./types";
// Docker
export const checkDocker = () => invoke<boolean>("check_docker");
@@ -13,7 +13,7 @@ export const listProjects = () => invoke<Project[]>("list_projects");
export const addProject = (name: string, paths: ProjectPath[]) =>
invoke<Project>("add_project", { name, paths });
export const removeProject = (projectId: string) =>
invoke<void>("remove_project", { projectId });
invoke<ProjectRemovalReport>("remove_project", { projectId });
export const updateProject = (project: Project) =>
invoke<Project>("update_project", { project });
export const startProjectContainer = (projectId: string) =>
+9
View File
@@ -77,6 +77,15 @@ export type ProjectStatus =
| "stopping"
| "error";
/** What `removeProject` could not delete. The project is removed from the
* sidebar either way; anything named here is recorded on the host and
* retried automatically the next time the app starts. */
export interface ProjectRemovalReport {
container: string | null;
image: string | null;
volumes: string[];
}
export type Backend =
| "anthropic"
| "bedrock"