Run console (Claude Agent SDK)

Premium

The person's answer to the agent's permission request: { id, approved, reason? }.

Markdown

Preview

It runs with a server and an agent, so there’s no preview here: install it to run it in your app.

Install

With the Candor CLI

First time? Get started sets up the registry and theme. Or with shadcn directly: npx shadcn@latest add @candor/run-console-claude

Brings in: button, card-registry, chat, claude-agent-server, composer, entity-chip, log-stream, message, ops, prompt-suggestions, run-log, run-receipt, run-recovery, standard-cards, status, thread, trace-viewer, use-claude-agent, work · npm: @anthropic-ai/claude-agent-sdk@^0.3.318, drizzle-orm@^0.45.4, lucide-react@^1.67.0, postgres@^4.0.0, zod@^4.6.7

<POST>

No props.

Also takes the props of Request.

<POST>

The agent, on the Claude Agent SDK. Set ANTHROPIC_API_KEY. Runs on one Node server, not spread over instances.

No props.

Also takes the props of Request.

<GET>

Every run with its log entry, trace, receipt (a cost not yet measured says so), log lines and, if it stopped, its recovery.

No props.

<ConsolePage>

Every run the agent started: the list, and for the one you open its recovery (if it stopped), trace, receipt and log.

No props.

<Page>

No props.

<RunConsoleChat>

The chat that drives runs. The console (consoleHref) shows them.

PropTypeDefaultDescription
chatrequiredRunChat—The chat state from useCandorChat (AI SDK) or useClaudeAgent (Claude Agent SDK); the block works with either.
runsApirequiredstring—The runs API route, polled so a stopped-run card knows where its run is now, e.g. "/run-console/api/runs".
consoleHrefrequiredstring—Where the run console lives, linked from the chat.

Functions

connectRunDb(url: string)

Open the block's database from a Postgres URL with the postgres-js driver. Swap the driver here (node-postgres, Neon's websocket Pool, …) if your app already uses another; it must support interactive transactions.

drizzleRunRepo(db: RunDb)
memoryRunRepo()

In this server process's memory: gone on restart and not shared between instances. Transactions run one at a time, and a transaction that throws is rolled back to the state it started from. There's no demo data: runs appear as the agent starts them.

setRunRepo(repo: RunRepo)

Use your own storage (or a database you opened yourself). Call it once, before the routes run.

runRepo()

The storage in use: whatever setRunRepo() was given, else Postgres when DATABASE_URL is set, else memory.

viewerOf(request: Request)

Who is asking. Replace with your auth; the demo reads a cookie (default Riley).

startRun(template: TemplateId, viewer: string)

Start a run and carry it as far as it goes.

retryStep(runId: string, stepId: string, viewer: string)

Retry the failed step, then go on. Two retries at once: one goes on, the other is refused.

skipStep(runId: string, stepId: string, viewer: string)

Skip the failed step, then go on.

stopRun(runId: string, viewer: string)

Stop a failed run. What finished stays done; the answer says what that was and how to undo it.

needsApproval(tool: string, input: unknown)

Whether a call changes something, so the person must allow it: starting a run with a writing step, or retrying or skipping when a writing step lies ahead. A call the store can't place counts as changing.

recordUsage(runIds: Set<string>, usage: Run["usage"])

Add what the agent SDK measured for one request to the run it acted on. Usage is only recorded when the request touched exactly one run, because one request's tokens can't honestly be split between runs.

priced(model: string, inputTokens: number, outputTokens: number, cachedTokens?: number)

Tokens to a cost, only with a price you set in modelPrices.

listRuns()

Every run, newest first, as the console shows it.

getRun(runId: string)
resetStore({ pace = 1 }: { pace?: number | undefined } = {})

For tests: start from no runs again, optionally with no waiting in steps. On a database this deletes the block's rows.

runToolTitle(tool: string, input: unknown)

Plain-words titles for the block's tools, the same for both agent SDKs.

useRuns(api: string, busy = false)

Every run from the store, checked every second while something runs or the agent replies.

Types

RunDb
PgDatabase<PgQueryResultHKT, any>
AttemptRecord
One attempt at a step, timed by the store (epoch milliseconds).
{ startedAt: number; endedAt?: number | undefined; error?: string | undefined }
StepRecord
{ id: string; title: string; kind: SpanKind; writes: boolean; undo?: string | undefined; status: RunStatus; attempts: AttemptRecord[] }
UsageRecord
{ model: string; inputTokens: number; outputTokens: number; cachedTokens?: number | undefined; cost?: number | undefined }
RunRecord
One run, as stored. Times are epoch milliseconds.
{ id: string template: string title: string environment: string status: RunStatus startedBy: string startedAt: number /** Set whenever the run stops (done, failed at a step, or stopped); cleared when it goes on. */ endedAt?: number | undefined steps: StepRecord[] /** Time the run spent waiting for the person after a failure. */ waits: { start: number; end?: number | undefined }[] log: LogItem[] /*
NewRunRecord
Omit<RunRecord, "id" | "rev">
RunPatch
`endedAt: null` clears it.
Partial<Pick<RunRecord, "status" | "steps" | "waits" | "log" | "usage">> & { endedAt?: number | null | undefined }
TemplateId
keyof typeof templates
RunView
{ entry: RunLogEntry status: RunStatus trace: { spans: TraceSpan[]; total: number } /** What the run used. A cost that wasn't measured is marked so on it, with why in costNote. */ receipt: RunReceipt log: LogItem[] recovery?: { title: string; steps: PlanStep[]; failedStepId: string; reason: string; retryWrites: boolean; skipWrites: boolean } | undefined }
RunChat
What either adapter hook returns (useCandorChat or useClaudeAgent): the block works with both.
{ messages: MessageData[] composer: React.ComponentProps<typeof Composer> approvals: { id: string; tool: string; input?: unknown; title?: string | undefined; reason?: string | undefined }[] respond: (id: string, approved: boolean, reason?: string) => unknown error?: string | undefined }