Users, file manager, image generation, budgets and agent workflow

- User management: local accounts (Argon2) and OpenID Connect, admin/user
  roles, per-user folders plus admin-shared folders, read-only session
  sharing, event filtering per user
- File manager with CodeMirror editor and git client (status, diff,
  stage, commit, branches, fetch/pull/push, log, clone/init)
- Image generation via fal.ai: generate_image tool and image page
- Budgets: monthly limits per provider, provider balances/costs
  (OpenRouter, Anthropic admin cost report, fal.ai), price discovery
- Subagents on other models, background tasks, concurrency limits per
  host/model/subagent
- MCP tools loaded on demand (mcp_search/mcp_call), read-only hints
- Permission mode "auto", safe read-only commands always allowed
- Persistent message queue with attachments, deletable
- Live reasoning incl. <think> tags, activity animations, CSS themes,
  mobile layout, statistics per user, upload size handling
- Docker: named volume for state, localhost-bound port

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Richard ZinkandClaude Opus 5.5 committed 2026-11-08 16:54:08 +01:00
1 parent f3a2fe3816
commit 49262c612c
70 files changed
+8495 -714

No files matched your search

+80
View File
@@ -0,0 +1,80 @@
import { useEffect } from "react";
import { useT } from "../i18n";
import { useStore, type Draft } from "../store";
import type { Part } from "../types";
type ToolResult = Extract<Part, { type: "tool_result" }>;
/** Animated three-dot pulse. */
export function Dots() {
return (
<span className="dots" aria-hidden="true">
<i />
<i />
<i />
</span>
);
}
export function Spinner({ small }: { small?: boolean }) {
return <span className={`spinner ${small ? "small" : ""}`} aria-hidden="true" />;
}
/** What the agent is doing right now, derived from the streaming message. */
export function ActivityLine({
draft,
results,
waiting,
permissionPending,
}: {
draft?: Draft;
results: Map<string, ToolResult>;
waiting?: string;
permissionPending: boolean;
}) {
const t = useT();
let label: string;
if (permissionPending) {
label = t("activity.waitingForYou");
} else if (waiting) {
const [kind, ...rest] = waiting.split(":");
const what = rest.join(":");
label = kind === "model" ? t("chat.waitingModel", { model: what }) : t("chat.waitingSubagent", { agent: what === "*" ? t("chat.anySubagent") : what });
} else {
const last = draft?.parts[draft.parts.length - 1];
const runningTool = draft?.parts.filter((p) => p.type === "tool_call" && !results.has(p.id)).pop() as
| Extract<Part, { type: "tool_call" }>
| undefined;
if (runningTool) {
label = runningTool.name === "task" ? t("activity.subagent", { name: runningTool.input?.description ?? "" }) : t("activity.tool", { tool: runningTool.name });
} else if (last?.type === "text") {
label = t("activity.writing");
} else {
label = t("activity.thinking");
}
}
return (
<div className={`activity ${permissionPending ? "paused" : ""}`} role="status" aria-live="polite">
{permissionPending ? <span className="pause-icon" aria-hidden="true" /> : <Dots />}
<span>{label}</span>
</div>
);
}
/** Thin animated bar shown under the top bar while a session works. */
export function WorkBar({ active }: { active: boolean }) {
return <div className={`workbar ${active ? "on" : ""}`} aria-hidden="true" />;
}
/** Prefix the browser tab title while agents work or wait for input. */
export function useTitleActivity() {
const t = useT();
const busyCount = useStore((s) => s.sessions.filter((x) => s.busy[x.id]).length);
const waiting = useStore((s) => s.permissions.length + s.questions.length);
useEffect(() => {
const base = "CodeCraft";
if (waiting > 0) document.title = `● ${t("activity.titleInput")} – ${base}`;
else if (busyCount > 0) document.title = `⏳ ${t("activity.titleBusy", { n: busyCount })} – ${base}`;
else document.title = base;
}, [busyCount, waiting, t]);
}