From d3aac0ad98bf5efb8d4bf6279d814fba9f360899 Mon Sep 17 00:00:00 2001 From: Anton Castro Date: Mon, 20 Jul 2026 14:09:23 -0700 Subject: [PATCH] feat: agent inbox, agent roster, context budget; a11y fixes (contrast, tap targets, focus, mobile nav, code highlighting) --- .github/ISSUE_TEMPLATE/bug_report.md | 14 ++ .github/ISSUE_TEMPLATE/component-proposal.md | 26 ++++ .github/PULL_REQUEST_TEMPLATE.md | 23 ++++ .github/workflows/ci.yml | 20 +++ CONTRIBUTING.md | 43 ++++++ README.md | 6 +- app/components/agent-inbox/demo.tsx | 52 +++++++ app/components/agent-inbox/page.tsx | 37 +++++ app/components/agent-roster/demo.tsx | 36 +++++ app/components/agent-roster/page.tsx | 40 ++++++ app/components/context-budget/demo.tsx | 31 +++++ app/components/context-budget/page.tsx | 40 ++++++ app/components/gallery-minis.tsx | 70 ++++++++++ app/components/page.tsx | 39 +----- app/globals.css | 18 ++- app/how-it-works/page.tsx | 2 +- app/page.tsx | 9 +- app/principles/page.tsx | 130 ++++++++++++++++++ components/docs/code-block.tsx | 54 +++++++- components/docs/docs-shell.tsx | 29 +++- components/docs/preview-pane.tsx | 2 +- components/docs/sidebar.tsx | 1 + lib/nav.ts | 21 +++ registry/agent-inbox/agent-inbox.tsx | 101 ++++++++++++++ registry/agent-roster/agent-roster.tsx | 101 ++++++++++++++ registry/approval-gate/approval-gate.tsx | 4 +- .../confidence-meter/confidence-meter.tsx | 2 +- registry/context-budget/context-budget.tsx | 100 ++++++++++++++ registry/interrupt-bar/interrupt-bar.tsx | 4 +- scripts/build-registry.mjs | 12 ++ 30 files changed, 1011 insertions(+), 56 deletions(-) create mode 100644 .github/ISSUE_TEMPLATE/bug_report.md create mode 100644 .github/ISSUE_TEMPLATE/component-proposal.md create mode 100644 .github/PULL_REQUEST_TEMPLATE.md create mode 100644 .github/workflows/ci.yml create mode 100644 CONTRIBUTING.md create mode 100644 app/components/agent-inbox/demo.tsx create mode 100644 app/components/agent-inbox/page.tsx create mode 100644 app/components/agent-roster/demo.tsx create mode 100644 app/components/agent-roster/page.tsx create mode 100644 app/components/context-budget/demo.tsx create mode 100644 app/components/context-budget/page.tsx create mode 100644 app/components/gallery-minis.tsx create mode 100644 app/principles/page.tsx create mode 100644 registry/agent-inbox/agent-inbox.tsx create mode 100644 registry/agent-roster/agent-roster.tsx create mode 100644 registry/context-budget/context-budget.tsx diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..f43aa2a --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,14 @@ +--- +name: Bug report +about: Something broken in a component, demo, or the site +title: "bug: " +labels: bug +--- + +## What happened + +## What you expected + +## Where + + diff --git a/.github/ISSUE_TEMPLATE/component-proposal.md b/.github/ISSUE_TEMPLATE/component-proposal.md new file mode 100644 index 0000000..406913a --- /dev/null +++ b/.github/ISSUE_TEMPLATE/component-proposal.md @@ -0,0 +1,26 @@ +--- +name: Component proposal +about: Propose a new component for an agent↔human sync moment +title: "proposal: " +labels: component, proposal +--- + +## The sync moment + + + +## Where you've needed it + + + +## Proposed behavior + + + +## Which principles it applies + + + +## Formats + + diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..15a6178 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,23 @@ +## What + + + +## Why + + + +## Definition of done (components only) + +- [ ] Single-file registry component (React + Tailwind + Motion only) +- [ ] Interactive demo showing every state +- [ ] Doc page (intro, when to use, behavior, usage) +- [ ] Registered in `lib/nav.ts` and `scripts/build-registry.mjs` +- [ ] Header comment cites the principles applied +- [ ] `npm run build` passes clean + +## Principle check + +- [ ] State is never communicated by color alone +- [ ] Failures keep their inputs visible +- [ ] Nothing irreversible happens without consent +- [ ] Any confidence display uses bands, not decimals diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..9716192 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,20 @@ +name: CI + +on: + pull_request: + branches: [main] + push: + branches: [main] + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 22 + cache: npm + cache-dependency-path: package.json + - run: npm install + - run: npm run build diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..e98c3e8 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,43 @@ +# Contributing to Agent Pit Stop + +Thanks for pulling into the pit. This doc is the whole contract: what a contribution needs to be, and how it gets in. + +## What we accept + +- **New components** — patterns for agent↔human sync moments. Propose first via a [component proposal issue](.github/ISSUE_TEMPLATE/component-proposal.md) so we agree on scope before you build. +- **Improvements** to existing components: accessibility, states, API clarity, motion. +- **Fixes** — bugs, typos, broken demos. No proposal needed; just PR. +- **Principle chapters** are held to the highest bar: a real claim plus a working component as evidence. Open a discussion before writing one. + +## Definition of done (components) + +Every component PR ships all five, or it isn't done: + +1. **Registry component** — one self-contained file in `registry//.tsx`. Dependencies: React, Tailwind, Motion. Nothing else. +2. **Demo** — `app/components//demo.tsx`, interactive, showing every state. +3. **Doc page** — `app/components//page.tsx` using `ComponentDoc`: intro, when to use, behavior, usage snippet. +4. **Registrations** — entry in `lib/nav.ts` (category + supported formats) and `scripts/build-registry.mjs` (title + description). +5. **Principles applied** — a header comment citing which principles the component applies, and honest adherence to them: state never by color alone, failures keep their inputs, interruptions acknowledged. + +## Code standards + +- TypeScript strict; exported props interfaces with doc comments on every prop. +- Design tokens only (`bg-asphalt`, `text-smoke`, `var(--color-pit)`); no hardcoded colors or spacing. +- Accessible by default: keyboard operable, `aria-*` where state changes, honest labels. +- Motion is choreography, not decoration: durations ≤ 400ms, respect the existing easing vocabulary. +- Components must work in every format they declare (`web`, `mobile`, `chat`, `cli`). + +## How changes land + +1. Fork, branch from `main` (`feat/` or `fix/`). +2. `npm install && npm run dev` to work; `npm run build` must pass clean. +3. Open a PR using the template. CI runs the build; a maintainer reviews for principle adherence, not just code. +4. Squash-merged. Releases roll weekly. + +## Writing standards (docs and chapters) + +Concise and concrete. Every sentence earns the next; no filler, no "delightful experiences." Claims need either a demo or a scar behind them. If a paragraph could appear in any design system's docs, cut it. + +## Questions + +Open a [discussion](https://github.com/acaspx/agent-pitstop/discussions) or an issue. Telling us which sync moment your product struggles with is itself a contribution; it drives the roadmap. diff --git a/README.md b/README.md index 7f62c09..9ce9275 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,11 @@ npm run dev ## Status -Early. v0.1 ships with 2 principles and 3 components. One new chapter or component lands weekly. Watch the repo or follow [@acaspx](https://github.com/acaspx). +Five principles, five components, three categories. One new chapter or component lands weekly. Watch the repo or follow [@acaspx](https://github.com/acaspx). + +## Contributing + +Component proposals, fixes, and scars from real agent products are all welcome. Start with [CONTRIBUTING.md](./CONTRIBUTING.md) — it's short, and the definition of done keeps the system clean as it grows. ## License diff --git a/app/components/agent-inbox/demo.tsx b/app/components/agent-inbox/demo.tsx new file mode 100644 index 0000000..cc0db1e --- /dev/null +++ b/app/components/agent-inbox/demo.tsx @@ -0,0 +1,52 @@ +"use client"; + +import { useState } from "react"; +import { AgentInbox, type InboxItem } from "@/registry/agent-inbox/agent-inbox"; + +const initial: InboxItem[] = [ + { + id: "1", + title: "Reconcile March invoices", + summary: "14 matched, 2 need a decision", + status: "needs_review", + reason: "2 matches below confidence threshold", + time: "2h ago", + }, + { + id: "2", + title: "Draft outreach for 8 leads", + summary: "Drafts ready, held before sending", + status: "needs_review", + reason: "sending email is irreversible", + time: "4h ago", + }, + { + id: "3", + title: "Weekly competitor scan", + summary: "Reading 12 changelog pages…", + status: "running", + time: "now", + }, + { + id: "4", + title: "Archive stale tickets", + summary: "62 archived, log attached", + status: "done", + time: "1d ago", + }, +]; + +export function AgentInboxDemo() { + const [opened, setOpened] = useState(null); + + return ( +
+ setOpened(id)} /> +

