Pro

Agent console

Pro template

A working console for running AI agents, for the Next.js App Router: emailed-code sign-in, a sidebar with workspaces, live runs with their full traces, an approvals inbox that pauses a run until someone decides, a knowledge base the agents search, per-agent model, tools, permissions and limits, usage against a monthly limit, and workspace settings. Runs come from a mock runner until a model is set (Anthropic, OpenAI or any OpenAI-compatible API), or until you plug yours in; the runner, auth and storage each sit behind one small file you swap.

Everything here is the template’s own code, with its mock runner in your browser. Two runs stop to ask in Approvals as it opens and more start every half minute; approve one and its run carries on. Turn a permission to “Ask every time” in Agents and the next run asks too.

Installation

This is a Pro template. UI Polish Pro costs $149 one time and includes every Pro component, block and template, now and later. Add your license key to your MCP connection or components.json, then install it either way below.

With UI Polish connected, ask your agent:

Set up the UI Polish Agent console template in this Next.js project.

Or with the shadcn CLI:

npx shadcn@latest add @uipolish/agent-console

Files

Routes in app/, the screens in components/app/, and the parts you swap (model, auth, storage) in lib/. The components it uses install to components/ui/.

app/21 files

  • (console)/
    • agents/
      • page.tsx
    • approvals/
      • page.tsx
    • knowledge/
      • page.tsx
    • runs/
      • page.tsx
    • settings/
      • page.tsx
    • usage/
      • page.tsx
    • actions.ts
    • error.tsx
    • layout.tsx
  • api/
    • approvals/
      • route.ts
    • knowledge/
      • route.ts
    • runs/
      • [id]/
        • route.ts
      • route.ts
    • status/
      • route.ts
  • sign-in/
    • actions.ts
    • page.tsx
    • sign-in-screen.tsx
    • sign-in.module.css
  • layout.module.css
  • layout.tsx
  • page.tsx

components/app/16 files

  • agents-screen.tsx
  • approvals-screen.tsx
  • console-context.tsx
  • console-shell.tsx
  • console-sidebar.tsx
  • console.module.css
  • http-api.ts
  • knowledge-screen.tsx
  • marks.tsx
  • new-run-dialog.tsx
  • page-bar.tsx
  • runs-screen.tsx
  • screens.module.css
  • settings-screen.tsx
  • usage-screen.tsx
  • use-live.ts

lib/17 files

  • agents/
    • connection.ts
    • engine.ts
    • mock.ts
    • model-call.ts
    • model-runner.ts
    • policy.ts
    • runner.ts
    • scenarios.ts
  • db/
    • database.ts
    • ids.ts
    • in-memory.ts
    • seed.ts
  • knowledge/
    • ingest.ts
  • product.ts
  • server.ts
  • service.ts
  • session.ts

A working console for running AI agents, for the Next.js App Router, assembled from the kit: people sign in with an emailed code, land on a live list of runs, open any run to see every model and tool call it made, decide what agents ask permission for in an approvals inbox (a run waits until they do), give the agents documents to answer from, set each agent's model, tools, permissions and daily limit, watch spend against a monthly limit, and manage the workspace. It runs the moment it's installed: a mock runner plays realistic runs (some retry, some fail, some stop to ask), and everything is kept in memory. Set a model (Anthropic, OpenAI or any OpenAI-compatible API) and new runs call it instead.

It's a starting point you own. Every screen is a kit block or component, and the three parts every product replaces (the agent runner, sign-in and storage) each sit behind one small file.

What's included

