Desk
Operator console for the Nexus agent platform
For a year the ARIA experience was two tabs — a read-only dashboard and a separate chat page. Desk is the consolidation, one console that talks to ARIA and shows the queues.
In late April 2026, Desk became the primary way I talk to and manage ARIA. Until then the setup was two browser tabs. Chancery was a read-only dashboard that showed pipeline health and agent transcripts — it could watch but not act. chat.niclydon.io was the interactive chat surface — it could talk but had no visibility into approvals, tool grants, or the proactive-insights queue waiting to be delivered. Neither felt finished on its own and I kept leaving three tabs open to three different views of the same thing. Desk consolidates both. One app, four routes, one place to do the work.
Four routes under a single Next.js 16 app running on port 3100 on Furnace. /aria is the interactive chat — multimodal composer, a runtime model picker that routes to Forge and can pick local Qwen, cloud Claude, or cloud Gemini for any turn, and assistant-ui primitives wired over a custom ChatTransport that hits Desk’s /api/chat/aria and streams SSE back from Nexus. /approvals holds the operator queues: agent actions pending authorization, tool-grant requests, merge review, proactive insights waiting to be delivered or dismissed. /agents is a thin directory of the seven agents in the platform. /settings has three tabs for agent config, LLM routing defaults, and platform-wide flags — everything that used to require a manual UPDATE against agent_config now has a form.
The memorable debugging story was the assistant-ui metadata allowlist. A per-message model-label feature had been plumbed end-to-end — Nexus’ task runner emitted the model id, the SSE done frame carried it, AI SDK v6’s UIMessage.metadata.model received it — and the UI rendered nothing. Journal logs showed the model arriving. Client source showed the handler wired. The bubble still read undefined. The culprit was assistant-ui’s internal external-message converter, which copies exactly seven allowlisted metadata keys from the AI SDK message into its own typed ThreadAssistantMessage. Anything outside that allowlist falls on the floor. The fix was to nest the model inside metadata.custom, the library’s designated escape hatch — three symmetric edits across the transport, the history hydrator, and the bubble selector. Lesson I’ll keep: when SSE evidence and client-source evidence both confirm a value is present but the UI reads undefined, it’s almost always a library’s type-narrowing converter, not your own code. Next up is Forge cloud pass-through so that a Claude or Gemini pick in the model picker actually reaches the cloud provider instead of silently falling through to local Qwen.