+ {opened + ? `Opened run #${opened} for review. In a product, this routes to the run's transcript with its approval gates.` + : "Tap a run to open it for review."} +

+
+ ); +} diff --git a/app/components/agent-inbox/page.tsx b/app/components/agent-inbox/page.tsx new file mode 100644 index 0000000..8c4816c --- /dev/null +++ b/app/components/agent-inbox/page.tsx @@ -0,0 +1,37 @@ +import { ComponentDoc } from "@/components/docs/doc-page"; +import { AgentInboxDemo } from "./demo"; + +export const metadata = { + title: "Agent Inbox — Agent Pit Stop", + description: "The review queue for background agents: results wait for human eyes, ordered by what needs you most.", +}; + +export default function Page() { + return ( + } + sections={[ + { + heading: "When to use", + body: "Any product with agents that outlive the session: overnight runs, scheduled jobs, long research tasks. Skip it if every run completes while the user watches; a queue of one is ceremony.", + }, + { + heading: "Behavior", + body: "Four statuses: needs you, running, done, failed. Needs-review items carry the reason (low confidence, irreversible step, failure), because a red dot without a why just manufactures anxiety. Nothing auto-completes past a consent point while sitting in the queue.", + }, + { + heading: "Usage", + code: `import { AgentInbox } from "@/components/agent-inbox"; + + router.push(\`/runs/\${id}\`)} +/>`, + }, + ]} + /> + ); +} diff --git a/app/components/agent-roster/demo.tsx b/app/components/agent-roster/demo.tsx new file mode 100644 index 0000000..eb35d78 --- /dev/null +++ b/app/components/agent-roster/demo.tsx @@ -0,0 +1,36 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { AgentRoster, type RosterAgent } from "@/registry/agent-roster/agent-roster"; + +const frames: RosterAgent[][] = [ + [ + { id: "o", name: "Orchestrator", role: "plans and delegates", state: "working", activity: "assigning research tasks" }, + { id: "r", name: "Researcher", role: "finds and verifies sources", state: "working", activity: "reading 4 filings" }, + { id: "w", name: "Writer", role: "drafts the report", state: "waiting", activity: "waiting on research" }, + { id: "c", name: "Checker", role: "verifies claims", state: "idle" }, + ], + [ + { id: "o", name: "Orchestrator", role: "plans and delegates", state: "waiting", activity: "monitoring the crew" }, + { id: "r", name: "Researcher", role: "finds and verifies sources", state: "blocked", activity: "paywalled source, needs a decision" }, + { id: "w", name: "Writer", role: "drafts the report", state: "working", activity: "drafting section 2 of 5" }, + { id: "c", name: "Checker", role: "verifies claims", state: "idle" }, + ], + [ + { id: "o", name: "Orchestrator", role: "plans and delegates", state: "waiting", activity: "monitoring the crew" }, + { id: "r", name: "Researcher", role: "finds and verifies sources", state: "idle" }, + { id: "w", name: "Writer", role: "drafts the report", state: "working", activity: "drafting section 4 of 5" }, + { id: "c", name: "Checker", role: "verifies claims", state: "working", activity: "checking 11 citations" }, + ], +]; + +export function AgentRosterDemo() { + const [frame, setFrame] = useState(0); + + useEffect(() => { + const t = setInterval(() => setFrame((f) => (f + 1) % frames.length), 3200); + return () => clearInterval(t); + }, []); + + return ; +} diff --git a/app/components/agent-roster/page.tsx b/app/components/agent-roster/page.tsx new file mode 100644 index 0000000..d0ac63d --- /dev/null +++ b/app/components/agent-roster/page.tsx @@ -0,0 +1,40 @@ +import { ComponentDoc } from "@/components/docs/doc-page"; +import { AgentRosterDemo } from "./demo"; + +export const metadata = { + title: "Agent Roster — Agent Pit Stop", + description: "The team sheet for multi-agent runs: who's working, who's waiting, who's blocked, and on what.", +}; + +export default function Page() { + return ( + } + sections={[ + { + heading: "When to use", + body: "Orchestrator-and-workers runs where more than one agent acts at once. For a single agent, use the Agent Task List instead; a roster of one is an org chart for a solo founder.", + }, + { + heading: "Behavior", + body: "Four states: working (pulsing), waiting, blocked, idle. Every state is written in words next to its dot. Blocked rows name the blocker in the activity line and read as the human's cue to intervene; pair with the Interrupt Bar for targeted steering.", + }, + { + heading: "Usage", + code: `import { AgentRoster } from "@/components/agent-roster"; + +`, + }, + ]} + /> + ); +} diff --git a/app/components/context-budget/demo.tsx b/app/components/context-budget/demo.tsx new file mode 100644 index 0000000..795a148 --- /dev/null +++ b/app/components/context-budget/demo.tsx @@ -0,0 +1,31 @@ +"use client"; + +import { useState } from "react"; +import { ContextBudget } from "@/registry/context-budget/context-budget"; + +export function ContextBudgetDemo() { + const [spendCap, setSpendCap] = useState(5); + const [raised, setRaised] = useState(false); + + return ( +
+ `$${n.toFixed(2)}` }, + { label: "Tokens", used: 412_000, cap: 1_000_000, format: (n) => `${Math.round(n / 1000)}k` }, + { label: "Time", used: 21, cap: 45, format: (n) => `${n}m` }, + ]} + onRaise={() => { + setSpendCap(10); + setRaised(true); + }} + /> +

