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,93 @@
|
||||
import { useMemo } from "react";
|
||||
import { fmtRelative, useT, type Key } from "../i18n";
|
||||
import { navigate } from "../router";
|
||||
import { useStore } from "../store";
|
||||
import type { Session } from "../types";
|
||||
|
||||
function basename(p: string) {
|
||||
return p.split("/").filter(Boolean).pop() ?? p;
|
||||
}
|
||||
|
||||
function groupKey(ms: number): Key {
|
||||
const day = 86400000;
|
||||
const startOfToday = new Date().setHours(0, 0, 0, 0);
|
||||
if (ms >= startOfToday) return "sidebar.today";
|
||||
if (ms >= startOfToday - day) return "sidebar.yesterday";
|
||||
if (ms >= startOfToday - 7 * day) return "sidebar.week";
|
||||
return "sidebar.older";
|
||||
}
|
||||
|
||||
export function Sidebar({ open, activeId, section }: { open: boolean; activeId?: string; section: string }) {
|
||||
const t = useT();
|
||||
const sessions = useStore((s) => s.sessions);
|
||||
const busy = useStore((s) => s.busy);
|
||||
const connected = useStore((s) => s.connected);
|
||||
const permissions = useStore((s) => s.permissions);
|
||||
const questions = useStore((s) => s.questions);
|
||||
|
||||
const attention = useMemo(() => {
|
||||
const ids = new Set<string>();
|
||||
for (const p of permissions) ids.add(p.root_session_id ?? p.session_id);
|
||||
for (const q of questions) ids.add(q.root_session_id ?? q.session_id);
|
||||
return ids;
|
||||
}, [permissions, questions]);
|
||||
|
||||
const groups = useMemo(() => {
|
||||
const out: { key: Key; items: Session[] }[] = [];
|
||||
for (const s of sessions) {
|
||||
const key = groupKey(s.updated_at);
|
||||
const g = out.find((x) => x.key === key);
|
||||
if (g) g.items.push(s);
|
||||
else out.push({ key, items: [s] });
|
||||
}
|
||||
return out;
|
||||
}, [sessions]);
|
||||
|
||||
return (
|
||||
<aside className={`sidebar ${open ? "open" : ""}`}>
|
||||
<div className="sidebar-head">
|
||||
<div className="logo">
|
||||
<img src="/icon.svg" alt="" />
|
||||
CodeCraft
|
||||
</div>
|
||||
<span className={`conn-dot ${connected ? "on" : ""}`} title={connected ? t("sidebar.connected") : t("sidebar.disconnected")} />
|
||||
</div>
|
||||
<div style={{ padding: "0 12px 8px" }}>
|
||||
<button className="primary" style={{ width: "100%" }} onClick={() => navigate("/new")}>
|
||||
+ {t("sidebar.newSession")}
|
||||
</button>
|
||||
</div>
|
||||
<nav className="session-list">
|
||||
{sessions.length === 0 && <div className="empty">{t("sidebar.noSessions")}</div>}
|
||||
{groups.map((g) => (
|
||||
<div key={g.key}>
|
||||
<div className="session-group">{t(g.key)}</div>
|
||||
{g.items.map((s) => (
|
||||
<button
|
||||
key={s.id}
|
||||
className={`session-item ${s.id === activeId ? "active" : ""}`}
|
||||
onClick={() => navigate(`/s/${s.id}`)}
|
||||
>
|
||||
<div className="title">
|
||||
{attention.has(s.id) ? <i className="attn-dot" title={t("sidebar.needsInput")} /> : busy[s.id] && <i className="busy-dot" />}
|
||||
<span>{s.title || t("session.untitled")}</span>
|
||||
</div>
|
||||
<div className="sub">
|
||||
{basename(s.cwd)} · {s.agent} · {fmtRelative(s.updated_at)}
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</nav>
|
||||
<div className="sidebar-foot">
|
||||
<button className={`ghost ${section === "stats" ? "active" : ""}`} onClick={() => navigate("/stats")}>
|
||||
{t("nav.stats")}
|
||||
</button>
|
||||
<button className={`ghost ${section === "settings" ? "active" : ""}`} onClick={() => navigate("/settings")}>
|
||||
{t("nav.settings")}
|
||||
</button>
|
||||
</div>
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user