Add web terminal, git credentials, OpenSCAD, review loops, automations

- Web terminal (PTY over WebSocket, xterm.js, sandboxed, reattach with replay)
- Per-user git credentials and commit identity (credential store in the user's home)
- OpenSCAD tool (STL/3MF + preview renders, bounding box) and cad skill; 3D model card
- Craft phase inferred from tool activity when the agent does not report it
- Configurable review loop per mode (rounds, reviewer model, criteria)
- Delete config and Markdown agents from the agent editor
- Automations: cron-scheduled agents from Markdown job files, schedule tool, page
- change_directory tool (only on the user's request)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Richard ZinkandClaude Opus 5.5 committed 2026-11-08 22:00:38 +01:00
1 parent a0d0e707f2
commit df1924b0a1
41 files changed
+2809 -34

No files matched your search

+15 -5
View File
@@ -85,6 +85,12 @@ const ICONS: Record<string, ReactNode> = {
<path d="m20 20-4.5-4.5M9 9l4 4M13 9l-4 4" />
</>,
),
cad: svg(
<>
<path d="m12 3 8 4.5v9L12 21l-8-4.5v-9Z" />
<path d="m4 7.5 8 4.5 8-4.5M12 12v9" />
</>,
),
folder: svg(<path d="M3 6a1 1 0 0 1 1-1h5l2 2h9a1 1 0 0 1 1 1v10a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1Z" />),
git: svg(
<>
@@ -190,12 +196,12 @@ export function NewSession({ onMenu, onCreated }: { onMenu: () => void; onCreate
}, [target]);
// Working on existing code usually happens in an existing folder.
useEffect(() => {
setFolderMode((m) => (m === "git" ? m : task === "craft" ? "new" : "existing"));
setFolderMode((m) => (m === "git" ? m : task === "craft" || task === "cad" ? "new" : "existing"));
}, [task]);
const repoName = repoUrl.trim().replace(/\/+$/, "").replace(/\.git$/, "").split(/[/:]/).pop() ?? "";
const autoName = useMemo(
() => (folderMode === "git" && repoName ? slug(repoName) : "") || slug(target === "website" && url ? url : text) || (task === "craft" ? `${target}-project` : "project"),
() => (folderMode === "git" && repoName ? slug(repoName) : "") || slug(target === "website" && url ? url : text) || (task === "craft" ? `${target}-project` : task === "cad" ? "3d-model" : "project"),
[text, url, target, task, folderMode, repoName],
);
useEffect(() => {
@@ -203,7 +209,9 @@ export function NewSession({ onMenu, onCreated }: { onMenu: () => void; onCreate
}, [autoName, nameTouched]);
const ideas: Key[] =
task !== "craft"
task === "cad"
? ["wizard.idea.cadBox", "wizard.idea.cadBracket"]
: task !== "craft"
? task === "plan"
? ["wizard.idea.planFeature", "wizard.idea.planRefactor"]
: ["wizard.idea.buildTests", "wizard.idea.buildBug"]
@@ -238,6 +246,7 @@ export function NewSession({ onMenu, onCreated }: { onMenu: () => void; onCreate
const prompts = (uploaded: string[]): string[] => {
const desc = text.trim();
const fileList = uploaded.length ? `\n\nFiles: ${uploaded.join(", ")}` : "";
if (task === "cad") return [`/cad ${desc}${images.length ? " (sketch attached)" : ""}${fileList}`];
if (task !== "craft") return [desc + fileList];
switch (target) {
case "website":
@@ -311,7 +320,7 @@ export function NewSession({ onMenu, onCreated }: { onMenu: () => void; onCreate
const s = await post<{ id: string }>("/sessions", {
cwd,
model: model || undefined,
agent: task,
agent: task === "cad" ? "build" : task,
clean_room: isCraft ? !deep : undefined,
});
const msgs = prompts(uploaded).filter((m) => m.trim());
@@ -343,6 +352,7 @@ export function NewSession({ onMenu, onCreated }: { onMenu: () => void; onCreate
<Step n={n++} title={t("wizard.whatToDo")}>
<div className="wiz-cards">
<Card icon="craft" title={t("wizard.task.craft")} desc={t("wizard.task.craftDesc")} active={task === "craft"} onClick={() => setTask("craft")} />
<Card icon="cad" title={t("wizard.task.cad")} desc={t("wizard.task.cadDesc")} active={task === "cad"} onClick={() => setTask("cad")} />
<Card icon="build" title={t("wizard.task.build")} desc={t("wizard.task.buildDesc")} active={task === "build"} onClick={() => setTask("build")} />
<Card icon="plan" title={t("wizard.task.plan")} desc={t("wizard.task.planDesc")} active={task === "plan"} onClick={() => setTask("plan")} />
{custom.map((a) => (
@@ -436,7 +446,7 @@ export function NewSession({ onMenu, onCreated }: { onMenu: () => void; onCreate
>
<textarea
value={text}
placeholder={t(task === "craft" ? (`wizard.placeholder.${target}` as Key) : "wizard.placeholder.task")}
placeholder={t(task === "craft" ? (`wizard.placeholder.${target}` as Key) : task === "cad" ? "wizard.placeholder.cad" : "wizard.placeholder.task")}
onChange={(e) => setText(e.target.value)}
onPaste={(e) => {
if (e.clipboardData.files.length) {