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:
commit
f3a2fe3816
62 files changed
+21835
No files matched your search
@@ -0,0 +1,116 @@
|
||||
import { useT } from "../i18n";
|
||||
import { navigate } from "../router";
|
||||
import { useStore } from "../store";
|
||||
import type { Part, Todo } from "../types";
|
||||
import { Diff } from "./Diff";
|
||||
|
||||
type ToolCall = Extract<Part, { type: "tool_call" }>;
|
||||
type ToolResult = Extract<Part, { type: "tool_result" }>;
|
||||
|
||||
function short(s: unknown, n = 90): string {
|
||||
const str = typeof s === "string" ? s : JSON.stringify(s);
|
||||
return str.length > n ? `${str.slice(0, n)}…` : str;
|
||||
}
|
||||
|
||||
/** One-line summary of a tool call. */
|
||||
export function toolTitle(name: string, input: any): string {
|
||||
switch (name) {
|
||||
case "read":
|
||||
case "write":
|
||||
case "edit":
|
||||
return input?.path ?? "";
|
||||
case "list":
|
||||
return input?.path ?? ".";
|
||||
case "bash":
|
||||
return input?.description || input?.command || "";
|
||||
case "glob":
|
||||
case "grep":
|
||||
return `${input?.pattern ?? ""}${input?.path ? ` in ${input.path}` : ""}`;
|
||||
case "web_fetch":
|
||||
case "browser_snapshot":
|
||||
return input?.url ?? "";
|
||||
case "task":
|
||||
return `${input?.subagent_type ?? "general"}: ${input?.description ?? ""}`;
|
||||
case "image_compare":
|
||||
return `${input?.original ?? ""} ↔ ${input?.candidate ?? ""}`;
|
||||
case "skill":
|
||||
return input?.name ?? "";
|
||||
case "ask_user":
|
||||
return input?.questions?.[0]?.question ?? "";
|
||||
default:
|
||||
return short(input);
|
||||
}
|
||||
}
|
||||
|
||||
export function TodoList({ todos }: { todos: Todo[] }) {
|
||||
const icon = { completed: "☑", in_progress: "▶", pending: "☐" } as const;
|
||||
return (
|
||||
<ul>
|
||||
{todos.map((td, i) => (
|
||||
<li key={i} className={td.status}>
|
||||
<span>{icon[td.status] ?? "☐"}</span>
|
||||
<span>{td.content}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
|
||||
export function ToolCard({ call, result, running }: { call: ToolCall; result?: ToolResult; running: boolean }) {
|
||||
const t = useT();
|
||||
const progress = useStore((s) => s.progress[call.id]);
|
||||
const status = result ? (result.is_error ? "err" : "ok") : running ? "run" : "err";
|
||||
const statusText = result ? (result.is_error ? "✗" : "✓") : running ? "…" : "–";
|
||||
const meta = result?.meta;
|
||||
|
||||
// The todo list is shown in its own panel; keep the card compact.
|
||||
if (call.name === "todo_write" && !result?.is_error) {
|
||||
return (
|
||||
<div className="todos">
|
||||
<div style={{ color: "var(--muted)", fontSize: 12 }}>{t("tool.todos")}</div>
|
||||
<TodoList todos={call.input?.todos ?? []} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<details className="tool" open={status === "err" && !!result}>
|
||||
<summary>
|
||||
<span className="tool-name">{call.name}</span>
|
||||
<span className="tool-title">{toolTitle(call.name, call.input)}</span>
|
||||
{meta?.similarity !== undefined && <span className="chip">{meta.similarity} %</span>}
|
||||
<span className={`status ${status}`}>{statusText}</span>
|
||||
</summary>
|
||||
<div className="tool-body">
|
||||
{call.name === "bash" && <pre className="mono">$ {call.input?.command}</pre>}
|
||||
{(call.name === "edit" || call.name === "write") && meta?.diff && <Diff diff={meta.diff} />}
|
||||
{call.name === "task" && meta?.session_id && (
|
||||
<button className="small" onClick={() => navigate(`/s/${meta.session_id}`)}>
|
||||
{t("tool.openSubagent")}
|
||||
</button>
|
||||
)}
|
||||
{!["bash", "edit", "write", "task"].includes(call.name) && (
|
||||
<>
|
||||
<div className="label">{t("tool.input")}</div>
|
||||
<pre className="mono">{JSON.stringify(call.input, null, 2)}</pre>
|
||||
</>
|
||||
)}
|
||||
{!result && progress && (
|
||||
<>
|
||||
<div className="label">{t("tool.output")}</div>
|
||||
<pre className="mono">{progress}</pre>
|
||||
</>
|
||||
)}
|
||||
{result && !((call.name === "edit" || call.name === "write") && meta?.diff && !result.is_error) && (
|
||||
<>
|
||||
<div className="label">{t("tool.output")}</div>
|
||||
<pre className="mono">{result.content}</pre>
|
||||
</>
|
||||
)}
|
||||
{result?.images?.map((img, i) => (
|
||||
<img key={i} src={`data:${img.media_type};base64,${img.data}`} alt="" />
|
||||
))}
|
||||
</div>
|
||||
</details>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user