Initial CodeCraft: web-based coding harness for AI models

Rust backend (axum/tokio): agent loop with streaming, tools (fs, bash,
web_fetch, todo, ask_user, subagents, skills, craft snapshots/compare),
permissions with modes and rules, Anthropic + OpenAI-compatible providers,
MCP client (stdio/HTTP, elicitation), SQLite sessions, usage statistics,
compaction, commented TOML config editable via toml_edit.

React PWA: chat with live streaming, permission/question cards, modes
(build/plan/craft), settings for providers/models/prices/MCP/permissions,
raw config editor, statistics dashboard, i18n (en/de). Docker setup.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Richard ZinkandClaude Opus 5.5 committed 2026-11-08 14:39:24 +01:00
commit f3a2fe3816
62 files changed
+21835

No files matched your search

+125
View File
@@ -0,0 +1,125 @@
import { useEffect, useState } from "react";
import { post } from "../api";
import { useAgents, useFetch } from "../hooks";
import { useT } from "../i18n";
import { ModelSelect } from "./ModelSelect";
interface DirListing {
path: string;
parent: string | null;
workspace: string;
dirs: { name: string; path: string; git: boolean }[];
}
const LAST_DIR = "codecraft.lastDir";
export function NewSession({ onMenu, onCreated }: { onMenu: () => void; onCreated: (id: string) => void }) {
const t = useT();
const [path, setPath] = useState<string>(localStorage.getItem(LAST_DIR) ?? "");
const { data: listing, error: dirError } = useFetch<DirListing>(`/fs/dirs${path ? `?path=${encodeURIComponent(path)}` : ""}`);
const config = useFetch<{ default_model?: string }>("/config").data;
const [model, setModel] = useState("");
const [agent, setAgent] = useState("build");
const [prompt, setPrompt] = useState("");
const [error, setError] = useState<string>();
const [creating, setCreating] = useState(false);
const agents = useAgents(listing?.path).filter((a) => a.mode === "primary");
useEffect(() => {
if (!model && config?.default_model) setModel(config.default_model);
}, [config, model]);
// A remembered directory that vanished: fall back to the workspace root.
useEffect(() => {
if (dirError && path) setPath("");
}, [dirError, path]);
const create = async () => {
if (!listing) return;
setCreating(true);
setError(undefined);
try {
localStorage.setItem(LAST_DIR, listing.path);
const s = await post<{ id: string }>("/sessions", { cwd: listing.path, model: model || undefined, agent });
if (prompt.trim()) await post(`/sessions/${s.id}/prompt`, { text: prompt.trim() });
onCreated(s.id);
} catch (e: any) {
setError(e.message);
setCreating(false);
}
};
const rel = listing ? listing.path.slice(listing.workspace.length) || "/" : "";
return (
<>
<div className="topbar">
<button className="ghost menu-btn" onClick={onMenu} aria-label={t("nav.menu")}>
☰
</button>
<div className="grow">
<h1>{t("new.title")}</h1>
</div>
</div>
<div className="page">
<div className="page-inner" style={{ maxWidth: 760 }}>
<div className="panel">
<h3>{t("new.directory")}</h3>
<div className="row" style={{ marginBottom: 8 }}>
<code className="grow" style={{ flex: 1 }}>
{rel}
</code>
{listing?.parent && (
<button className="small" onClick={() => setPath(listing.parent!)}>
↑ {t("new.up")}
</button>
)}
</div>
<div className="dir-list">
{listing?.dirs.length === 0 && <div className="empty" style={{ padding: 16 }}>{t("new.noSubdirs")}</div>}
{listing?.dirs.map((d) => (
<button key={d.path} onClick={() => setPath(d.path)}>
<span>📁</span>
<span style={{ flex: 1 }}>{d.name}</span>
{d.git && <span className="chip">git</span>}
</button>
))}
</div>
</div>
<div className="panel">
<div className="form">
<label>{t("new.model")}</label>
<ModelSelect value={model} onChange={setModel} />
<label>{t("new.mode")}</label>
<select value={agent} onChange={(e) => setAgent(e.target.value)}>
{agents.map((a) => (
<option key={a.name} value={a.name}>
{a.name} – {a.description}
</option>
))}
</select>
</div>
</div>
<div className="panel">
<h3>{t("new.firstPrompt")}</h3>
<textarea
style={{ width: "100%", minHeight: 110 }}
value={prompt}
placeholder={t("composer.placeholder")}
onChange={(e) => setPrompt(e.target.value)}
onKeyDown={(e) => {
if (e.key === "Enter" && (e.ctrlKey || e.metaKey)) void create();
}}
/>
</div>
{error && <div className="notice err">{error}</div>}
<button className="primary" disabled={!listing || creating} onClick={create}>
{t("new.start", { dir: rel })}
</button>
</div>
</div>
</>
);
}