Adds <LlmConfigBootstrap /> in root.tsx that, when localStorage has no
active LLM settings, fetches enabled configurations from arcadia and
seeds the active settings (provider/model/baseURL/secretName + reasoning
effort) from the preferred row. Idempotent and silent on auth failure;
retries on session change.
Selection: prefer metadata.default === true, otherwise first enabled row.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Bring the LLM agent's prompts and tools current with the new /search
section and arcadia-search admin sidecar:
- New tools in admin-tools.ts:
- list_search_corpora: enumerate tenants + corpora with build status,
so the agent can pick a real corpus instead of guessing.
- rebuild_search_corpus(tenant, corpus): isWrite=true, surfaces a
confirm card. Use after uploads or when results look stale.
- search_kb description updated: names docs / operator-tools / files
explicitly, and points at list_search_corpora when unsure.
- ARCADIA_KNOWLEDGE: adds search-corpus terminology, /search route,
and a one-liner pointer to the three new tools.
- assistant.tsx UI_CONTROL_PREFACE: nav-search added, full Search
page action catalog (search-refresh / -restart / -new-tenant /
-new-corpus, corpora-search, per-row corpus-{t}-{c}-{rebuild,edit,
delete,actions}, tenant-{id}-delete, dialog form fields). Recipe
for the manual rebuild path, plus a note steering the agent to
the rebuild_search_corpus tool by default.
- search.tsx publishes a "search" surface to admin-context with
tenants + corpora summary, so the agent gets live state without
needing a tool call when /search is mounted.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
New /search route manages tenants and corpora on the arcadia-search
box via its privileged /admin/* surface (default :7801) — KPI tiles,
flat tenant×corpus table with Rebuild / Edit config / Delete
actions, New tenant / New corpus dialogs, and a Restart service
button. New app/lib/search-admin.ts wraps the bearer-token fetch.
Configured by VITE_ARCADIA_SEARCH_ADMIN_URL +
VITE_ARCADIA_SEARCH_ADMIN_TOKEN; the route renders a warning banner
when the token is unset. Token ships in the client bundle — fine for
this internal tool, called out in CLAUDE.md and the source comments.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Arcadia wiring:
- home: real Overview dashboard (tenants/users/audit/health probe) replacing the inherited Vibespace welcome tiles; skeleton loaders, refresh button, registers admin context
- profile: split into Account (synced via getUser/updateUser of session user) and local Preferences; updateSessionUser keeps the appbar in sync after edits
- session: drop unused signIn mock, add updateSessionUser, refresh tests
- profile schema: drop redundant Profile.name/email (session is the source of truth)
- routes: delete orphaned resources route + lib
Auth flows that previously 404'd:
- /signup, /login/forgot, /login/reset, /login/2fa wired via @crema/arcadia-auth-ui
- shared AuthShell + AuthBrand wrapper
Assistant tools (admin-tools.ts):
- +10 tools: deactivate_tenant, set_user_status, delete_user, list_memberships, list_roles, revoke_api_key, create_user, update_user, assign_role, remove_role
- list_memberships gains user_id filter for "tenants this user belongs to" queries
- search_kb / read_chunk: new token resolution (window override → VITE_ARCADIA_SEARCH_TOKEN service token → operator session JWT → "dev"); on 401/403 emit a tailored hint based on which token was used
UI consistency:
- new PageHeader component
- AppShell.title was unrendered — dropped; first-child padding on #main-content keeps the floating actions pill from colliding with header content
- removed dead "Sign in required" fallback cards from 14 routes (AppShell already redirects)
- stripped p-6 from outer wrappers across 14 routes (was double-padding under AppShell's own p-6)
- migrated home + tenants to PageHeader
arcadia-search ergonomics:
- scripts/mint-search-token.mjs + `npm run mint:search-token` mints HS512 JWT with required tenant_id claim, upserts VITE_ARCADIA_SEARCH_TOKEN into .env.local
- README/.env document the new VITE_ARCADIA_SEARCH_URL / VITE_ARCADIA_SEARCH_TOKEN knobs
- .env.local now gitignored
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three problems surfaced when driving search_kb / read_chunk through a
real DeepSeek chat for the first time:
1. vite.config.ts: minisearch wasn't in `aliasedDeps`, so when
@crema/lexical-rag-ui imports it, Vite couldn't resolve the bare
specifier from the sibling lib's location. Added it next to the
other shared deps. tsconfig paths alone is not enough — the Vite
alias is what propagates resolution to sibling-lib code.
2. ai.tsx reindexKB: was using `toast.show?.()` which doesn't exist on
useToast()'s API. Optional chaining silently no-op'd, so the button
click ran the fetch but produced zero UI feedback (success or
failure). Switched to the actual API: toast.info / toast.success /
toast.error. Added a console.error in the catch arm so the
underlying exception is visible in DevTools when something does go
wrong.
3. ai.tsx MAX_TOOL_ITERATIONS: cap was 3, which is too tight for
agentic search→read→search loops on real questions. Bumped to 6.
More importantly, when the cap IS reached, the runner now
synthesises tool-error messages for each pending tool_call and
continues the chat — instead of silently dropping them, which left
the conversation with an assistant.tool_calls turn but no matching
tool messages. DeepSeek (and the OpenAI spec) reject that
conversation with 400 ("insufficient tool messages following
tool_calls"), poisoning the thread. Now the model gets a clean
"max iterations reached" signal and produces a final answer.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds the agent-facing surface for the new Tantivy lexical search service
(arcadia-search). Sits alongside the existing search_docs (browser
MiniSearch) — agent picks based on tool description.
- admin-tools.ts: new search_kb(query, corpus, limit?, tags?) and
read_chunk(chunk_id, corpus) tools. KB_BASE_URL honors
window.__ARCADIA_SEARCH_URL runtime override + VITE_ARCADIA_SEARCH_URL
build env, defaults to localhost:7800. Token resolved per-call from
sessionStorage.arcadia_access_token (matching lib-arcadia-client's
storage convention) with "dev" fallback for unauthenticated dev.
- assistant.tsx: system-prompt section telling the agent when to pick
search_docs (browser, bundled) vs search_kb (server, dynamic +
expandable via read_chunk).
- ai.tsx: reindexKB() helper + "reindex kb (docs)" button on the empty
state, next to the existing block-preview button. Toasts on
start/success/failure. Wired with data-action="kb-reindex-docs" so
the agent can also trigger via the command bus.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replace the in-app docs-search.ts and build-docs-index.mjs with the
new sibling lib (@crema/lexical-rag-ui). Wire-up only — same index
shape, same tool response shape, same MiniSearch config, so the agent
sees no behavior change.
- tsconfig + app.css: wire the lib; alias minisearch to consumer's
node_modules so sibling-lib resolution works.
- admin-tools.ts: createRAGClient("/docs-index.json"), keep search_docs
tool's response shape unchanged (collapse tags[] back to category).
- ai.tsx: define DocHit locally — it's the tool-response shape, no
longer the lib's internal type.
- scripts/build-docs-index.mjs: thin wrapper that injects MiniSearch
and calls buildIndex. Per-app sources list and tags live here.
- package.json: add minisearch dep + build:docs script + prebuild hook.
- .gitignore: don't commit the generated /public/docs-index.json.
Delete: app/lib/docs-search.ts (was untracked; its logic moved to lib).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Assistant replies can now emit typed fenced blocks that render as
@crema/*-ui components inline at their position in the reply.
- message-body.tsx: segmented rendering — alternating prose chunks and
block dispatch (was: all blocks appended at end). Renderers for kpi,
table, chart-bar/-line/-donut/-spark, code, diff, flowchart, orgchart,
steps, checklist, welcome, hint, plus the legacy card kinds.
- block-schemas.ts: single source of truth — BLOCK_INDEX (one-line
purpose per kind, always in prompt) + SCHEMAS (full JSON shape +
example, fetched on demand).
- admin-tools.ts: new get_block_schema(kind) tool the model calls once
per kind per thread to fetch the exact schema. Keeps the always-on
prompt small (~110 tokens vs ~400 inline).
- assistant.tsx: replaces the inline schema dump with the generated
thin index.
- ai.tsx: empty-state preview button injects a synthetic assistant
message exercising every block, for renderer/theme smoke-testing.
- console.css + ai.tsx: shrink ATLAS headline so it doesn't slip under
the composer with the added preview button.
- tsconfig.json + app.css: wire lib-data-ui, lib-code-ui, lib-diagram-ui,
lib-onboarding-ui as siblings.
Adding a new block kind = add the lib paths, add a renderer case, add
a schema entry. No prompt edits required.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Matches the arcadia-app providers map update — direct-mode "Test
connection" was probing https://api.deepseek.com/v1/models which 404s
on DeepSeek's new endpoint. Now probes /models at the host root.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The login page is the operator's entry point — it should look the
same every time, not flip between light and dark depending on what
the previous session left in localStorage.
Adds the `dark` class to the login wrapper div instead of
documentElement, so:
- Skyrise's .dark tokens cascade into all descendants (CSS vars defined
under .dark apply to the subtree).
- After sign-in and navigation, the user's saved light/dark preference
takes back over for the rest of the app.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The handoffNote useMemo I added in the agent-history work referenced
useMemo without importing it. Vite was happy at compile time (lazy
binding) but the page crashes at first paint with "Can't find
variable: useMemo".
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Pulls the reasoning storage out of ai.tsx and into the shared
llm-configs.ts helpers so Settings → LLM and the /ai composer
coordinate via one localStorage key (crema.ai.reasoning):
- loadActiveReasoning / saveActiveReasoning: read/write helpers.
- subscribeActiveReasoning: dispatches a CustomEvent on writes
(same-tab) plus a storage-event listener (cross-tab), so the
chip updates live when the operator stars a different config in
another tab or in the settings panel.
Wiring:
- Settings panel onMakeActive() now also calls
saveActiveReasoning(c.reasoning_effort ?? "off"). Starring a
config seeds the chip with that config's default.
- /ai chip useEffect subscribes to changes; a star in Settings
while /ai is open flips the chip in real time.
- resetAndClear no longer wipes reasoningEffort. Clearing the
conversation shouldn't silently undo the operator's stated
intent for thinking-mode (which is bound to their active config,
not to the conversation).
Net behaviour:
- Star a config with reasoning_effort=medium → chip on /ai shows
THINK MEDIUM next time you visit (or immediately if /ai is open).
- Cycle the chip while on /ai → just an override for the current
conversation, not back-propagated to the saved config.
- Edit the config in Settings to change its default → propagates to
the chip on next star (intentional — direct edits don't auto-
re-activate).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two layers for thinking-mode control:
1. Per-config default (Settings → LLM)
New "Reasoning effort" Select in the Add/Edit dialog with
off/low/medium/high/max + a budget hint per option (~2k, ~8k,
~24k, ~64k thinking tokens). Saved row meta line surfaces the
level inline so it's visible without opening the editor.
2. Per-message override (composer chip)
New ReasoningChip next to the model picker. Click cycles through
the same five levels. Hidden chrome when off (muted "think" pill);
sodium-amber active style with the level label when set.
Persisted to crema.ai.reasoning so a refresh keeps the operator's
intent, wiped together with the conversation on Clear.
When sending, withReasoning() merges reasoning_effort into the request
body as a top-level field. The proxy forwards it untouched to OpenAI /
DeepSeek (native field) and translates to Anthropic's thinking block
server-side.
reasoningEffortRef sidesteps a useCallback ordering issue —
regenerateLast/continueLast are declared before the state hook, so
they read the ref instead of a stale closure.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Reloading the page mid-conversation used to keep the messages (via
LIVE_KEY) but lose the two agent-tracking maps, so:
- row signatures snapped back to whoever was currently active
- the next turn after reload didn't include the PRIOR HAND-OFF
block, even though the transcript clearly had multiple personas
Both maps are now stored alongside the live snapshot:
AGENTS_KEY crema.ai.agent-history set of Agent
MSG_AGENTS_KEY crema.ai.message-agents index -> Agent
Stored as JSON arrays since Maps don't serialize. Hydrated on mount
via useState lazy initializer, persisted on every change via two
useEffects, cleared in lockstep with LIVE_KEY when the operator
hits Clear conversation.
Reload mid-thread now reads identically to the pre-reload state:
- atlas» turns 1-3 stay attributed to atlas
- pythia» turn 4 stays attributed to pythia
- next turn after reload still carries the hand-off note
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two related fixes for the "switching agent mid-thread loses context"
issue:
1. LLM-context fix
The system prompt now includes a PRIOR HAND-OFF block whenever the
current conversation has been touched by more than one agent. It
lists the prior personas (name + role) and tells the new agent:
"Earlier turns were produced by other personas. Read them as
context, but answer in your own voice as the current persona."
Without this, switching from Atlas (Operator) to Pythia (Researcher)
left Pythia answering as if she'd produced Atlas's prior turns.
Tracked via two-trigger useEffect:
- On agent change with messages already in the thread, the prior
agent gets locked into history.
- On stream finish, the current active agent gets added (it just
produced a turn).
Cleared with the conversation.
2. UI-attribution fix
Each assistant turn now records which agent produced it
(messageAgents map: index -> Agent). The row signature in
MessageRow now reads that stamped agent rather than always echoing
the currently-active one. Switching agents mid-thread no longer
retroactively re-attributes prior responses.
Both maps are wiped by Clear conversation alongside the live snapshot
and initialLive ref, so a fresh thread starts truly fresh.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
useChat exposes reset() which calls setMessages(opts.initialMessages),
and the AI page passes initialLive.current as initialMessages — a ref
captured once on mount from localStorage.
resetAndClear was calling reset() then clearLive(). Sequence problem:
reset() → setMessages(initialLive.current) // populated old array
clearLive() → localStorage.removeItem(LIVE_KEY) // does nothing to memory
The ref still held the original messages, so reset re-seeded them and
the conversation appeared to "come back" the moment you typed anything
(or sometimes immediately, depending on render timing).
Fix: blank the ref, clear localStorage, and call setMessages([])
directly. reset() is no longer needed at this call site so it's been
dropped from the useChat destructure.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The project uses @base-ui/react, whose Button has no asChild prop —
just a `render` prop that takes a React element to merge into. About
14 call sites across the routes still use the Radix-shaped
`<Button asChild><Link to="…">…</Link></Button>` pattern, which until
now was producing nested-button DOM violations and asChild leaking as
a DOM attribute.
Bridges asChild → render inside the Button wrapper:
<Button asChild><Link to="/login">Sign in</Link></Button>
…now renders as a single <a class="…btn classes…">Sign in</a> instead
of <button asChild><a>…</a></button>.
No call-site changes required; consumers keep the Radix ergonomic and
get correct DOM under the hood.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The composer's model picker had <DropdownMenuTrigger asChild><button>...
which is the Radix Slot pattern. Project uses @base-ui/react where
Menu.Trigger has no asChild prop and renders its own <button>, so the
result was nested-button-inside-button (DOM-nesting violation) plus
asChild leaking as an unknown DOM attribute (React warning).
Dropped the inner <button> and put className/data-action straight on
the Trigger. Visual output identical, no more console errors.
This pattern is used by ~14 other routes (Button asChild + Link),
mostly behind sign-in-required states. They're broken too but rarely
fire — separate followup.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two bugs in the previous /ai redesign:
1. theme="console" on AppShell put the entire shell (sidebar, appbar,
appbar dropdowns, the lot) inside [data-theme="console"], so the
console palette + JetBrains Mono override leaked into the sidebar
and made light mode look broken on /ai. Scoped now: the AppShell
stays in skyrise (so light/dark toggle keeps working everywhere),
and only the route content area gets data-theme="console" via an
inner wrapper.
2. The Google Fonts @import inside console.css was being silently
dropped because @import rules must precede all other rules in the
final bundle, and skyrise's content lands first. Moved JetBrains
Mono + Newsreader into app.css's top-level @import url() alongside
the existing Inter/Instrument Sans/Geist Mono families.
Atmosphere ::before was also position: fixed, which painted the grain
overlay across the whole viewport (including the sidebar) regardless
of where data-theme lived. Now position: absolute on the wrapper, with
isolation: isolate to keep z-index local.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces the conventional chat aesthetic on /ai with a brutalist-mono
operator deck. The page now reads as a flight recorder — turn numbers
in the gutter, hairline rules, sodium-amber phosphor primary on
deep-ink ground, vim-style modeline at the foot.
Type system is the design's load-bearing element:
- JetBrains Mono for everything system-y (operator lines, signatures,
modeline, session ids, tool calls)
- Newsreader serif for the agent's prose only — the synthesis voice
literally lifts off the page in a different family from the machine
voice. Operator and agent are typographically inseparable from their
speaker.
Layout changes:
- Sticky session header with a giant base36 session id ("3K9P · A4C2")
and a metadata strip showing agent, model, turn count, status. The
status pill flips colour: AMBER on stream, ROSE on awaiting confirm,
MINT on ready, MUTED on mock.
- Empty state is no longer the apologetic "How can I help you today?".
It's "ATLAS. standing by." in oversize mono with the agent name in
italic serif amber, a hairline divider, and a single one-liner
instruction prefixed with ›. Lines stagger in via animation-delay.
- Operator turns: monospace, 14px, sodium-amber › prompt, no bubble.
Hangs from a left gutter with T01/T02… turn number + UTC timestamp.
- Agent turns: serif, 17px/1.55, with a tiny mono signature underneath
("atlas» 03:14:08Z · recv"). Cyan accent column instead of amber.
- Composer: terminal frame (square, 1px border, focus ring is amber
glow). Internal ›_ prompt mark in front of the textarea, mono input.
- Bottom modeline: utc clock + turn count + estimated tokens on the
left, keyboard hints on the right. Streaming flips the right side
to a pulsing phosphor bar + STREAM label.
Atmosphere details:
- 2px scanline overlay (very faint, 1.2% opacity)
- Corner phosphor blooms (amber top-right, cyan bottom-left)
- Inline SVG turbulence grain (3.5% opacity) over the whole theme
- Cursor blink animation on the prompt mark
- Consolas-tier ligatures on the mono via JetBrains Mono ss01/calt
All theming scoped via [data-theme="console"] — picks up automatically
because /ai's AppShell now passes theme="console". Other routes are
untouched. Tool-call cards from @crema/agent-ui inherit the palette
via overridden CSS variables (--card, --border, --primary, etc) plus
a [data-slot="tool-call-card"] override for the frame.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Previously gated to tenant-owned rows (tenant_id != null), which made
seed-from-catalog rows uneditable since they default to platform scope
on the backend. The backend doesn't enforce extra ownership rules on
update/delete either, so the gate was a UI overreach.
Buttons now appear on every row. Tooltips clarify when a row is a
platform default so the operator knows the change applies broadly.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces the free-text secret name input with a Select populated from
/api/v1/admin/secrets, filtered to category=api_key + enabled. Each
option shows the secret name plus its description for context.
Includes "(none — keyless / local)" for lmstudio-style configs and a
"Type a name…" escape hatch for secrets that don't exist in the vault
yet (the proxy will fail loudly at request time if the name is wrong,
which is the right behaviour — better than silently saving a config
that can't authenticate).
Secrets are fetched once on panel load alongside configs/catalog/usage,
not per modal-open, so the dialog opens instantly.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Reworks the LLM settings surface based on UX feedback. Drops the
separate "Active LLM (this session)" card — its functionality is now
inline on each saved config as a star toggle (writes the same
localStorage key the Assistant reads via @crema/llm-providers-ui's
saveSettings, so the existing assistant code picks the change up
without any plumbing).
Per-row controls now include:
- Star: make this config active for the current browser
- Switch: enable/disable server-side
- Pencil: edit (modal, not inline-expand)
- Trash: delete (with confirm)
- Spend (30d): cost + request count, sourced from
/api/v1/ai/llm/usage/by-model and matched on (provider, model)
Other improvements:
- Add wizard moved to a Dialog modal instead of pushing the list
around. Same form handles edit.
- Empty state: "Seed from catalog" button creates a curated starter
set (GPT-4o mini/4o, Sonnet 4.6, Haiku 4.5, DeepSeek V4 Flash, LM
Studio) so first-time operators don't face a blank panel.
- Catalog dropdown picks now auto-fill input/output costs as you
switch models, so the rates always reflect the chosen model unless
manually overridden.
- The lib's full settings card (system prompt, transport, context
budget) is still reachable for advanced cases — collapsed into a
<details> below the panel.
Adds llm-configs.ts: getUsageByModel + findSpend helper for the
per-row spend lookup.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The bootstrap script previously fell back to the OS color-scheme
preference and left font-scale unset until the user picked one. For
admin work — dense tables, lots of small text, monitoring dashboards —
dark + sm is the better starting point and matches what most operators
end up choosing anyway.
Users who've already picked a theme/font keep their stored preference;
this only affects fresh sessions where localStorage has no value yet.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The previous datalist-on-input approach was fragile — Safari hid the
suggestions, and there was no visual cue that a dropdown existed.
Replace with a proper Select populated from the catalog. Each option
shows the per-1M-token rates inline so operators see cost while
choosing. "Custom…" switches to free-text for models the catalog
doesn't know about, with a "Catalog" button to flip back.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces the localStorage-only LLM settings with a persisted catalogue
backed by /api/v1/admin/llm-configurations. The Settings → LLM screen
now has two cards:
- "Saved configurations" — full CRUD against the server. Each row shows
provider/model/secret/published per-1M-token costs. Add wizard
auto-fills costs from the curated catalog. One-click "Import local"
button promotes any pre-existing localStorage settings into a server
row, then clears the local store.
- "Active LLM (this session)" — the existing LLMProvidersSettingsCard,
scoped down to "what does the Assistant use right now" (still
localStorage; per-operator).
Spend (30d) tile in the configurations card header reads
/api/v1/ai/llm/usage/summary and surfaces total cost / requests /
tokens. First visible cost roll-up in the admin UI.
New module app/lib/arcadia/llm-configs.ts: typed CRUD client,
catalog lookup, computeCostCents helper (mirrors the server's
LlmConfiguration.compute_cost_cents/3), and getUsageSummary.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three things from the latest arcadia-app pull:
- health.ts: client for /api/v1/health{,/:service,/detailed,/host}.
monitoring.tsx now reads real per-subsystem probe state instead of
synthesizing it from indirect signals (rate limits, sessions, jobs).
- New Host tab on Monitoring with KPI tiles + per-core CPU bars,
load-avg cards, memory + swap usage, and per-mount disk bars,
backed by /api/v1/health/host.
- llm-proxy.ts: typed errors (secret_disabled, ip_not_allowed, etc.)
and a probeProxy() that round-trips a 1-token chat. settings.tsx's
"Test connection" in proxy mode now exercises the real endpoint
instead of just confirming the adapter built. Contract doc flipped
from "not yet implemented" to "implemented".
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Full set of admin surfaces on top of /platform/* and /admin/* endpoints,
plus a migration of /assistant onto @crema/llm-providers-ui.
Buckets (/buckets):
S3-level CRUD over /platform/buckets — list, create, delete (with the
6-digit confirmation flow the backend enforces), per-bucket configure
for versioning / CORS rules / policy JSON, plus an object browser
with FileGrid/FileList from @crema/file-ui and presigned-URL reveal.
Storage-config picker scopes the view to one credential at a time.
Monitoring (/monitoring):
Live dashboard. Service health board derived from indirect signals
(status-ui OverallStatus + ComponentRow). KPI tiles for sessions,
jobs, audit. Tabs: background jobs (Donut + BarChart + retry recent),
sessions (Sparkline of last 24h sign-ins), audit activity (BarChart
of severity / top resource types), infrastructure (DO summary +
WorldMapSvg coloured by droplet region + droplet list + Spaces),
rate limits. 30s auto-refresh.
Memberships (/memberships):
M:N glue between users and tenants over /admin/memberships. Add /
edit / suspend / activate / remove with role multi-select.
Networking (/networking):
Tabs over /platform/{firewalls,vpcs,domains,floating_ips}.
Read/delete on firewalls, read on VPCs, full DNS-record CRUD, and
inline assign/unassign for floating IPs.
SSO (/sso):
/sso/identity-providers CRUD with PEM cert as write-only field, plus
/sso/sessions list with destroy.
Announcements (/announcements):
/admin/announcements CRUD. Platform-wide vs per-tenant audience,
schedule windows, dismissible + active toggles.
Status page (/status-page):
/admin/status-page/{components,incidents,subscribers}. Components
CRUD, incidents with timeline + post-update + resolve flow,
subscriber list. Public preview at the top using StatusBoard +
IncidentTimeline from @crema/status-ui.
Assistant migration:
/assistant now uses @crema/llm-providers-ui (provider catalog +
vault key resolution) instead of ~/lib/llm-settings. Same async
buildAdapter() flow used by /ai. The legacy lib file is now
unreferenced and can be removed when ready.
New sibling libs wired (cloned from CremaUIStudio):
lib-file-ui, lib-card-ui, lib-dashboard-ui, lib-chart-ui,
lib-map-ui, lib-status-ui.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces the single-base-URL LLM settings with the new providers lib
(OpenAI, Anthropic, DeepSeek, Qwen, LM Studio). Settings/LLM hosts the
catalog-aware card; the /ai route builds adapters via buildAdapter()
and resolves API keys from the arcadia vault per-call (direct mode).
Anthropic skips the /v1/models probe (no such endpoint) and uses
catalog defaults; failed probes for keyed providers fall back to the
catalog instead of dropping to mock.
AI conversation now persists across navigation and refresh via a new
crema.ai.live localStorage key (separate from the compact-snapshot
key). useChat hydrates from initialMessages on mount, saves on every
change, and "Clear conversation" wipes both state and storage.
Vite needs explicit resolve.alias for @crema/llm-ui and
@crema/llm-providers-ui — when a sibling lib imports another @crema/*,
tsconfigPaths can't resolve it (the importing file isn't in this
project's tsconfig scope).
Adds docs/LLM_PROXY_CONTRACT.md describing the
POST /api/v1/ai/llm/chat endpoint the backend needs for proxy mode
(keys never leave the server). Direct mode works against today's
arcadia; proxy mode unblocks once that endpoint ships.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Full management surfaces for the platform-admin tenant, mirroring the
existing Tenants pattern (DataTable + row actions + create/edit dialogs +
ConfirmDialog for destructive ops, all data-action tagged for the
command bus, useRegisterAdminContext publishing for the assistant).
- Storage (/storage): backends + credentials. Write-only secret fields,
Validate/Activate/Deactivate/Set-default/Mark-degraded/Maintenance.
- Users (/users): tabs for Users, Invitations, Roles. Per-user View
drawer with profile, role add/remove, API keys (one-time reveal on
create), usage + quota.
- Secrets (/secrets): /api/v1/admin/secrets — create/rotate/rollback,
versions dialog, enable/disable, generate-value helper.
- Webhooks (/webhooks): CRUD, pause/resume, regenerate-secret with
one-time reveal, send test event, deliveries dialog.
- Scheduled tasks (/scheduled-tasks): cron CRUD, run-now trigger,
enable/disable, expandable run history.
- Audit log (/activity): replaces the empty stub. Filter by severity,
resource type, date range; click for full JSON detail.
All endpoints are hand-rolled HTTP because most aren't covered by the
generated OpenAPI typed paths yet — switch to arcadia.typed.* when the
backend wires them into OpenApiSpex.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three layers:
1. GFM markdown — add remark-gfm so tables, task lists, strikethrough,
autolinks render properly. Style table elements (overflow-aware
container, muted header, divider rows). Render `[ ]` task list items
as visible checkboxes.
2. Structured tool-result rendering — new `tool-result-renderers.tsx`
dispatches by tool name to render a small UI block beneath each
ToolCallCard:
- list_tenants → table with status pills + plan column
- get_tenant → tenant detail card
- get_platform_stats → KPI tiles (total + per-status)
- list_audit_log → timeline rows with actor_type + action
- list_users → user list with role chips
- suspend_tenant / activate_tenant → tenant card with action confirm
ToolCallCard collapses by default — operators expand for raw JSON.
3. Custom ```card``` blocks the LLM can emit inline:
- {"kind":"pill","status":"…"} — status pill
- {"kind":"stat","label":"…","value":…} — stat tile
- {"kind":"callout","tone":"info|warning|danger|success",…} — callout
Malformed blocks fall through to the prose unchanged. Client strips
well-formed blocks from prose and renders them as components.
Domain primer updated to teach the model the card schemas and remind it
NOT to re-render tool-result data as markdown tables (that's done
automatically — it should add commentary only).
Layers are independent: 1 + 2 always work; 3 is purely additive.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Persistent presence is less jumpy than pop-in/out. When the model isn't
actively doing anything, the avatar collapses to the `sm` size variant,
loses its activity label, and dims to 50% opacity. Springs back to
full-size + label when activity resumes.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The /ai surface now renders agent-ui primitives instead of homegrown
tool/typing widgets:
- AgentAvatar with activity (thinking / working / waiting / speaking /
idle) replaces TypingIndicator. Pulses while the model is generating,
shows "waiting" while a write is held for confirmation, "working" while
a confirmed write is executing, "speaking" once tokens are streaming.
- ToolCallCard renders each native tool_call with typed status (pending
/ running / success / error). Built from the assistant message's
toolCalls plus the matching tool result message. Tool messages no
longer render standalone — absorbed into their parent assistant turn.
- Empty assistant bubbles (no prose, only tool_calls) collapse so the
ToolCallCards carry the visual weight.
Wiring: add @crema/agent-ui path entry to tsconfig and @source line to
app.css. Sibling lib-agent-ui must be cloned next to arcadia-admin.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
New tools in admin-tools.ts:
- list_audit_log({limit?}) — recent audit entries (terse: actor, action,
target, timestamp). Hits /api/v1/admin/audit-log.
- get_platform_stats() — aggregate counts (tenants by status + by plan),
composed locally from list_tenants until arcadia exposes a real stats
endpoint.
- list_users({limit?}) — users in the currently-selected tenant via
/api/v1/users.
- suspend_tenant({slug}) — write tool, suspends a tenant by slug.
- activate_tenant({slug}) — write tool, restores a suspended/deactivated
tenant.
Inline write confirmation:
- New ConfirmCard component renders below the assistant message that
proposed a write. Shows tool(args) and Confirm/Deny buttons.
- classifyCalls() splits LLM tool calls into reads/writes. Auto-loop
runs reads immediately; for any writes, holds them in pendingConfirm
state instead of dispatching.
- On Confirm: runs writes with allowWrites:true, prepends prior read
results, continueChat to produce the final answer.
- On Deny: synthesises tool-result messages telling the model the user
declined; continueChat so it can acknowledge.
- Arcadia-knowledge primer updated to tell the model the user sees an
inline confirm card automatically — it shouldn't ask in prose first.
Wired into both /ai and /assistant.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Make /ai and /assistant operate as the platform admin's assistant
against arcadia-app's API:
- Add `arcadia-knowledge.ts` — domain primer (multi-tenant Phoenix
backend, tenant lifecycle, platform_admins identity, etc.) baked into
every system prompt.
- Add `admin-tools.ts` — curated tool registry exposing `list_tenants`
and `get_tenant`, callable via OpenAI-native function calling. Tools
hit arcadia through `useArcadiaClient()` and inherit the operator's
JWT + tenant header. `runLLMToolCalls()` returns `tool` role messages
ready to push back into history.
- Add `admin-context.ts` — runtime registry pages publish to so the
assistant can answer factual questions about live UI state without
scraping the DOM. Tenants page registers its summary on mount.
- Replace generic Vibespace personas (Atlas/Forge/Inkwell/Pilot/Cursor)
with arcadia-flavoured ones: Operator, Auditor, Triage, Analyst,
UI Operator. Auto-migrate stored agents from the legacy set.
- /assistant: build admin preface (role + primer + persona + ctx) and
pass it as the `useChat` system at construction. Pass `tools` on every
`send()`. Auto-loop reads `toolCalls` off the streaming assistant
message and uses `continueChat()` to push tool results.
- /ai: same wiring (this is the canonical admin chat surface; the user
prefers its look).
- MessageBody renders tool-result cards (role: "tool") and a "Called X"
pill on assistant messages with toolCalls. Strips Qwen-style
`<tool_call>` XML from prose when the tags were converted to
structured calls.
- Extend ThreadMessage with the `tool` role + tool-call metadata so
conversations round-trip through localStorage.
- Tenants page: row actions get `data-action="tenant-<slug>-{suspend,
activate,deactivate}"` (via lib-table-ui's new dataAction prop);
registers tenant summary into admin-context.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds an "AI" item to the sidebar nav (mirrors Vibespace's /ai route,
already wired in routes.ts). vite.config.ts now lists openapi-fetch,
phoenix, lucide-react, clsx, tailwind-merge, class-variance-authority
in optimizeDeps.include and adds explicit resolve.alias entries for
the arcadia and crema sibling libs so Vite resolves them on first
load instead of triggering a re-optimize mid-session.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces the hand-rolled shadcn primitives in app/routes/tenants.tsx
with manifest libs: DataTable + ActionsCell + BadgeCell + DateCell +
Pagination + useTable from table-ui, SearchInput from search-ui, and
AlertBanner + ConfirmDialog + EmptyState + LoadingOverlay from feedback-ui.
Behaviour preserved: same columns, same row actions, same suspend/
deactivate/activate handlers. Gains: built-in sortable columns,
pagination controls, density toggle support, proper confirmation dialogs
for destructive actions, accessible empty/loading/error states.
Wires three new sibling libs into tsconfig.json paths and app.css
@source lines: lib-table-ui, lib-search-ui, lib-feedback-ui, plus
lib-auth-ui (used by lib-arcadia-auth-ui after its refactor).
Corrects the earlier miss of not checking docs/LIBS.md before rolling
custom UI — the manifest already had what we needed.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds the first admin screen — /tenants — listing tenants from
GET /api/v1/admin/tenants with search, status badges, plan, created
date, and a per-row menu with suspend / activate / deactivate actions.
Hand-typed shapes in app/lib/arcadia/tenants.ts because arcadia's admin
endpoints aren't yet covered by /api/openapi (same 'ok'-placeholder
issue documented in lib-arcadia-client/scripts/sync-spec.mjs). When
the spec gains coverage, switch to arcadia.typed.GET(...) and drop the
manual types.
Trims the inherited consumer-app sidenav (Resources / Assistant / AI /
Library) down to admin-shaped items: Overview, Tenants, Audit log,
Settings. The unused route files stay in place; they just aren't linked.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Initial commit. Spun up via the docs/STARTER.md recipe: cp from vibespace,
reset git, rename package, set brand to "Arcadia Admin" with Shield icon
in app/lib/identity.ts.
Inherits the full Crema sibling-lib wiring including @crema/arcadia-client
(typed HTTP + Phoenix Channels realtime against arcadia-core) and
@crema/arcadia-auth-ui (login/signup/password-reset/2FA forms). The /login
route already renders <LoginForm>; <ArcadiaProvider> in app/root.tsx reads
VITE_ARCADIA_URL (default localhost:4000) and VITE_ARCADIA_TENANT (default
"default").
CLAUDE.md and README rewritten to frame this as the admin app for
arcadia-core. docs/STARTER.md removed — arcadia-admin is a leaf consumer,
not a downstream starter.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>