+ {raised + ? "Cap raised to $10.00 by you. The run resumes; the receipt records who raised it." + : "The run hit its $5 spend cap and paused. Raising the cap is a human decision."} +

+
+ ); +} diff --git a/app/components/context-budget/page.tsx b/app/components/context-budget/page.tsx new file mode 100644 index 0000000..b628b10 --- /dev/null +++ b/app/components/context-budget/page.tsx @@ -0,0 +1,40 @@ +import { ComponentDoc } from "@/components/docs/doc-page"; +import { ContextBudgetDemo } from "./demo"; + +export const metadata = { + title: "Context Budget — Agent Pit Stop", + description: "A delegation contract's limits made visible: tokens, spend, or time against their caps.", +}; + +export default function Page() { + return ( + } + sections={[ + { + heading: "When to use", + body: "Any run where the user set or implied a limit: spend, tokens, time, API calls. Show it during the run, not in a postmortem. Skip it for instant, effectively-free actions.", + }, + { + heading: "Behavior", + body: "Three bands, each labeled in words: fine, near cap, at cap. Hitting a cap pauses the run and states that nothing was spent past it; raising the cap is an explicit human action that belongs in the receipt trail. Bars carry ARIA progressbar semantics.", + }, + { + heading: "Usage", + code: `import { ContextBudget } from "@/components/context-budget"; + + \`$\${n.toFixed(2)}\` }, + { label: "Time", used: 21, cap: 45, format: (n) => \`\${n}m\` }, + ]} + onRaise={(which) => askUserToRaise(which)} +/>`, + }, + ]} + /> + ); +} diff --git a/app/components/gallery-minis.tsx b/app/components/gallery-minis.tsx new file mode 100644 index 0000000..14d78bf --- /dev/null +++ b/app/components/gallery-minis.tsx @@ -0,0 +1,70 @@ +"use client"; + +import type { ReactNode } from "react"; +import { ToolCallCard } from "@/registry/tool-call-card/tool-call-card"; +import { AgentTaskList } from "@/registry/agent-task-list/agent-task-list"; +import { ApprovalGate } from "@/registry/approval-gate/approval-gate"; +import { InterruptBar } from "@/registry/interrupt-bar/interrupt-bar"; +import { ConfidenceMeter } from "@/registry/confidence-meter/confidence-meter"; +import { AgentRoster } from "@/registry/agent-roster/agent-roster"; +import { AgentInbox } from "@/registry/agent-inbox/agent-inbox"; +import { ContextBudget } from "@/registry/context-budget/context-budget"; + +export const minis: Record = { + "tool-call-card": ( + + ), + "agent-task-list": ( + + ), + "approval-gate": ( + + ), + "interrupt-bar": , + "confidence-meter": ( + + ), + "agent-roster": ( + + ), + "agent-inbox": ( + + ), + "context-budget": ( + `$${n.toFixed(2)}` }, + { label: "Time", used: 21, cap: 45, format: (n) => `${n}m` }, + ]} + /> + ), +}; diff --git a/app/components/page.tsx b/app/components/page.tsx index f0da9fa..ab95d71 100644 --- a/app/components/page.tsx +++ b/app/components/page.tsx @@ -1,47 +1,12 @@ import Link from "next/link"; import { componentCategories, componentsIn } from "@/lib/nav"; -import { ToolCallCard } from "@/registry/tool-call-card/tool-call-card"; -import { AgentTaskList } from "@/registry/agent-task-list/agent-task-list"; -import { ApprovalGate } from "@/registry/approval-gate/approval-gate"; -import { InterruptBar } from "@/registry/interrupt-bar/interrupt-bar"; -import { ConfidenceMeter } from "@/registry/confidence-meter/confidence-meter"; +import { minis } from "./gallery-minis"; export const metadata = { title: "Components — Agent Pit Stop", description: "Every component, with live previews. Installable via the shadcn registry.", }; -const minis: Record = { - "tool-call-card": ( - - ), - "agent-task-list": ( - - ), - "approval-gate": ( - - ), - "interrupt-bar": ( - - ), - "confidence-meter": ( - - ), -}; export default function ComponentsOverview() { return ( @@ -60,7 +25,7 @@ export default function ComponentsOverview() {
{minis[c.slug]}
diff --git a/app/globals.css b/app/globals.css index 35aa86b..763f2a5 100644 --- a/app/globals.css +++ b/app/globals.css @@ -5,9 +5,10 @@ --color-asphalt: #131316; --color-barrier: #1f1f24; --color-line: #2c2c33; + --color-carbon: #0e0e10; --color-chalk: #ececf1; --color-smoke: #9a9aa3; - --color-ash: #5c5c66; + --color-ash: #7f7f8a; --color-signal: #34d399; --color-caution: #fbbf24; --color-flag: #f87171; @@ -28,3 +29,18 @@ html { ::selection { background: color-mix(in srgb, var(--color-pit) 30%, transparent); } + +:focus-visible { + outline: 2px solid var(--color-pit); + outline-offset: 2px; + border-radius: 4px; +} + +@media (prefers-reduced-motion: reduce) { + *, + *::before, + *::after { + animation-duration: 0.01ms !important; + transition-duration: 0.01ms !important; + } +} diff --git a/app/how-it-works/page.tsx b/app/how-it-works/page.tsx index 64297ba..d87adba 100644 --- a/app/how-it-works/page.tsx +++ b/app/how-it-works/page.tsx @@ -89,7 +89,7 @@ export default function HowItWorksPage() { where humans intervene. Design the pit well and users delegate more laps, the same way drivers go faster in cars with better brakes.

-
+

diff --git a/app/page.tsx b/app/page.tsx index 101e1b0..846d62b 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -17,6 +17,9 @@ const components = [ { title: "Agent Task List", href: "/components/agent-task-list", status: "Live" }, { title: "Interrupt Bar", href: "/components/interrupt-bar", status: "Live" }, { title: "Confidence Meter", href: "/components/confidence-meter", status: "Live" }, + { title: "Agent Roster", href: "/components/agent-roster", status: "Live" }, + { title: "Agent Inbox", href: "/components/agent-inbox", status: "Live" }, + { title: "Context Budget", href: "/components/context-budget", status: "Live" }, ]; /* two-tone icons in the site accent */ @@ -116,7 +119,7 @@ export default function Home() {

{/* large card: headline / flag / paragraph */} -
+

An open source design system to keep agents on track.

@@ -135,7 +138,7 @@ export default function Home() { {card.icon}
@@ -149,7 +152,7 @@ export default function Home() { {card.icon}
diff --git a/app/principles/page.tsx b/app/principles/page.tsx new file mode 100644 index 0000000..fb2f296 --- /dev/null +++ b/app/principles/page.tsx @@ -0,0 +1,130 @@ +import Link from "next/link"; +import { principles } from "@/lib/nav"; + +export const metadata = { + title: "Principles — Agent Pit Stop", + description: + "The philosophy, rules, and anti-patterns for building agent interfaces. Concise on purpose; the deep dives live in the chapters.", +}; + +const rules = [ + "Show intent before action, state during, evidence on demand.", + "Never communicate state by color alone; icon and label, always.", + "Render the plan before execution; done stays visible, failed stays in place.", + "Itemize scope before consent; mark what can't be undone.", + "Gate irreversible actions only; never ask permission to read.", + "Stop is always visible and never destructive; the receipt says what survived.", + "Acknowledge every steer the moment it lands.", + "Confidence comes in bands, never decimals; claims travel with their basis.", + "Verification is one tap away and cheaper than redoing.", + "Failure is a rendered state; keep the inputs attached, bound the retries.", +]; + +const donts = [ + ["Dump raw chain-of-thought and call it transparency.", "Structure beats volume; that's Legible Thinking."], + ["Fake a progress bar over indeterminate work.", "An honest pulse beats a lying percentage."], + ["Offer blanket “always allow.”", "Scope a remembered choice as narrowly as the choice."], + ["Show “87.3% confident.”", "Decimal confidence is theater; use bands."], + ["Narrate (“Let me search…”) instead of showing.", "Narration is prose about work; legibility is the work."], + ["Discard partial work on failure or stop.", "Three done steps are three done steps."], + ["Retry silently.", "Show the attempt count and the bound."], + ["Hide the stop control in a menu.", "A brake you have to find is not a brake."], +]; + +const essentials = [ + { need: "A visible plan", href: "/components/agent-task-list", comp: "Agent Task List" }, + { need: "Legible tool use", href: "/components/tool-call-card", comp: "Tool Call Card" }, + { need: "Consent before irreversible actions", href: "/components/approval-gate", comp: "Approval Gate" }, + { need: "Stop and steer, always reachable", href: "/components/interrupt-bar", comp: "Interrupt Bar" }, + { need: "Honest uncertainty with verification", href: "/components/confidence-meter", comp: "Confidence Meter" }, +]; + +export default function PrinciplesOverview() { + return ( +
+
Principles
+

+ Rules for the pit +

+

+ The short version of the whole system. Concise on purpose; each idea gets its + full argument, with live demos, in the chapters below. +

+ +

Philosophy

+
    +
  • + Agents run laps; humans work the pit. Design + effort goes to the sync moments: approval, inspection, handoff, recovery. +
  • +
  • + Interface, not architecture. This system covers + what people see, approve, interrupt, and trust, not how the agent is orchestrated. +
  • +
  • + Evidence over opinion. A principle that can't + be demonstrated with a working component isn't done being designed. +
  • +
  • + Built to be copied. Single-file components, three + dependencies, MIT. Take the code and own it. +
  • +
  • + One system for humans and agents. The registry is + machine-readable, so AI assistants install these components the same way you do. +
  • +
+ +

The rules

+
    + {rules.map((r, i) => ( +
  1. + {String(i + 1).padStart(2, "0")} + {r} +
  2. + ))} +
+ +

Don't

+
    + {donts.map(([bad, why], i) => ( +
  • + {bad}{" "} + {why} +
  • + ))} +
+ +

+ The essentials +

+

+ The minimum kit for any agent interface. If your product has an agent and is + missing one of these, that's the next thing to design. +

+
    + {essentials.map((e) => ( +
  • + {e.need} + + {e.comp} → + +
  • + ))} +
+ +

+ Going deeper +

+
    + {principles.map((p) => ( +
  • + + {p.title} → + +
  • + ))} +
+
+ ); +} diff --git a/components/docs/code-block.tsx b/components/docs/code-block.tsx index cd2cecb..d5eb549 100644 --- a/components/docs/code-block.tsx +++ b/components/docs/code-block.tsx @@ -1,9 +1,48 @@ "use client"; -import { useState } from "react"; +import { useMemo, useState } from "react"; + +/** + * Lightweight syntax highlighting: strings, comments, keywords, and + * numbers get token colors; everything else stays smoke. No dependencies, + * safe output (renders via React text nodes, never innerHTML). + */ + +const KEYWORDS = new Set([ + "import", "from", "export", "default", "const", "let", "var", "function", + "return", "if", "else", "type", "interface", "extends", "true", "false", + "null", "undefined", "new", "async", "await", "npx", "npm", "git", +]); + +type Token = { text: string; kind: "plain" | "string" | "comment" | "keyword" | "number" }; + +function tokenize(code: string): Token[] { + const tokens: Token[] = []; + const re = /("(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'|`(?:[^`\\]|\\.)*`)|(\/\/[^\n]*|\/\*[\s\S]*?\*\/|#[^\n]*)|\b(\d+(?:\.\d+)?)\b|([A-Za-z_$][\w$]*)|([\s\S])/g; + let m: RegExpExecArray | null; + while ((m = re.exec(code)) !== null) { + const [, str, comment, num, word, other] = m; + if (str !== undefined) tokens.push({ text: str, kind: "string" }); + else if (comment !== undefined) tokens.push({ text: comment, kind: "comment" }); + else if (num !== undefined) tokens.push({ text: num, kind: "number" }); + else if (word !== undefined) + tokens.push({ text: word, kind: KEYWORDS.has(word) ? "keyword" : "plain" }); + else tokens.push({ text: other ?? "", kind: "plain" }); + } + return tokens; +} + +const kindClass: Record = { + plain: "", + string: "text-signal", + comment: "text-ash italic", + keyword: "text-pit", + number: "text-caution", +}; export function CodeBlock({ code, title }: { code: string; title?: string }) { const [copied, setCopied] = useState(false); + const tokens = useMemo(() => tokenize(code), [code]); const copy = async () => { try { @@ -17,17 +56,24 @@ export function CodeBlock({ code, title }: { code: string; title?: string }) { return (
-
+
{title ?? "tsx"}
-
{code}
+
+        {tokens.map((t, i) => (
+          
+            {t.text}
+          
+        ))}
+      
); } diff --git a/components/docs/docs-shell.tsx b/components/docs/docs-shell.tsx index e5c242f..fbc2f49 100644 --- a/components/docs/docs-shell.tsx +++ b/components/docs/docs-shell.tsx @@ -1,10 +1,33 @@ +import Link from "next/link"; import { Sidebar } from "./sidebar"; +import { PitFlag } from "./pit-flag"; export function DocsShell({ children }: { children: React.ReactNode }) { return ( -
- -
{children}
+
+ {/* mobile nav: the sidebar is desktop-only, so phones get a top bar */} + + +
+ +
{children}
+
); } diff --git a/components/docs/preview-pane.tsx b/components/docs/preview-pane.tsx index d64a63e..9fe11cd 100644 --- a/components/docs/preview-pane.tsx +++ b/components/docs/preview-pane.tsx @@ -112,7 +112,7 @@ export function PreviewPane({ formats, children, variants, chatPrompt }: Preview ))}
-
+
{format === "mobile" ? (
diff --git a/components/docs/sidebar.tsx b/components/docs/sidebar.tsx index 260ce38..469ed1f 100644 --- a/components/docs/sidebar.tsx +++ b/components/docs/sidebar.tsx @@ -57,6 +57,7 @@ export function Sidebar() {
Principles
+ {principles.map((p) => ( void; + label?: string; +} + +const statusMeta: Record = { + needs_review: { label: "Needs you", dot: "bg-caution", text: "text-caution" }, + running: { label: "Running", dot: "bg-pit", text: "text-pit" }, + done: { label: "Done", dot: "bg-signal", text: "text-signal" }, + failed: { label: "Failed", dot: "bg-flag", text: "text-flag" }, +}; + +export function AgentInbox({ items, onOpen, label = "Agent inbox" }: AgentInboxProps) { + const needsYou = items.filter((i) => i.status === "needs_review").length; + + return ( +
+
+ {label} + + {needsYou > 0 ? `${needsYou} need${needsYou === 1 ? "s" : ""} you` : "all clear"} + +
+
    + + {items.map((item) => { + const meta = statusMeta[item.status]; + return ( + + + + ); + })} + +
+
+ ); +} diff --git a/registry/agent-roster/agent-roster.tsx b/registry/agent-roster/agent-roster.tsx new file mode 100644 index 0000000..7db48c2 --- /dev/null +++ b/registry/agent-roster/agent-roster.tsx @@ -0,0 +1,101 @@ +"use client"; + +/** + * AgentRoster — Agent Pit Stop + * The team sheet for multi-agent runs: who's working, who's waiting, + * who's blocked, and on what. An orchestrator without a roster is a + * black box with extra steps. + * + * Principles applied: + * - Legible thinking: one line per agent — role, state, and current + * activity — never a log dump + * - Graceful failure: a blocked agent stays on the roster with its + * blocker named; the team doesn't pretend to be smaller + * - Interruptibility: the roster is the map for targeted intervention; + * pair rows with your stop/steer controls + */ + +import { motion } from "motion/react"; + +export type AgentState = "working" | "waiting" | "blocked" | "idle"; + +export interface RosterAgent { + id: string; + /** Display name, e.g. "Researcher" */ + name: string; + /** What it's for, e.g. "finds and verifies sources" */ + role?: string; + state: AgentState; + /** Live activity or blocker, e.g. "reading 4 filings" / "needs API key" */ + activity?: string; +} + +export interface AgentRosterProps { + agents: RosterAgent[]; + label?: string; +} + +const stateMeta: Record = { + working: { label: "Working", text: "text-pit", pulse: true }, + waiting: { label: "Waiting", text: "text-smoke", pulse: false }, + blocked: { label: "Blocked", text: "text-flag", pulse: false }, + idle: { label: "Idle", text: "text-ash", pulse: false }, +}; + +function StateDot({ state }: { state: AgentState }) { + const color = + state === "working" ? "bg-pit" : state === "blocked" ? "bg-flag" : state === "waiting" ? "bg-smoke" : "bg-ash"; + return ( + + {stateMeta[state].pulse && ( + + )} + + + ); +} + +export function AgentRoster({ agents, label = "Crew" }: AgentRosterProps) { + const working = agents.filter((a) => a.state === "working").length; + + return ( +
+
+ {label} + + {working}/{agents.length} working + +
+
    + {agents.map((agent) => { + const meta = stateMeta[agent.state]; + return ( +
  • + + + + {agent.name} + {agent.role && {agent.role}} + + {agent.activity && ( + + {agent.activity} + + )} + + {meta.label} +
  • + ); + })} +
+
+ ); +} diff --git a/registry/approval-gate/approval-gate.tsx b/registry/approval-gate/approval-gate.tsx index 4bf713d..b812e9e 100644 --- a/registry/approval-gate/approval-gate.tsx +++ b/registry/approval-gate/approval-gate.tsx @@ -109,14 +109,14 @@ export function ApprovalGate({ diff --git a/registry/confidence-meter/confidence-meter.tsx b/registry/confidence-meter/confidence-meter.tsx index 471e1ba..a9615fe 100644 --- a/registry/confidence-meter/confidence-meter.tsx +++ b/registry/confidence-meter/confidence-meter.tsx @@ -92,7 +92,7 @@ export function ConfidenceMeter({ diff --git a/registry/context-budget/context-budget.tsx b/registry/context-budget/context-budget.tsx new file mode 100644 index 0000000..cbb0463 --- /dev/null +++ b/registry/context-budget/context-budget.tsx @@ -0,0 +1,100 @@ +"use client"; + +/** + * ContextBudget — Agent Pit Stop + * A delegation contract's limits, made visible: tokens, spend, or time + * against their caps. Budgets nobody can see are budgets nobody trusts. + * + * Principles applied: + * - Delegation contracts: the cap is part of the agreed scope; showing + * consumption is honoring the contract in public + * - Calibrated trust: honest bands (fine / near cap / over), labeled in + * words, never color alone + * - Graceful failure: hitting a cap is a rendered state with a next step, + * not a silent stall + */ + +import { motion } from "motion/react"; + +export interface BudgetLine { + /** e.g. "Spend", "Tokens", "Time" */ + label: string; + used: number; + cap: number; + /** Formatter for display, e.g. (n) => `$${n}` */ + format?: (n: number) => string; +} + +export interface ContextBudgetProps { + lines: BudgetLine[]; + label?: string; + /** Called when the human raises a cap after one is hit */ + onRaise?: (label: string) => void; +} + +function bandFor(pct: number): { word: string; bar: string; text: string } { + if (pct >= 1) return { word: "At cap", bar: "bg-flag", text: "text-flag" }; + if (pct >= 0.8) return { word: "Near cap", bar: "bg-caution", text: "text-caution" }; + return { word: "Fine", bar: "bg-pit", text: "text-smoke" }; +} + +export function ContextBudget({ lines, label = "Run budget", onRaise }: ContextBudgetProps) { + return ( +
+
+ {label} +
+
    + {lines.map((line) => { + const pct = Math.min(line.used / line.cap, 1); + const band = bandFor(line.used / line.cap); + const fmt = line.format ?? ((n: number) => String(n)); + const atCap = line.used >= line.cap; + return ( +
  • +
    + {line.label} + + {fmt(line.used)} + / {fmt(line.cap)} + {band.word} + +
    +
    + +
    + {atCap && ( +
    + + Paused at the {line.label.toLowerCase()} cap. Nothing spent past it. + + {onRaise && ( + + )} +
    + )} +
  • + ); + })} +
+
+ ); +} diff --git a/registry/interrupt-bar/interrupt-bar.tsx b/registry/interrupt-bar/interrupt-bar.tsx index 6ffacc7..75115fe 100644 --- a/registry/interrupt-bar/interrupt-bar.tsx +++ b/registry/interrupt-bar/interrupt-bar.tsx @@ -81,7 +81,7 @@ export function InterruptBar({ @@ -116,7 +116,7 @@ export function InterruptBar({ type="button" onClick={steer} disabled={!draft.trim()} - className="shrink-0 rounded-lg bg-barrier px-3 py-1 text-[12px] font-medium text-smoke transition-colors enabled:hover:text-chalk disabled:opacity-40" + className="min-h-9 shrink-0 rounded-lg bg-barrier px-3 py-1 text-[12px] font-medium text-smoke transition-colors enabled:hover:text-chalk disabled:opacity-40 pointer-coarse:min-h-11" > Send diff --git a/scripts/build-registry.mjs b/scripts/build-registry.mjs index 5fe3751..2b9a70f 100644 --- a/scripts/build-registry.mjs +++ b/scripts/build-registry.mjs @@ -32,6 +32,18 @@ const meta = { title: "Confidence Meter", description: "Honest uncertainty as coarse bands, paired with a verification affordance below the threshold.", }, + "agent-roster": { + title: "Agent Roster", + description: "The team sheet for multi-agent runs: role, state, and live activity per agent, blockers named.", + }, + "agent-inbox": { + title: "Agent Inbox", + description: "The review queue for background agents. Needs-review items carry the reason, not just a red dot.", + }, + "context-budget": { + title: "Context Budget", + description: "A delegation contract's limits made visible: consumption vs caps in honest bands, cap-hit as a rendered state.", + }, }; await mkdir(OUT_DIR, { recursive: true });