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:
61
app/src/hooks/useDocker.ts
Normal file
61
app/src/hooks/useDocker.ts
Normal 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,
|
||||
};
|
||||
}
|
||||
91
app/src/hooks/useProjects.ts
Normal file
91
app/src/hooks/useProjects.ts
Normal 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,
|
||||
};
|
||||
}
|
||||
38
app/src/hooks/useSettings.ts
Normal file
38
app/src/hooks/useSettings.ts
Normal 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,
|
||||
};
|
||||
}
|
||||
74
app/src/hooks/useTerminal.ts
Normal file
74
app/src/hooks/useTerminal.ts
Normal 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,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user