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

+24 -2
View File
@@ -1,6 +1,8 @@
import { useSyncExternalStore } from "react";
import { get } from "./api";
import type {
QueueItem,
User,
McpServerStatus,
Message,
Part,
@@ -22,11 +24,14 @@ export interface SessionError {
}
export interface State {
me: User | null;
connected: boolean;
sessions: Session[];
/** Includes subagent sessions loaded on demand. */
sessionById: Record<string, Session>;
messages: Record<string, Message[]>;
/** Sessions the current user may only read (shared with them). */
readOnly: Record<string, boolean>;
drafts: Record<string, Draft | undefined>;
busy: Record<string, boolean>;
progress: Record<string, string>;
@@ -34,13 +39,19 @@ export interface State {
questions: QuestionRequest[];
errors: Record<string, SessionError[]>;
mcp: Record<string, McpServerStatus>;
/** Messages waiting for a busy session. */
queue: Record<string, QueueItem[]>;
/** Why a session waits: "model:<spec>" or "subagent:<name>". */
waiting: Record<string, string | undefined>;
}
let state: State = {
me: null,
connected: false,
sessions: [],
sessionById: {},
messages: {},
readOnly: {},
drafts: {},
busy: {},
progress: {},
@@ -48,6 +59,8 @@ let state: State = {
questions: [],
errors: {},
mcp: {},
queue: {},
waiting: {},
};
const listeners = new Set<() => void>();
@@ -157,6 +170,12 @@ export function applyEvent(ev: ServerEvent) {
},
}));
break;
case "queue_updated":
setState((s) => ({ queue: { ...s.queue, [ev.session_id]: ev.items } }));
break;
case "waiting":
setState((s) => ({ waiting: { ...s.waiting, [ev.session_id]: ev.reason ?? undefined } }));
break;
case "mcp_status":
setState(() => ({ mcp: ev.servers }));
break;
@@ -188,18 +207,21 @@ export async function loadSessions() {
}
export async function loadSession(id: string) {
const data = await get<{ session: Session; messages: Message[]; busy: boolean }>(`/sessions/${id}`);
const data = await get<{ session: Session; messages: Message[]; busy: boolean; can_write: boolean; queue: QueueItem[] }>(`/sessions/${id}`);
setState((s) => ({
...upsertSession(s, data.session),
sessionById: { ...s.sessionById, [id]: data.session },
messages: { ...s.messages, [id]: data.messages },
busy: { ...s.busy, [id]: data.busy },
readOnly: { ...s.readOnly, [id]: !data.can_write },
queue: { ...s.queue, [id]: data.queue ?? [] },
}));
}
/** After reconnects: reload everything that could have changed. */
export async function resync() {
const [pending, mcp] = await Promise.all([get<any[]>("/pending"), get("/mcp")]);
const isAdmin = getState().me?.role === "admin";
const [pending, mcp] = await Promise.all([get<any[]>("/pending"), isAdmin ? get("/mcp") : Promise.resolve({})]);
setState(() => ({
permissions: pending.filter((e) => e.type === "permission_request"),
questions: pending.filter((e) => e.type === "question_request"),