Review canvas
ProThe governed review surface (Candor's ReviewCanvas): the plan on one side; select a step and its log, diff, blast radius and reasoning appear on the other. The approval sits under the evidence, so a decision is made looking at exactly what it covers. Tabs with nothing for the step say so instead of disappearing.
Preview
Install
With the Candor CLI
First time? Get started sets up the registry and theme. Or with shadcn directly: npx shadcn@latest add @candor/review-canvas
Brings in: approval-request, blast-radius, chat, locale, log-stream, metadata-diff, plan, reasoning, tabs, work, utils
Usage
"use client"
import type { AgentIdentity, ReasoningEntry } from "@/lib/chat"
import type { ApprovalRequest, BlastRadiusScope, FileDiff, LogItem, PlanStep } from "@/lib/work"
import { ReviewCanvas } from "@/components/review-canvas"
const dev: AgentIdentity = { id: "a-dev", name: "Dev agent", role: "dev" }
const migration = { kind: "migration", name: "0142_add_customer_region", label: "Add customers.region" }
const header = { kind: "component", name: "web/src/customers/CustomerHeader.tsx", label: "CustomerHeader" }
const steps: PlanStep[] = [
{ id: "s1", title: "Add the region column", status: "succeeded", agent: dev, duration: "14s", touches: [migration] },
{ id: "s2", title: "Show it on the customer page", status: "succeeded", agent: dev, duration: "9s", dependsOn: ["s1"], touches: [header] },
{ id: "s3", title: "Backfill from billing country", status: "running", agent: dev, dependsOn: ["s1"] },
{ id: "s4", title: "Test on Staging", status: "blocked", agent: { id: "a-qa", name: "QA agent", role: "qa" }, dependsOn: ["s2", "s3"] },
]
const logs: LogItem[] = [
{ kind: "line", id: "l1", at: "10:42:01", severity: "info", actor: "Dev agent", stepId: "s1", message: "Running migration 0142 on Staging" },
{ kind: "line", id: "l2", at: "10:42:14", severity: "info", actor: "staging", stepId: "s1", message: "Migration applied: 1 column, 1 index" },
{ kind: "line", id: "l3", at: "10:42:15", severity: "info", actor: "Dev agent", stepId: "s2", message: "Updating CustomerHeader" },
{ kind: "line", id: "l4", at: "10:42:33", severity: "info", actor: "Dev agent", stepId: "s3", message: "Backfill: batch 1 of 19 (1,000 customers)" },
{ kind: "line", id: "l5", at: "10:43:44", severity: "warn", actor: "staging", stepId: "s3", message: "Lock timeout on customers id 48213. Retrying batch 12." },
]
const diffs: Record<string, FileDiff[]> = {
s1: [
{
path: "db/migrations/0142_add_customer_region.sql",
entity: migration,
change: "create",
language: "sql",
hunks: [{ lines: [{ kind: "add", text: "ALTER TABLE customers ADD COLUMN region region;", newNo: 1 }] }],
},
],
s2: [
{
path: "web/src/customers/CustomerHeader.tsx",
entity: header,
change: "update",
language: "tsx",
hunks: [
{
lines: [
{ kind: "remove", text: '<Fact label="Partner tier" value={customer.partnerTierOld} />', oldNo: 44 },
{ kind: "add", text: '<Fact label="Region" value={regionLabel(customer.region)} />', newNo: 44 },
],
},
],
},
],
}
const reach: Record<string, BlastRadiusScope> = {
s1: { kind: "measured", unread: [], components: [{ entity: migration, change: "create", diff: { added: 1, removed: 0 } }] },
s2: { kind: "measured", unread: ["Mobile app screens"], components: [{ entity: header, change: "update", diff: { added: 1, removed: 1 } }] },
s3: { kind: "unknown", reason: "A backfill touches rows, not code. Which rows change depends on billing country at run time." },
}
const reasoning: Record<string, ReasoningEntry[]> = {
s3: [
{ id: "x1", title: "Map billing country to region", basis: "verified", evidence: "Data model notes v3 lists the country table" },
{ id: "x2", title: "Use other for countries not in the table", basis: "assumed", assumption: "The team prefers a catch-all to leaving region empty" },
],
}
const approval: ApprovalRequest = {
id: "ap1",
title: "Add region to customers",
summary: "A region column on customers, shown on the customer page and backfilled from billing country.",
requester: { agent: { id: "a-orch", name: "Keel", role: "orchestrator" } },
environment: "Acme web",
production: true,
artifactHash: "9f2c4e81b7a03d55c6e1f0a9b8d7c6e5f4a3b2c1d0e9f8a7b6c5d4e3f2a1b0c9",
impact: { components: [migration, header], rows: "unknown" },
rollback: { available: true, how: "Revert the pull request and run the migration's down step." },
createdAt: new Date(Date.now() - 25 * 60_000).toISOString(),
expiresAt: new Date(Date.now() + 7 * 86_400_000).toISOString(),
state: "pending",
}
export default function Example() {
return (
<ReviewCanvas
title="Add region to customers"
steps={steps}
logs={logs}
diffs={diffs}
reach={reach}
reasoning={reasoning}
approval={approval}
onApprove={() => {}}
onReject={() => {}}
live
/>
)
}<ReviewCanvas>
| Prop | Type | Default | Description |
|---|---|---|---|
| titlerequired | string | — | The plan's title. |
| stepsrequired | PlanStep[] | — | The plan's steps; the first running, waiting or failed one is selected to start. |
| logsrequired | LogItem[] | — | The run's log, filtered to the selected step. |
| diffs | Record<string, FileDiff[]> | undefined | {} | File diffs per step id. A step without any says it changes no metadata. |
| reach | Record<string, BlastRadiusScope> | undefined | {} | Blast radius per step id. A step without one says reach isn't measured. |
| reasoning | Record<string, ReasoningEntry[]> | undefined | {} | The agent's reasoning per step id. A step without any says none was recorded. |
| approval | ApprovalData | undefined | — | The approval request, shown under the evidence. |
| onApprove | ((hash: string) => void) | undefined | — | Called on approve with the change hash. |
| onReject | ((hash: string) => void) | undefined | — | Called on reject with the change hash. |
| live | boolean | undefined | false | Shows the log as live while the selected step is running. Defaults to false. |
| className | string | undefined | — | Extra classes for the outer element. |
Strings
The fixed words, in English by default. Change any of them under reviewCanvas in <LocaleProvider strings={{ … }}>; the table is exported as reviewCanvasStrings.
| Key | English |
|---|---|
| evidenceFor | Evidence for {step} |
| evidence | Evidence |
| selectStep | Select a step |
| log | Log |
| diff | Diff |
| reach | Reach |
| why | Why |
| noDiff | This step doesn’t change any metadata. |
| noReach | Reach isn’t measured for this step. |
| noReasoning | The agent didn’t record reasoning for this step. |