Initial commit: Triple-C app, container, and CI

Tauri v2 desktop app (React/TypeScript + Rust) for managing
containerized Claude Code environments. Includes Gitea Actions
workflow for building and pushing the sandbox container image,
and a BUILDING.md guide for manual app builds on Linux and Windows.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-27 04:29:51 +00:00
commit 97a0745ead
65 changed files with 17202 additions and 0 deletions

View File

@@ -0,0 +1,61 @@
import { useCallback } from "react";
import { listen } from "@tauri-apps/api/event";
import { useAppState } from "../store/appState";
import * as commands from "../lib/tauri-commands";
export function useDocker() {
const {
dockerAvailable,
setDockerAvailable,
imageExists,
setImageExists,
} = useAppState();
const checkDocker = useCallback(async () => {
try {
const available = await commands.checkDocker();
setDockerAvailable(available);
return available;
} catch {
setDockerAvailable(false);
return false;
}
}, [setDockerAvailable]);
const checkImage = useCallback(async () => {
try {
const exists = await commands.checkImageExists();
setImageExists(exists);
return exists;
} catch {
setImageExists(false);
return false;
}
}, [setImageExists]);
const buildImage = useCallback(
async (onProgress?: (msg: string) => void) => {
const unlisten = onProgress
? await listen<string>("image-build-progress", (event) => {
onProgress(event.payload);
})
: null;
try {
await commands.buildImage();
setImageExists(true);
} finally {
unlisten?.();
}
},
[setImageExists],
);
return {
dockerAvailable,
imageExists,
checkDocker,
checkImage,
buildImage,
};
}

View File

@@ -0,0 +1,91 @@
import { useCallback } from "react";
import { useAppState } from "../store/appState";
import * as commands from "../lib/tauri-commands";
export function useProjects() {
const {
projects,
selectedProjectId,
setProjects,
setSelectedProject,
updateProjectInList,
removeProjectFromList,
} = useAppState();
const selectedProject = projects.find((p) => p.id === selectedProjectId) ?? null;
const refresh = useCallback(async () => {
const list = await commands.listProjects();
setProjects(list);
}, [setProjects]);
const add = useCallback(
async (name: string, path: string) => {
const project = await commands.addProject(name, path);
// Refresh from backend to avoid stale closure issues
const list = await commands.listProjects();
setProjects(list);
setSelectedProject(project.id);
return project;
},
[setProjects, setSelectedProject],
);
const remove = useCallback(
async (id: string) => {
await commands.removeProject(id);
removeProjectFromList(id);
},
[removeProjectFromList],
);
const start = useCallback(
async (id: string) => {
const updated = await commands.startProjectContainer(id);
updateProjectInList(updated);
return updated;
},
[updateProjectInList],
);
const stop = useCallback(
async (id: string) => {
await commands.stopProjectContainer(id);
const list = await commands.listProjects();
setProjects(list);
},
[setProjects],
);
const rebuild = useCallback(
async (id: string) => {
const updated = await commands.rebuildProjectContainer(id);
updateProjectInList(updated);
return updated;
},
[updateProjectInList],
);
const update = useCallback(
async (project: Parameters<typeof commands.updateProject>[0]) => {
const updated = await commands.updateProject(project);
updateProjectInList(updated);
return updated;
},
[updateProjectInList],
);
return {
projects,
selectedProject,
selectedProjectId,
setSelectedProject,
refresh,
add,
remove,
start,
stop,
rebuild,
update,
};
}

View File

@@ -0,0 +1,38 @@
import { useCallback } from "react";
import { useAppState } from "../store/appState";
import * as commands from "../lib/tauri-commands";
export function useSettings() {
const { hasKey, setHasKey } = useAppState();
const checkApiKey = useCallback(async () => {
try {
const has = await commands.hasApiKey();
setHasKey(has);
return has;
} catch {
setHasKey(false);
return false;
}
}, [setHasKey]);
const saveApiKey = useCallback(
async (key: string) => {
await commands.setApiKey(key);
setHasKey(true);
},
[setHasKey],
);
const removeApiKey = useCallback(async () => {
await commands.deleteApiKey();
setHasKey(false);
}, [setHasKey]);
return {
hasKey,
checkApiKey,
saveApiKey,
removeApiKey,
};
}

View File

@@ -0,0 +1,74 @@
import { useCallback } from "react";
import { listen } from "@tauri-apps/api/event";
import { useAppState } from "../store/appState";
import * as commands from "../lib/tauri-commands";
export function useTerminal() {
const { sessions, activeSessionId, addSession, removeSession, setActiveSession } =
useAppState();
const open = useCallback(
async (projectId: string, projectName: string) => {
const sessionId = crypto.randomUUID();
await commands.openTerminalSession(projectId, sessionId);
addSession({ id: sessionId, projectId, projectName });
return sessionId;
},
[addSession],
);
const close = useCallback(
async (sessionId: string) => {
await commands.closeTerminalSession(sessionId);
removeSession(sessionId);
},
[removeSession],
);
const sendInput = useCallback(
async (sessionId: string, data: string) => {
const bytes = Array.from(new TextEncoder().encode(data));
await commands.terminalInput(sessionId, bytes);
},
[],
);
const resize = useCallback(
async (sessionId: string, cols: number, rows: number) => {
await commands.terminalResize(sessionId, cols, rows);
},
[],
);
const onOutput = useCallback(
(sessionId: string, callback: (data: Uint8Array) => void) => {
const eventName = `terminal-output-${sessionId}`;
return listen<number[]>(eventName, (event) => {
callback(new Uint8Array(event.payload));
});
},
[],
);
const onExit = useCallback(
(sessionId: string, callback: () => void) => {
const eventName = `terminal-exit-${sessionId}`;
return listen<void>(eventName, () => {
callback();
});
},
[],
);
return {
sessions,
activeSessionId,
setActiveSession,
open,
close,
sendInput,
resize,
onOutput,
onExit,
};
}