AreaWhat you getBuilt from
Sign inEmail, then a six-digit code, with resend, wrong and expired codes, and rate limits. A signed session cookieSign in
App shellWorkspace switcher, Runs, Approvals (with a count of what's waiting), Knowledge, Agents, Usage, Settings, the account menu. Folds to a rail with ⌘B; a drawer on phonesSidebar
RunsEvery run, filtered and paged, live ones counting up in place; new ones held behind "Show n new runs". A run's trace, retries and the failing step, tokens and cost, the output. New run, Re-run, Cancel run, Share linkRun history, Dialog, Select, Text field
ApprovalsWhat runs stopped to ask: the email, refund or change drawn as it will happen, editable values, the raw tool call. Approve resumes the run with any edits; Decline stops it with the noteApprovals inbox
KnowledgeFiles, sites and apps moving through reading, chunking and indexing; failures with the fix; a test panel showing what a question retrieves. The support agent's search reads itKnowledge base
AgentsEach agent's instructions, model, MCP connections, what it may do without asking, and its daily limit, saved row by rowAgent settings, Connections, Tabs
UsageThis month's spend against the workspace limit, split by agent; runs, tokens, success rate; a table per agentUsage meter, Stat, Data table
SettingsWorkspace name, URL and time zone; API keys shown once; models and the monthly spend limit; notification switches with a test sendSettings
StatesLoading a screen, a screen that failed, a run not in this workspace, a tool that fails and retries, a run that fails, a declined or expired approval, a server signed out, an agent over its daily limitEmpty state, Alert, Toast

Run it

  1. Install it into a Next.js App Router project (below). It brings its own app/layout.tsx and app/page.tsx, so start from a fresh npx create-next-app@latest, or merge those two by hand.
  2. Make sure app/globals.css imports the tokens. The shadcn CLI adds @import "../components/ui/uipolish/tokens.css"; for you; by hand, add foundation/tokens.css there and import it.
  3. npm run dev, open /, sign in with any email. The code is printed in the terminal running the app.

Then, in .env.local:

VariableWhat it does
SESSION_SECRETSigns the session cookie. Required in production
ALLOW_DEV_SIGN_IN1 lets the development sign-in run in a production build, for a staging demo

To run agents on a real model, connect one (below).

Your first sign-in creates two sample workspaces: Acme, with three agents (support, billing, research), a week of their runs, five knowledge sources and two API keys; and Acme Sandbox, nearly empty. While the mock runner is in, each workspace starts two runs as it opens (one stops to send a reply, one to refund a charge) and another every 15 to 40 seconds while fewer than three are running.

Connect a model

Set these in .env.local and restart the server. lib/agents/connection.ts reads them once.

VariableWhat it does
AI_PROVIDERanthropic, openai, openai-compatible or mock. Unset, it's picked from what else is set (below)
AI_MODELThe model every agent runs on. Unset, each agent runs on its own (Agents → Model), which are Claude ids; OpenAI defaults to gpt-5, and OpenAI-compatible APIs need it
AI_API_KEYThe key. ANTHROPIC_API_KEY and OPENAI_API_KEY work too
AI_BASE_URLThe API's address, up to /v1, for openai-compatible. With anthropic or openai, a gateway in front of them

With no AI_PROVIDER: AI_BASE_URL means openai-compatible, ANTHROPIC_API_KEY means anthropic, OPENAI_API_KEY means openai, and AI_API_KEY alone means anthropic for an sk-ant- key and openai otherwise. Nothing set, or a setting that can't work (logged at start), keeps the mock.

Shell
# Anthropic: each agent on the Claude model it's set toAI_PROVIDER=anthropicAI_API_KEY=sk-ant-...# OpenAIAI_PROVIDER=openaiAI_API_KEY=sk-...AI_MODEL=gpt-5# OpenRouter: any model it listsAI_PROVIDER=openai-compatibleAI_BASE_URL=https://openrouter.ai/api/v1AI_API_KEY=sk-or-...AI_MODEL=openai/gpt-5# Ollama, on this machine: no keyAI_PROVIDER=openai-compatibleAI_BASE_URL=http://localhost:11434/v1AI_MODEL=llama3.3

Groq, Together, Fireworks, DeepSeek, LM Studio, vLLM and gateways such as Vercel AI Gateway work the same way as OpenRouter.

With a model set, lib/agents/model-runner.ts runs each new run as one model call: the agent's instructions as the system prompt and the task as the message. An agent allowed search_knowledge (the support agent) first searches Knowledge, as a tool step, and gets what it found with the task. The answer streams into the trace's model step and becomes the run's output, with the tokens the API reports (estimated at four characters a token when it doesn't) priced at SAMPLE_PRICES. It doesn't call the agents' MCP tools yet, and the sample traffic stops: only runs people start run. Runs from the seed stay as they were.

