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

+29 -9
View File
@@ -2,6 +2,7 @@ import { useT } from "../i18n";
import { navigate } from "../router";
import { useStore } from "../store";
import type { Part, Todo } from "../types";
import { Spinner } from "./Activity";
import { Diff } from "./Diff";
type ToolCall = Extract<Part, { type: "tool_call" }>;
@@ -63,23 +64,34 @@ export function ToolCard({ call, result, running }: { call: ToolCall; result?: T
const statusText = result ? (result.is_error ? "✗" : "✓") : running ? "…" : "–";
const meta = result?.meta;
// The todo list is shown in its own panel; keep the card compact.
// The live task list has its own panel; updates are a single line here.
if (call.name === "todo_write" && !result?.is_error) {
const todos: Todo[] = call.input?.todos ?? [];
const done = todos.filter((x) => x.status === "completed").length;
const current = todos.find((x) => x.status === "in_progress");
return (
<div className="todos">
<div style={{ color: "var(--muted)", fontSize: 12 }}>{t("tool.todos")}</div>
<TodoList todos={call.input?.todos ?? []} />
<div className="todo-line">
☑ {t("tool.todosUpdated", { done, total: todos.length })}
{current && <span> · ▶ {current.content}</span>}
</div>
);
}
// Short diffs are shown right away, like in a terminal agent.
const diffLines = typeof meta?.diff === "string" ? meta.diff.split("\n").length : 0;
const openByDefault =
(status === "err" && !!result) ||
(!!result && !result.is_error && ((diffLines > 0 && diffLines <= 40) || call.name === "generate_image"));
return (
<details className="tool" open={status === "err" && !!result}>
<details className={`tool ${status === "run" ? "running" : ""}`} open={openByDefault}>
<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>
{call.name === "task" && meta?.model && <span className="chip">{meta.model}</span>}
{call.name === "task" && call.input?.background && <span className="chip">{t("tool.background")}</span>}
{status === "run" ? <Spinner small /> : <span className={`status ${status}`}>{statusText}</span>}
</summary>
<div className="tool-body">
{call.name === "bash" && <pre className="mono">$ {call.input?.command}</pre>}
@@ -107,9 +119,17 @@ export function ToolCard({ call, result, running }: { call: ToolCall; result?: T
<pre className="mono">{result.content}</pre>
</>
)}
{result?.images?.map((img, i) => (
<img key={i} src={`data:${img.media_type};base64,${img.data}`} alt="" />
))}
{Array.isArray(meta?.image_ids) ? (
<div className="gallery small">
{meta.image_ids.map((id: string) => (
<a key={id} href={`/api/images/${id}/file`} target="_blank" rel="noreferrer" className="gallery-item">
<img src={`/api/images/${id}/file`} alt="" />
</a>
))}
</div>
) : (
result?.images?.map((img, i) => <img key={i} src={`data:${img.media_type};base64,${img.data}`} alt="" />)
)}
</div>
</details>
);