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

+19 -1
View File
@@ -1,4 +1,5 @@
import { useMemo } from "react";
import { post } from "../api";
import { fmtRelative, useT, type Key } from "../i18n";
import { navigate } from "../router";
import { useStore } from "../store";
@@ -24,6 +25,7 @@ export function Sidebar({ open, activeId, section }: { open: boolean; activeId?:
const connected = useStore((s) => s.connected);
const permissions = useStore((s) => s.permissions);
const questions = useStore((s) => s.questions);
const me = useStore((s) => s.me);
const attention = useMemo(() => {
const ids = new Set<string>();
@@ -69,10 +71,11 @@ export function Sidebar({ open, activeId, section }: { open: boolean; activeId?:
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" />}
{attention.has(s.id) ? <i className="attn-dot" title={t("sidebar.needsInput")} /> : busy[s.id] && <i className="busy-ring" title={t("activity.working")} />}
<span>{s.title || t("session.untitled")}</span>
</div>
<div className="sub">
{s.shared && <span title={t("sidebar.shared")}>⇄ </span>}
{basename(s.cwd)} · {s.agent} · {fmtRelative(s.updated_at)}
</div>
</button>
@@ -80,7 +83,22 @@ export function Sidebar({ open, activeId, section }: { open: boolean; activeId?:
</div>
))}
</nav>
<div className="sidebar-user">
<span title={me?.username}>
{me?.display_name || me?.username}
{me?.role === "admin" && <span className="chip" style={{ marginLeft: 6 }}>admin</span>}
</span>
<button className="small ghost" onClick={() => post("/auth/logout").then(() => location.reload())}>
{t("nav.logout")}
</button>
</div>
<div className="sidebar-foot">
<button className={`ghost ${section === "files" ? "active" : ""}`} onClick={() => navigate("/files")}>
{t("nav.files")}
</button>
<button className={`ghost ${section === "images" ? "active" : ""}`} onClick={() => navigate("/images")}>
{t("nav.images")}
</button>
<button className={`ghost ${section === "stats" ? "active" : ""}`} onClick={() => navigate("/stats")}>
{t("nav.stats")}
</button>