How a run travels

  1. Something starts it: New run in Runs or Agents (the startRun server action), Re-run, or a schedule or webhook. In the sample, engine.keepBusy() stands in for those.
  2. lib/agents/engine.ts saves the run as running and hands it to the runner. Each step the runner yields (a model call or a tool call, started, then finished, failed or retried) is saved as it comes, with the run's tokens and cost added up.
  3. Before each tool call the runner asks authorize. The engine checks the agent's policy (lib/agents/policy.ts): a tool set to Always allow goes ahead; otherwise reads and changes follow Agents → Permissions, and sends, deletes and spends always ask. A server that's off or signed out fails the call with the fix.
  4. When it must ask, the engine adds a "Wait for approval" step, files an Approval with what the tool will do, and holds the run. Approve, and the run goes on with any edited values (an amount, a reply's text); Decline, and it stops as cancelled with the note. An approval nobody decides expires after an hour (APPROVAL_TTL_MS).
  5. The runner returns (succeeded), throws (failed, with the message), or is aborted by Cancel run (cancelled, "Cancelled by Ana Ruiz.").
  6. The screens poll: Runs every 2 seconds and an open running trace every 1.2, Approvals every 3, Knowledge every second while a source moves, the sidebar's count every 4. All reads go through app/api/*; every change through app/(console)/actions.ts.

Swap in your own

The runner. lib/agents/runner.ts defines AgentRunner: take a task and the agent, yield step and output events, and call context.authorize(request) before any tool. mock.ts plays scripts from scenarios.ts; model-runner.ts makes one model call through model-call.ts (the Anthropic SDK, or Chat Completions with fetch). A full agent is a tool loop on top of it (or the Claude Agent SDK, or your own worker): send the agent's tools with each call, yield an llm step around each model call with its tokens and cost, and a tool step around each tool call after authorize allows it. Return it from getRunner(); the engine, approvals and every screen stay as they are. Each tool request carries what the approval shows: a title, a note, a preview (email, payment, details, diff, code…) and the fields a person may edit.

Where runs execute. The engine runs agents in the server's process, which suits a long-lived Node server. On serverless, start runs in a durable worker (a queue, Inngest, Trigger.dev, Vercel Workflow) and replace the in-memory waiters with a row the worker polls or a resume webhook; engine.decide() is where a decision lands.

Sign-in. lib/session.ts sends codes, checks them and keeps a signed cookie. Replace sendEmail() with your email provider and it's ready. For an auth library (Auth.js, Clerk, WorkOS, Supabase Auth), make getUser() return its user; every page, action and route goes through consoleFor() or apiOrNull() in lib/server.ts, which call it.

Storage. lib/db/database.ts is the interface: workspaces, settings, API keys, notifications, agents, runs, approvals and knowledge sources, one method per query. in-memory.ts implements it; seed.ts fills a new workspace. For Postgres with Drizzle, define tables with the same fields (runs with workspace_id, agent_id, status, started_at, tokens, cost, and steps and output as jsonb; approvals with run_id, status, expires_at, and preview, tool, fields, outcome as jsonb), implement Database, and export it as db.

Knowledge. lib/knowledge/ingest.ts walks each source through the stages on timers and scores passages by shared words. Replace walk with your pipeline (parse, chunk, embed, upsert) writing progress to the store, and search with a query to your vector index; the support agent's search_knowledge and the test panel both call it.

Usage and billing. Usage adds up what runs cost, at the sample prices in lib/product.ts (SAMPLE_PRICES; a model not listed there is priced as the first in MODELS). Use your provider's prices, or read metered usage from your billing provider, in usage() in lib/service.ts.

Customizing

  • Product name, models and choices: PRODUCT_NAME, MODELS, PLAN, SCOPES, FEATURES and NOTIFICATION_EVENTS in lib/product.ts.
  • The mark: ProductMark in components/app/marks.tsx, on the switcher's ink tile and the sign-in page. The nav's glyphs are beside it.
  • Agents: seed.ts creates the sample three. Their scripts are in scenarios.ts; delete both once your runner and data are real.
  • Screens: each is one file in components/app/ that reads the console context (useConsole()), never the router or the server, and one page in app/(console)/ that loads its first data. Add a screen to SCREENS in console-sidebar.tsx with a glyph, and a page beside the others.
  • Permissions: what asks is needsApproval() in lib/agents/policy.ts. Change it there (by tool, by amount, by customer) and the engine follows.

States

StateWhat you seeWhere
Signed outAny app URL goes to /sign-inconsoleFor() in the console layout
First runSample workspaces, agents, runs and sourcesseed.ts
Loading a screenThe bar, then the block's own loading state after 300msloading on each block
Screen failed"This screen didn't load" with Try again; the sidebar keeps working(console)/error.tsx
Live runThe row counts up and finishes in place; the open trace grows; Cancel run in the barruns-screen.tsx
New runs"Show 2 new runs" over the table; runs you start show at onceruns-screen.tsx
Run waitingA running "Wait for approval" step; the request in Approvals; the sidebar's countengine.ts
Approved, editedThe run goes on with the edited value; "Approved by …" on the stepengine.decide()
DeclinedThe run cancelled with "Declined by …: “note”"engine.decide()
ExpiredAfter an hour, the approval moves to Expired and its run stopsAPPROVAL_TTL_MS
Edit out of rangeThe field says so as you type; the server refuses it too, and the inbox puts the request back with the reasonapprovals-screen.tsx, engine.decide()
Tool flaky, tool failedAn amber retried step then attempt 2; or the failed step open with its errormock.ts, Run history
Server off or signed outThe tool call fails with the fix ("Sign in again in Agents → Connections")policy.ts
Over the daily limitNew runs refuse with the limit and where to raise itengine.start()
Run not here"There's no run with that id in this workspace."run() in service.ts
Knowledge failedThe file's failure and its fix, with Retryingest.ts

Installing as a template

A template is a registry item with "type": "template". Its own files are listed in meta.json files by the path they install at, from the project root, or under src/ when the project has one. get_component returns them headed by that path; the shadcn item (/r/agent-console.json) is a registry:block whose files carry it as target. The components and blocks it uses come in the same answer at components/ui/<name>/, which is where its imports (@/components/ui/run-history/run-history) point. Its one npm package is @anthropic-ai/sdk, loaded only on a run when Claude is the provider; the components need @base-ui/react.

Accessibility

  • Every screen has one h1: the block's title (on Agents, the open agent's name), or Usage's own. The sidebar is a nav named for the workspace.
  • Runs, Approvals and Knowledge announce what changes through their blocks' polite regions; Instructions says "Saved" or why not in a status line.
  • Focus: opening a run moves focus to its title and Runs returns it to the row; deciding a request moves to the next; dialogs return focus to the button that opened them.
  • Keyboard: ⌘B folds the sidebar; in Approvals, J and K move, A approves, D declines, E edits; / searches Runs.