Compare commits

..

22 Commits

Author SHA1 Message Date
scotttong
eec4ff855a Phase 4 complete: polish, run-notes, rubric self-check
Consolidates run-a-notes.md from a chronological log into a review
artifact (Header → Decisions → DS deviations → Findings → Deferred →
Rubric alignment → Friction log highlights), and adds
run-a-self-check.md walking each rubric item with pass/fail + reasoning.

Section 5 scoring distinguished into clean-pass vs pass-with-
justification:
- Clean (4): no new tokens, no component extraction, no chart
  tokenization, pause neutral styling (vacuous).
- With justification (4): rounded corners (rounded-full on glyphs,
  rounded-md inherited from shadcn), live-run distinction, raw-
  palette drift (bg-red-400/amber-300/emerald-300 introduced by the
  budget card — mirror BudgetPolicyCard's treatment), no out-of-scope
  changes (additive opt-in extensions to ActivityCharts.tsx that
  leave pages/Dashboard.tsx behaviorally unchanged).

Section 6 qualitative scores are 4/5 across the board with honest
reasoning — deliberately conservative. Improvement is evolutionary
not revolutionary; restraint has two specific decorative choices
(priority-flash highlight, cost-line middot separators) that a
stricter pass would cut; hierarchy has below-hero sequential-section
flatness; live-run's in-card spinner is small relative to the pill.

Preserved commits listed in run-a-notes.md §1 as the full chain from
Phase 2 structural frame through Phase 4 polish.

Two pre-existing untracked files (debug-storybook.log, prototype-ref/)
intentionally left untracked — pre-dates Run A, not in scope.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 16:46:19 -07:00
scotttong
873f15d990 Phase 4 polish: suppress chart legend when there's nothing to legend
When an agent has zero runs in the 14-day window, RunActivityChart
renders "No runs yet" text instead of bars. The legend below was still
rendering — looking orphaned and implying a chart exists when it
doesn't. Gate the legend on runs.length > 0 so the empty state is
clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 16:27:32 -07:00
scotttong
ec37967dd3 Phase 4 polish: normalize module heading hierarchy
Three inconsistent patterns for module labels:
- ChartCard title: text-xs font-medium text-muted-foreground
- Costs eyebrow: text-xs text-muted-foreground (missing font-medium)
- In-flight tasks h3: text-sm font-medium (bigger, not muted)

Normalized to the ChartCard pattern — one tier for all module labels,
giving uniform visual rhythm down the page. In-flight's "View all →"
link is also text-xs, so baselines now match cleanly.

Section 6 "Visual restraint" benefit: single type size for module
labels reduces typographic noise; the page reads as one aesthetic
system rather than three.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 16:26:27 -07:00
scotttong
859523b58b Phase 3d: unified costs
Merge the KPI totals strip and per-run cost table into one coherent
accountability surface. Outer bordered container wraps a summary block
above a borderless table; a single border-t on the table's thead
separates summary from detail.

Structure:
- One container, one outline (Common Region).
- Title "Costs — session" internal, matching ChartCard pattern. The
  external <h3>Costs</h3> in AgentOverview is gone.
- Summary: inline one-liner "$X.XX cumulative · N in · N out · N
  cached" with primary (cost, text-sm font-semibold) and secondary
  (tokens, text-xs muted) hierarchy. Middot separators.
- Table: same columns (Date, Run, Input, Output, Cost), same 10-row
  cap, tabular-nums preserved. Switched rows from border-b+last: to
  border-t for simpler logic; no visual change.

Empty states:
- Neither runtime state nor runs-with-cost → one compact card with
  eyebrow + "No cost data yet."
- Runtime state without runs-with-cost → summary only.
- Runs-with-cost without runtime state → table only (no aggregate
  fallback — per concept §5 honest scoping; session vs all-runs would
  mix vocabularies).
- Both present → both render, separated by the table-header border-t.

Scope preserved:
- CostsSection data sources unchanged — still runtimeState + runs.
- No new queries, hooks, or helpers. runMetrics, formatCents,
  formatTokens, formatDate reused as-is.
- AgentOverview only lost the external Costs <h3> wrapper.

DS deviations: none introduced. All classes already present in the
dashboard region.

Lenses cited in run-a-notes.md: Common Region, Prägnanz, Progressive
Disclosure, Information Scent, Aesthetic-Usability Effect, Jakob's Law.

Findings re-surfaced / carried forward:
- Per-task cost attribution is weak (HeartbeatRun has no reliable
  Issue link). Table shows cost-per-run, not cost-per-task. Relevant
  for rubric Section 2 "Accountability (spend)"; data-model gap, out
  of redesign scope.
- Vocabulary overlap between session-cumulative cost (costs module)
  and month-to-date observed (hero budget). Legible because each is
  explicitly labeled, but fragile.

Typecheck clean. No new tests (no new component, no new helper).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 16:05:57 -07:00
scotttong
917b72aaf4 Phase 3c: priority affordance
Wire the existing PriorityIcon popover picker into the in-flight tasks
list. Click a priority icon → popover with 4 options → pick → row
updates optimistically, may reposition by the priority-DESC sort, and
briefly highlights.

PriorityIcon already ships the full picker (used in IssueDetail and
IssueProperties). Passing onChange turns its icon into a shadcn Popover
trigger with 4 Button options — keyboard-accessible via Tab/Enter/
Escape. Reused without modification.

Mutation:
- issuesApi.update(id, { priority })
- Optimistic: mutate the dashboard-scoped query cache
  ([...queryKeys.issues.list(companyId), "participant-agent", agentId])
  so inFlightTasks resorts immediately.
- onError: revert snapshot + shadcn error toast with server message.
- onSettled: invalidate queryKeys.issues.list(companyId) so other views
  converge.

Repositioning:
- The existing inFlightTasks memo sorts priority-DESC then updatedAt-DESC.
  Optimistic mutation triggers re-memo → row reposition. No animation
  framework.
- Post-change highlight: 1000ms bg-accent/30 on the changed row via
  local recentlyChangedId state + transition-colors fade. Highlight
  travels with the repositioned row (Zeigarnik / Goal-Gradient).

Nested-interactive concession:
- EntityRow wraps as <Link>; PriorityIcon popover trigger is inside it.
  Wrapped the trigger in a <span> that stopPropagation/preventDefault
  on click + Enter/Space keydown, so icon clicks don't leak to the
  Link. Keyboard focus on the trigger behaves correctly.
- Nested-interactive a11y pattern flagged in run-a-notes — Phase 4
  candidate if we want to refactor EntityRow.

Permissions:
- No client-side gate (matches IssueDetail.tsx pattern — existing app
  behavior is "anyone who can view can edit"). Server rejection
  surfaces via error toast. Flagged in run-a-notes so product knows
  this is deliberate continuity, not silent granting.

DS deviations (pass-with-justification):
- PopoverContent rounded-md inherited from shadcn primitive (accepted
  per Step 0 exemption).
- bg-accent/30 for the highlight — existing DS token + standard Tailwind
  opacity modifier. No new raw-palette drift.

Rubric:
- Section 3 "Change task priority": pass (explicit selector, 4-value
  enum, keyboard-accessible).
- Section 2 "Operations": pass (change priority from dashboard without
  leaving page).
- Section 3 "Navigate to detail": unchanged (Link still routes to
  /issues/{identifier}).

Typecheck clean. Existing ActivityCharts tests pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 15:57:49 -07:00
scotttong
0710f166da Phase 3b polish: chart legend and rich tooltips
Pre-existing gap surfaced by chart consolidation. RunActivityChart had
no legend (despite ChartLegend existing in the same file and being
used by PriorityChart/IssueStatusChart) and only a native-title
tooltip showing daily total without the succeeded/failed/other
breakdown.

Scope-preserving implementation:

- Export ChartLegend from ActivityCharts.tsx (previously file-private).
  Rendered inline in AgentOverview as a sibling of RunActivityChart
  inside ChartCard. Zero-touch to the chart component.

- Add opt-in richTooltips?: boolean prop to RunActivityChart. When
  true, each day column is wrapped in a shadcn Tooltip (established
  project pattern) with day + per-category breakdown. When false
  (default), native-title behavior preserved.

Dashboard.tsx (the other consumer) continues to render
RunActivityChart without richTooltips — unchanged behavior there. All
ActivityCharts.tsx changes are additive and backward-compatible.

The rich-tooltip branch uses a <button> (not <div>) as the
TooltipTrigger child for keyboard focusability. cursor-default
preserves visual mouse semantics.

Legend renders all three categories unconditionally for vocabulary
stability across states.

Lenses cited in run-a-notes.md: Recognition over Recall (legend),
Information Scent (breakdown tooltips), Doherty Threshold (shadcn
zero-delay vs native ~500ms), Jakob's Law.

Chart sparseness observation from the smoke test filed as a Phase 4
polish candidate — premature to judge before 3c and 3d land.

Existing ActivityCharts.test.tsx passes unchanged (2 tests green).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 15:47:47 -07:00
scotttong
51dd51b98a Phase 3b: chart consolidation
Four charts → one. Dropped Issues by Priority, Issues by Status, and
Success Rate from the dashboard composition. Kept RunActivityChart —
the only uniquely-dashboard-shaped temporal signal. Success Rate
merged into the chart card's subtitle as "N% success · Last 14 days."

Reachability for dropped chart data:
- Issues by Priority/Status → /issues?participantAgentId=<id> via
  the existing "View all →" link, then filter. Two interactions,
  satisfies rubric Section 4.
- Success Rate → visible as subtitle on the surviving card (zero
  interactions).

Layout collapses from grid-cols-4 to a single full-width ChartCard.
No wrapping grid; ChartCard is already block-level. No chart
tokenization (Step 0 policy preserved). No new wrappers.

Components still exported from ActivityCharts.tsx because
pages/Dashboard.tsx uses them. Scope-respecting: only removed from
AgentDetail.tsx imports and JSX.

Success-rate formula: succeeded/total — matches the stacked bars
visually, same as the dropped SuccessRateChart.

All decisions, lenses cited (Information Scent, Hick's Law, Pareto,
Prägnanz), and reachability checks captured in run-a-notes.md.

Carried forward to Phase 4: Latest Run card right edge doesn't align
with the content grid right edge.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 15:39:56 -07:00
scotttong
774679bf50 Phase 3a polish round 3: hero-spanning activity pill
Lifts the activity pill out of the left zone and promotes it to a
hero-level header above the 75/25 grid. The Latest Run card and
budget card now share a baseline; the prior top-of-zone mismatch is
gone.

Mode 2 alignment exploration resolved in favor of Option C:
- Semantic honesty — the pill describes the agent, not the
  current-work column. Moving it up reflects its actual scope.
- Von Restorff preserved — the cyan signal keeps its spatial
  isolation, important for rubric Section 6 "Live-run signal
  strength."
- Zero structural cost — no new elements or tokens; the pill changes
  position, nothing else changes.
- Latest Run card meaning sharpens — now unambiguously "a specific
  run" rather than conflating agent state.
- Zero-runs gracefully handled without conditional complexity.

Pill-grounding treatment (chosen to avoid the "lonely pill above a
wide grid" risk): tight-proximity via space-y-3 (12px) on the hero
wrapper. No border-b separator — a horizontal rule would read as a
section divider, heavier than the pill deserves. Gestalt Proximity
groups the pill with the grid below without needing a visual
connector. Aesthetic-Usability Effect: restraint.

Structure:
- Hero wrapper: outer div with space-y-3.
- Child 1: activity pill (flex row, dot + label).
- Child 2: the existing grid-cols-[3fr_1fr] with left + right zones.
  Left zone's internal space-y-3 unchanged; now starts directly with
  Latest Run card.

1440px no-scroll: confirmed still passing. Delta is zero to within
rounding — the ~20px gained at the hero top is offset by the ~20px
left zone shrinks.

All decisions and operating-principles citations captured in
run-a-notes.md.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 15:34:28 -07:00
scotttong
4996295b07 Phase 3a polish round 2: compact run feed replaces icon strip
Concept-level revision. The icon-strip treatment from concept §7 was
compact but information-sparse — run id, outcome detail, timestamp all
hidden behind hover. Replaced with a 3-row compact feed below the
Latest Run card; information readable at a glance.

Layout
- Hero back to two-zone: 75/25 (lg:grid-cols-[3fr_1fr]).
- Left zone (75%): activity pill + Latest Run card + prior-runs feed +
  idle hint.
- Right zone (25%): budget card alone.
- Three-zone layout from polish round 1 collapsed back — feed needs
  card width for tabular-aligned rows.

Prior-runs feed
- Three one-line rows in a bordered container, border-t between rows.
- Each row: colored status icon (h-3 w-3, shape+color per runStatusIcons),
  mono run id (first 8 chars), plain status label, right-aligned
  relative time. Entire row is a Link.
- Rows are the 3 runs before the Latest Run card's pick — filtered by
  id so Latest Run isn't duplicated.
- No tooltips — all info visible in the row.

LatestRunCard refactor
- Signature changed from { runs, agentId } to { run, agentId }. Caller
  (AgentOverview) now owns the sort/liveRun/latestRun/priorRuns
  computation via a single sortedRuns memo. Simpler component; prevents
  duplication with the prior-runs feed.

Jakob's Law, Recognition over Recall, and Information Scent cited in
run-a-notes.md. Concept doc gets a "Phase 3a revision" note recording
the drift from §7 — original §7 preserved as authored.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 14:55:55 -07:00
scotttong
568e1c15df Phase 3a polish: 50/25/25 hero layout and shadcn tooltips
Two follow-ups from the 3a smoke-test.

Layout: 50/50 right zone still felt cramped with budget + recent-runs
stacked. Split into 50/25/25 — left stays for current-work, middle 25%
is budget alone, right 25% is recent-runs alone. Each peer gets
horizontal breathing room. Budget card's dense `$X of $Y` + `N%` line
verified to fit at ~275px content width with `justify-between` and
`gap-2`; worst-case values still fit.

Tooltips: native HTML `title` on the recent-runs <Link> was unreliable
(delay + styling OS-dependent, sometimes suppressed on anchors).
Replaced with shadcn Tooltip primitive — already project-standard
(TooltipProvider mounted globally in main.tsx, used elsewhere e.g.
HintIcon in agent-config-primitives). Content now carries the full
concept-specified triplet: run id + outcome + relative time.
aria-label retained for screen readers.

Decisions + findings captured in run-a-notes.md Phase 3a polish section.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 14:06:13 -07:00
scotttong
6b0f407943 Phase 3a: hero module redesign
Rebuild the load-bearing hero surface per concept §1 with full styling
attention. Adds the "Next up" forward-looking signal the concept flagged,
relocates cyan to the activity pill, and rebuilds the recent-runs strip
for WCAG color-independence.

Structure
- Hero grid: 55/45 → 50/50 (grid-cols-2 at lg:). Right zone no longer
  cramped; both zones read as parallel questions per Common Region.
- Left zone: activity pill + current-work card + idle "Next up" line.
- Right zone: budget card + recent-runs icon strip.

Activity-state pill
- Minimal dot + text; no pill container. Text color `text-foreground`
  default, overridden to cyan-600 (running) or red-600 (error). The dot
  carries severity for paused/pending_approval/terminated. All classes
  already present in the dashboard region — no raw-palette drift.
- Passes `active` through as a distinct state rather than collapsing to
  idle; matches the DS dot catalog.

LatestRunCard
- Stripped the header row (redundant "Live Run"/"Latest Run" label,
  pulse dot, and separate "View details →" link). The card itself is
  the only Link; click anywhere navigates. aria-label on the Link
  restores the screen-reader context.
- Removed card-level `border-cyan-500/30` + glow shadow. The activity
  pill above carries the external liveness signal; the internal
  StatusIcon (cyan spinning Loader2) is the local signal. Eliminates
  the three-way cyan duplication.
- Moved the early return below the hooks to preserve hook ordering
  while handling the zero-runs case cleanly in the caller.

Idle "Next up" hint
- When activityStatus is `idle` or `active` and in-flight tasks exist,
  renders a one-line link to the top-priority task. When there are
  zero in-flight tasks, renders "No pending work". Derived from the
  existing inFlightTasks memo — no new data fetches.

Recent-runs strip
- Replaced dots with colored lucide icons (CheckCircle2, XCircle,
  Loader2, Clock, etc.) from runStatusIcons, at h-3.5 w-3.5. Each icon
  is a Link with aria-label + title. Shape + color gives WCAG
  color-independence. Removes the Phase 2 `text-*.replace(/text-/g, "bg-")`
  hack.

Budget card
- Extracted "resets in N days" label into a useMemo for cleaner JSX.
  Behavior unchanged from the Phase 2 fix.

All decisions, DS deviations, and surfaced findings captured in
projects/agent-profile-dashboard/run-a-notes.md with operating-principles
citations.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 12:23:20 -07:00
scotttong
389f8105ac Phase 2 fix: derive dashboard activity-state from runs, complete budget window
Two issues surfaced by the 1440px smoke test on the Phase 2 structural frame:

1. The hero activity-state pill read "Idle" while the Live Run card below
   read "Running" with a cyan pulse — the dashboard was contradicting itself
   during live runs. Root cause: agent.status drifts stale in the cache
   (queryKey mismatch between the URL-ref-keyed detail query and the
   UUID-keyed agent.status live-event invalidation). Fix: derive
   running-ness from the runs array — the same signal the live-run card
   trusts — rather than reading agent.status directly. Resolution order is
   paused > pending_approval > terminated > running(derived) > error >
   idle, so agent-level terminal states still take precedence.

2. Budget card was missing the "resets in N days" window copy the concept
   specified. Added using the existing budgetSummary.windowEnd; suppressed
   when the synthesized-fallback policy is in use (policyId === "") since
   its windowEnd is meaningless.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 12:02:01 -07:00
scotttong
78eed3b197 Phase 2: Agent Profile Dashboard structural frame
Stub-level structural implementation of the concept-approved frame:

- Thread agentBudgetSummary into AgentOverview as optional prop
- Top-level layout rewrite: two-zone hero | chart band | in-flight
  tasks | costs
- Hero left zone: activity-state dot + agent.status label; existing
  LatestRunCard as the current-work card (unchanged)
- Hero right zone: compact budget-position card (progress bar mirrors
  BudgetPolicyCard treatment); 7-dot recent-runs strip, each linking
  to its run detail
- In-flight tasks: filter status in {todo, in_progress, blocked},
  sort by priority DESC then updatedAt DESC, limit 7, read-only
  PriorityIcon as the Phase 3c priority-affordance placeholder
- Chart band and CostsSection carried over unchanged (Phase 3b/3d)

No visual polish, no empty-state work, no chart merges, no priority
mutation wired. This commit lands the structural frame only so the
1440px no-scroll smoke test can happen at the Phase 2 checkpoint.
2026-04-23 01:48:22 -07:00
scotttong
aecf32e60b Add Run A concept document 2026-04-23 01:43:25 -07:00
scotttong
2ede0af41f Add Run A execution plan
Five-phase structure: concept, structural frame, module-level
redesign (4 sub-modules), polish + rubric self-check.
Mandatory checkpoints after concept, after structural frame,
after each Phase 3 sub-module, and at final review.
2026-04-23 01:20:23 -07:00
scotttong
31e9a92853 Add Run A discovery document
Structural analysis of AgentOverview, LatestRunCard, CostsSection
plus data-model reality check for the three brief action affordances.
Surfaced three brief edits now landed on shared baseline.
2026-04-23 01:20:23 -07:00
scotttong
0f5895e4e5 Add shared UX operating principles for Run A and Run B
Both paths work from the same UX operating manual — role definition,
design lenses, visual quality bar, DS-first discipline, working rules.
Path-specific operational mechanics are not in this document.

Isolating the shared principles removes a confound from the experiment:
any differences between Run A and Run B outputs reflect execution-path
differences, not differences in design guidance.
2026-04-23 01:20:18 -07:00
scotttong
2f9f657df5 Remove stale pause references from Agent Profile Dashboard brief
Pause is handled by existing page-chrome control and is out of scope
for this redesign (per earlier brief update). DS-policy bullet and
Known deferrals section were vestigial references to pause styling
that no longer apply. The --signal-warning deferral is documented
in tokens-review.md where it belongs.
2026-04-23 01:02:04 -07:00
scotttong
53db6095b0 Update Agent Profile Dashboard brief and rubric based on Run A discovery
Discovery surfaced three brief-vs-reality gaps that required edits before
either run proceeds:

- Backend does not support arbitrary task reordering. Action reframed
  from "reorder in-flight tasks" to "change task priority."
- Pause is already fully implemented in page chrome (PauseResumeButton).
  Removed from dashboard's in-scope actions to avoid duplication.
- Live economics requirement clarified: both budget position and
  session burn rate required on dashboard; budget position specifically
  satisfies the monitoring-frame rubric test.

These edits preserve experiment symmetry — both Run A and Run B work
from the updated baseline.
2026-04-23 00:47:24 -07:00
scotttong
3907d6dc40 Add brief, rubric, and reference screenshots for Agent Profile Dashboard experiment
Shared baseline for Run A (Claude Code) and Run B (Paperclip) two-path
experiment on the Dashboard tab of the Agent Profile page.

- projects/agent-profile-dashboard/brief.md — what the redesign is
- projects/agent-profile-dashboard/rubric.md — how to evaluate success
- projects/agent-profile-dashboard/reference/ — screenshots of current state

Both run branches will fast-forward to this commit so both runs start
from identical shared baseline.
2026-04-22 17:53:06 -07:00
scotttong
1d74e78a96 ds: token cleanup, radius monotonic restore, plugin SDK contract tagging
Five source-code changes tied to the DS decision lock-ins (doc/design-system/REVIEW.md):

1. Signal tokens added in ui/src/index.css. --signal-success (oklch green-700/600) and --signal-success-foreground (white) paired with --destructive as the DS's action-severity vocabulary. Sourced from the canonical approve-button treatment (ApprovalCard, ApprovalDetail, Inbox). No call sites migrated. Tailwind aliases exposed via @theme inline.

2. destructive-foreground light-mode value fixed. Was oklch(0.577 0.245 27.325) (equalled --destructive — would render invisible); now oklch(0.985 0 0) (white, matching dark mode). Zero production consumers; safe change.

3. Radius scale restored to monotonic. --radius-lg: 0px → 0.625rem (10px) and --radius-xl: 0px → 0.75rem (12px). 226 call sites across 58 files in ui/src/ migrated from rounded-lg/rounded-xl to rounded-none to preserve the existing flat-Swiss aesthetic on dashboard surfaces. Shadcn primitives excluded — ui/src/components/ui/dialog.tsx retains rounded-lg so DialogContent now renders with real 10px-rounded corners (the first observable visual change from the radius work, intentional). One test assertion updated in lockstep (ProjectWorkspaceSummaryCard.test.tsx:135).

4. 13 dead tokens documented as reserved. --chart-1..5 and all 8 --sidebar-* tokens kept in index.css with JSDoc-style comments explaining their reserved/aspirational status (chart = future chart tokenization; sidebar = shadcn sidebar primitive compatibility).

5. Plugin SDK hybrid status made visible. 9 unimplemented contract components in packages/plugins/sdk/src/ui/components.ts now carry @status contract-only JSDoc tags so plugin authors see the status in IDE tooltips at call sites. MetricCard and StatusBadge are implemented; the other 9 will fail at runtime if rendered. Prioritization deferred to a separate plugin-SDK roadmap conversation.

Explicitly not in scope: status-colors.ts tokenization, chart color tokenization, component merges, renames, --signal-warning/--signal-info variants, elevation/motion/spacing tokens.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 16:34:53 -07:00
scotttong
0875aa54ff ds: extract design system documentation under doc/design-system/
Four-stage design system extraction (ds-discovery + ds-extraction skills):
- _discovery.json — styling stack, token sources, component layout, Storybook inventory
- tokens/ — canonical token inventory (37 tokens across color + radius), machine-readable tokens.json, drift review
- components/ — 135-component index with status markers, 53 per-component detail files, cross-cutting review (duplicates, naming, token non-compliance, story gaps, plugin SDK contract status)
- patterns/ — 10 pattern docs (list-page, detail-page, sidebar-chrome, finance-card, entity-properties-panel, entity-creation-dialog, status-display, entity-row, subscription-panel, quota-display), variance analysis, pattern opportunities
- REVIEW.md — entry point ordered by expected human value; recommended review order with time estimates; confidence levels; scope limitations

All extraction decisions captured in place. Components-review.md structured as a persistent backlog so future developers can pick any section up cold.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 16:34:29 -07:00
904 changed files with 17611 additions and 191853 deletions

View File

@@ -177,12 +177,8 @@ real name or email). To find GitHub usernames:
**Never expose contributor email addresses.** Use `@username` only.
Exclude bot accounts (e.g. `lockfile-bot`, `dependabot`) from the list.
Exclude Paperclip founders from the list (e.g. `cryppadotta`, `forgottendev`, `devinfoley`, `sockmonster`, `scotttong`)
List contributors in alphabetical order by GitHub username (case-insensitive).
If there are no contributors left after exclusions, then just skip this section and don't mention it.
Exclude bot accounts (e.g. `lockfile-bot`, `dependabot`) from the list. List contributors
in alphabetical order by GitHub username (case-insensitive).
## Step 6 — Review Before Release

View File

@@ -14,7 +14,7 @@ permissions:
jobs:
build-and-push:
runs-on: ubuntu-latest
timeout-minutes: 60
timeout-minutes: 30
concurrency:
group: docker-${{ github.ref }}
cancel-in-progress: true

View File

@@ -23,9 +23,7 @@ jobs:
- name: Block manual lockfile edits
if: github.head_ref != 'chore/refresh-lockfile'
run: |
# Diff the PR branch against its merge base so recent base-branch commits
# do not masquerade as changes made by the PR itself.
changed="$(git diff --name-only "${{ github.event.pull_request.base.sha }}...${{ github.event.pull_request.head.sha }}")"
changed="$(git diff --name-only "${{ github.event.pull_request.base.sha }}" "${{ github.event.pull_request.head.sha }}")"
if printf '%s\n' "$changed" | grep -qx 'pnpm-lock.yaml'; then
echo "Do not commit pnpm-lock.yaml in pull requests. CI owns lockfile updates."
exit 1
@@ -43,20 +41,48 @@ jobs:
node-version: 24
- name: Validate Dockerfile deps stage
run: node ./scripts/check-docker-deps-stage.mjs
- name: Validate release package manifest
run: node ./scripts/release-package-map.mjs check
- name: Verify release package bootstrap for changed manifests
run: |
mapfile -t changed_paths < <(git diff --name-only "${{ github.event.pull_request.base.sha }}...${{ github.event.pull_request.head.sha }}")
PAPERCLIP_RELEASE_BOOTSTRAP_BASE_SHA="${{ github.event.pull_request.base.sha }}" \
node ./scripts/check-release-package-bootstrap.mjs "${changed_paths[@]}"
missing=0
# Extract only the deps stage from the Dockerfile
deps_stage="$(awk '/^FROM .* AS deps$/{found=1; next} found && /^FROM /{exit} found{print}' Dockerfile)"
if [ -z "$deps_stage" ]; then
echo "::error::Could not extract deps stage from Dockerfile (expected 'FROM ... AS deps')"
exit 1
fi
# Derive workspace search roots from pnpm-workspace.yaml (exclude dev-only packages)
search_roots="$(grep '^ *- ' pnpm-workspace.yaml | sed 's/^ *- //' | sed 's/\*$//' | grep -v 'examples' | grep -v 'create-paperclip-plugin' | tr '\n' ' ')"
if [ -z "$search_roots" ]; then
echo "::error::Could not derive workspace roots from pnpm-workspace.yaml"
exit 1
fi
# Check all workspace package.json files are copied in the deps stage
for pkg in $(find $search_roots -maxdepth 2 -name package.json -not -path '*/examples/*' -not -path '*/create-paperclip-plugin/*' -not -path '*/node_modules/*' 2>/dev/null | sort -u); do
dir="$(dirname "$pkg")"
if ! echo "$deps_stage" | grep -q "^COPY ${dir}/package.json"; then
echo "::error::Dockerfile deps stage missing: COPY ${pkg} ${dir}/"
missing=1
fi
done
# Check patches directory is copied if it exists
if [ -d patches ] && ! echo "$deps_stage" | grep -q '^COPY patches/'; then
echo "::error::Dockerfile deps stage missing: COPY patches/ patches/"
missing=1
fi
if [ "$missing" -eq 1 ]; then
echo "Dockerfile deps stage is out of sync. Update it to include the missing files."
exit 1
fi
- name: Validate dependency resolution when manifests change
run: |
changed="$(git diff --name-only "${{ github.event.pull_request.base.sha }}...${{ github.event.pull_request.head.sha }}")"
changed="$(git diff --name-only "${{ github.event.pull_request.base.sha }}" "${{ github.event.pull_request.head.sha }}")"
manifest_pattern='(^|/)package\.json$|^pnpm-workspace\.yaml$|^\.npmrc$|^pnpmfile\.(cjs|js|mjs)$'
if printf '%s\n' "$changed" | grep -Eq "$manifest_pattern"; then
pnpm install --lockfile-only --ignore-scripts --no-frozen-lockfile
@@ -85,88 +111,16 @@ jobs:
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Typecheck workspaces whose build scripts skip TypeScript
run: pnpm run typecheck:build-gaps
- name: Typecheck
run: pnpm -r typecheck
- name: Run general test suites
run: pnpm test:run:general
- name: Verify release registry test coverage
run: pnpm run test:release-registry
- name: Run tests
run: pnpm test:run
- name: Build
run: pnpm build
verify_serialized_server:
name: Verify serialized server suites (${{ matrix.shard_label }})
needs: [policy]
runs-on: ubuntu-latest
timeout-minutes: 20
strategy:
fail-fast: false
matrix:
include:
- shard_index: 0
shard_count: 4
shard_label: 1/4
- shard_index: 1
shard_count: 4
shard_label: 2/4
- shard_index: 2
shard_count: 4
shard_label: 3/4
- shard_index: 3
shard_count: 4
shard_label: 4/4
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
version: 9.15.4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 24
cache: pnpm
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Run serialized server test shard
run: pnpm test:run:serialized -- --shard-index ${{ matrix.shard_index }} --shard-count ${{ matrix.shard_count }}
canary_dry_run:
name: Canary Dry Run
needs: [policy]
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
version: 9.15.4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 24
cache: pnpm
- name: Install dependencies
run: pnpm install --frozen-lockfile
# `release.sh` always executes its Step 2/7 workspace build, even when
# `--skip-verify` bypasses the initial verification gate.
- name: Release canary dry run via release.sh internal build
- name: Release canary dry run
run: |
git checkout -B master HEAD
git checkout -- pnpm-lock.yaml
@@ -195,6 +149,9 @@ jobs:
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Build
run: pnpm build
- name: Install Playwright
run: npx playwright install --with-deps chromium

View File

@@ -50,9 +50,6 @@ jobs:
node-version: 24
cache: pnpm
- name: Validate release package manifest
run: node ./scripts/release-package-map.mjs check
- name: Install dependencies
run: pnpm install --no-frozen-lockfile
@@ -92,9 +89,6 @@ jobs:
node-version: 24
cache: pnpm
- name: Validate release package manifest
run: node ./scripts/release-package-map.mjs check
- name: Install dependencies
run: pnpm install --no-frozen-lockfile
@@ -145,9 +139,6 @@ jobs:
node-version: 24
cache: pnpm
- name: Validate release package manifest
run: node ./scripts/release-package-map.mjs check
- name: Install dependencies
run: pnpm install --no-frozen-lockfile
@@ -186,9 +177,6 @@ jobs:
node-version: 24
cache: pnpm
- name: Validate release package manifest
run: node ./scripts/release-package-map.mjs check
- name: Install dependencies
run: pnpm install --no-frozen-lockfile

1
.gitignore vendored
View File

@@ -3,7 +3,6 @@ node_modules/
**/node_modules
**/node_modules/
dist/
ui/storybook-static/
.env
*.tsbuildinfo
drizzle/meta/

View File

@@ -123,9 +123,7 @@ pnpm test:release-smoke
Run the browser suites only when your change touches them or when you are explicitly verifying CI/release flows.
For normal issue work, run the smallest relevant verification first. Do not default to repo-wide typecheck/build/test on every heartbeat when a narrower check is enough to prove the change.
Run this full check before claiming repo work done in a PR-ready hand-off, or when the change scope is broad enough that targeted checks are not sufficient:
Run this full check before claiming done:
```sh
pnpm -r typecheck

View File

@@ -1,4 +1,3 @@
# syntax=docker/dockerfile:1.20
FROM node:lts-trixie-slim AS base
ARG USER_UID=1000
ARG USER_GID=1000
@@ -22,7 +21,6 @@ COPY packages/shared/package.json packages/shared/
COPY packages/db/package.json packages/db/
COPY packages/adapter-utils/package.json packages/adapter-utils/
COPY packages/mcp-server/package.json packages/mcp-server/
COPY packages/adapters/acpx-local/package.json packages/adapters/acpx-local/
COPY packages/adapters/claude-local/package.json packages/adapters/claude-local/
COPY packages/adapters/codex-local/package.json packages/adapters/codex-local/
COPY packages/adapters/cursor-local/package.json packages/adapters/cursor-local/
@@ -31,8 +29,6 @@ COPY packages/adapters/openclaw-gateway/package.json packages/adapters/openclaw-
COPY packages/adapters/opencode-local/package.json packages/adapters/opencode-local/
COPY packages/adapters/pi-local/package.json packages/adapters/pi-local/
COPY packages/plugins/sdk/package.json packages/plugins/sdk/
COPY --parents packages/plugins/sandbox-providers/./*/package.json packages/plugins/sandbox-providers/
COPY packages/plugins/paperclip-plugin-fake-sandbox/package.json packages/plugins/paperclip-plugin-fake-sandbox/
COPY patches/ patches/
RUN pnpm install --frozen-lockfile

113
README.md
View File

@@ -6,8 +6,7 @@
<a href="#quickstart"><strong>Quickstart</strong></a> &middot;
<a href="https://paperclip.ing/docs"><strong>Docs</strong></a> &middot;
<a href="https://github.com/paperclipai/paperclip"><strong>GitHub</strong></a> &middot;
<a href="https://discord.gg/m4HZY7xNG3"><strong>Discord</strong></a> &middot;
<a href="https://x.com/papercliping"><strong>Twitter</strong></a>
<a href="https://discord.gg/m4HZY7xNG3"><strong>Discord</strong></a>
</p>
<p align="center">
@@ -157,115 +156,6 @@ Paperclip handles the hard orchestration details correctly.
<br/>
## What's Under the Hood
Paperclip is a full control plane, not a wrapper. Before you build any of this yourself, know that it already exists:
```
┌──────────────────────────────────────────────────────────────┐
│ PAPERCLIP SERVER │
│ │
│ ┌───────────┐ ┌───────────┐ ┌───────────┐ ┌───────────┐ │
│ │Identity & │ │ Work & │ │ Heartbeat │ │Governance │ │
│ │ Access │ │ Tasks │ │ Execution │ │& Approvals│ │
│ └───────────┘ └───────────┘ └───────────┘ └───────────┘ │
│ │
│ ┌───────────┐ ┌───────────┐ ┌───────────┐ ┌───────────┐ │
│ │ Org Chart │ │Workspaces │ │ Plugins │ │ Budget │ │
│ │ & Agents │ │ & Runtime │ │ │ │ & Costs │ │
│ └───────────┘ └───────────┘ └───────────┘ └───────────┘ │
│ │
│ ┌───────────┐ ┌───────────┐ ┌───────────┐ ┌───────────┐ │
│ │ Routines │ │ Secrets & │ │ Activity │ │ Company │ │
│ │& Schedules│ │ Storage │ │ & Events │ │Portability│ │
│ └───────────┘ └───────────┘ └───────────┘ └───────────┘ │
└──────────────────────────────────────────────────────────────┘
▲ ▲ ▲ ▲
┌─────┴─────┐ ┌─────┴─────┐ ┌─────┴─────┐ ┌─────┴─────┐
│ Claude │ │ Codex │ │ CLI │ │ HTTP/web │
│ Code │ │ │ │ agents │ │ bots │
└───────────┘ └───────────┘ └───────────┘ └───────────┘
```
### The Systems
<table>
<tr>
<td width="50%">
**Identity & Access** — Two deployment modes (trusted local or authenticated), board users, agent API keys, short-lived run JWTs, company memberships, invite flows, and OpenClaw onboarding. Every mutating request is traced to an actor.
</td>
<td width="50%">
**Org Chart & Agents** — Agents have roles, titles, reporting lines, permissions, and budgets. Adapter examples match the diagram: Claude Code, Codex, CLI agents such as Cursor/Gemini/bash, HTTP/webhook bots such as OpenClaw, and external adapter plugins. If it can receive a heartbeat, it's hired.
</td>
</tr>
<tr>
<td>
**Work & Task System** — Issues carry company/project/goal/parent links, atomic checkout with execution locks, first-class blocker dependencies, comments, documents, attachments, work products, labels, and inbox state. No double-work, no lost context.
</td>
<td>
**Heartbeat Execution** — DB-backed wakeup queue with coalescing, budget checks, workspace resolution, secret injection, skill loading, and adapter invocation. Runs produce structured logs, cost events, session state, and audit trails. Recovery handles orphaned runs automatically.
</td>
</tr>
<tr>
<td>
**Workspaces & Runtime** — Project workspaces, isolated execution workspaces (git worktrees, operator branches), and runtime services (dev servers, preview URLs). Agents work in the right directory with the right context every time.
</td>
<td>
**Governance & Approvals** — Board approval workflows, execution policies with review/approval stages, decision tracking, budget hard-stops, agent pause/resume/terminate, and full audit logging. You're the board — nothing ships without your sign-off.
</td>
</tr>
<tr>
<td>
**Budget & Cost Control** — Token and cost tracking by company, agent, project, goal, issue, provider, and model. Scoped budget policies with warning thresholds and hard stops. Overspend pauses agents and cancels queued work automatically.
</td>
<td>
**Routines & Schedules** — Recurring tasks with cron, webhook, and API triggers. Concurrency and catch-up policies. Each routine execution creates a tracked issue and wakes the assigned agent — no manual kick-offs needed.
</td>
</tr>
<tr>
<td>
**Plugins** — Instance-wide plugin system with out-of-process workers, capability-gated host services, job scheduling, tool exposure, and UI contributions. Extend Paperclip without forking it.
</td>
<td>
**Secrets & Storage** — Instance and company secrets, encrypted local storage, provider-backed object storage, attachments, and work products. Sensitive values stay out of prompts unless a scoped run explicitly needs them.
</td>
</tr>
<tr>
<td>
**Activity & Events** — Mutating actions, heartbeat state changes, cost events, approvals, comments, and work products are recorded as durable activity so operators can audit what happened and why.
</td>
<td>
**Company Portability** — Export and import entire organizations — agents, skills, projects, routines, and issues — with secret scrubbing and collision handling. One deployment, many companies, complete data isolation.
</td>
</tr>
</table>
<br/>
## What Paperclip is not
| | |
@@ -410,7 +300,6 @@ We welcome contributions. See the [contributing guide](CONTRIBUTING.md) for deta
## Community
- [Discord](https://discord.gg/m4HZY7xNG3) — Join the community
- [Twitter / X](https://x.com/papercliping) — Follow updates and announcements
- [GitHub Issues](https://github.com/paperclipai/paperclip/issues) — bugs and feature requests
- [GitHub Discussions](https://github.com/paperclipai/paperclip/discussions) — ideas and RFC

View File

@@ -6,8 +6,7 @@
<a href="#quickstart"><strong>Quickstart</strong></a> &middot;
<a href="https://paperclip.ing/docs"><strong>Docs</strong></a> &middot;
<a href="https://github.com/paperclipai/paperclip"><strong>GitHub</strong></a> &middot;
<a href="https://discord.gg/m4HZY7xNG3"><strong>Discord</strong></a> &middot;
<a href="https://x.com/papercliping"><strong>Twitter</strong></a>
<a href="https://discord.gg/m4HZY7xNG3"><strong>Discord</strong></a>
</p>
<p align="center">
@@ -279,7 +278,6 @@ We welcome contributions. See the [contributing guide](https://github.com/paperc
## Community
- [Discord](https://discord.gg/m4HZY7xNG3) — Join the community
- [Twitter / X](https://x.com/papercliping) — Follow updates and announcements
- [GitHub Issues](https://github.com/paperclipai/paperclip/issues) — bugs and feature requests
- [GitHub Discussions](https://github.com/paperclipai/paperclip/discussions) — ideas and RFC

View File

@@ -37,7 +37,6 @@
},
"dependencies": {
"@clack/prompts": "^0.10.0",
"@paperclipai/adapter-acpx-local": "workspace:*",
"@paperclipai/adapter-claude-local": "workspace:*",
"@paperclipai/adapter-codex-local": "workspace:*",
"@paperclipai/adapter-cursor-local": "workspace:*",

View File

@@ -14,7 +14,6 @@ function makeCompany(overrides: Partial<Company>): Company {
issueCounter: 1,
budgetMonthlyCents: 0,
spentMonthlyCents: 0,
attachmentMaxBytes: 10 * 1024 * 1024,
requireBoardApprovalForNewAgents: false,
feedbackDataSharingEnabled: false,
feedbackDataSharingConsentAt: null,

View File

@@ -1,5 +1,5 @@
import { execFile, spawn } from "node:child_process";
import { existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs";
import { mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs";
import net from "node:net";
import os from "node:os";
import path from "node:path";
@@ -104,50 +104,20 @@ function writeTestConfig(configPath: string, tempRoot: string, port: number, con
writeFileSync(configPath, `${JSON.stringify(config, null, 2)}\n`, "utf8");
}
interface TestPaperclipEnv {
configPath: string;
paperclipHome: string;
instanceId: string;
shellHome?: string;
}
function createBasePaperclipEnv(options: TestPaperclipEnv) {
function createServerEnv(configPath: string, port: number, connectionString: string) {
const env = { ...process.env };
for (const key of Object.keys(env)) {
if (key.startsWith("PAPERCLIP_")) {
delete env[key];
}
}
env.PAPERCLIP_CONFIG = options.configPath;
env.PAPERCLIP_HOME = options.paperclipHome;
env.PAPERCLIP_INSTANCE_ID = options.instanceId;
env.PAPERCLIP_CONTEXT = path.join(options.paperclipHome, "context.json");
env.PAPERCLIP_AUTH_STORE = path.join(options.paperclipHome, "auth.json");
if (options.shellHome) {
env.HOME = options.shellHome;
}
return env;
}
function createServerEnv(
configPath: string,
port: number,
connectionString: string,
options: Omit<TestPaperclipEnv, "configPath">,
) {
const env = createBasePaperclipEnv({
configPath,
...options,
});
delete env.DATABASE_URL;
delete env.PORT;
delete env.HOST;
delete env.SERVE_UI;
delete env.HEARTBEAT_SCHEDULER_ENABLED;
env.PAPERCLIP_CONFIG = configPath;
env.DATABASE_URL = connectionString;
env.HOST = "127.0.0.1";
env.PORT = String(port);
@@ -160,8 +130,13 @@ function createServerEnv(
return env;
}
function createCliEnv(options: TestPaperclipEnv) {
const env = createBasePaperclipEnv(options);
function createCliEnv() {
const env = { ...process.env };
for (const key of Object.keys(env)) {
if (key.startsWith("PAPERCLIP_")) {
delete env[key];
}
}
delete env.DATABASE_URL;
delete env.PORT;
delete env.HOST;
@@ -208,25 +183,14 @@ async function api<T>(baseUrl: string, pathname: string, init?: RequestInit): Pr
return text ? JSON.parse(text) as T : (null as T);
}
async function runCliJson<T>(
args: string[],
opts: TestPaperclipEnv & { apiBase?: string; includeConfigArg?: boolean },
) {
async function runCliJson<T>(args: string[], opts: { apiBase: string; configPath: string }) {
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../../..");
const cliArgs = ["--silent", "paperclipai", ...args];
if (opts.apiBase) {
cliArgs.push("--api-base", opts.apiBase);
}
if (opts.includeConfigArg !== false) {
cliArgs.push("--config", opts.configPath);
}
cliArgs.push("--json");
const result = await execFileAsync(
"pnpm",
cliArgs,
["--silent", "paperclipai", ...args, "--api-base", opts.apiBase, "--config", opts.configPath, "--json"],
{
cwd: repoRoot,
env: createCliEnv(opts),
env: createCliEnv(),
maxBuffer: 10 * 1024 * 1024,
},
);
@@ -271,9 +235,6 @@ describeEmbeddedPostgres("paperclipai company import/export e2e", () => {
let configPath = "";
let exportDir = "";
let apiBase = "";
let paperclipHome = "";
let cliShellHome = "";
let paperclipInstanceId = "";
let serverProcess: ServerProcess | null = null;
let tempDb: Awaited<ReturnType<typeof startEmbeddedPostgresTestDatabase>> | null = null;
@@ -281,11 +242,6 @@ describeEmbeddedPostgres("paperclipai company import/export e2e", () => {
tempRoot = mkdtempSync(path.join(os.tmpdir(), "paperclip-company-cli-e2e-"));
configPath = path.join(tempRoot, "config", "config.json");
exportDir = path.join(tempRoot, "exported-company");
paperclipHome = path.join(tempRoot, "paperclip-home");
cliShellHome = path.join(tempRoot, "shell-home");
paperclipInstanceId = "company-cli-e2e";
mkdirSync(paperclipHome, { recursive: true });
mkdirSync(cliShellHome, { recursive: true });
tempDb = await startEmbeddedPostgresTestDatabase("paperclip-company-cli-db-");
@@ -300,11 +256,7 @@ describeEmbeddedPostgres("paperclipai company import/export e2e", () => {
["paperclipai", "run", "--config", configPath],
{
cwd: repoRoot,
env: createServerEnv(configPath, port, tempDb.connectionString, {
paperclipHome,
instanceId: paperclipInstanceId,
shellHome: cliShellHome,
}),
env: createServerEnv(configPath, port, tempDb.connectionString),
stdio: ["ignore", "pipe", "pipe"],
},
);
@@ -330,31 +282,6 @@ describeEmbeddedPostgres("paperclipai company import/export e2e", () => {
it("exports a company package and imports it into new and existing companies", async () => {
expect(serverProcess).not.toBeNull();
const cliContext = await runCliJson<{
contextPath: string;
profileName: string;
profile: { apiBase?: string };
}>(
["context", "set", "--profile", "isolation-check", "--api-base", "https://example.test"],
{
configPath,
paperclipHome,
instanceId: paperclipInstanceId,
shellHome: cliShellHome,
includeConfigArg: false,
},
);
const expectedContextPath = path.join(paperclipHome, "context.json");
const leakedContextPath = path.join(cliShellHome, ".paperclip", "context.json");
expect(cliContext.contextPath).toBe(expectedContextPath);
expect(cliContext.profileName).toBe("isolation-check");
expect(cliContext.profile.apiBase).toBe("https://example.test");
expect(existsSync(expectedContextPath)).toBe(true);
expect(existsSync(leakedContextPath)).toBe(false);
rmSync(expectedContextPath, { force: true });
expect(existsSync(expectedContextPath)).toBe(false);
const sourceCompany = await api<{ id: string; name: string; issuePrefix: string }>(apiBase, "/api/companies", {
method: "POST",
headers: { "content-type": "application/json" },
@@ -376,11 +303,8 @@ describeEmbeddedPostgres("paperclipai company import/export e2e", () => {
name: "Export Engineer",
role: "engineer",
adapterType: "claude_local",
adapterConfig: {},
instructionsBundle: {
files: {
"AGENTS.md": "You verify company portability.",
},
adapterConfig: {
promptTemplate: "You verify company portability.",
},
}),
},
@@ -431,13 +355,7 @@ describeEmbeddedPostgres("paperclipai company import/export e2e", () => {
"--include",
"company,agents,projects,issues",
],
{
apiBase,
configPath,
paperclipHome,
instanceId: paperclipInstanceId,
shellHome: cliShellHome,
},
{ apiBase, configPath },
);
expect(exportResult.ok).toBe(true);
@@ -461,13 +379,7 @@ describeEmbeddedPostgres("paperclipai company import/export e2e", () => {
"company,agents,projects,issues",
"--yes",
],
{
apiBase,
configPath,
paperclipHome,
instanceId: paperclipInstanceId,
shellHome: cliShellHome,
},
{ apiBase, configPath },
);
expect(importedNew.company.action).toBe("created");
@@ -486,11 +398,10 @@ describeEmbeddedPostgres("paperclipai company import/export e2e", () => {
apiBase,
`/api/companies/${importedNew.company.id}/issues`,
);
const importedMatchingIssues = importedIssues.filter((issue) => issue.title === sourceIssue.title);
expect(importedAgents.map((agent) => agent.name)).toContain(sourceAgent.name);
expect(importedProjects.map((project) => project.name)).toContain(sourceProject.name);
expect(importedMatchingIssues).toHaveLength(1);
expect(importedIssues.map((issue) => issue.title)).toContain(sourceIssue.title);
const previewExisting = await runCliJson<{
errors: string[];
@@ -515,13 +426,7 @@ describeEmbeddedPostgres("paperclipai company import/export e2e", () => {
"rename",
"--dry-run",
],
{
apiBase,
configPath,
paperclipHome,
instanceId: paperclipInstanceId,
shellHome: cliShellHome,
},
{ apiBase, configPath },
);
expect(previewExisting.errors).toEqual([]);
@@ -548,13 +453,7 @@ describeEmbeddedPostgres("paperclipai company import/export e2e", () => {
"rename",
"--yes",
],
{
apiBase,
configPath,
paperclipHome,
instanceId: paperclipInstanceId,
shellHome: cliShellHome,
},
{ apiBase, configPath },
);
expect(importedExisting.company.action).toBe("unchanged");
@@ -572,13 +471,11 @@ describeEmbeddedPostgres("paperclipai company import/export e2e", () => {
apiBase,
`/api/companies/${importedNew.company.id}/issues`,
);
const twiceImportedMatchingIssues = twiceImportedIssues.filter((issue) => issue.title === sourceIssue.title);
expect(twiceImportedAgents).toHaveLength(2);
expect(new Set(twiceImportedAgents.map((agent) => agent.name)).size).toBe(2);
expect(twiceImportedProjects).toHaveLength(2);
expect(twiceImportedMatchingIssues).toHaveLength(2);
expect(new Set(twiceImportedMatchingIssues.map((issue) => issue.identifier)).size).toBe(2);
expect(twiceImportedIssues).toHaveLength(2);
const zipPath = path.join(tempRoot, "exported-company.zip");
const portableFiles: Record<string, string> = {};
@@ -601,16 +498,10 @@ describeEmbeddedPostgres("paperclipai company import/export e2e", () => {
"company,agents,projects,issues",
"--yes",
],
{
apiBase,
configPath,
paperclipHome,
instanceId: paperclipInstanceId,
shellHome: cliShellHome,
},
{ apiBase, configPath },
);
expect(importedFromZip.company.action).toBe("created");
expect(importedFromZip.agents.some((agent) => agent.action === "created")).toBe(true);
}, 90_000);
}, 60_000);
});

View File

@@ -160,7 +160,6 @@ describe("renderCompanyImportPreview", () => {
path: "COMPANY.md",
name: "Source Co",
description: null,
attachmentMaxBytes: null,
brandColor: null,
logoPath: null,
requireBoardApprovalForNewAgents: false,
@@ -376,7 +375,6 @@ describe("import selection catalog", () => {
path: "COMPANY.md",
name: "Source Co",
description: null,
attachmentMaxBytes: null,
brandColor: null,
logoPath: "images/company-logo.png",
requireBoardApprovalForNewAgents: false,

View File

@@ -1,24 +0,0 @@
import path from "node:path";
import { describe, expect, it } from "vitest";
import { collectEnvLabDoctorStatus, resolveEnvLabSshStatePath } from "../commands/env-lab.js";
describe("env-lab command", () => {
it("resolves the default SSH fixture state path under the instance root", () => {
const statePath = resolveEnvLabSshStatePath("fixture-test");
expect(statePath).toContain(
path.join("instances", "fixture-test", "env-lab", "ssh-fixture", "state.json"),
);
});
it("reports doctor status for an instance without a running fixture", async () => {
const status = await collectEnvLabDoctorStatus({ instance: "fixture-test-missing" });
expect(status.statePath).toContain(
path.join("instances", "fixture-test-missing", "env-lab", "ssh-fixture", "state.json"),
);
expect(typeof status.ssh.supported).toBe("boolean");
expect(status.ssh.running).toBe(false);
expect(status.ssh.environment).toBeNull();
});
});

View File

@@ -190,9 +190,8 @@ describe("worktree helpers", () => {
).toEqual(["worktree", "add", "-b", "my-worktree", "/tmp/my-worktree", "origin/main"]);
});
it("rewrites auth URLs only when they already include a port", () => {
it("rewrites loopback auth URLs to the new port only", () => {
expect(rewriteLocalUrlPort("http://127.0.0.1:3100", 3110)).toBe("http://127.0.0.1:3110/");
expect(rewriteLocalUrlPort("http://my-host.ts.net:3100", 3110)).toBe("http://my-host.ts.net:3110/");
expect(rewriteLocalUrlPort("https://paperclip.example", 3110)).toBe("https://paperclip.example");
});
@@ -600,7 +599,7 @@ describe("worktree helpers", () => {
fs.rmSync(tempRoot, { recursive: true, force: true });
}
},
30000,
20000,
);
it("avoids ports already claimed by sibling worktree instance configs", async () => {
@@ -882,7 +881,7 @@ describe("worktree helpers", () => {
}
fs.rmSync(tempRoot, { recursive: true, force: true });
}
}, 30_000);
}, 20_000);
it("restores the current worktree config and instance data if reseed fails", async () => {
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "paperclip-worktree-reseed-rollback-"));
@@ -1039,7 +1038,7 @@ describe("worktree helpers", () => {
execFileSync("git", ["worktree", "remove", "--force", worktreePath], { cwd: repoRoot, stdio: "ignore" });
fs.rmSync(tempRoot, { recursive: true, force: true });
}
}, 15_000);
});
it("creates and initializes a worktree from the top-level worktree:make command", async () => {
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "paperclip-worktree-make-"));

View File

@@ -1,5 +1,4 @@
import type { CLIAdapterModule } from "@paperclipai/adapter-utils";
import { printAcpxStreamEvent } from "@paperclipai/adapter-acpx-local/cli";
import { printClaudeStreamEvent } from "@paperclipai/adapter-claude-local/cli";
import { printCodexStreamEvent } from "@paperclipai/adapter-codex-local/cli";
import { printCursorStreamEvent } from "@paperclipai/adapter-cursor-local/cli";
@@ -15,11 +14,6 @@ const claudeLocalCLIAdapter: CLIAdapterModule = {
formatStdoutEvent: printClaudeStreamEvent,
};
const acpxLocalCLIAdapter: CLIAdapterModule = {
type: "acpx_local",
formatStdoutEvent: printAcpxStreamEvent,
};
const codexLocalCLIAdapter: CLIAdapterModule = {
type: "codex_local",
formatStdoutEvent: printCodexStreamEvent,
@@ -52,7 +46,6 @@ const openclawGatewayCLIAdapter: CLIAdapterModule = {
const adaptersByType = new Map<string, CLIAdapterModule>(
[
acpxLocalCLIAdapter,
claudeLocalCLIAdapter,
codexLocalCLIAdapter,
openCodeLocalCLIAdapter,

View File

@@ -61,7 +61,6 @@ interface IssueUpdateOptions extends BaseClientOptions {
interface IssueCommentOptions extends BaseClientOptions {
body: string;
reopen?: boolean;
resume?: boolean;
}
interface IssueCheckoutOptions extends BaseClientOptions {
@@ -242,14 +241,12 @@ export function registerIssueCommands(program: Command): void {
.argument("<issueId>", "Issue ID")
.requiredOption("--body <text>", "Comment body")
.option("--reopen", "Reopen if issue is done/cancelled")
.option("--resume", "Request explicit follow-up and wake the assignee when resumable")
.action(async (issueId: string, opts: IssueCommentOptions) => {
try {
const ctx = resolveCommandContext(opts);
const payload = addIssueCommentSchema.parse({
body: opts.body,
reopen: opts.reopen,
resume: opts.resume,
});
const comment = await ctx.api.post<IssueComment>(`/api/issues/${issueId}/comments`, payload);
printOutput(comment, { json: ctx.json });

View File

@@ -1,174 +0,0 @@
import path from "node:path";
import type { Command } from "commander";
import * as p from "@clack/prompts";
import pc from "picocolors";
import {
buildSshEnvLabFixtureConfig,
getSshEnvLabSupport,
readSshEnvLabFixtureStatus,
startSshEnvLabFixture,
stopSshEnvLabFixture,
} from "@paperclipai/adapter-utils/ssh";
import { resolvePaperclipInstanceId, resolvePaperclipInstanceRoot } from "../config/home.js";
export function resolveEnvLabSshStatePath(instanceId?: string): string {
const resolvedInstanceId = resolvePaperclipInstanceId(instanceId);
return path.resolve(
resolvePaperclipInstanceRoot(resolvedInstanceId),
"env-lab",
"ssh-fixture",
"state.json",
);
}
function printJson(value: unknown) {
process.stdout.write(`${JSON.stringify(value, null, 2)}\n`);
}
function summarizeFixture(state: {
host: string;
port: number;
username: string;
workspaceDir: string;
sshdLogPath: string;
}) {
p.log.message(`Host: ${pc.cyan(state.host)}:${pc.cyan(String(state.port))}`);
p.log.message(`User: ${pc.cyan(state.username)}`);
p.log.message(`Workspace: ${pc.cyan(state.workspaceDir)}`);
p.log.message(`Log: ${pc.dim(state.sshdLogPath)}`);
}
export async function collectEnvLabDoctorStatus(opts: { instance?: string }) {
const statePath = resolveEnvLabSshStatePath(opts.instance);
const [sshSupport, sshStatus] = await Promise.all([
getSshEnvLabSupport(),
readSshEnvLabFixtureStatus(statePath),
]);
const environment = sshStatus.state ? await buildSshEnvLabFixtureConfig(sshStatus.state) : null;
return {
statePath,
ssh: {
supported: sshSupport.supported,
reason: sshSupport.reason,
running: sshStatus.running,
state: sshStatus.state,
environment,
},
};
}
export async function envLabUpCommand(opts: { instance?: string; json?: boolean }) {
const statePath = resolveEnvLabSshStatePath(opts.instance);
const state = await startSshEnvLabFixture({ statePath });
const environment = await buildSshEnvLabFixtureConfig(state);
if (opts.json) {
printJson({ state, environment });
return;
}
p.log.success("SSH env-lab fixture is running.");
summarizeFixture(state);
p.log.message(`State: ${pc.dim(statePath)}`);
}
export async function envLabStatusCommand(opts: { instance?: string; json?: boolean }) {
const statePath = resolveEnvLabSshStatePath(opts.instance);
const status = await readSshEnvLabFixtureStatus(statePath);
const environment = status.state ? await buildSshEnvLabFixtureConfig(status.state) : null;
if (opts.json) {
printJson({ ...status, environment, statePath });
return;
}
if (!status.state || !status.running) {
p.log.info(`SSH env-lab fixture is not running (${pc.dim(statePath)}).`);
return;
}
p.log.success("SSH env-lab fixture is running.");
summarizeFixture(status.state);
p.log.message(`State: ${pc.dim(statePath)}`);
}
export async function envLabDownCommand(opts: { instance?: string; json?: boolean }) {
const statePath = resolveEnvLabSshStatePath(opts.instance);
const stopped = await stopSshEnvLabFixture(statePath);
if (opts.json) {
printJson({ stopped, statePath });
return;
}
if (!stopped) {
p.log.info(`No SSH env-lab fixture was running (${pc.dim(statePath)}).`);
return;
}
p.log.success("SSH env-lab fixture stopped.");
p.log.message(`State: ${pc.dim(statePath)}`);
}
export async function envLabDoctorCommand(opts: { instance?: string; json?: boolean }) {
const status = await collectEnvLabDoctorStatus(opts);
if (opts.json) {
printJson(status);
return;
}
if (status.ssh.supported) {
p.log.success("SSH fixture prerequisites are installed.");
} else {
p.log.warn(`SSH fixture prerequisites are incomplete: ${status.ssh.reason ?? "unknown reason"}`);
}
if (status.ssh.state && status.ssh.running) {
p.log.success("SSH env-lab fixture is running.");
summarizeFixture(status.ssh.state);
p.log.message(`Private key: ${pc.dim(status.ssh.state.clientPrivateKeyPath)}`);
p.log.message(`Known hosts: ${pc.dim(status.ssh.state.knownHostsPath)}`);
} else if (status.ssh.state) {
p.log.warn("SSH env-lab fixture state exists, but the process is not running.");
p.log.message(`State: ${pc.dim(status.statePath)}`);
} else {
p.log.info("SSH env-lab fixture is not running.");
p.log.message(`State: ${pc.dim(status.statePath)}`);
}
p.log.message(`Cleanup: ${pc.dim("pnpm paperclipai env-lab down")}`);
}
export function registerEnvLabCommands(program: Command) {
const envLab = program.command("env-lab").description("Deterministic local environment fixtures");
envLab
.command("up")
.description("Start the default SSH env-lab fixture")
.option("-i, --instance <id>", "Paperclip instance id (default: current/default)")
.option("--json", "Print machine-readable fixture details")
.action(envLabUpCommand);
envLab
.command("status")
.description("Show the current SSH env-lab fixture state")
.option("-i, --instance <id>", "Paperclip instance id (default: current/default)")
.option("--json", "Print machine-readable fixture details")
.action(envLabStatusCommand);
envLab
.command("down")
.description("Stop the default SSH env-lab fixture")
.option("-i, --instance <id>", "Paperclip instance id (default: current/default)")
.option("--json", "Print machine-readable stop details")
.action(envLabDownCommand);
envLab
.command("doctor")
.description("Check SSH fixture prerequisites and current status")
.option("-i, --instance <id>", "Paperclip instance id (default: current/default)")
.option("--json", "Print machine-readable diagnostic details")
.action(envLabDoctorCommand);
}

View File

@@ -75,6 +75,11 @@ function nonEmpty(value: string | null | undefined): string | null {
return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
}
function isLoopbackHost(hostname: string): boolean {
const value = hostname.trim().toLowerCase();
return value === "127.0.0.1" || value === "localhost" || value === "::1";
}
export function sanitizeWorktreeInstanceId(rawValue: string): string {
const trimmed = rawValue.trim().toLowerCase();
const normalized = trimmed
@@ -163,8 +168,7 @@ export function rewriteLocalUrlPort(rawUrl: string | undefined, port: number): s
if (!rawUrl) return undefined;
try {
const parsed = new URL(rawUrl);
// The URL API normalizes default ports like :80/:443 to "", so treat them as stable URLs.
if (!parsed.port) return rawUrl;
if (!isLoopbackHost(parsed.hostname)) return rawUrl;
parsed.port = String(port);
return parsed.toString();
} catch {

View File

@@ -1311,7 +1311,6 @@ async function seedWorktreeDatabase(input: {
backupDir: path.resolve(input.targetPaths.backupDir, "seed"),
retention: { dailyDays: 7, weeklyWeeks: 4, monthlyMonths: 1 },
filenamePrefix: `${input.instanceId}-seed`,
backupEngine: "javascript",
includeMigrationJournal: true,
excludeTables: seedPlan.excludedTables,
nullifyColumns: seedPlan.nullifyColumns,

View File

@@ -8,7 +8,6 @@ import { heartbeatRun } from "./commands/heartbeat-run.js";
import { runCommand } from "./commands/run.js";
import { bootstrapCeoInvite } from "./commands/auth-bootstrap-ceo.js";
import { dbBackupCommand } from "./commands/db-backup.js";
import { registerEnvLabCommands } from "./commands/env-lab.js";
import { registerContextCommands } from "./commands/client/context.js";
import { registerCompanyCommands } from "./commands/client/company.js";
import { registerIssueCommands } from "./commands/client/issue.js";
@@ -148,7 +147,6 @@ registerDashboardCommands(program);
registerRoutineCommands(program);
registerFeedbackCommands(program);
registerWorktreeCommands(program);
registerEnvLabCommands(program);
registerPluginCommands(program);
const auth = program.command("auth").description("Authentication and bootstrap utilities");

View File

@@ -2,7 +2,7 @@
Paperclip CLI now supports both:
- instance setup/diagnostics (`onboard`, `doctor`, `configure`, `env`, `allowed-hostname`, `env-lab`)
- instance setup/diagnostics (`onboard`, `doctor`, `configure`, `env`, `allowed-hostname`)
- control-plane client operations (issues, approvals, agents, activity, dashboard)
## Base Usage
@@ -45,15 +45,6 @@ Allow an authenticated/private hostname (for example custom Tailscale DNS):
pnpm paperclipai allowed-hostname dotta-macbook-pro
```
Bring up the default local SSH fixture for environment testing:
```sh
pnpm paperclipai env-lab up
pnpm paperclipai env-lab doctor
pnpm paperclipai env-lab status --json
pnpm paperclipai env-lab down
```
All client commands support:
- `--data-dir <path>`

View File

@@ -59,11 +59,11 @@ cp .env.example .env
# DATABASE_URL=postgres://paperclip:paperclip@localhost:5432/paperclip
```
Run migrations:
Run migrations (once the migration generation issue is fixed) or use `drizzle-kit push`:
```sh
DATABASE_URL=postgres://paperclip:paperclip@localhost:5432/paperclip \
pnpm db:migrate
npx drizzle-kit push
```
Start the server:
@@ -100,27 +100,37 @@ postgres://postgres.[PROJECT-REF]:[PASSWORD]@aws-0-[REGION].pooler.supabase.com:
### Configure
For the application runtime, use a direct PostgreSQL connection unless the database client has explicit prepared-statement configuration for your pooling mode:
Set `DATABASE_URL` in your `.env`:
```sh
DATABASE_URL=postgres://postgres.[PROJECT-REF]:[PASSWORD]@aws-0-[REGION].pooler.supabase.com:5432/postgres
DATABASE_URL=postgres://postgres.[PROJECT-REF]:[PASSWORD]@aws-0-[REGION].pooler.supabase.com:6543/postgres
```
If you later run the app with a pooled runtime URL, set `DATABASE_MIGRATION_URL` to the direct connection URL. Paperclip uses it for startup schema checks/migrations and plugin namespace migrations, while the app continues to use `DATABASE_URL` for runtime queries:
For hosted deployments that use a pooled runtime URL, set
`DATABASE_MIGRATION_URL` to the direct connection URL. Paperclip uses it for
startup schema checks/migrations and plugin namespace migrations, while the app
continues to use `DATABASE_URL` for runtime queries:
```sh
DATABASE_URL=postgres://postgres.[PROJECT-REF]:[PASSWORD]@aws-0-[REGION].pooler.supabase.com:6543/postgres
DATABASE_MIGRATION_URL=postgres://postgres.[PROJECT-REF]:[PASSWORD]@aws-0-[REGION].pooler.supabase.com:5432/postgres
```
If your hosted database requires transaction-pooling-only connections, use a direct or session-pooled connection for Paperclip until runtime pooling support is documented in this guide. Do not edit database client source files as part of deployment setup.
If using connection pooling (port 6543), the `postgres` client must disable prepared statements. Update `packages/db/src/client.ts`:
```ts
export function createDb(url: string) {
const sql = postgres(url, { prepare: false });
return drizzlePg(sql, { schema });
}
```
### Push the schema
```sh
# Use the direct connection (port 5432) for schema changes
DATABASE_URL=postgres://postgres.[PROJECT-REF]:[PASSWORD]@...5432/postgres \
pnpm db:migrate
npx drizzle-kit push
```
### Free tier limits
@@ -143,22 +153,6 @@ The database mode is controlled by `DATABASE_URL`:
Your Drizzle schema (`packages/db/src/schema/`) stays the same regardless of mode.
## Plugin database namespaces
The plugin runtime tracks plugin-owned database namespaces and migrations in `plugin_database_namespaces` and `plugin_migrations`. Hosted deployments that separate runtime and migration connections should set `DATABASE_MIGRATION_URL`; plugin namespace migration work uses the migration connection when present.
## Backups
Paperclip supports automatic and manual logical database backups. These dumps include
non-system database schemas such as `public`, the Drizzle migration journal, and
plugin-owned database schemas. See `doc/DEVELOPING.md` for the current
`paperclipai db:backup` / `pnpm db:backup` commands and backup retention
configuration.
Database backups do not include non-database instance files such as local-disk
uploads, workspace files, or the local encrypted secrets master key. Back those paths
up separately when you need full instance disaster recovery.
## Secret storage
Paperclip stores secret metadata and versions in:

View File

@@ -43,8 +43,6 @@ This starts:
`pnpm dev` and `pnpm dev:once` are now idempotent for the current repo and instance: if the matching Paperclip dev runner is already alive, Paperclip reports the existing process instead of starting a duplicate.
Issue execution may also use project execution workspace policies and workspace runtime services for per-project worktrees, preview servers, and managed dev commands. Configure those through the project workspace/runtime surfaces rather than starting long-running unmanaged processes when a task needs a reusable service.
## Storybook
The board UI Storybook keeps stories and Storybook config under `ui/storybook/` so component review files stay out of the app source routes.
@@ -115,8 +113,6 @@ pnpm test:release-smoke
These browser suites are intended for targeted local verification and CI, not the default agent/human test command.
For normal issue work, start with the smallest targeted check that proves the change. Reserve repo-wide typecheck/build/test runs for PR-ready handoff or changes broad enough that narrow checks do not cover the risk.
## One-Command Local Run
For a first-time local install, you can bootstrap and run in one command:
@@ -198,8 +194,6 @@ For `codex_local`, Paperclip also manages a per-company Codex home under the ins
If the `codex` CLI is not installed or not on `PATH`, `codex_local` agent runs fail at execution time with a clear adapter error. Quota polling uses a short-lived `codex app-server` subprocess: when `codex` cannot be spawned, that provider reports `ok: false` in aggregated quota results and the API server keeps running (it must not exit on a missing binary).
Local adapters require their corresponding CLI/session setup on the machine running Paperclip. External adapters are installed through the adapter/plugin flow and should not require hardcoded imports in `server/` or `ui/`.
## Worktree-local Instances
When developing from multiple git worktrees, do not point two Paperclip servers at the same embedded PostgreSQL data directory.
@@ -421,9 +415,7 @@ If you set `DATABASE_URL`, the server will use that instead of embedded PostgreS
## Automatic DB Backups
Paperclip can run automatic logical database backups on a timer. These backups cover
non-system database schemas, including migration history and plugin-owned database
schemas. Defaults:
Paperclip can run automatic DB backups on a timer. Defaults:
- enabled
- every 60 minutes
@@ -451,10 +443,6 @@ Environment overrides:
- `PAPERCLIP_DB_BACKUP_RETENTION_DAYS=<days>`
- `PAPERCLIP_DB_BACKUP_DIR=/absolute/or/~/path`
DB backups are not full instance filesystem backups. For full local disaster
recovery, also back up local storage files and the local encrypted secrets key if
those providers are enabled.
## Secrets in Dev
Agent env vars now support secret references. By default, secret values are stored with local encryption and only secret refs are persisted in agent config.

View File

@@ -23,7 +23,7 @@ Paperclip is the command, communication, and control plane for a company of AI a
- **Track work in real time** — see at any moment what every agent is working on
- **Control costs** — token salary budgets per agent, spend tracking, burn rate
- **Align to goals** — agents see how their work serves the bigger mission
- **Preserve work context** — comments, documents, work products, attachments, and company state stay attached to the work
- **Store company knowledge** — a shared brain for the organization
## Architecture
@@ -36,20 +36,17 @@ The central nervous system. Manages:
- Agent registry and org chart
- Task assignment and status
- Budget and token spend tracking
- Issue comments, documents, work products, attachments, and company state
- Company knowledge base
- Goal hierarchy (company → team → agent → task)
- Heartbeat monitoring — know when agents are alive, idle, or stuck
It also enforces execution-control semantics such as single-assignee issues, atomic checkout and execution locks, blockers, recovery issues, and workspace/runtime controls.
### 2. Execution Services (adapters)
Agents run externally and report into the control plane. Adapters connect different execution environments and define how a heartbeat is invoked, observed, and cancelled:
Agents run externally and report into the control plane. An agent is just Python code that gets kicked off and does work. Adapters connect different execution environments:
- **Local CLI/session adapters** — built-in adapters for tools such as Claude Code, Codex, Gemini, OpenCode, Pi, and Cursor
- **HTTP/process-style adapters** — command or webhook/API integrations for custom runtimes
- **OpenClaw gateway** — integration for OpenClaw-style remote agents
- **External adapter plugins** — dynamically loaded adapters installed outside the core app
- **OpenClaw** — initial adapter target
- **Heartbeat loop** — simple custom Python that loops, checks in, does work
- **Others** — any runtime that can call an API
The control plane doesn't run agents. It orchestrates them. Agents run wherever they run and phone home.

View File

@@ -32,14 +32,12 @@ Then you define who reports to the CEO: a CTO managing programmers, a CMO managi
### Agent Execution
Paperclip supports several ways to run an agent's heartbeat:
There are two fundamental modes for running an agent's heartbeat:
1. **Local CLI/session adapters** — Paperclip starts or resumes local coding-tool sessions such as Claude Code, Codex, Gemini, OpenCode, Pi, and Cursor, then tracks the run.
2. **Run a command** — Paperclip kicks off a process (shell command, Python script, etc.) and tracks it. The heartbeat is "execute this and monitor it."
3. **Fire and forget a request** — Paperclip sends a webhook/API call to an externally running agent. The heartbeat is "notify this agent to wake up." OpenClaw-style hooks work this way.
4. **External adapter plugins** — Paperclip loads adapter packages through the plugin/adapter flow so self-hosted installs can add runtimes without hardcoding them in core.
1. **Run a command** — Paperclip kicks off a process (shell command, Python script, etc.) and tracks it. The heartbeat is "execute this and monitor it."
2. **Fire and forget a request** — Paperclip sends a webhook/API call to an externally running agent. The heartbeat is "notify this agent to wake up." (OpenClaw hooks work this way.)
Agent runs can use project and execution workspaces, managed runtime services such as preview/dev servers, adapter-specific session state, and HTTP/webhook-style execution. We provide sensible defaults, but the adapter is still the boundary: if a runtime can be invoked, observed, and authorized, Paperclip can coordinate it.
We provide sensible defaults — a default agent that shells out to Claude Code or Codex with your configuration, remembers session IDs, runs basic scripts. But you can plug in anything.
### Task Management
@@ -56,7 +54,7 @@ I am researching the Facebook ads Granola uses (current task)
Tasks have parentage. Every task exists in service of a parent task, all the way up to the company goal. This is what keeps autonomous agents aligned — they can always answer "why am I doing this?"
The current issue model includes stable issue identifiers, parent/sub-issues, blockers, a single assignee, comments, issue documents, attachments and work products, and review/approval handoffs. That structure keeps work inspectable by both the board and agents while still allowing agents to decompose work into smaller tasks.
More detailed task structure TBD.
## Principles
@@ -117,7 +115,7 @@ Paperclips core identity is a **control plane for autonomous AI companies**,
- Do not make the core product a general chat app. The current product definition is explicitly task/comment-centric and “not a chatbot,” and that boundary is valuable.
- Do not build a complete Jira/GitHub replacement. The repo/docs already position Paperclip as organization orchestration, not focused on pull-request review.
- Do not build enterprise-grade RBAC first. Paperclip now has authenticated mode, company memberships, instance roles, and permission grants, but fine-grained enterprise governance should remain secondary to the core company control plane.
- Do not build enterprise-grade RBAC first. The current V1 spec still treats multi-board governance and fine-grained human permissions as out of scope, so the first multi-user version should be coarse and company-scoped.
- Do not lead with raw bash logs and transcripts. Default view should be human-readable intent/progress, with raw detail beneath.
- Do not force users to understand provider/API-key plumbing unless absolutely necessary. There are active onboarding/auth issues already; friction here is clearly real.
@@ -138,14 +136,11 @@ Paperclips core identity is a **control plane for autonomous AI companies**,
5. **Output-first**
Work is not done until the user can see the result: file, document, preview link, screenshot, plan, or PR.
6. **Execution visibility without log worship**
Active runs, recovery issues, productivity review states, blockers, and work products should be first-class surfaces. Raw transcripts are available when needed, but they are not the primary product surface.
7. **Local-first, cloud-ready**
6. **Local-first, cloud-ready**
The mental model should not change between local solo use and shared/private or public/cloud deployment.
8. **Safe autonomy**
7. **Safe autonomy**
Auto mode is allowed; hidden token burn is not.
9. **Thin core, rich edges**
8. **Thin core, rich edges**
Put optional chat, knowledge, and special surfaces into plugins/extensions rather than bloating the control plane.

View File

@@ -143,13 +143,6 @@ This keeps the default install path unchanged while allowing explicit installs w
npx paperclipai@canary onboard
```
The release script now verifies two things after a canary publish:
- the `canary` dist-tag resolves to the version that was just published
- every published internal `@paperclipai/*` dependency referenced by that manifest exists on npm
It also treats `latest -> canary` as a failure by default, because npm metadata can otherwise leave the default install path pointing at an unreleased canary dependency graph. Only pass `./scripts/release.sh canary --allow-canary-latest` when that `latest` behavior is explicitly intended.
### Stable
Stable publishes use the npm dist-tag `latest`.
@@ -176,58 +169,6 @@ That means:
See [doc/RELEASE-AUTOMATION-SETUP.md](RELEASE-AUTOMATION-SETUP.md) for the GitHub/npm setup steps.
## Release enrollment for new public packages
Paperclip does not auto-publish every non-private workspace package anymore.
CI publishing is controlled by [`scripts/release-package-manifest.json`](../scripts/release-package-manifest.json).
When you add a new public package:
1. add it to the manifest and decide whether CI should publish it immediately
2. if CI should publish it, bootstrap the package on npm before merge
3. if CI should not publish it yet, keep `"publishFromCi": false`
4. only enable `"publishFromCi": true` after npm trusted publishing is configured for that package
PR CI now checks changed release-enabled package manifests against npm. That catches a missing first-publish bootstrap before the change reaches `master`.
### One-time bootstrap sequence for a new package
The first publish of a brand-new package still needs one human maintainer with npm write access.
After that, trusted publishing can take over.
Example for `@paperclipai/adapter-acpx-local` from the repo root:
```bash
# safe preview
pnpm run release:bootstrap-package -- @paperclipai/adapter-acpx-local
# one-time first publish from an authenticated maintainer machine
pnpm run release:bootstrap-package -- @paperclipai/adapter-acpx-local --publish --otp 123456
```
The helper script:
- checks that the package does not already exist on npm
- builds the target package unless `--skip-build` is passed
- runs `npm pack --dry-run` in the package directory
- only runs the real `npm publish --access public` when `--publish --otp <code>` is provided
For the real `--publish` step, the maintainer machine must already be authenticated to npm.
If `npm whoami` returns `401`, first run `npm logout --registry=https://registry.npmjs.org/` to clear any stale local auth, then run `npm login` or `npm adduser` locally as an npm org member, and finally rerun the helper.
That local human auth is fine for the one-time bootstrap publish; we just do not want the same auth model inside CI.
The helper now requires `--otp <code>` up front for `--publish`, so it fails before the real publish attempt if the one-time password is missing.
After that first publish succeeds:
1. open `https://www.npmjs.com/package/@paperclipai/adapter-acpx-local`
2. go to `Settings``Trusted publishing`
3. add repository `paperclipai/paperclip`
4. set workflow filename to `release.yml`
5. optionally go to `Settings``Publishing access` and enable `Require two-factor authentication and disallow tokens`
6. keep `publishFromCi: true` in [`scripts/release-package-manifest.json`](../scripts/release-package-manifest.json)
Once those steps are done, future canary and stable publishes for that package are automated through GitHub OIDC. The manual step is only the first package creation on npm.
## Rollback model
Rollback does not unpublish anything.

View File

@@ -67,27 +67,6 @@ Why:
- the single `release.yml` workflow handles both canary and stable publishing
- GitHub environments `npm-canary` and `npm-stable` still enforce different approval rules on the GitHub side
### 2.2.1. Newly added public packages need a bootstrap phase
Trusted publishing is configured on the npm package itself, not at the repo scope.
That means a brand-new public package must not be auto-enrolled into CI publishing until its npm package exists and its trusted publisher has been configured.
Repo policy:
1. add every non-private package to [`scripts/release-package-manifest.json`](../scripts/release-package-manifest.json)
2. set `"publishFromCi": true` only when CI is expected to publish that package
3. if the package is not ready for CI publishing yet, keep `"publishFromCi": false`
4. complete the package bootstrap before merging any PR that changes a release-enabled new package
Bootstrap sequence for a new package:
1. publish the package once from a trusted maintainer machine using normal npm auth
2. open that package on npm and add the `paperclipai/paperclip` trusted publisher for `.github/workflows/release.yml`
3. rerun or dry-run the release flow as needed to confirm CI publishing now works
4. only then enable `"publishFromCi": true`
PR CI enforces this by checking changed release-enabled package manifests against npm. That keeps `master` canary publishing healthy while preserving the no-long-lived-token model for normal CI releases.
### 2.3. Verify trusted publishing before removing old auth
After the workflows are live:

View File

@@ -63,8 +63,6 @@ It:
- verifies the pushed commit
- computes the canary version for the current UTC date
- publishes under npm dist-tag `canary`
- verifies that `canary` resolves to the just-published version and that published internal dependencies exist on npm
- fails by default if npm leaves `latest` pointing at a canary; use `--allow-canary-latest` only when that state is intentional
- creates a git tag `canary/vYYYY.MDD.P-canary.N`
Users install canaries with:

View File

@@ -1,7 +1,7 @@
# Paperclip V1 Implementation Spec
Status: Implementation contract for first release (V1)
Date: 2026-04-28
Date: 2026-02-17
Audience: Product, engineering, and agent-integration authors
Source inputs: `GOAL.md`, `PRODUCT.md`, `SPEC.md`, `DATABASE.md`, current monorepo code
@@ -37,9 +37,8 @@ These decisions close open questions from `SPEC.md` for V1.
| Visibility | Full visibility to board and all agents in same company |
| Communication | Tasks + comments only (no separate chat system) |
| Task ownership | Single assignee; atomic checkout required for `in_progress` transition |
| Recovery | Liveness/watchdog recovery preserves explicit ownership: retry lost execution continuity where safe, otherwise create visible recovery issues or require human escalation (see `doc/execution-semantics.md`) |
| Agent adapters | Built-in `process`, `http`, local CLI/session adapters, and OpenClaw gateway support; external adapters can also be loaded through the adapter plugin flow |
| Plugin framework | Local/self-hosted early plugin runtime is in scope; cloud marketplace and packaged public distribution remain out of scope |
| Recovery | No automatic reassignment; work recovery stays manual/explicit |
| Agent adapters | Built-in `process` and `http` adapters |
| Auth | Mode-dependent human auth (`local_trusted` implicit board in current code; authenticated mode uses sessions), API keys for agents |
| Budget period | Monthly UTC calendar window |
| Budget enforcement | Soft alerts + hard limit auto-pause |
@@ -74,7 +73,7 @@ V1 implementation extends this baseline into a company-centric, governance-aware
## 5.2 Out of Scope (V1)
- Cloud-grade plugin marketplace/distribution beyond the local/self-hosted plugin runtime
- Plugin framework and third-party extension SDK
- Revenue/expense accounting beyond model/token costs
- Knowledge base subsystem
- Public marketplace (ClipHub)
@@ -124,16 +123,6 @@ Human auth tables (`users`, `sessions`, and provider-specific auth artifacts) ar
- `name` text not null
- `description` text null
- `status` enum: `active | paused | archived`
- `pause_reason` text null
- `paused_at` timestamptz null
- `issue_prefix` text not null
- `issue_counter` int not null
- `budget_monthly_cents` int not null default 0
- `spent_monthly_cents` int not null default 0
- `attachment_max_bytes` int not null
- `require_board_approval_for_new_agents` boolean not null default false
- feedback sharing consent fields
- branding fields such as `brand_color`
Invariant: every business record belongs to exactly one company.
@@ -144,21 +133,15 @@ Invariant: every business record belongs to exactly one company.
- `name` text not null
- `role` text not null
- `title` text null
- `icon` text null
- `status` enum: `active | paused | idle | running | error | pending_approval | terminated`
- `status` enum: `active | paused | idle | running | error | terminated`
- `reports_to` uuid fk `agents.id` null
- `capabilities` text null
- `adapter_type` text; built-ins include `process`, `http`, `claude_local`, `codex_local`, `gemini_local`, `opencode_local`, `pi_local`, `cursor`, and `openclaw_gateway`
- `adapter_type` enum: `process | http`
- `adapter_config` jsonb not null
- `runtime_config` jsonb not null default `{}`; may include Paperclip runtime policy such as `modelProfiles.cheap.adapterConfig` for an optional low-cost model lane that does not change the primary adapter config
- `default_environment_id` uuid fk `environments.id` null
- `context_mode` enum: `thin | fat` default `thin`
- `budget_monthly_cents` int not null default 0
- `spent_monthly_cents` int not null default 0
- pause fields: `pause_reason`, `paused_at`
- `permissions` jsonb not null default `{}`
- `last_heartbeat_at` timestamptz null
- `metadata` jsonb null
Invariants:
@@ -212,7 +195,6 @@ Invariant:
- `id` uuid pk
- `company_id` uuid fk not null
- `project_id` uuid fk `projects.id` null
- `project_workspace_id` uuid fk `project_workspaces.id` null
- `goal_id` uuid fk `goals.id` null
- `parent_id` uuid fk `issues.id` null
- `title` text not null
@@ -220,22 +202,13 @@ Invariant:
- `status` enum: `backlog | todo | in_progress | in_review | done | blocked | cancelled`
- `priority` enum: `critical | high | medium | low`
- `assignee_agent_id` uuid fk `agents.id` null
- `assignee_user_id` text null
- checkout/execution locks: `checkout_run_id`, `execution_run_id`, `execution_agent_name_key`, `execution_locked_at`
- `created_by_agent_id` uuid fk `agents.id` null
- `created_by_user_id` uuid fk `users.id` null
- identifier fields: `issue_number`, `identifier`
- origin fields: `origin_kind`, `origin_id`, `origin_run_id`, `origin_fingerprint`
- `request_depth` int not null default 0
- `billing_code` text null
- `assignee_adapter_overrides` jsonb null
- `execution_policy` jsonb null
- `execution_state` jsonb null
- execution workspace fields: `execution_workspace_id`, `execution_workspace_preference`, `execution_workspace_settings`
- `started_at` timestamptz null
- `completed_at` timestamptz null
- `cancelled_at` timestamptz null
- `hidden_at` timestamptz null
Invariants:
@@ -288,10 +261,10 @@ Invariant: each event must attach to agent and company; rollups are aggregation,
- `id` uuid pk
- `company_id` uuid fk not null
- `type` enum: `hire_agent | approve_ceo_strategy | budget_override_required | request_board_approval`
- `type` enum: `hire_agent | approve_ceo_strategy`
- `requested_by_agent_id` uuid fk `agents.id` null
- `requested_by_user_id` uuid fk `users.id` null
- `status` enum: `pending | revision_requested | approved | rejected | cancelled`
- `status` enum: `pending | approved | rejected | cancelled`
- `payload` jsonb not null
- `decision_note` text null
- `decided_by_user_id` uuid fk `users.id` null
@@ -390,15 +363,6 @@ Operational policy:
- `document_id` uuid fk not null
- `key` text not null (`plan`, `design`, `notes`, etc.)
## 7.16 Current Implementation Addenda
The current implementation includes additional V1-control-plane tables beyond the original February snapshot:
- Issue structure and review: `issue_relations` for blockers, `labels`/`issue_labels`, `issue_thread_interactions`, `issue_approvals`, `issue_execution_decisions`, `issue_work_products`, `issue_inbox_archives`, `issue_read_states`, and issue reference mention indexes.
- Execution and workspace control: `execution_workspaces`, `project_workspaces`, `workspace_runtime_services`, `workspace_operations`, `environments`, `environment_leases`, `agent_task_sessions`, `agent_runtime_state`, `agent_wakeup_requests`, heartbeat events, and watchdog decision tables.
- Plugins and routines: `plugins`, plugin config/state/entities/jobs/logs/webhooks, plugin database namespaces/migrations, plugin company settings, and `routines`.
- Access and operations: company memberships, instance roles, principal permission grants, invites, join requests, board API keys, CLI auth challenges, budget policies/incidents, feedback exports/votes, company skills, sidebar preferences, and company logos.
## 8. State Machines
## 8.1 Agent Status
@@ -431,14 +395,7 @@ Side effects:
- entering `done` sets `completed_at`
- entering `cancelled` sets `cancelled_at`
V1 non-terminal liveness rule:
- agent-owned `todo`, `in_progress`, `in_review`, and `blocked` issues must have a live execution path, an explicit waiting path, or an explicit recovery path
- `in_review` is healthy only when a typed execution participant, pending issue-thread interaction or approval, user owner, active run, queued wake, or explicit recovery issue owns the next action
- a blocked chain is covered only when each unresolved leaf issue is live or explicitly waiting
- when Paperclip cannot safely infer the next action, it surfaces the problem through visible blocked/recovery work instead of silently completing or reassigning work
Detailed ownership, execution, blocker, active-run watchdog, crash-recovery, and non-terminal liveness semantics are documented in `doc/execution-semantics.md`.
Detailed ownership, execution, blocker, and crash-recovery semantics are documented in `doc/execution-semantics.md`.
## 8.3 Approval Status
@@ -527,7 +484,6 @@ All endpoints are under `/api` and return JSON.
- `DELETE /issues/:issueId/documents/:key`
- `POST /issues/:issueId/checkout`
- `POST /issues/:issueId/release`
- `POST /issues/:issueId/admin/force-release` (board-only lock recovery)
- `POST /issues/:issueId/comments`
- `GET /issues/:issueId/comments`
- `POST /companies/:companyId/issues/:issueId/attachments` (multipart upload)
@@ -552,8 +508,6 @@ Server behavior:
2. if updated row count is 0, return `409` with current owner/status
3. successful checkout sets `assignee_agent_id`, `status = in_progress`, and `started_at`
`POST /issues/:issueId/admin/force-release` is an operator recovery endpoint for stale harness locks. It requires board access to the issue company, clears checkout and execution run lock fields, and may clear the agent assignee when `clearAssignee=true` is passed. The route must write an `issue.admin_force_release` activity log entry containing the previous checkout and execution run IDs.
## 10.5 Projects
- `GET /companies/:companyId/projects`
@@ -599,17 +553,6 @@ Dashboard payload must include:
- `422` semantic rule violation
- `500` server error
## 10.10 Current Implementation API Addenda
The current app also exposes V1-supporting surfaces for:
- issue thread interactions (`suggest_tasks`, `ask_user_questions`, `request_confirmation`)
- issue approvals, issue references/search, labels, read state, inbox/archive state, and work products
- execution workspaces, project workspaces, workspace runtime services, and workspace operations
- routines and scheduled/API/webhook triggers
- plugin installation, configuration, state, jobs, logs, webhooks, and plugin database namespace migration
- company import/export preview/apply, feedback export/vote routes, instance backup/config routes, invites, join requests, memberships, and permission grants
## 11. Heartbeat and Adapter Contract
## 11.1 Adapter Interface
@@ -676,7 +619,7 @@ Per-agent schedule fields in `adapter_config`:
- `enabled` boolean
- `intervalSec` integer (minimum 30)
- `maxConcurrentRuns` integer; new agents default to `20`; scheduler clamps configured values to `1..50`
- `maxConcurrentRuns` integer; new agents default to `5`
Scheduler must skip invocation when:
@@ -785,14 +728,13 @@ Required UX behaviors:
- Node 20+
- `DATABASE_URL` optional
- if unset, auto-use embedded PostgreSQL under `~/.paperclip/instances/default/db`
- if unset, auto-use PGlite and push schema
## 15.2 Migrations
- Drizzle migrations are source of truth
- local/dev startup applies pending migrations automatically where supported
- `pnpm db:migrate` applies pending migrations manually
- no destructive migration in-place for V1 upgrade path
- provide migration script from existing minimal tables to company-scoped schema
## 15.3 Logging and Audit
@@ -847,8 +789,6 @@ A release candidate is blocked unless these pass:
## 18. Delivery Plan
Current implementation note: the milestones below describe the original V1 sequencing. Several systems originally framed as future work have since shipped or advanced materially, including issue documents/interactions, blockers, routines, execution workspaces, import/export portability, authenticated deployment modes, multi-user basics, and the local/self-hosted plugin runtime.
## Milestone 1: Company Core and Auth
- add `companies` and company scoping to existing entities
@@ -901,7 +841,7 @@ V1 is complete only when all criteria are true:
## 20. Post-V1 Backlog (Explicitly Deferred)
- cloud-grade plugin marketplace/distribution
- plugin architecture
- richer workflow-state customization per team
- milestones/labels/dependency graph depth beyond V1 minimum
- realtime transport optimization (SSE/WebSockets)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 174 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 174 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 177 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 177 KiB

122
doc/design-system/REVIEW.md Normal file
View File

@@ -0,0 +1,122 @@
# Paperclip DS Extraction — Review
- **Generated:** 2026-04-21
- **Repo SHA:** `a26e1288b627e82c554445732c7d844648e6b5e1`
- **Branch:** `sockmonster-ds-extraction`
- **Discovery config:** [`_discovery.json`](./_discovery.json)
- **Scope:** `ui/` (`@paperclipai/ui`). Plugin SDK (`packages/plugins/sdk/src/ui/`) treated as contract surface, not implementation surface.
This is the entry point. Everything else is linked from here. Contents are ordered by **expected human value**, not by stage.
---
## Bottom line
One finding sits upstream of most of the others — resolving it moves four pattern docs from "pending" to "codifiable" and unblocks the single biggest token gap.
> **The app has a canonical status/priority color catalog (`ui/src/lib/status-colors.ts`) that bypasses the DS token layer and uses raw Tailwind palette classes across 11 hues and ~24 status keys.** Status indicators (`StatusIcon`, `StatusBadge`, `PriorityIcon`, `agentStatusDot`), chart colors (`ActivityCharts.tsx`, hardcoded hex), budget severity indicators (`BudgetPolicyCard`, `BudgetIncidentCard`, `BudgetSidebarMarker`), and quota fills (`QuotaBar`) are **four distinct systems encoding the same red/amber/green severity concept**, none of which share DS tokens.
A `--signal-*` token family would collapse four surfaces onto one vocabulary and make [status-display.md](./patterns/status-display.md), [quota-display.md](./patterns/quota-display.md), and the severity-indicator pattern opportunity all codifiable. See [tokens-review.md §4](./tokens/tokens-review.md#4-status-colorsts-is-a-canonical-semantic-color-catalog-that-bypasses-the-ds) and [patterns-review.md §6](./patterns/patterns-review.md#6-severity-indicator-3-level-health-display--pattern-opportunity).
Three other findings are high-value but smaller in scope:
- **`destructive-foreground` has a buggy light-mode value** equal to `destructive` itself (would render invisible if anyone used it — nobody does, so the bug is masked). [tokens-review.md §2](./tokens/tokens-review.md#2-destructive-foreground-has-a-wrong-light-mode-value-and-is-unused)
- **13 color tokens are dead** (all 5 `chart-*`, all 8 `sidebar-*`). Consolidating would drop color-token count from 32 → 19. [tokens-review.md §1, §3](./tokens/tokens-review.md#1-chart--tokens-are-dead)
- **The radius scale is non-monotonic and under-specified** — 227 uses of `rounded-lg` / `rounded-xl` resolve to square corners because `--radius-lg` / `--radius-xl` = 0. Needs a founder call on whether this is intentional flat-design or a stale migration state. [tokens-review.md §Radius scale](./tokens/tokens-review.md#radius-scale--under-founder-review)
---
## Recommended review order
Sequenced so each step unblocks the next. Total time estimated ~23 hours.
| # | Read | Decide | Est. |
|---|---|---|---|
| 1 | [tokens-review.md §High-confidence drift](./tokens/tokens-review.md#high-confidence-drift-likely-should-be-fixed) | Scope the signal-token work. Confirm dead-token deletions (chart-*, sidebar-*, destructive-foreground). | **25 min** |
| 2 | [tokens-review.md §Radius scale](./tokens/tokens-review.md#radius-scale--under-founder-review) | One call: intentional flat lg/xl, or restore a monotonic scale. | **15 min** |
| 3 | [components-review.md §Likely duplicates](./components/components-review.md#likely-duplicates) | Nine duplicate families. For each, note "merge/keep/defer" — patterns flow from the decisions. | **30 min** |
| 4 | [components-review.md §Plugin SDK contract gap](./components/components-review.md#plugin-sdk-contract-gap) | Choose: fulfill the 9 missing contracts, shrink them, or hybrid. | **15 min** |
| 5 | [patterns-review.md §Variance across documented patterns](./patterns/patterns-review.md#variance-across-documented-patterns-whats-inconsistent-between-instances) | Look at the status-element variance in detail pages (four different treatments across eight pages). | **15 min** |
| 6 | [patterns-review.md §Paperclip-domain patterns](./patterns/patterns-review.md#paperclip-domain-patterns-worth-calling-out-opportunities-not-ratified-patterns) | Reality-check the run-transcript / heartbeat / metric-cell opportunities before any codify step. | **20 min** |
| 7 | [components-review.md §Naming inconsistencies](./components/components-review.md#naming-inconsistencies) | Lower priority — no decision required today, but at least skim. | **10 min** |
| 8 | [components-review.md §Story coverage gaps](./components/components-review.md#story-coverage-gaps) | Shadcn primitives missing from `foundations.stories.tsx` (`collapsible`, `dropdown-menu`, `avatar`, `skeleton`, `scroll-area`) is a small, targeted fix. | **10 min** |
---
## Confidence
### High confidence (probably correct, spot-check only)
- **32 color tokens** extracted from `ui/src/index.css` (19 semantic surfaces, 5 chart, 8 sidebar).
- **5 radius tokens**, with value + definition-site recorded.
- **Usage counts per color and radius token** computed by unioning Tailwind-utility occurrences and `var(--token)` references across `ui/src/**/*.{ts,tsx,css}` (excluding the definition file itself). Counts are rough by intent — within ±10%.
- **135 component files** enumerated, classified into 22 primitives / 64 composites / 47 standalones / 2 non-component utilities.
- **104 components** cross-referenced against 14 Storybook files via import-graph parsing.
- **50 pages** enumerated; per-page import set captured.
- **11 plugin SDK ambient components** enumerated with host-implementation status.
- **4 components confirmed as storybook-only** (0 production uses): `AccountingModelCard`, `AgentProperties`, `CompanySwitcher`, `ExecutionParticipantPicker`.
### Medium confidence (review carefully)
- **Duplicate-family flags.** Eight families surfaced ([components-review.md §Likely duplicates](./components/components-review.md#likely-duplicates)) are based on name parallelism and/or shared imports. The strongest signals (entity-creation dialogs, subscription panels) need a side-by-side diff to confirm merge-ability; this extraction didn't do that.
- **`BillerSpendCard` vs `FinanceBillerCard` as likely-true-duplicate.** Flagged per directive. Not confirmed without a diff.
- **Pattern instance counts.** The `detail-page` and `list-page` patterns were identified by import-set intersection, which is a proxy for structural similarity. A page can import a component and not actually render it in the expected position; pattern shape is inferred, not verified pixel-by-pixel.
- **CVA variant extraction for primitives.** Parsed 3 files successfully (`button`, `badge`, one more). The rest of the primitives likely have variants that the static parser missed.
- **The severity-indicator pattern ([patterns-review.md §6](./patterns/patterns-review.md#6-severity-indicator-3-level-health-display--pattern-opportunity)).** Named as an *opportunity*, not a ratified pattern — cross-system evidence is strong but the four systems weren't compared pixel-for-pixel; they may not actually agree on what "warning" looks like.
- **Story coverage set.** Computed by parsing imports in `.stories.tsx` files. A component that's imported by a story but never actually rendered would falsely appear covered. Low risk given story-file structure but not validated.
### Low confidence (likely wrong, incomplete, or judgment-heavy)
- **Motion tokens.** None exist as variables — motion is inline `@keyframes` + `cubic-bezier()`. Pattern docs don't describe motion. The 5 keyframes in `index.css` are listed; their callers are not cross-referenced.
- **Typography.** No project-local font/type tokens found — the section is near-empty because Tailwind v4 defaults carry the load plus `@tailwindcss/typography`. If there are intended type-scale conventions in components that weren't captured by token extraction, those are missed.
- **Elevation / shadows.** No tokens, so no inventory. Ad-hoc `shadow-[…]` values across polished surfaces were enumerated in [tokens-review.md §9](./tokens/tokens-review.md#9-arbitrary-shadow-values-in-production-surfaces), but the list is not exhaustive.
- **Prop extraction for primitives using `React.ComponentProps<"button"> & VariantProps<...>`.** The static parser looks for `*Props` interfaces; inline-type components (most shadcn primitives) get "no Props interface found" in their detail files.
- **Per-component token consumption cross-reference.** Components/detail files don't list which specific tokens each component consumes (would require per-file class-attribute parsing). Token usage counts are global; per-component token drift is flagged only where specific drift was found.
- **Pattern: "detail-page header."** Called out as a sub-pattern inside detail-page doc but not given its own file — instances share only 45 imports, not a complete shape.
---
## Known scope limitations
- **Plugin SDK UI.** In-scope as a contract surface (documented in [components/index.md §Plugin SDK contracts](./components/index.md#plugin-sdk-contracts-11)). Not in-scope for pattern extraction — host implementations are covered; plugin-side usage patterns are not.
- **Low-usage components (12 code imports, 76 of them).** Listed in [components/index.md](./components/index.md) with status marker `📘 below-threshold`; no dedicated detail file. Per the directive: *nothing gets silently dropped*.
- **Pattern documentation:** capped at 10 real patterns. Eleven pattern files exist because the duplicate-family directive required documenting three below-threshold pairs (subscription-panel, sidebar-menu pair inside sidebar-chrome, quota-display). Pattern opportunities surfaced in patterns-review.md are not yet pattern files.
- **UX Lab pages (`InviteUxLab`, `IssueChatUxLab`, `RunTranscriptUxLab`).** Acknowledged prototypes with distinct visual language. Excluded from pattern extraction. Their raw-palette usage is counted in drift stats but not pursued.
- **Hermes / adapter code.** `ui/src/adapters/` contains per-adapter config fields. Not a DS concern; skipped.
- **Mobile treatments.** `MobileBottomNav` and `SwipeToArchive` are noted but not extracted as their own pattern. Mobile patterns appear to live inside individual list/detail pages rather than as shared primitives.
- **Diff mode.** This is a fresh run; `doc/design-system/` did not exist before. No diff was generated. Subsequent re-runs should run in diff mode (see [ds-extraction skill §Diff mode](../../.agents/skills/ds-extraction/SKILL.md#diff-mode)).
---
## What's on disk
```
doc/design-system/
├── REVIEW.md ← you are here
├── _discovery.json ← Stage 0 output
├── _pages.json ← Stage 2 scratch (50 pages)
├── _composition-graph.json ← Stage 2 scratch (135 components)
├── _stories.json ← Stage 2 scratch (14 stories)
├── tokens/
│ ├── tokens.md ← canonical human-readable inventory
│ ├── tokens.json ← machine-readable for downstream tooling
│ └── tokens-review.md ← the high-value drift artifact
├── components/
│ ├── index.md ← all 135 files + 11 SDK contracts, with status markers
│ ├── components-review.md ← duplicates, naming, token non-compliance, story gaps, SDK gap
│ └── [ComponentName].md × 53 ← per-component detail files (3+ uses threshold)
└── patterns/
├── index.md
├── patterns-review.md ← variance, opportunities, what to resolve before re-running
├── list-page.md ← 12 instances
├── detail-page.md ← 8 instances
├── sidebar-chrome.md ← 6 + 2 instances
├── finance-card.md ← 5 instances
├── entity-properties-panel.md ← 4 + 1 instances (open Q on generic)
├── entity-creation-dialog.md ← 4 instances
├── status-display.md ← 3 components + catalog (pending signal tokens)
├── entity-row.md ← 3 instances
├── subscription-panel.md ← 2 instances (below threshold — documented)
└── quota-display.md ← 2 instances (below threshold — documented)
```
Total: **~80 files**.

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,229 @@
{
"generated_at": "2026-04-21T00:00:00Z",
"repo_sha": "a26e1288b627e82c554445732c7d844648e6b5e1",
"branch": "sockmonster-ds-extraction",
"styling": {
"tailwind_version": "v4",
"tailwind_config": null,
"tailwind_config_note": "No tailwind.config.* file. Tailwind v4 CSS-first config: theme is declared via @theme inline blocks in ui/src/index.css. Build integration via @tailwindcss/vite.",
"css_variables_file": "ui/src/index.css",
"uses_css_variables": true,
"uses_cva": true,
"uses_cn_helper": true,
"cn_helper_location": "ui/src/lib/utils.ts:6",
"shadcn_present": true,
"shadcn_style": "new-york",
"shadcn_base_color": "neutral",
"shadcn_css_variables": true,
"shadcn_rsc": false,
"shadcn_icon_library": "lucide",
"shadcn_aliases": {
"components": "@/components",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks",
"utils": "@/lib/utils"
},
"shadcn_skill_path": ".agents/skills/shadcn/SKILL.md",
"other_styling": [
{
"library": "@tailwindcss/typography",
"usage": "@plugin in ui/src/index.css; prose class styling for markdown"
}
],
"notes": "Tailwind v4 with shadcn/ui in new-york style. components.json present. cn() = clsx + tailwind-merge. Custom @custom-variant dark (&:is(.dark *)). No tailwindcss-animate plugin."
},
"tokens": {
"color": {
"sources": ["ui/src/index.css"],
"authoritative_source": "ui/src/index.css",
"definition_blocks": [
{ "block": "@theme inline", "role": "exposes --color-* aliases to Tailwind", "line_range": "6-43" },
{ "block": ":root", "role": "authoritative light-mode values", "line_range": "45-80" },
{ "block": ".dark", "role": "dark-mode overrides", "line_range": "82-115" }
],
"count_estimate": 32,
"categories": {
"semantic_neutral_and_intent": [
"background", "foreground", "card", "card-foreground", "popover", "popover-foreground",
"primary", "primary-foreground", "secondary", "secondary-foreground",
"muted", "muted-foreground", "accent", "accent-foreground",
"destructive", "destructive-foreground",
"border", "input", "ring"
],
"chart": ["chart-1", "chart-2", "chart-3", "chart-4", "chart-5"],
"sidebar": [
"sidebar", "sidebar-foreground",
"sidebar-primary", "sidebar-primary-foreground",
"sidebar-accent", "sidebar-accent-foreground",
"sidebar-border", "sidebar-ring"
]
},
"includes_signal_green": false,
"value_format": "oklch",
"dark_mode_convention": ".dark class selector via @custom-variant"
},
"spacing": {
"sources": [],
"authoritative_source": null,
"count_estimate": 0,
"note": "No project-local spacing tokens. Spacing uses Tailwind v4 defaults inherited from tailwindcss package."
},
"type": {
"sources": [],
"authoritative_source": null,
"count_estimate": 0,
"font_faces": [],
"google_fonts": [],
"note": "No project-local font/type tokens. Typography uses Tailwind v4 defaults + @tailwindcss/typography plugin. Markdown styling via `.paperclip-markdown` and `.paperclip-mdxeditor-content` classes with hardcoded font-size/line-height values in index.css."
},
"radius": {
"sources": ["ui/src/index.css"],
"authoritative_source": "ui/src/index.css",
"count_estimate": 5,
"tokens": [
{ "name": "--radius", "value": "0", "defined_at": "ui/src/index.css:47", "scope": ":root" },
{ "name": "--radius-sm", "value": "0.375rem", "defined_at": "ui/src/index.css:39", "scope": "@theme" },
{ "name": "--radius-md", "value": "0.5rem", "defined_at": "ui/src/index.css:40", "scope": "@theme" },
{ "name": "--radius-lg", "value": "0px", "defined_at": "ui/src/index.css:41", "scope": "@theme" },
{ "name": "--radius-xl", "value": "0px", "defined_at": "ui/src/index.css:42", "scope": "@theme" }
],
"note": "Unusual: --radius-lg and --radius-xl are 0px while --radius-sm and --radius-md are non-zero. Likely intentional flat-design choice at the outer scale, but worth confirming. Also --radius (base, :root) = 0 used by MDXEditor integration; not part of @theme."
},
"motion": {
"sources": ["ui/src/index.css"],
"authoritative_source": null,
"count_estimate": 0,
"css_variable_tokens": [],
"keyframes": [
{ "name": "dashboard-activity-enter", "defined_at": "ui/src/index.css:228" },
{ "name": "dashboard-activity-highlight", "defined_at": "ui/src/index.css:246" },
{ "name": "cot-line-slide-in", "defined_at": "ui/src/index.css:272" },
{ "name": "cot-line-slide-out", "defined_at": "ui/src/index.css:277" },
{ "name": "shimmer-text-slide", "defined_at": "ui/src/index.css:298" }
],
"tailwindcss_animate_plugin": false,
"note": "No --motion-* or --duration-* tokens. Motion is defined as inline @keyframes + inline cubic-bezier values (commonly cubic-bezier(0.16, 1, 0.3, 1) and cubic-bezier(0.4, 0, 0.2, 1)). prefers-reduced-motion respected."
},
"elevation": {
"sources": [],
"authoritative_source": null,
"count_estimate": 0,
"note": "No --shadow-* tokens and no theme.boxShadow. Project appears to avoid shadows as a design choice (borders and background shifts carry elevation). Verify during extraction by grepping for box-shadow usage in components."
},
"scoped_non_ds_variables": [
{
"group": "MDXEditor theme bridge",
"selector": ".paperclip-mdxeditor-scope, .paperclip-mdxeditor",
"line_range": "332-361",
"variable_count": 24,
"role": "Maps host DS tokens onto MDXEditor's internal token names (--baseBase, --accentSolid, etc.). Consumed alias layer, not authoritative DS tokens."
},
{
"group": "Shimmer text effect",
"selector": ".shimmer-text",
"variable_count": 2,
"role": "Component-local (--shimmer-base, --shimmer-highlight)."
}
]
},
"components": {
"primary_root": "ui/src/components/",
"layout": "mixed",
"layout_notes": "Not purely flat and not purely nested. Subdirectories exist for a specific subset; the majority live flat at the top level. Naming convention for primitives is lowercase-kebab (button.tsx, dropdown-menu.tsx); composites/features use PascalCase (AgentConfigForm.tsx).",
"subdirectories": [
{ "path": "ui/src/components/ui/", "role": "shadcn primitives", "file_count": 22 },
{ "path": "ui/src/components/access/", "role": "access-control feature cluster", "file_count": 3 },
{ "path": "ui/src/components/transcript/", "role": "run transcript feature cluster", "file_count": 2 }
],
"top_level_tsx_count": 108,
"primitives_in_components_ui": [
"avatar", "badge", "breadcrumb", "button", "card", "checkbox", "collapsible",
"command", "dialog", "dropdown-menu", "input", "label", "popover",
"scroll-area", "select", "separator", "sheet", "skeleton", "tabs",
"textarea", "toggle-switch", "tooltip"
],
"count_estimate": 133,
"plugin_sdk_components": {
"path": "packages/plugins/sdk/src/ui/components.ts",
"status": "ambient-types-only",
"count": 11,
"declared_components": [
"MetricCard", "StatusBadge", "DataTable", "TimeseriesChart", "MarkdownBlock",
"KeyValueList", "ActionBar", "LogView", "JsonTree", "Spinner", "ErrorBoundary"
],
"runtime_model": "Host provides implementations via renderSdkUiComponent(name, props) runtime injection. Plugin bundles ship type declarations only.",
"name_collision_check": "MetricCard.tsx and StatusBadge.tsx exist at ui/src/components/ top-level. The other 9 declared components have no obvious matching file — may exist under different names (e.g., MarkdownBody ≈ MarkdownBlock, JsonSchemaForm unrelated) or may not be implemented yet."
}
},
"usage_surfaces": {
"pages_root": "ui/src/pages/",
"page_count_estimate": 50,
"page_count_method": "find ui/src/pages -name '*.tsx' -not -name '*.test.tsx'",
"other_surfaces": [
{ "path": "ui/src/App.tsx", "role": "root router" },
{ "path": "ui/src/plugins/", "role": "plugin slot & launcher rendering (slots.tsx, launchers.tsx)" }
],
"notable_pages": {
"design_guide": "ui/src/pages/DesignGuide.tsx — an existing in-app design reference page. Imports shadcn primitives; worth cross-referencing during Stage 3 for intended-vs-actual primitive usage. Its presence means a partial DS narrative already exists in-code.",
"ux_labs": [
"ui/src/pages/InviteUxLab.tsx",
"ui/src/pages/IssueChatUxLab.tsx",
"ui/src/pages/RunTranscriptUxLab.tsx"
]
}
},
"storybook": {
"present": true,
"version": "10.3.5",
"config_path": "ui/storybook/.storybook/main.ts",
"stories_location": "centralized",
"stories_glob": "ui/storybook/stories/**/*.stories.@(ts|tsx|mdx)",
"story_file_count": 14,
"story_organization": "thematic",
"story_organization_note": "Stories are organized by domain/theme (foundations, navigation-layout, dialogs-modals, chat-comments, forms-editors, status-language, data-viz-misc, agent-management, issue-management, projects-goals-workspaces, budget-finance, control-plane-surfaces, ux-labs, overview) — NOT one-story-per-component. A single .stories.tsx file typically imports and composes many components. 'Component covered by story' must be computed by parsing import graphs of story files, not by file naming.",
"story_files": [
"foundations.stories.tsx",
"overview.stories.tsx",
"status-language.stories.tsx",
"navigation-layout.stories.tsx",
"dialogs-modals.stories.tsx",
"forms-editors.stories.tsx",
"chat-comments.stories.tsx",
"data-viz-misc.stories.tsx",
"agent-management.stories.tsx",
"issue-management.stories.tsx",
"projects-goals-workspaces.stories.tsx",
"budget-finance.stories.tsx",
"control-plane-surfaces.stories.tsx",
"ux-labs.stories.tsx"
],
"addons": ["@storybook/addon-docs", "@storybook/addon-a11y"],
"covered_components": null,
"covered_components_note": "Deferred to Stage 2 (parse story imports). Discovery only confirms stories exist, not per-component coverage."
},
"existing_docs": {
"design_system_dir_present": false,
"locations": [
{ "path": "ui/README.md", "role": "package readme; non-DS" },
{ "path": "ui/src/pages/DesignGuide.tsx", "role": "in-app design reference page (component-level showcase)" }
],
"figma_sync_config": null,
"style_dictionary_config": null,
"tokens_studio_config": null
},
"known_gaps": [
"Plugin SDK UI is a types-only ambient bridge (packages/plugins/sdk/src/ui/components.ts). The host-provided component kit promised by the SDK is partial: only MetricCard and StatusBadge have matching host implementations by name. 9 other declared components (DataTable, TimeseriesChart, MarkdownBlock, KeyValueList, ActionBar, LogView, JsonTree, Spinner, ErrorBoundary) have no obvious host implementation. PLUGIN_SPEC.md:30 confirms: 'The current runtime does not yet ship a real host-provided plugin UI component kit'.",
"No dedicated spacing, type, or elevation tokens. Those categories rely on Tailwind v4 defaults. Extraction should not synthesize repo-specific tokens where none exist.",
"No central motion token language. Motion is expressed as per-feature @keyframes with inline easing. Treat motion as a candidate for future tokenization rather than documenting a current system.",
"No Figma/style-dictionary/tokens-studio integration. The design system is code-authored, not design-tool-synced."
],
"uncertainties": [
"Storybook organization is thematic (14 composite stories), not per-component. The extraction skill's 'covered_by_story' signal needs to be computed by parsing each story file's import graph and surfacing components used inside render bodies. Flag before Stage 2 so the skill doesn't default to file-name matching.",
"Radius scale is non-monotonic: --radius-sm = 0.375rem, --radius-md = 0.5rem, --radius-lg = 0px, --radius-xl = 0px. Flat-design choice or stale values? Also --radius (base) = 0 at :root coexists with the @theme tokens; which is canonical for Tailwind rounded utilities? Worth confirming before Stage 1 drift analysis flags every rounded-lg usage.",
"Plugin SDK UI contracts 11 shared components but only 2 appear to be implemented by that name in ui/. For extraction scope, should we (a) treat the SDK declarations as DS contract and flag missing implementations as gaps, or (b) ignore the SDK and document only what exists in ui/? Recommendation: (a) — it's higher-value signal for the human reviewer.",
"ui/src/components/ has a mixed layout: 22 shadcn primitives in a 'ui/' subdirectory and 108 components flat at the top level. The top-level mix contains features, composites, one-off pages pieces, and true reusable patterns. Stage 3 will need a heuristic (composition-graph-based) rather than directory-based category inference.",
"Total component count (~133) is meaningfully larger than the skill's illustrative example (87). With the 3+-usage threshold for detail files, output should stay tractable, but the skill should confirm the threshold is right at this scale before Stage 3 starts generating per-component .md files.",
"MDXEditor CSS variable bridge (.paperclip-mdxeditor-scope, 24 --base*/--accent* variables) is DS-adjacent — it consumes host tokens and maps them to MDXEditor internals. Should Stage 1 include these in tokens.json? Recommendation: no — they are consumed aliases, not authoritative tokens. Flag them in tokens-review.md as 'integration layer' rather than as drift."
]
}

View File

@@ -0,0 +1,646 @@
{
"generated_at": "2026-04-21T00:00:00Z",
"repo_sha": "a26e1288b627e82c554445732c7d844648e6b5e1",
"page_count": 50,
"method": "Import-graph extraction: 'from \"@/components/<name>\"' and relative imports. Top-level JSX tree not parsed \u2014 import set is the proxy for rendered-components set. Components a page imports but never renders would inflate the count slightly; low risk here given app style.",
"pages": {
"Activity": {
"path": "ui/src/pages/Activity.tsx",
"components_imported": [
"ActivityRow",
"EmptyState",
"PageSkeleton",
"select"
],
"component_count": 4
},
"AdapterManager": {
"path": "ui/src/pages/AdapterManager.tsx",
"components_imported": [
"PathInstructionsModal",
"badge",
"button",
"card",
"dialog",
"input",
"label"
],
"component_count": 7
},
"AgentDetail": {
"path": "ui/src/pages/AgentDetail.tsx",
"components_imported": [
"ActivityCharts",
"AgentActionButtons",
"AgentConfigForm",
"AgentIconPicker",
"BudgetPolicyCard",
"CopyText",
"EntityRow",
"Identity",
"MarkdownBody",
"MarkdownEditor",
"PackageFileTree",
"PageSkeleton",
"PageTabBar",
"RunTranscriptView",
"ScrollToBottom",
"StatusBadge",
"agent-config-primitives",
"button",
"collapsible",
"input",
"popover",
"skeleton",
"tabs",
"toggle-switch",
"tooltip"
],
"component_count": 25
},
"Agents": {
"path": "ui/src/pages/Agents.tsx",
"components_imported": [
"EmptyState",
"EntityRow",
"PageSkeleton",
"PageTabBar",
"StatusBadge",
"button",
"tabs"
],
"component_count": 7
},
"ApprovalDetail": {
"path": "ui/src/pages/ApprovalDetail.tsx",
"components_imported": [
"ApprovalPayload",
"Identity",
"MarkdownBody",
"PageSkeleton",
"StatusBadge",
"button",
"textarea"
],
"component_count": 7
},
"Approvals": {
"path": "ui/src/pages/Approvals.tsx",
"components_imported": [
"ApprovalCard",
"PageSkeleton",
"PageTabBar",
"tabs"
],
"component_count": 4
},
"Auth": {
"path": "ui/src/pages/Auth.tsx",
"components_imported": [
"AsciiArtAnimation",
"button"
],
"component_count": 2
},
"BoardClaim": {
"path": "ui/src/pages/BoardClaim.tsx",
"components_imported": [
"button"
],
"component_count": 1
},
"CliAuth": {
"path": "ui/src/pages/CliAuth.tsx",
"components_imported": [
"button"
],
"component_count": 1
},
"Companies": {
"path": "ui/src/pages/Companies.tsx",
"components_imported": [
"button",
"dropdown-menu",
"input"
],
"component_count": 3
},
"CompanyAccess": {
"path": "ui/src/pages/CompanyAccess.tsx",
"components_imported": [
"badge",
"button",
"checkbox",
"dialog"
],
"component_count": 4
},
"CompanyExport": {
"path": "ui/src/pages/CompanyExport.tsx",
"components_imported": [
"EmptyState",
"MarkdownBody",
"PackageFileTree",
"PageSkeleton",
"button"
],
"component_count": 5
},
"CompanyImport": {
"path": "ui/src/pages/CompanyImport.tsx",
"components_imported": [
"AgentConfigForm",
"EmptyState",
"MarkdownBody",
"PackageFileTree",
"agent-config-defaults",
"agent-config-primitives",
"button"
],
"component_count": 7
},
"CompanyInvites": {
"path": "ui/src/pages/CompanyInvites.tsx",
"components_imported": [
"button"
],
"component_count": 1
},
"CompanySettings": {
"path": "ui/src/pages/CompanySettings.tsx",
"components_imported": [
"CompanyPatternIcon",
"agent-config-primitives",
"button"
],
"component_count": 3
},
"CompanySkills": {
"path": "ui/src/pages/CompanySkills.tsx",
"components_imported": [
"EmptyState",
"MarkdownBody",
"MarkdownEditor",
"PageSkeleton",
"button",
"dialog",
"input",
"textarea",
"tooltip"
],
"component_count": 9
},
"Costs": {
"path": "ui/src/pages/Costs.tsx",
"components_imported": [
"BillerSpendCard",
"BudgetIncidentCard",
"BudgetPolicyCard",
"EmptyState",
"FinanceBillerCard",
"FinanceKindCard",
"FinanceTimelineCard",
"Identity",
"PageSkeleton",
"PageTabBar",
"ProviderQuotaCard",
"StatusBadge",
"button",
"card",
"tabs"
],
"component_count": 15
},
"Dashboard": {
"path": "ui/src/pages/Dashboard.tsx",
"components_imported": [
"ActiveAgentsPanel",
"ActivityCharts",
"ActivityRow",
"EmptyState",
"Identity",
"MetricCard",
"PageSkeleton",
"StatusIcon"
],
"component_count": 8
},
"DesignGuide": {
"path": "ui/src/pages/DesignGuide.tsx",
"components_imported": [
"EmptyState",
"EntityRow",
"FilterBar",
"Identity",
"InlineEditor",
"IssueReferencePill",
"MetricCard",
"PageSkeleton",
"PriorityIcon",
"StatusBadge",
"StatusIcon",
"avatar",
"badge",
"breadcrumb",
"button",
"card",
"checkbox",
"collapsible",
"command",
"dialog",
"dropdown-menu",
"input",
"label",
"popover",
"scroll-area",
"select",
"separator",
"sheet",
"skeleton",
"tabs",
"textarea",
"tooltip"
],
"component_count": 32
},
"ExecutionWorkspaceDetail": {
"path": "ui/src/pages/ExecutionWorkspaceDetail.tsx",
"components_imported": [
"CopyText",
"ExecutionWorkspaceCloseDialog",
"IssuesList",
"PageTabBar",
"WorkspaceRuntimeControls",
"button",
"card",
"input",
"separator",
"tabs",
"textarea"
],
"component_count": 11
},
"GoalDetail": {
"path": "ui/src/pages/GoalDetail.tsx",
"components_imported": [
"EntityRow",
"GoalProperties",
"GoalTree",
"InlineEditor",
"PageSkeleton",
"StatusBadge",
"button",
"tabs"
],
"component_count": 8
},
"Goals": {
"path": "ui/src/pages/Goals.tsx",
"components_imported": [
"EmptyState",
"GoalTree",
"PageSkeleton",
"button"
],
"component_count": 4
},
"Inbox": {
"path": "ui/src/pages/Inbox.tsx",
"components_imported": [
"ApprovalPayload",
"EmptyState",
"IssueColumns",
"IssueFiltersPopover",
"IssueGroupHeader",
"IssueRow",
"PageSkeleton",
"PageTabBar",
"StatusBadge",
"StatusIcon",
"SwipeToArchive",
"button",
"dialog",
"input",
"popover",
"select",
"separator",
"tabs"
],
"component_count": 18
},
"InstanceAccess": {
"path": "ui/src/pages/InstanceAccess.tsx",
"components_imported": [
"button",
"checkbox"
],
"component_count": 2
},
"InstanceExperimentalSettings": {
"path": "ui/src/pages/InstanceExperimentalSettings.tsx",
"components_imported": [
"toggle-switch"
],
"component_count": 1
},
"InstanceGeneralSettings": {
"path": "ui/src/pages/InstanceGeneralSettings.tsx",
"components_imported": [
"ModeBadge",
"button",
"toggle-switch"
],
"component_count": 3
},
"InstanceSettings": {
"path": "ui/src/pages/InstanceSettings.tsx",
"components_imported": [
"EmptyState",
"badge",
"button",
"card"
],
"component_count": 4
},
"InviteLanding": {
"path": "ui/src/pages/InviteLanding.tsx",
"components_imported": [
"CompanyPatternIcon",
"button"
],
"component_count": 2
},
"InviteUxLab": {
"path": "ui/src/pages/InviteUxLab.tsx",
"components_imported": [
"CompanyPatternIcon",
"badge",
"button",
"card"
],
"component_count": 4
},
"IssueChatUxLab": {
"path": "ui/src/pages/IssueChatUxLab.tsx",
"components_imported": [
"IssueChatThread",
"badge",
"button",
"card"
],
"component_count": 4
},
"IssueDetail": {
"path": "ui/src/pages/IssueDetail.tsx",
"components_imported": [
"ApprovalCard",
"Identity",
"ImageGalleryModal",
"InlineEditor",
"IssueChatThread",
"IssueContinuationHandoff",
"IssueDocumentsSection",
"IssueProperties",
"IssueReferenceActivitySummary",
"IssueRelatedWorkPanel",
"IssueRunLedger",
"IssueWorkspaceCard",
"IssuesList",
"MarkdownEditor",
"PriorityIcon",
"ScrollToBottom",
"StatusIcon",
"button",
"popover",
"scroll-area",
"separator",
"sheet",
"skeleton",
"tabs"
],
"component_count": 24
},
"Issues": {
"path": "ui/src/pages/Issues.tsx",
"components_imported": [
"EmptyState",
"IssuesList"
],
"component_count": 2
},
"JoinRequestQueue": {
"path": "ui/src/pages/JoinRequestQueue.tsx",
"components_imported": [
"badge",
"button"
],
"component_count": 2
},
"MyIssues": {
"path": "ui/src/pages/MyIssues.tsx",
"components_imported": [
"EmptyState",
"EntityRow",
"PageSkeleton",
"StatusIcon"
],
"component_count": 4
},
"NewAgent": {
"path": "ui/src/pages/NewAgent.tsx",
"components_imported": [
"AgentConfigForm",
"ReportsToPicker",
"agent-config-defaults",
"agent-config-primitives",
"button",
"checkbox",
"popover"
],
"component_count": 7
},
"NotFound": {
"path": "ui/src/pages/NotFound.tsx",
"components_imported": [
"button"
],
"component_count": 1
},
"Org": {
"path": "ui/src/pages/Org.tsx",
"components_imported": [
"EmptyState",
"PageSkeleton",
"StatusBadge"
],
"component_count": 3
},
"OrgChart": {
"path": "ui/src/pages/OrgChart.tsx",
"components_imported": [
"AgentIconPicker",
"EmptyState",
"PageSkeleton",
"button"
],
"component_count": 4
},
"PluginManager": {
"path": "ui/src/pages/PluginManager.tsx",
"components_imported": [
"badge",
"button",
"card",
"dialog",
"input",
"label"
],
"component_count": 6
},
"PluginPage": {
"path": "ui/src/pages/PluginPage.tsx",
"components_imported": [
"button"
],
"component_count": 1
},
"PluginSettings": {
"path": "ui/src/pages/PluginSettings.tsx",
"components_imported": [
"JsonSchemaForm",
"PageTabBar",
"badge",
"button",
"card",
"separator",
"tabs"
],
"component_count": 7
},
"ProfileSettings": {
"path": "ui/src/pages/ProfileSettings.tsx",
"components_imported": [
"avatar",
"button",
"input",
"label"
],
"component_count": 4
},
"ProjectDetail": {
"path": "ui/src/pages/ProjectDetail.tsx",
"components_imported": [
"BudgetPolicyCard",
"InlineEditor",
"IssuesList",
"PageSkeleton",
"PageTabBar",
"ProjectProperties",
"ProjectWorkspacesContent",
"StatusBadge",
"button",
"tabs"
],
"component_count": 10
},
"ProjectWorkspaceDetail": {
"path": "ui/src/pages/ProjectWorkspaceDetail.tsx",
"components_imported": [
"PathInstructionsModal",
"WorkspaceRuntimeControls",
"button",
"separator"
],
"component_count": 4
},
"Projects": {
"path": "ui/src/pages/Projects.tsx",
"components_imported": [
"EmptyState",
"EntityRow",
"PageSkeleton",
"StatusBadge",
"button"
],
"component_count": 5
},
"RoutineDetail": {
"path": "ui/src/pages/RoutineDetail.tsx",
"components_imported": [
"AgentActionButtons",
"AgentIconPicker",
"EmptyState",
"InlineEntitySelector",
"LiveRunWidget",
"MarkdownEditor",
"PageSkeleton",
"RoutineRunVariablesDialog",
"RoutineVariablesEditor",
"ScheduleEditor",
"badge",
"button",
"collapsible",
"input",
"label",
"select",
"separator",
"tabs",
"toggle-switch"
],
"component_count": 19
},
"Routines": {
"path": "ui/src/pages/Routines.tsx",
"components_imported": [
"AgentIconPicker",
"EmptyState",
"InlineEntitySelector",
"IssuesList",
"MarkdownEditor",
"PageSkeleton",
"PageTabBar",
"RoutineRunVariablesDialog",
"RoutineVariablesEditor",
"button",
"card",
"collapsible",
"dialog",
"dropdown-menu",
"popover",
"select",
"tabs",
"toggle-switch"
],
"component_count": 18
},
"RunTranscriptUxLab": {
"path": "ui/src/pages/RunTranscriptUxLab.tsx",
"components_imported": [
"Identity",
"RunTranscriptView",
"StatusBadge",
"badge",
"button"
],
"component_count": 5
},
"UserProfile": {
"path": "ui/src/pages/UserProfile.tsx",
"components_imported": [
"EmptyState",
"PageSkeleton",
"StatusBadge",
"avatar"
],
"component_count": 4
},
"Workspaces": {
"path": "ui/src/pages/Workspaces.tsx",
"components_imported": [
"PageSkeleton",
"ProjectWorkspacesContent"
],
"component_count": 2
}
}
}

View File

@@ -0,0 +1,338 @@
{
"generated_at": "2026-04-21T00:00:00Z",
"repo_sha": "a26e1288b627e82c554445732c7d844648e6b5e1",
"story_files": {
"agent-management.stories.tsx": {
"path": "ui/storybook/stories/agent-management.stories.tsx",
"components_imported": [
"ActiveAgentsPanel",
"AgentActionButtons",
"AgentConfigForm",
"AgentIconPicker",
"AgentProperties",
"agent-config-defaults",
"agent-config-primitives",
"badge",
"button",
"card",
"select",
"separator"
],
"component_count": 12
},
"budget-finance.stories.tsx": {
"path": "ui/storybook/stories/budget-finance.stories.tsx",
"components_imported": [
"AccountingModelCard",
"BillerSpendCard",
"BudgetIncidentCard",
"BudgetSidebarMarker",
"ClaudeSubscriptionPanel",
"CodexSubscriptionPanel",
"FinanceBillerCard",
"FinanceKindCard",
"FinanceTimelineCard",
"ProviderQuotaCard",
"badge",
"card"
],
"component_count": 12
},
"chat-comments.stories.tsx": {
"path": "ui/storybook/stories/chat-comments.stories.tsx",
"components_imported": [
"CommentThread",
"InlineEntitySelector",
"IssueChatThread",
"MarkdownEditor",
"RunChatSurface",
"badge",
"card"
],
"component_count": 7
},
"control-plane-surfaces.stories.tsx": {
"path": "ui/storybook/stories/control-plane-surfaces.stories.tsx",
"components_imported": [
"ActivityRow",
"ApprovalCard",
"BudgetPolicyCard",
"Identity",
"IssueRow",
"PriorityIcon",
"StatusBadge",
"badge",
"card"
],
"component_count": 9
},
"data-viz-misc.stories.tsx": {
"path": "ui/storybook/stories/data-viz-misc.stories.tsx",
"components_imported": [
"ActivityCharts",
"AsciiArtAnimation",
"CompanyPatternIcon",
"EntityRow",
"FilterBar",
"KanbanBoard",
"LiveRunWidget",
"OnboardingWizard",
"PackageFileTree",
"PageSkeleton",
"StatusBadge",
"SwipeToArchive",
"badge",
"button",
"card"
],
"component_count": 15
},
"dialogs-modals.stories.tsx": {
"path": "ui/storybook/stories/dialogs-modals.stories.tsx",
"components_imported": [
"DocumentDiffModal",
"ExecutionWorkspaceCloseDialog",
"ImageGalleryModal",
"NewAgentDialog",
"NewGoalDialog",
"NewIssueDialog",
"NewProjectDialog",
"PathInstructionsModal",
"badge"
],
"component_count": 9
},
"forms-editors.stories.tsx": {
"path": "ui/storybook/stories/forms-editors.stories.tsx",
"components_imported": [
"EnvVarEditor",
"ExecutionParticipantPicker",
"InlineEditor",
"InlineEntitySelector",
"JsonSchemaForm",
"MarkdownBody",
"MarkdownEditor",
"ReportsToPicker",
"RoutineRunVariablesDialog",
"RoutineVariablesEditor",
"ScheduleEditor",
"badge",
"button"
],
"component_count": 13
},
"foundations.stories.tsx": {
"path": "ui/storybook/stories/foundations.stories.tsx",
"components_imported": [
"badge",
"button",
"card",
"checkbox",
"dialog",
"input",
"label",
"popover",
"select",
"separator",
"tabs",
"textarea",
"toggle-switch",
"tooltip"
],
"component_count": 14
},
"issue-management.stories.tsx": {
"path": "ui/storybook/stories/issue-management.stories.tsx",
"components_imported": [
"Identity",
"IssueColumns",
"IssueContinuationHandoff",
"IssueDocumentsSection",
"IssueFiltersPopover",
"IssueGroupHeader",
"IssueLinkQuicklook",
"IssueProperties",
"IssueRunLedger",
"IssueWorkspaceCard",
"IssuesList",
"IssuesQuicklook",
"PriorityIcon",
"StatusBadge",
"badge",
"button",
"card"
],
"component_count": 17
},
"navigation-layout.stories.tsx": {
"path": "ui/storybook/stories/navigation-layout.stories.tsx",
"components_imported": [
"BreadcrumbBar",
"CommandPalette",
"CompanyRail",
"CompanySwitcher",
"KeyboardShortcutsCheatsheet",
"MobileBottomNav",
"PageTabBar",
"Sidebar",
"SidebarAccountMenu",
"SidebarCompanyMenu",
"StatusBadge",
"badge",
"command",
"tabs"
],
"component_count": 14
},
"overview.stories.tsx": {
"path": "ui/storybook/stories/overview.stories.tsx",
"components_imported": [
"badge",
"card"
],
"component_count": 2
},
"projects-goals-workspaces.stories.tsx": {
"path": "ui/storybook/stories/projects-goals-workspaces.stories.tsx",
"components_imported": [
"GoalProperties",
"GoalTree",
"ProjectProperties",
"ProjectWorkspaceSummaryCard",
"ProjectWorkspacesContent",
"WorkspaceRuntimeControls",
"WorktreeBanner",
"badge",
"card"
],
"component_count": 9
},
"status-language.stories.tsx": {
"path": "ui/storybook/stories/status-language.stories.tsx",
"components_imported": [
"CopyText",
"EmptyState",
"Identity",
"MetricCard",
"PriorityIcon",
"QuotaBar",
"StatusBadge",
"card"
],
"component_count": 8
},
"ux-labs.stories.tsx": {
"path": "ui/storybook/stories/ux-labs.stories.tsx",
"components_imported": [],
"component_count": 0
}
},
"covered_components_count": 104,
"covered_components": [
"AccountingModelCard",
"ActiveAgentsPanel",
"ActivityCharts",
"ActivityRow",
"AgentActionButtons",
"AgentConfigForm",
"AgentIconPicker",
"AgentProperties",
"ApprovalCard",
"AsciiArtAnimation",
"BillerSpendCard",
"BreadcrumbBar",
"BudgetIncidentCard",
"BudgetPolicyCard",
"BudgetSidebarMarker",
"ClaudeSubscriptionPanel",
"CodexSubscriptionPanel",
"CommandPalette",
"CommentThread",
"CompanyPatternIcon",
"CompanyRail",
"CompanySwitcher",
"CopyText",
"DocumentDiffModal",
"EmptyState",
"EntityRow",
"EnvVarEditor",
"ExecutionParticipantPicker",
"ExecutionWorkspaceCloseDialog",
"FilterBar",
"FinanceBillerCard",
"FinanceKindCard",
"FinanceTimelineCard",
"GoalProperties",
"GoalTree",
"Identity",
"ImageGalleryModal",
"InlineEditor",
"InlineEntitySelector",
"IssueChatThread",
"IssueColumns",
"IssueContinuationHandoff",
"IssueDocumentsSection",
"IssueFiltersPopover",
"IssueGroupHeader",
"IssueLinkQuicklook",
"IssueProperties",
"IssueRow",
"IssueRunLedger",
"IssueWorkspaceCard",
"IssuesList",
"IssuesQuicklook",
"JsonSchemaForm",
"KanbanBoard",
"KeyboardShortcutsCheatsheet",
"LiveRunWidget",
"MarkdownBody",
"MarkdownEditor",
"MetricCard",
"MobileBottomNav",
"NewAgentDialog",
"NewGoalDialog",
"NewIssueDialog",
"NewProjectDialog",
"OnboardingWizard",
"PackageFileTree",
"PageSkeleton",
"PageTabBar",
"PathInstructionsModal",
"PriorityIcon",
"ProjectProperties",
"ProjectWorkspaceSummaryCard",
"ProjectWorkspacesContent",
"ProviderQuotaCard",
"QuotaBar",
"ReportsToPicker",
"RoutineRunVariablesDialog",
"RoutineVariablesEditor",
"RunChatSurface",
"ScheduleEditor",
"Sidebar",
"SidebarAccountMenu",
"SidebarCompanyMenu",
"StatusBadge",
"SwipeToArchive",
"WorkspaceRuntimeControls",
"WorktreeBanner",
"agent-config-defaults",
"agent-config-primitives",
"badge",
"button",
"card",
"checkbox",
"command",
"dialog",
"input",
"label",
"popover",
"select",
"separator",
"tabs",
"textarea",
"toggle-switch",
"tooltip"
],
"covered_components_note": "Computed by parsing 'from \"@/components/...\"' and relative imports across all .stories.tsx files. Coverage is set membership \u2014 a component appears once if any story imports it, regardless of how many variants/states are rendered."
}

View File

@@ -0,0 +1,21 @@
# ActivityCharts
`ui/src/components/ActivityCharts.tsx`
[INFER] Standalone component. No file-level docstring.
## Quick facts
- **Category:** `standalone`
- **Usage:** 3 imports (2 pages, 1 components)
- **Storybook:** yes — see ui/storybook/stories/ (thematic stories, not per-component)
- **File size:** 274 lines
- **Sibling exports:** ChartCard, IssueStatusChart, PriorityChart, RunActivityChart, SuccessRateChart
## Props
[INFER] No `*Props` interface/type found by static extraction. See source file.
## Used by
- **Pages:** `AgentDetail`, `Dashboard`

View File

@@ -0,0 +1,40 @@
# AgentConfigForm
`ui/src/components/AgentConfigForm.tsx`
[INFER] Composite component. No file-level docstring.
## Quick facts
- **Category:** `composite`
- **Usage:** 5 imports (3 pages, 0 components)
- **Storybook:** yes — see ui/storybook/stories/ (thematic stories, not per-component)
- **File size:** 1403 lines
## Props
### `AgentConfigFormProps`
```ts
adapterModels?: AdapterModel[];
onDirtyChange?: (dirty: boolean) => void;
onSaveActionChange?: (save: (() => void) | null) => void;
onCancelActionChange?: (cancel: (() => void) | null) => void;
hideInlineSave?: boolean;
showAdapterTypeField?: boolean;
showAdapterTestEnvironmentButton?: boolean;
showCreateRunPolicySection?: boolean;
hideInstructionsFile?: boolean;
/** Hide the prompt template field from the Identity section (used when it's shown in a separate Prompts tab). */
hidePromptTemplate?: boolean;
/** "cards" renders each section as heading + bordered card (for settings pages). Default: "inline" (border-b dividers). */
sectionLayout?: "inline" | "cards";
```
## Composes
- **Primitives:** [button](./Button.md), [popover](./Popover.md)
## Used by
- **Pages:** `AgentDetail`, `CompanyImport`, `NewAgent`

View File

@@ -0,0 +1,38 @@
# AgentIconPicker
`ui/src/components/AgentIconPicker.tsx`
[INFER] Composite component. No file-level docstring.
## Quick facts
- **Category:** `composite`
- **Usage:** 13 imports (4 pages, 9 components)
- **Storybook:** yes — see ui/storybook/stories/ (thematic stories, not per-component)
- **File size:** 81 lines
- **Sibling exports:** AgentIcon
## Props
### `AgentIconProps`
```ts
icon: string | null | undefined;
className?: string;
```
### `AgentIconPickerProps`
```ts
value: string | null | undefined;
onChange: (icon: string) => void;
children: React.ReactNode;
```
## Composes
- **Primitives:** [input](./Input.md), [popover](./Popover.md)
## Used by
- **Pages:** `AgentDetail`, `OrgChart`, `RoutineDetail`, `Routines`

View File

@@ -0,0 +1,24 @@
# ApprovalCard
`ui/src/components/ApprovalCard.tsx`
[INFER] Composite component. No file-level docstring.
## Quick facts
- **Category:** `composite`
- **Usage:** 3 imports (2 pages, 1 components)
- **Storybook:** yes — see ui/storybook/stories/ (thematic stories, not per-component)
- **File size:** 153 lines
## Props
[INFER] No `*Props` interface/type found by static extraction. See source file.
## Composes
- **Primitives:** [badge](./Badge.md), [button](./Button.md)
## Used by
- **Pages:** `Approvals`, `IssueDetail`

View File

@@ -0,0 +1,21 @@
# ApprovalPayload
`ui/src/components/ApprovalPayload.tsx`
[INFER] Standalone component. No file-level docstring.
## Quick facts
- **Category:** `standalone`
- **Usage:** 4 imports (2 pages, 2 components)
- **Storybook:** no
- **File size:** 248 lines
- **Sibling exports:** ApprovalPayloadRenderer, BoardApprovalPayload, BudgetOverridePayload, CeoStrategyPayload, HireAgentPayload
## Props
[INFER] No `*Props` interface/type found by static extraction. See source file.
## Used by
- **Pages:** `ApprovalDetail`, `Inbox`

View File

@@ -0,0 +1,22 @@
# Avatar
`ui/src/components/ui/avatar.tsx`
[INFER] Primitive component. No file-level docstring.
## Quick facts
- **Category:** `primitive`
- **Usage:** 7 imports (3 pages, 4 components)
- **Storybook:** no
- **File size:** 108 lines
- **Sibling exports:** AvatarBadge, AvatarFallback, AvatarGroup, AvatarGroupCount, AvatarImage
## Props
[INFER] No `*Props` interface/type found by static extraction. See source file.
## Used by
- **Pages:** `DesignGuide`, `ProfileSettings`, `UserProfile`
- **Components:** `CommentThread`, `Identity`, `IssueChatThread`, `SidebarAccountMenu`

View File

@@ -0,0 +1,26 @@
# Badge
`ui/src/components/ui/badge.tsx`
[INFER] Primitive component. No file-level docstring.
## Quick facts
- **Category:** `primitive`
- **Usage:** 18 imports (11 pages, 7 components)
- **Storybook:** yes — see ui/storybook/stories/ (thematic stories, not per-component)
- **File size:** 49 lines
## Props
[INFER] No `*Props` interface/type found by static extraction. See source file.
## Variants (CVA)
- **variant**: `default`, `secondary`, `destructive`, `outline`, `ghost`, `link`
## Used by
- **Pages:** `AdapterManager`, `CompanyAccess`, `DesignGuide`, `InstanceSettings`, `InviteUxLab` … (+6 more)
- **Components:** `ApprovalCard`, `BudgetIncidentCard`, `FilterBar`, `FinanceTimelineCard`, `IssueFiltersPopover` … (+2 more)

View File

@@ -0,0 +1,24 @@
# BudgetPolicyCard
`ui/src/components/BudgetPolicyCard.tsx`
[INFER] Composite component. No file-level docstring.
## Quick facts
- **Category:** `composite`
- **Usage:** 3 imports (3 pages, 0 components)
- **Storybook:** yes — see ui/storybook/stories/ (thematic stories, not per-component)
- **File size:** 220 lines
## Props
[INFER] No `*Props` interface/type found by static extraction. See source file.
## Composes
- **Primitives:** [button](./Button.md), [card](./Card.md), [input](./Input.md)
## Used by
- **Pages:** `AgentDetail`, `Costs`, `ProjectDetail`

View File

@@ -0,0 +1,27 @@
# Button
`ui/src/components/ui/button.tsx`
[INFER] Primitive component. No file-level docstring.
## Quick facts
- **Category:** `primitive`
- **Usage:** 81 imports (41 pages, 38 components)
- **Storybook:** yes — see ui/storybook/stories/ (thematic stories, not per-component)
- **File size:** 71 lines
## Props
[INFER] No `*Props` interface/type found by static extraction. See source file.
## Variants (CVA)
- **variant**: `default`, `destructive`, `outline`, `secondary`, `ghost`, `link`
- **size**: `default`, `xs`, `sm`, `lg`, `icon`, `icon-xs`, `icon-sm`, `icon-lg`
## Used by
- **Pages:** `AdapterManager`, `AgentDetail`, `Agents`, `ApprovalDetail`, `Auth` … (+36 more)
- **Components:** `AgentActionButtons`, `AgentConfigForm`, `ApprovalCard`, `BreadcrumbBar`, `BudgetIncidentCard` … (+33 more)

View File

@@ -0,0 +1,22 @@
# Card
`ui/src/components/ui/card.tsx`
[INFER] Primitive component. No file-level docstring.
## Quick facts
- **Category:** `primitive`
- **Usage:** 18 imports (10 pages, 8 components)
- **Storybook:** yes — see ui/storybook/stories/ (thematic stories, not per-component)
- **File size:** 93 lines
- **Sibling exports:** CardAction, CardContent, CardDescription, CardFooter, CardHeader, CardTitle
## Props
[INFER] No `*Props` interface/type found by static extraction. See source file.
## Used by
- **Pages:** `AdapterManager`, `Costs`, `DesignGuide`, `ExecutionWorkspaceDetail`, `InstanceSettings` … (+5 more)
- **Components:** `AccountingModelCard`, `BillerSpendCard`, `BudgetIncidentCard`, `BudgetPolicyCard`, `FinanceBillerCard` … (+3 more)

View File

@@ -0,0 +1,21 @@
# Checkbox
`ui/src/components/ui/checkbox.tsx`
[INFER] Primitive component. No file-level docstring.
## Quick facts
- **Category:** `primitive`
- **Usage:** 6 imports (4 pages, 2 components)
- **Storybook:** yes — see ui/storybook/stories/ (thematic stories, not per-component)
- **File size:** 33 lines
## Props
[INFER] No `*Props` interface/type found by static extraction. See source file.
## Used by
- **Pages:** `CompanyAccess`, `DesignGuide`, `InstanceAccess`, `NewAgent`
- **Components:** `IssueFiltersPopover`, `JsonSchemaForm`

View File

@@ -0,0 +1,22 @@
# Collapsible
`ui/src/components/ui/collapsible.tsx`
[INFER] Primitive component. No file-level docstring.
## Quick facts
- **Category:** `primitive`
- **Usage:** 8 imports (4 pages, 4 components)
- **Storybook:** no
- **File size:** 34 lines
- **Sibling exports:** CollapsibleContent, CollapsibleTrigger
## Props
[INFER] No `*Props` interface/type found by static extraction. See source file.
## Used by
- **Pages:** `AgentDetail`, `DesignGuide`, `RoutineDetail`, `Routines`
- **Components:** `IssuesList`, `RoutineVariablesEditor`, `SidebarAgents`, `SidebarProjects`

View File

@@ -0,0 +1,28 @@
# CompanyPatternIcon
`ui/src/components/CompanyPatternIcon.tsx`
[INFER] Standalone component. No file-level docstring.
## Quick facts
- **Category:** `standalone`
- **Usage:** 4 imports (3 pages, 1 components)
- **Storybook:** yes — see ui/storybook/stories/ (thematic stories, not per-component)
- **File size:** 218 lines
## Props
### `CompanyPatternIconProps`
```ts
companyName: string;
logoUrl?: string | null;
brandColor?: string | null;
className?: string;
logoFit?: "cover" | "contain";
```
## Used by
- **Pages:** `CompanySettings`, `InviteLanding`, `InviteUxLab`

View File

@@ -0,0 +1,32 @@
# CopyText
`ui/src/components/CopyText.tsx`
[INFER] Standalone component. No file-level docstring.
## Quick facts
- **Category:** `standalone`
- **Usage:** 3 imports (2 pages, 1 components)
- **Storybook:** yes — see ui/storybook/stories/ (thematic stories, not per-component)
- **File size:** 88 lines
## Props
### `CopyTextProps`
```ts
text: string;
/** What to display. Defaults to `text`. */
children?: React.ReactNode;
containerClassName?: string;
className?: string;
ariaLabel?: string;
title?: string;
/** Tooltip message shown after copying. Default: "Copied!" */
copiedLabel?: string;
```
## Used by
- **Pages:** `AgentDetail`, `ExecutionWorkspaceDetail`

View File

@@ -0,0 +1,26 @@
# Dialog
`ui/src/components/ui/dialog.tsx`
[INFER] Primitive component. No file-level docstring.
## Quick facts
- **Category:** `primitive`
- **Usage:** 21 imports (7 pages, 14 components)
- **Storybook:** yes — see ui/storybook/stories/ (thematic stories, not per-component)
- **File size:** 157 lines
- **Sibling exports:** DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogOverlay
## Props
[INFER] No `*Props` interface/type found by static extraction. See source file.
## Composes
- **Primitives:** [button](./Button.md)
## Used by
- **Pages:** `AdapterManager`, `CompanyAccess`, `CompanySkills`, `DesignGuide`, `Inbox` … (+2 more)
- **Components:** `DocumentDiffModal`, `IssueChatThread`, `KeyboardShortcutsCheatsheet`, `NewAgentDialog`, `NewGoalDialog` … (+9 more)

View File

@@ -0,0 +1,22 @@
# DropdownMenu
`ui/src/components/ui/dropdown-menu.tsx`
[INFER] Primitive component. No file-level docstring.
## Quick facts
- **Category:** `primitive`
- **Usage:** 8 imports (3 pages, 5 components)
- **Storybook:** no
- **File size:** 258 lines
- **Sibling exports:** DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuPortal
## Props
[INFER] No `*Props` interface/type found by static extraction. See source file.
## Used by
- **Pages:** `Companies`, `DesignGuide`, `Routines`
- **Components:** `CompanySwitcher`, `IssueChatThread`, `IssueColumns`, `IssueDocumentsSection`, `SidebarCompanyMenu`

View File

@@ -0,0 +1,31 @@
# EmptyState
`ui/src/components/EmptyState.tsx`
[INFER] Composite component. No file-level docstring.
## Quick facts
- **Category:** `composite`
- **Usage:** 20 imports (19 pages, 1 components)
- **Storybook:** yes — see ui/storybook/stories/ (thematic stories, not per-component)
- **File size:** 28 lines
## Props
### `EmptyStateProps`
```ts
icon: LucideIcon;
message: string;
action?: string;
onAction?: () => void;
```
## Composes
- **Primitives:** [button](./Button.md)
## Used by
- **Pages:** `Activity`, `Agents`, `CompanyExport`, `CompanyImport`, `CompanySkills` … (+14 more)

View File

@@ -0,0 +1,32 @@
# EntityRow
`ui/src/components/EntityRow.tsx`
[INFER] Standalone component. No file-level docstring.
## Quick facts
- **Category:** `standalone`
- **Usage:** 6 imports (6 pages, 0 components)
- **Storybook:** yes — see ui/storybook/stories/ (thematic stories, not per-component)
- **File size:** 70 lines
## Props
### `EntityRowProps`
```ts
leading?: ReactNode;
identifier?: string;
title: string;
subtitle?: string;
trailing?: ReactNode;
selected?: boolean;
to?: string;
onClick?: () => void;
className?: string;
```
## Used by
- **Pages:** `AgentDetail`, `Agents`, `DesignGuide`, `GoalDetail`, `MyIssues` … (+1 more)

View File

@@ -0,0 +1,32 @@
# Identity
`ui/src/components/Identity.tsx`
[INFER] Composite component. No file-level docstring.
## Quick facts
- **Category:** `composite`
- **Usage:** 19 imports (7 pages, 12 components)
- **Storybook:** yes — see ui/storybook/stories/ (thematic stories, not per-component)
- **File size:** 40 lines
## Props
### `IdentityProps`
```ts
name: string;
avatarUrl?: string | null;
initials?: string;
size?: IdentitySize;
className?: string;
```
## Composes
- **Primitives:** [avatar](./Avatar.md)
## Used by
- **Pages:** `AgentDetail`, `ApprovalDetail`, `Costs`, `Dashboard`, `DesignGuide` … (+2 more)

View File

@@ -0,0 +1,34 @@
# InlineEditor
`ui/src/components/InlineEditor.tsx`
[INFER] Standalone component. No file-level docstring.
## Quick facts
- **Category:** `standalone`
- **Usage:** 6 imports (4 pages, 2 components)
- **Storybook:** yes — see ui/storybook/stories/ (thematic stories, not per-component)
- **File size:** 310 lines
## Props
### `InlineEditorProps`
```ts
value: string;
onSave: (value: string) => void | Promise<unknown>;
as?: "h1" | "h2" | "p" | "span";
className?: string;
placeholder?: string;
multiline?: boolean;
imageUploadHandler?: (file: File) => Promise<string>;
/** Called when a non-image file is dropped onto the editor. */
onDropFile?: (file: File) => Promise<void>;
mentions?: MentionOption[];
nullable?: boolean;
```
## Used by
- **Pages:** `DesignGuide`, `GoalDetail`, `IssueDetail`, `ProjectDetail`

View File

@@ -0,0 +1,43 @@
# InlineEntitySelector
`ui/src/components/InlineEntitySelector.tsx`
[INFER] Composite component. No file-level docstring.
## Quick facts
- **Category:** `composite`
- **Usage:** 8 imports (2 pages, 4 components)
- **Storybook:** yes — see ui/storybook/stories/ (thematic stories, not per-component)
- **File size:** 215 lines
## Props
### `InlineEntitySelectorProps`
```ts
value: string;
options: InlineEntityOption[];
placeholder: string;
noneLabel: string;
searchPlaceholder: string;
emptyMessage: string;
onChange: (id: string) => void;
onConfirm?: () => void;
className?: string;
renderTriggerValue?: (option: InlineEntityOption | null) => ReactNode;
renderOption?: (option: InlineEntityOption, isSelected: boolean) => ReactNode;
recentOptionIds?: string[];
/** Skip the Portal so the popover stays in the DOM tree (fixes scroll inside Dialogs). */
disablePortal?: boolean;
/** Open the popover when the trigger receives keyboard/programmatic focus. */
openOnFocus?: boolean;
```
## Composes
- **Primitives:** [popover](./Popover.md)
## Used by
- **Pages:** `RoutineDetail`, `Routines`

View File

@@ -0,0 +1,21 @@
# Input
`ui/src/components/ui/input.tsx`
[INFER] Primitive component. No file-level docstring.
## Quick facts
- **Category:** `primitive`
- **Usage:** 20 imports (10 pages, 10 components)
- **Storybook:** yes — see ui/storybook/stories/ (thematic stories, not per-component)
- **File size:** 22 lines
## Props
[INFER] No `*Props` interface/type found by static extraction. See source file.
## Used by
- **Pages:** `AdapterManager`, `AgentDetail`, `Companies`, `CompanySkills`, `DesignGuide` … (+5 more)
- **Components:** `AgentIconPicker`, `BudgetIncidentCard`, `BudgetPolicyCard`, `IssueDocumentsSection`, `IssueFiltersPopover` … (+5 more)

View File

@@ -0,0 +1,73 @@
# IssueChatThread
`ui/src/components/IssueChatThread.tsx`
[INFER] Composite component. No file-level docstring.
## Quick facts
- **Category:** `composite`
- **Usage:** 4 imports (2 pages, 2 components)
- **Storybook:** yes — see ui/storybook/stories/ (thematic stories, not per-component)
- **File size:** 2399 lines
## Props
### `IssueChatComposerProps`
```ts
onImageUpload?: (file: File) => Promise<string>;
onAttachImage?: (file: File) => Promise<void>;
draftKey?: string;
enableReassign?: boolean;
reassignOptions?: InlineEntityOption[];
currentAssigneeValue?: string;
suggestedAssigneeValue?: string;
mentions?: MentionOption[];
agentMap?: Map<string, Agent>;
composerDisabledReason?: string | null;
issueStatus?: string;
```
### `IssueChatThreadProps`
```ts
comments: IssueChatComment[];
feedbackVotes?: FeedbackVote[];
feedbackDataSharingPreference?: FeedbackDataSharingPreference;
feedbackTermsUrl?: string | null;
linkedRuns?: IssueChatLinkedRun[];
timelineEvents?: IssueTimelineEvent[];
liveRuns?: LiveRunForIssue[];
activeRun?: ActiveRunForIssue | null;
blockedBy?: IssueRelationIssueSummary[];
companyId?: string | null;
projectId?: string | null;
issueStatus?: string;
agentMap?: Map<string, Agent>;
currentUserId?: string | null;
userLabelMap?: ReadonlyMap<string, string> | null;
userProfileMap?: ReadonlyMap<string, CompanyUserProfile> | null;
onVote?: (
commentId: string,
vote: FeedbackVoteValue,
options?: { allowSharing?: boolean; reason?: string
```
### `IssueChatErrorBoundaryProps`
```ts
resetKey: string;
messages: readonly ThreadMessage[];
emptyMessage: string;
variant: "full" | "embedded";
children: ReactNode;
```
## Composes
- **Primitives:** [avatar](./Avatar.md), [button](./Button.md), [dialog](./Dialog.md), `dropdown-menu`, [popover](./Popover.md), [textarea](./Textarea.md), [tooltip](./Tooltip.md)
## Used by
- **Pages:** `IssueChatUxLab`, `IssueDetail`

View File

@@ -0,0 +1,24 @@
# IssueFiltersPopover
`ui/src/components/IssueFiltersPopover.tsx`
[INFER] Composite component. No file-level docstring.
## Quick facts
- **Category:** `composite`
- **Usage:** 3 imports (1 pages, 2 components)
- **Storybook:** yes — see ui/storybook/stories/ (thematic stories, not per-component)
- **File size:** 366 lines
## Props
[INFER] No `*Props` interface/type found by static extraction. See source file.
## Composes
- **Primitives:** [badge](./Badge.md), [button](./Button.md), [checkbox](./Checkbox.md), [input](./Input.md), [popover](./Popover.md)
## Used by
- **Pages:** `Inbox`

View File

@@ -0,0 +1,25 @@
# IssueLinkQuicklook
`ui/src/components/IssueLinkQuicklook.tsx`
[INFER] Composite component. No file-level docstring.
## Quick facts
- **Category:** `composite`
- **Usage:** 3 imports (0 pages, 2 components)
- **Storybook:** yes — see ui/storybook/stories/ (thematic stories, not per-component)
- **File size:** 182 lines
- **Sibling exports:** IssueQuicklookCard
## Props
[INFER] No `*Props` interface/type found by static extraction. See source file.
## Composes
- **Primitives:** [popover](./Popover.md)
- **Composites:** [StatusIcon](./StatusIcon.md)
## Used by

View File

@@ -0,0 +1,20 @@
# IssueReferencePill
`ui/src/components/IssueReferencePill.tsx`
[INFER] Standalone component. No file-level docstring.
## Quick facts
- **Category:** `standalone`
- **Usage:** 4 imports (1 pages, 3 components)
- **Storybook:** no
- **File size:** 56 lines
## Props
[INFER] No `*Props` interface/type found by static extraction. See source file.
## Used by
- **Pages:** `DesignGuide`

View File

@@ -0,0 +1,38 @@
# IssueRow
`ui/src/components/IssueRow.tsx`
[INFER] Standalone component. No file-level docstring.
## Quick facts
- **Category:** `standalone`
- **Usage:** 3 imports (1 pages, 2 components)
- **Storybook:** yes — see ui/storybook/stories/ (thematic stories, not per-component)
- **File size:** 169 lines
## Props
### `IssueRowProps`
```ts
issue: Issue;
issueLinkState?: unknown;
selected?: boolean;
mobileLeading?: ReactNode;
desktopMetaLeading?: ReactNode;
desktopLeadingSpacer?: boolean;
mobileMeta?: ReactNode;
desktopTrailing?: ReactNode;
trailingMeta?: ReactNode;
titleSuffix?: ReactNode;
unreadState?: UnreadState | null;
onMarkRead?: () => void;
onArchive?: () => void;
archiveDisabled?: boolean;
className?: string;
```
## Used by
- **Pages:** `Inbox`

View File

@@ -0,0 +1,41 @@
# IssueWorkspaceCard
`ui/src/components/IssueWorkspaceCard.tsx`
[INFER] Composite component. No file-level docstring.
## Quick facts
- **Category:** `composite`
- **Usage:** 3 imports (1 pages, 2 components)
- **Storybook:** yes — see ui/storybook/stories/ (thematic stories, not per-component)
- **File size:** 522 lines
## Props
### `IssueWorkspaceCardProps`
```ts
issue: Omit<
Pick<
Issue,
| "companyId"
| "projectId"
| "projectWorkspaceId"
| "executionWorkspaceId"
| "executionWorkspacePreference"
| "executionWorkspaceSettings"
>,
"companyId"
> & {
companyId: string | null;
currentExecutionWorkspace?: ExecutionWorkspace | null;
```
## Composes
- **Primitives:** [button](./Button.md), [skeleton](./Skeleton.md)
## Used by
- **Pages:** `IssueDetail`

View File

@@ -0,0 +1,45 @@
# IssuesList
`ui/src/components/IssuesList.tsx`
[INFER] Composite component. No file-level docstring.
## Quick facts
- **Category:** `composite`
- **Usage:** 6 imports (5 pages, 1 components)
- **Storybook:** yes — see ui/storybook/stories/ (thematic stories, not per-component)
- **File size:** 1170 lines
## Props
### `IssuesListProps`
```ts
issues: Issue[];
isLoading?: boolean;
error?: Error | null;
agents?: Agent[];
projects?: ProjectOption[];
liveIssueIds?: Set<string>;
projectId?: string;
viewStateKey: string;
issueLinkState?: unknown;
initialAssignees?: string[];
initialWorkspaces?: string[];
initialSearch?: string;
searchFilters?: Omit<IssueListRequestFilters, "q" | "projectId" | "limit" | "includeRoutineExecutions">;
baseCreateIssueDefaults?: Record<string, unknown>;
createIssueLabel?: string;
enableRoutineVisibilityFilter?: boolean;
onSearchChange?: (search: string) => void;
onUpdateIssue: (id: string, data: Record<string, unknown>) => void;
```
## Composes
- **Primitives:** [button](./Button.md), [collapsible](./Collapsible.md), [input](./Input.md), [popover](./Popover.md)
## Used by
- **Pages:** `ExecutionWorkspaceDetail`, `IssueDetail`, `Issues`, `ProjectDetail`, `Routines`

View File

@@ -0,0 +1,21 @@
# Label
`ui/src/components/ui/label.tsx`
[INFER] Primitive component. No file-level docstring.
## Quick facts
- **Category:** `primitive`
- **Usage:** 8 imports (5 pages, 3 components)
- **Storybook:** yes — see ui/storybook/stories/ (thematic stories, not per-component)
- **File size:** 23 lines
## Props
[INFER] No `*Props` interface/type found by static extraction. See source file.
## Used by
- **Pages:** `AdapterManager`, `DesignGuide`, `PluginManager`, `ProfileSettings`, `RoutineDetail`
- **Components:** `JsonSchemaForm`, `RoutineRunVariablesDialog`, `RoutineVariablesEditor`

View File

@@ -0,0 +1,32 @@
# MarkdownBody
`ui/src/components/MarkdownBody.tsx`
[INFER] Standalone component. No file-level docstring.
## Quick facts
- **Category:** `standalone`
- **Usage:** 11 imports (5 pages, 6 components)
- **Storybook:** yes — see ui/storybook/stories/ (thematic stories, not per-component)
- **File size:** 326 lines
## Props
### `MarkdownBodyProps`
```ts
children: string;
className?: string;
style?: React.CSSProperties;
softBreaks?: boolean;
linkIssueReferences?: boolean;
/** Optional resolver for relative image paths (e.g. within export packages) */
resolveImageSrc?: (src: string) => string | null;
/** Called when a user clicks an inline image */
onImageClick?: (src: string) => void;
```
## Used by
- **Pages:** `AgentDetail`, `ApprovalDetail`, `CompanyExport`, `CompanyImport`, `CompanySkills`

View File

@@ -0,0 +1,39 @@
# MarkdownEditor
`ui/src/components/MarkdownEditor.tsx`
[INFER] Standalone component. No file-level docstring.
## Quick facts
- **Category:** `standalone`
- **Usage:** 16 imports (5 pages, 9 components)
- **Storybook:** yes — see ui/storybook/stories/ (thematic stories, not per-component)
- **File size:** 1204 lines
## Props
### `MarkdownEditorProps`
```ts
value: string;
onChange: (value: string) => void;
placeholder?: string;
className?: string;
contentClassName?: string;
onBlur?: () => void;
imageUploadHandler?: (file: File) => Promise<string>;
/** Called when a non-image file is dropped onto the editor (e.g. .zip). */
onDropFile?: (file: File) => Promise<void>;
bordered?: boolean;
/** List of mentionable entities. Enables @-mention autocomplete. */
mentions?: MentionOption[];
/** Called on Cmd/Ctrl+Enter */
onSubmit?: () => void;
/** Render the rich editor without allowing edits. */
readOnly?: boolean;
```
## Used by
- **Pages:** `AgentDetail`, `CompanySkills`, `IssueDetail`, `RoutineDetail`, `Routines`

View File

@@ -0,0 +1,21 @@
# PackageFileTree
`ui/src/components/PackageFileTree.tsx`
[INFER] Standalone component. No file-level docstring.
## Quick facts
- **Category:** `standalone`
- **Usage:** 3 imports (3 pages, 0 components)
- **Storybook:** yes — see ui/storybook/stories/ (thematic stories, not per-component)
- **File size:** 327 lines
- **Sibling exports:** FRONTMATTER_FIELD_LABELS
## Props
[INFER] No `*Props` interface/type found by static extraction. See source file.
## Used by
- **Pages:** `AgentDetail`, `CompanyExport`, `CompanyImport`

View File

@@ -0,0 +1,36 @@
# PageSkeleton
`ui/src/components/PageSkeleton.tsx`
[INFER] Composite component. No file-level docstring.
## Quick facts
- **Category:** `composite`
- **Usage:** 23 imports (22 pages, 1 components)
- **Storybook:** yes — see ui/storybook/stories/ (thematic stories, not per-component)
- **File size:** 181 lines
## Props
### `PageSkeletonProps`
```ts
variant?:
| "list"
| "issues-list"
| "detail"
| "dashboard"
| "approvals"
| "costs"
| "inbox"
| "org-chart";
```
## Composes
- **Primitives:** [skeleton](./Skeleton.md)
## Used by
- **Pages:** `Activity`, `AgentDetail`, `Agents`, `ApprovalDetail`, `Approvals` … (+17 more)

View File

@@ -0,0 +1,32 @@
# PageTabBar
`ui/src/components/PageTabBar.tsx`
[INFER] Composite component. No file-level docstring.
## Quick facts
- **Category:** `composite`
- **Usage:** 10 imports (9 pages, 1 components)
- **Storybook:** yes — see ui/storybook/stories/ (thematic stories, not per-component)
- **File size:** 46 lines
## Props
### `PageTabBarProps`
```ts
items: PageTabItem[];
value?: string;
onValueChange?: (value: string) => void;
align?: "center" | "start";
```
## Composes
- **Primitives:** [tabs](./Tabs.md)
## Used by
- **Pages:** `AgentDetail`, `Agents`, `Approvals`, `Costs`, `ExecutionWorkspaceDetail` … (+4 more)
- **Components:** `CompanySettingsNav`

View File

@@ -0,0 +1,30 @@
# PathInstructionsModal
`ui/src/components/PathInstructionsModal.tsx`
[INFER] Composite component. No file-level docstring.
## Quick facts
- **Category:** `composite`
- **Usage:** 12 imports (2 pages, 3 components)
- **Storybook:** yes — see ui/storybook/stories/ (thematic stories, not per-component)
- **File size:** 144 lines
- **Sibling exports:** ChoosePathButton
## Props
### `PathInstructionsModalProps`
```ts
open: boolean;
onOpenChange: (open: boolean) => void;
```
## Composes
- **Primitives:** [dialog](./Dialog.md)
## Used by
- **Pages:** `AdapterManager`, `ProjectWorkspaceDetail`

View File

@@ -0,0 +1,22 @@
# Popover
`ui/src/components/ui/popover.tsx`
[INFER] Primitive component. No file-level docstring.
## Quick facts
- **Category:** `primitive`
- **Usage:** 27 imports (6 pages, 20 components)
- **Storybook:** yes — see ui/storybook/stories/ (thematic stories, not per-component)
- **File size:** 89 lines
- **Sibling exports:** PopoverAnchor, PopoverContent, PopoverDescription, PopoverHeader, PopoverTitle, PopoverTrigger
## Props
[INFER] No `*Props` interface/type found by static extraction. See source file.
## Used by
- **Pages:** `AgentDetail`, `DesignGuide`, `Inbox`, `IssueDetail`, `NewAgent` … (+1 more)
- **Components:** `AgentConfigForm`, `AgentIconPicker`, `ExecutionParticipantPicker`, `GoalProperties`, `InlineEntitySelector` … (+15 more)

View File

@@ -0,0 +1,31 @@
# PriorityIcon
`ui/src/components/PriorityIcon.tsx`
[INFER] Composite component. No file-level docstring.
## Quick facts
- **Category:** `composite`
- **Usage:** 5 imports (2 pages, 3 components)
- **Storybook:** yes — see ui/storybook/stories/ (thematic stories, not per-component)
- **File size:** 78 lines
## Props
### `PriorityIconProps`
```ts
priority: string;
onChange?: (priority: string) => void;
className?: string;
showLabel?: boolean;
```
## Composes
- **Primitives:** [button](./Button.md), [popover](./Popover.md)
## Used by
- **Pages:** `DesignGuide`, `IssueDetail`

View File

@@ -0,0 +1,24 @@
# RoutineRunVariablesDialog
`ui/src/components/RoutineRunVariablesDialog.tsx`
[INFER] Composite component. No file-level docstring.
## Quick facts
- **Category:** `composite`
- **Usage:** 3 imports (2 pages, 1 components)
- **Storybook:** yes — see ui/storybook/stories/ (thematic stories, not per-component)
- **File size:** 519 lines
## Props
[INFER] No `*Props` interface/type found by static extraction. See source file.
## Composes
- **Primitives:** [button](./Button.md), [dialog](./Dialog.md), [input](./Input.md), [label](./Label.md), [select](./Select.md), [textarea](./Textarea.md)
## Used by
- **Pages:** `RoutineDetail`, `Routines`

View File

@@ -0,0 +1,32 @@
# RunTranscriptView
`ui/src/components/transcript/RunTranscriptView.tsx`
[INFER] Standalone component. No file-level docstring.
## Quick facts
- **Category:** `standalone`
- **Usage:** 3 imports (2 pages, 1 components)
- **Storybook:** no
- **File size:** 1446 lines
## Props
### `RunTranscriptViewProps`
```ts
entries: TranscriptEntry[];
mode?: TranscriptMode;
density?: TranscriptDensity;
limit?: number;
streaming?: boolean;
collapseStdout?: boolean;
emptyMessage?: string;
className?: string;
thinkingClassName?: string;
```
## Used by
- **Pages:** `AgentDetail`, `RunTranscriptUxLab`

View File

@@ -0,0 +1,22 @@
# ScrollArea
`ui/src/components/ui/scroll-area.tsx`
[INFER] Primitive component. No file-level docstring.
## Quick facts
- **Category:** `primitive`
- **Usage:** 3 imports (2 pages, 1 components)
- **Storybook:** no
- **File size:** 57 lines
- **Sibling exports:** ScrollBar
## Props
[INFER] No `*Props` interface/type found by static extraction. See source file.
## Used by
- **Pages:** `DesignGuide`, `IssueDetail`
- **Components:** `PropertiesPanel`

View File

@@ -0,0 +1,22 @@
# Select
`ui/src/components/ui/select.tsx`
[INFER] Primitive component. No file-level docstring.
## Quick facts
- **Category:** `primitive`
- **Usage:** 10 imports (5 pages, 5 components)
- **Storybook:** yes — see ui/storybook/stories/ (thematic stories, not per-component)
- **File size:** 189 lines
- **Sibling exports:** SelectContent, SelectGroup, SelectItem, SelectLabel, SelectScrollDownButton, SelectScrollUpButton
## Props
[INFER] No `*Props` interface/type found by static extraction. See source file.
## Used by
- **Pages:** `Activity`, `DesignGuide`, `Inbox`, `RoutineDetail`, `Routines`
- **Components:** `DocumentDiffModal`, `JsonSchemaForm`, `RoutineRunVariablesDialog`, `RoutineVariablesEditor`, `ScheduleEditor`

View File

@@ -0,0 +1,21 @@
# Separator
`ui/src/components/ui/separator.tsx`
[INFER] Primitive component. No file-level docstring.
## Quick facts
- **Category:** `primitive`
- **Usage:** 11 imports (7 pages, 4 components)
- **Storybook:** yes — see ui/storybook/stories/ (thematic stories, not per-component)
- **File size:** 29 lines
## Props
[INFER] No `*Props` interface/type found by static extraction. See source file.
## Used by
- **Pages:** `DesignGuide`, `ExecutionWorkspaceDetail`, `Inbox`, `IssueDetail`, `PluginSettings` … (+2 more)
- **Components:** `AgentProperties`, `GoalProperties`, `IssueProperties`, `ProjectProperties`

View File

@@ -0,0 +1,33 @@
# SidebarNavItem
`ui/src/components/SidebarNavItem.tsx`
[INFER] Standalone component. No file-level docstring.
## Quick facts
- **Category:** `standalone`
- **Usage:** 3 imports (0 pages, 3 components)
- **Storybook:** no
- **File size:** 95 lines
## Props
### `SidebarNavItemProps`
```ts
to: string;
label: string;
icon: LucideIcon;
end?: boolean;
className?: string;
badge?: number;
badgeTone?: "default" | "danger";
textBadge?: string;
textBadgeTone?: "default" | "amber";
alert?: boolean;
liveCount?: number;
```
## Used by

View File

@@ -0,0 +1,21 @@
# Skeleton
`ui/src/components/ui/skeleton.tsx`
[INFER] Primitive component. No file-level docstring.
## Quick facts
- **Category:** `primitive`
- **Usage:** 6 imports (3 pages, 3 components)
- **Storybook:** no
- **File size:** 14 lines
## Props
[INFER] No `*Props` interface/type found by static extraction. See source file.
## Used by
- **Pages:** `AgentDetail`, `DesignGuide`, `IssueDetail`
- **Components:** `IssueWorkspaceCard`, `PageSkeleton`, `ProviderQuotaCard`

View File

@@ -0,0 +1,20 @@
# StatusBadge
`ui/src/components/StatusBadge.tsx`
[INFER] Standalone component. No file-level docstring.
## Quick facts
- **Category:** `standalone`
- **Usage:** 19 imports (12 pages, 7 components)
- **Storybook:** yes — see ui/storybook/stories/ (thematic stories, not per-component)
- **File size:** 16 lines
## Props
[INFER] No `*Props` interface/type found by static extraction. See source file.
## Used by
- **Pages:** `AgentDetail`, `Agents`, `ApprovalDetail`, `Costs`, `DesignGuide` … (+7 more)

View File

@@ -0,0 +1,32 @@
# StatusIcon
`ui/src/components/StatusIcon.tsx`
[INFER] Composite component. No file-level docstring.
## Quick facts
- **Category:** `composite`
- **Usage:** 14 imports (5 pages, 9 components)
- **Storybook:** no
- **File size:** 72 lines
## Props
### `StatusIconProps`
```ts
status: string;
onChange?: (status: string) => void;
className?: string;
showLabel?: boolean;
```
## Composes
- **Primitives:** [button](./Button.md), [popover](./Popover.md)
## Used by
- **Pages:** `Dashboard`, `DesignGuide`, `Inbox`, `IssueDetail`, `MyIssues`
- **Components:** `IssueLinkQuicklook` … (+8 more)

View File

@@ -0,0 +1,27 @@
# Tabs
`ui/src/components/ui/tabs.tsx`
[INFER] Primitive component. No file-level docstring.
## Quick facts
- **Category:** `primitive`
- **Usage:** 15 imports (13 pages, 2 components)
- **Storybook:** yes — see ui/storybook/stories/ (thematic stories, not per-component)
- **File size:** 90 lines
- **Sibling exports:** TabsContent, TabsList, TabsTrigger
## Props
[INFER] No `*Props` interface/type found by static extraction. See source file.
## Variants (CVA)
- **variant**: `default`, `line`
## Used by
- **Pages:** `AgentDetail`, `Agents`, `Approvals`, `Costs`, `DesignGuide` … (+8 more)
- **Components:** `CompanySettingsNav`, `PageTabBar`

View File

@@ -0,0 +1,21 @@
# Textarea
`ui/src/components/ui/textarea.tsx`
[INFER] Primitive component. No file-level docstring.
## Quick facts
- **Category:** `primitive`
- **Usage:** 9 imports (4 pages, 5 components)
- **Storybook:** yes — see ui/storybook/stories/ (thematic stories, not per-component)
- **File size:** 19 lines
## Props
[INFER] No `*Props` interface/type found by static extraction. See source file.
## Used by
- **Pages:** `ApprovalDetail`, `CompanySkills`, `DesignGuide`, `ExecutionWorkspaceDetail`
- **Components:** `IssueChatThread`, `JsonSchemaForm`, `OutputFeedbackButtons`, `RoutineRunVariablesDialog`, `RoutineVariablesEditor`

View File

@@ -0,0 +1,21 @@
# ToggleSwitch
`ui/src/components/ui/toggle-switch.tsx`
[INFER] Primitive component. No file-level docstring.
## Quick facts
- **Category:** `primitive`
- **Usage:** 8 imports (5 pages, 3 components)
- **Storybook:** yes — see ui/storybook/stories/ (thematic stories, not per-component)
- **File size:** 60 lines
## Props
[INFER] No `*Props` interface/type found by static extraction. See source file.
## Used by
- **Pages:** `AgentDetail`, `InstanceExperimentalSettings`, `InstanceGeneralSettings`, `RoutineDetail`, `Routines`
- **Components:** `NewIssueDialog`, `ProjectProperties`, `agent-config-primitives`

View File

@@ -0,0 +1,22 @@
# Tooltip
`ui/src/components/ui/tooltip.tsx`
[INFER] Primitive component. No file-level docstring.
## Quick facts
- **Category:** `primitive`
- **Usage:** 11 imports (3 pages, 7 components)
- **Storybook:** yes — see ui/storybook/stories/ (thematic stories, not per-component)
- **File size:** 58 lines
- **Sibling exports:** TooltipContent, TooltipProvider, TooltipTrigger
## Props
[INFER] No `*Props` interface/type found by static extraction. See source file.
## Used by
- **Pages:** `AgentDetail`, `CompanySkills`, `DesignGuide`
- **Components:** `CompanyRail`, `IssueChatThread`, `IssueColumns`, `NewProjectDialog`, `ProjectProperties` … (+2 more)

View File

@@ -0,0 +1,38 @@
# WorkspaceRuntimeControls
`ui/src/components/WorkspaceRuntimeControls.tsx`
[INFER] Composite component. No file-level docstring.
## Quick facts
- **Category:** `composite`
- **Usage:** 3 imports (2 pages, 1 components)
- **Storybook:** yes — see ui/storybook/stories/ (thematic stories, not per-component)
- **File size:** 454 lines
## Props
### `WorkspaceRuntimeControlsProps`
```ts
sections: WorkspaceRuntimeControlSections;
items?: never;
isPending?: boolean;
pendingRequest?: WorkspaceRuntimeControlRequest | null;
serviceEmptyMessage?: string;
jobEmptyMessage?: string;
emptyMessage?: never;
disabledHint?: string | null;
onAction: (request: WorkspaceRuntimeControlRequest) => void;
className?: string;
square?: boolean;
```
## Composes
- **Primitives:** [button](./Button.md)
## Used by
- **Pages:** `ExecutionWorkspaceDetail`, `ProjectWorkspaceDetail`

View File

@@ -0,0 +1,25 @@
# agent-config-primitives
`ui/src/components/agent-config-primitives.tsx`
[INFER] Composite component. No file-level docstring.
## Quick facts
- **Category:** `composite`
- **Usage:** 19 imports (4 pages, 3 components)
- **Storybook:** yes — see ui/storybook/stories/ (thematic stories, not per-component)
- **File size:** 464 lines
- **Sibling exports:** AutoExpandTextarea, ChoosePathButton, CollapsibleSection, DraftInput, DraftNumberInput, DraftTextarea
## Props
[INFER] No `*Props` interface/type found by static extraction. See source file.
## Composes
- **Primitives:** [button](./Button.md), [dialog](./Dialog.md), `toggle-switch`, [tooltip](./Tooltip.md)
## Used by
- **Pages:** `AgentDetail`, `CompanyImport`, `CompanySettings`, `NewAgent`

View File

@@ -0,0 +1,393 @@
# Components Review
- **Generated:** 2026-04-21
- **Repo SHA:** a26e1288b627e82c554445732c7d844648e6b5e1
- **Inventory:** [index.md](./index.md)
- **Token drift feeding components:** [../tokens/tokens-review.md](../tokens/tokens-review.md)
- **Pattern docs drawn from this review:** [../patterns/index.md](../patterns/index.md)
---
## How to read this document
This is a **persistent backlog**, not just a snapshot review. It captures opportunities identified during DS extraction. **All items are intentionally deferred** per the 2026-04-21 decision: no component merges, no renames, no file-casing changes were made. Address opportunistically when the relevant code is touched for other reasons, or schedule as standalone follow-up projects.
Each entry below includes:
- A **problem statement** — what was observed and why it might matter.
- **Affected files** — so the next developer doesn't have to re-trace.
- A **suggested resolution** — or "needs discussion" when the right call is not obvious.
Entries are ordered by expected value, not by stage. Sections are self-contained — you can pick one up cold without reading the rest.
---
## Likely duplicates
Pairs or families whose names, compositions, or role overlap enough that they may be consolidatable. None of these are automatic merges — each is a judgment call. Items 17 and 9 are documented as single patterns in [../patterns/](../patterns/) so the family is visible to future pattern extraction; this section captures the consolidation opportunity.
### 1. Entity-creation dialog family: `NewAgentDialog` / `NewGoalDialog` / `NewIssueDialog` / `NewProjectDialog`
Four parallel "create new X" dialogs, each used 12 times, each a composite that wraps a form-in-a-dialog. Strong candidate for a single generic `NewEntityDialog` or a `useNewEntityForm` hook plus entity-specific field sets.
| File | Uses |
|---|---|
| `ui/src/components/NewAgentDialog.tsx` | 1 |
| `ui/src/components/NewGoalDialog.tsx` | 1 |
| `ui/src/components/NewIssueDialog.tsx` | 2 |
| `ui/src/components/NewProjectDialog.tsx` | 1 |
**Verify:** open the four files and diff them. If ≥60% body overlap, consolidate. Impact on Stage 4: otherwise a "New-entity dialog" pattern would be proposed with 4 instances, which is better represented as a single component.
### 2. Properties-panel family: `AgentProperties` / `GoalProperties` / `IssueProperties` / `ProjectProperties` (+ generic `PropertiesPanel`)
**Problem.** Four entity-specific property-panel components sit next to a generic `PropertiesPanel`. It is unclear whether `PropertiesPanel` **composes** the four (i.e., hosts their body via a context slot — which is what the 29-line `PropertiesPanel.tsx` appears to do) or whether the four **duplicate** the outer-chrome work that `PropertiesPanel` could own. `AgentProperties` is also unused in production (Storybook-only).
**Affected files.**
| File | Uses | Notes |
|---|---|---|
| `ui/src/components/PropertiesPanel.tsx` | 1 | 29-line generic chrome; reads `panelContent` from `usePanel()` context |
| `ui/src/components/IssueProperties.tsx` | 2 | 1370-line entity-specific body |
| `ui/src/components/GoalProperties.tsx` | 1 | Entity-specific body |
| `ui/src/components/ProjectProperties.tsx` | 1 | 1140-line entity-specific body |
| `ui/src/components/AgentProperties.tsx` | **0** | Storybook-only — see §Unused components |
**Suggested resolution: needs discussion.** Per the 2026-04-21 decision, the open question of "composes vs duplicates" is **flagged but not resolved**. Reading the source suggests `PropertiesPanel` is the outer slot and the four bodies are what gets passed into it — so not a literal duplicate. But the four bodies each re-roll section headers, separators, save-state plumbing, and field layout; those might be factor-able into a shared `<PropertiesPanelBody>` helper. Open `IssueProperties.tsx` and `ProjectProperties.tsx` side-by-side before making any call. Documented as-is in [../patterns/entity-properties-panel.md](../patterns/entity-properties-panel.md).
### 3. Subscription panel pair: `ClaudeSubscriptionPanel` ↔ `CodexSubscriptionPanel`
**Problem.** Two components with parallel names, parallel props shape (both accept `windows: QuotaWindow[]` + optional `source` / `error`), rendering ordered subscription-quota windows for a single provider. Both used exactly once — always dispatched from `ProviderQuotaCard`. When a third vendor (Gemini? Cursor?) is added, the pattern becomes "a third copy-paste" unless consolidated. Today's cost of keeping them separate is minor because the pair is only 2.
**Affected files.**
- `ui/src/components/ClaudeSubscriptionPanel.tsx` (1 use — 140 lines)
- `ui/src/components/CodexSubscriptionPanel.tsx` (1 use)
- `ui/src/components/ProviderQuotaCard.tsx` — composes both, dispatches by vendor
**Suggested resolution.** Diff-test the two files. If ≥70% body overlap, collapse to `SubscriptionPanel({ vendor: "claude" | "codex" | … })` with per-vendor window-key config as data. If divergence is higher, keep the pair and let `ProviderQuotaCard` continue to dispatch. Documented as-is in [../patterns/subscription-panel.md](../patterns/subscription-panel.md).
### 4. Sidebar-menu pair: `SidebarAccountMenu` ↔ `SidebarCompanyMenu`
**Problem.** Two dropdown components anchored to the sidebar slot — one for account actions (user profile, sign out), one for company actions (switch company, settings). Same visual affordance (dropdown triggered from a sidebar button), different data subject. Each used twice; neither is a hotspot.
**Affected files.**
- `ui/src/components/SidebarAccountMenu.tsx` (2 uses)
- `ui/src/components/SidebarCompanyMenu.tsx` (2 uses)
**Suggested resolution.** Read both and check the body overlap. Natural consolidations if similar: `<SidebarMenu kind="account" | "company">` driven by kind, or a more general `<SidebarMenu>` that accepts items via a `children` slot. If the bodies are genuinely different (different menu items, different trigger layout), the pair stays and the shared pattern is the sidebar-slot anchoring — which is already captured by `SidebarNavItem`. Documented as-is in [../patterns/sidebar-chrome.md — §The sidebar-menu pair](../patterns/sidebar-chrome.md#the-sidebar-menu-pair).
### 5. Finance card family: `BillerSpendCard` / `FinanceBillerCard` / `FinanceKindCard` / `FinanceTimelineCard` / `AccountingModelCard`
**Problem.** Five cards in the finance/accounting surface, each used 01 times, all sharing the shadcn `Card` family as substrate. Two specific flags surfaced per the 2026-04-21 directive:
1. **`BillerSpendCard``FinanceBillerCard` — likely a true duplicate.** Both name "Biller" in their filename. Both render a per-biller financial summary. They consume **different data models** (`CostByBiller` vs `FinanceByBiller`) — either two genuinely different reporting concepts whose names fail to distinguish them, or one superseded the other and the predecessor survived. Line counts differ (145 vs 44), consistent with a "rich" / "slim" pair.
2. **`AccountingModelCard` is unused.** Zero imports anywhere in the codebase; only appears in Storybook. Either abandoned or awaiting a page.
**Affected files.**
| File | Uses | Data model | Lines |
|---|---|---|---|
| `ui/src/components/BillerSpendCard.tsx` | 1 | `CostByBiller` + `CostByProviderModel` | 145 |
| `ui/src/components/FinanceBillerCard.tsx` | 1 | `FinanceByBiller` | 44 |
| `ui/src/components/FinanceKindCard.tsx` | 1 | `FinanceByKind` (inferred) | — |
| `ui/src/components/FinanceTimelineCard.tsx` | 1 | timeline rollup | — |
| `ui/src/components/AccountingModelCard.tsx` | **0** | — | — |
**Suggested resolution.**
- Diff `BillerSpendCard` vs `FinanceBillerCard` side-by-side. Rename for clarity, merge, or confirm as distinct-but-adjacent.
- Decide `AccountingModelCard`'s fate — adopt it into a page, or delete.
- The broader family (4 cards) is naturally a "Finance / accounting card" pattern. Documented as-is in [../patterns/finance-card.md](../patterns/finance-card.md).
### 6. Row family: `ActivityRow` / `EntityRow` / `IssueRow`
**Problem.** Three row components, one generic and two entity-specific. `EntityRow` is the generic (6 uses), `IssueRow` is issue-specific with an extensive slot interface (3 uses — Inbox + SwipeToArchive), `ActivityRow` is activity-event-specific (2 uses — Activity page). `IssueRow`'s 15-field prop shape (six of them optional `ReactNode` slot props) looks like it could be expressed as `<EntityRow kind="issue" />` plus issue-specific defaults.
**Affected files.**
| File | Uses | Notes |
|---|---|---|
| `ui/src/components/EntityRow.tsx` | 6 | Truly generic — leading / identifier / title / subtitle / trailing slot API |
| `ui/src/components/IssueRow.tsx` | 3 | Composes `StatusIcon`; unread-state + archive action + mobile/desktop split |
| `ui/src/components/ActivityRow.tsx` | 2 | Composes `Identity`, `IssueReferenceActivitySummary` — activity-event-specific |
**Suggested resolution.** Compare `IssueRow` against `EntityRow` directly. If the six slot props plus `unreadState` / `onArchive` can be implemented as `EntityRow` extensions (default slots keyed on `issue`), collapse. `ActivityRow`'s activity-verb formatting is genuinely specialized and is likely worth keeping separate even if `IssueRow` consolidates. Also relevant: the main list pages (Issues, Agents, Projects, …) don't use `EntityRow` today — see [../patterns/list-page.md — Open questions](../patterns/list-page.md#open-questions--risks). Documented as-is in [../patterns/entity-row.md](../patterns/entity-row.md).
### 7. Sidebar triad: `Sidebar` / `InstanceSidebar` / `CompanySettingsSidebar` + `CompanyRail` + `CompanySettingsNav` + `MobileBottomNav`
**Problem.** Six components with overlapping responsibilities — all are "chrome around the main view." Not literal duplicates because each targets a different surface (main nav vs instance settings vs company settings vs company switcher vs tab nav vs mobile bottom bar), but the three-word vocabulary (Sidebar / Rail / Nav) obscures whether these are variants of one structural pattern or separate components that happen to live near each other. They share navigation primitives (`SidebarNavItem`, `SidebarSection`) but not a unifying wrapper. The dead `sidebar-*` tokens in `ui/src/index.css` were designed for this family and none of these consume them (see [tokens-review.md §3](../tokens/tokens-review.md#3-sidebar--tokens-are-dead)).
**Affected files.**
| File | Uses | Role (inferred) |
|---|---|---|
| `ui/src/components/Sidebar.tsx` | ≥3 | Main app navigation |
| `ui/src/components/InstanceSidebar.tsx` | 1 | Instance-settings scope |
| `ui/src/components/CompanySettingsSidebar.tsx` | 2 | Company-settings scope |
| `ui/src/components/CompanyRail.tsx` | 1 (260 lines, dnd-kit-driven) | Sortable company switcher rail |
| `ui/src/components/access/CompanySettingsNav.tsx` | 1 | Settings-page top tab nav |
| `ui/src/components/MobileBottomNav.tsx` | ≥3 | Mobile-bottom-tab alternative |
**Suggested resolution: needs discussion.** Three possible framings:
- **(a) They are genuinely different components** (different layouts, different primitives, different affordances) and the naming convergence is coincidental. Closest to how the code reads today.
- **(b) They are variants of a `<Sidebar variant="main" | "settings" | "rail" | …>`** and should consolidate under one name. Requires auditing their visual shape.
- **(c) They are three distinct patterns** — "sidebar" (persistent rail), "rail" (narrow-strip), "nav" (tab-bar) — and the current spread is correct but the naming convention for picking between them isn't written down anywhere.
Tied to [§Naming inconsistencies — Sidebar / Rail / Nav](#sidebar--rail--nav). Documented as-is in [../patterns/sidebar-chrome.md](../patterns/sidebar-chrome.md).
### 8. Status display triad: `StatusIcon` / `StatusBadge` / `PriorityIcon`
**Problem.** Three components render entity status/priority across the app with the same visual language but different affordances. All three consume `ui/src/lib/status-colors.ts` — a canonical TypeScript catalog mapping status strings to raw Tailwind-palette classes. Two of the three (`StatusIcon`, `PriorityIcon`) type their primary prop as an **untyped string**; `StatusBadge` uses a typed variant. This inconsistency plus the fact that the catalog bypasses the DS token layer makes this a pattern-shape-pending item rather than a straightforward duplicate flag.
**Affected files.**
| File | Uses | Prop type | Notes |
|---|---|---|---|
| `ui/src/components/StatusIcon.tsx` | 14 | `status: string` (untyped) | Circle + popover picker |
| `ui/src/components/StatusBadge.tsx` | 19 | `{ status: string }` wrapping `statusBadge[status]` | 15-line pill |
| `ui/src/components/PriorityIcon.tsx` | 5 | `priority: string` (untyped) | Arrow/triangle + popover picker |
| `ui/src/lib/status-colors.ts` | — | — | Catalog consumed by all three |
| `ui/src/components/AgentActionButtons.tsx` | — | — | Consumes `agentStatusDot` from the same catalog |
**2026-04-21 status.** The token-side of this problem was **partially addressed**: `--signal-success` / `--signal-success-foreground` landed as action-severity tokens paired with `--destructive` (see [tokens-review.md §4](../tokens/tokens-review.md#4-status-colorsts-is-a-canonical-semantic-color-catalog-that-bypasses-the-ds)). `status-colors.ts` itself is **not** touched — tokenizing its entity-state coloring into a `--status-*` family is a deferred future project. So the two problems here are now independent:
- **Signal (action severity)** = tokens exist, no consumers yet, opt-in.
- **Status (entity state)** = catalog stays as raw Tailwind palette, unchanged.
**Suggested resolution: do not codify a unified status-display pattern in this DS pass.** Pattern shape is explicitly pending the eventual signal-token / status-token scoping. When that project happens, the shape of these three components (typed enum props, class naming, default-fallback behavior) will want to change in sync. Documented as-is in [../patterns/status-display.md](../patterns/status-display.md).
Separately — and independent of the tokens — the `status: string` / `priority: string` untyped props could be typed as string literal unions over the keys of `status-colors.ts`'s records without touching colors. That's a small, self-contained follow-up.
### 9. Quota display: `ProviderQuotaCard` ↔ `QuotaBar`
**Problem.** Two components share the "quota" root name but differ in suffix and in role. `QuotaBar` is a rendering primitive — one horizontal bar with a percent-used fill and a three-level color threshold. `ProviderQuotaCard` is a card composer that *uses* `QuotaBar` (multiple times, for different time windows) plus `ClaudeSubscriptionPanel` / `CodexSubscriptionPanel`. Functionally different, but the `-Bar` / `-Card` naming spread suggests parallelism that isn't there.
Secondary concern: `QuotaBar` hardcodes three-level severity as raw Tailwind palette (`bg-red-400`, `bg-yellow-400`, `bg-green-400`). It is one of four places in the codebase that encode the same red/amber/green severity language without shared tokens — see [../tokens/tokens-review.md §4](../tokens/tokens-review.md#4-status-colorsts-is-a-canonical-semantic-color-catalog-that-bypasses-the-ds) and [../patterns/patterns-review.md §6 — Severity indicator](../patterns/patterns-review.md#6-severity-indicator-3-level-health-display--pattern-opportunity).
**Affected files.**
- `ui/src/components/QuotaBar.tsx` (2 uses — 65 lines — primitive bar)
- `ui/src/components/ProviderQuotaCard.tsx` (1 use — 416 lines — card composer that embeds `QuotaBar`)
**Suggested resolution.** Not a consolidation target — different roles. The naming can be clearer if a future refactor happens (e.g., rename the composer to `ProviderQuotaSummary` or `QuotaOverviewCard`). Separate concern: the three-level severity coloring in `QuotaBar` would collapse onto signal/status tokens when that broader work lands. Documented as-is in [../patterns/quota-display.md](../patterns/quota-display.md).
---
## Naming inconsistencies
**Status: deferred (2026-04-21).** All vocabulary decisions below (Dialog-vs-Modal, Picker-vs-Selector, Editor-vs-Form, Sidebar-vs-Rail-vs-Nav, Card-vs-Panel-vs-Widget, file-name casing, prop-vocabulary conventions) are intentionally unresolved. Addressing opportunistically when the relevant code is touched for other reasons, or as a standalone naming-pass project. Each subsection is self-contained — pick any one up cold without reading the others. In pattern docs under `../patterns/`, vocabulary is noted as observed variance (e.g., "dialog pattern — some implementations named `*Modal`, same primitive") rather than as a canonical prescription.
### Container-word proliferation: `Card` vs `Panel` vs `Widget` vs `Modal` vs `Dialog`
Counts of components by suffix:
| Suffix | Count | Examples |
|---|---|---|
| `Card` | 12 | `ApprovalCard`, `BudgetPolicyCard`, `MetricCard`, … |
| `Panel` | 5 | `ActiveAgentsPanel`, `ClaudeSubscriptionPanel`, `PropertiesPanel`, … |
| `Widget` | 1 | `LiveRunWidget` |
| `Modal` | 3 | `DocumentDiffModal`, `ImageGalleryModal`, `PathInstructionsModal` |
| `Dialog` | 6 | `NewAgentDialog`, `ExecutionWorkspaceCloseDialog`, `RoutineRunVariablesDialog`, … |
**Dialog vs Modal:** both Modal- and Dialog-named components use the same `dialog.tsx` primitive. There's no structural distinction — just two names for the same thing. Pick one. The shadcn default is `Dialog`; keeping `Dialog` is the lower-friction move.
**Card vs Panel:** less clear-cut. Rough pattern in this codebase:
- `*Card` when the thing is a discrete piece of content in a grid (`BudgetPolicyCard`, `FinanceBillerCard`).
- `*Panel` when the thing is a larger region that groups related content (`ActiveAgentsPanel`, `PropertiesPanel`).
- But there are violations: `ClaudeSubscriptionPanel` and `AccountingModelCard` look alike structurally and are adjacent in usage.
**Widget (1):** only `LiveRunWidget` uses this. Either absorb into `Card`/`Panel` or codify `Widget` as a distinct concept (e.g., "dashboard-tile with its own data fetch and refresh cadence") and use it consistently.
### `Picker` vs `Selector`
| Name | Uses | Picks what |
|---|---|---|
| `AgentIconPicker` | 13 | An icon |
| `ExecutionParticipantPicker` | 0 (unused) | A participant |
| `ReportsToPicker` | 2 | An agent |
| `InlineEntitySelector` | 1 | An entity |
All four wrap a popover-plus-list. Picker and Selector are synonymous here. Pick one term.
### `Editor` vs `Form`
| Name | Uses | Purpose |
|---|---|---|
| `MarkdownEditor` | 16 | Markdown input |
| `InlineEditor` | 6 | Generic text editor |
| `EnvVarEditor` | 2 | Structured key=value list |
| `RoutineVariablesEditor` | 2 | Structured variable list |
| `ScheduleEditor` | 1 | Cron schedule |
| `AgentConfigForm` | 5 | Full agent config |
| `JsonSchemaForm` | 1 | Schema-driven form |
The line between Editor and Form is fuzzy: `RoutineVariablesEditor` looks like a form, `JsonSchemaForm` could have been named `JsonSchemaEditor`. Suggested rule: **Editor** for content inputs (text, schedule, markdown); **Form** for labeled-field structured forms. Audit whether any renaming is worth the churn; otherwise document the rule and enforce for new additions.
### Sidebar / Rail / Nav
For what are structurally all "chrome around the main content area":
- `Sidebar.tsx`
- `InstanceSidebar.tsx`
- `CompanySettingsSidebar.tsx`
- `CompanyRail.tsx`
- `MobileBottomNav.tsx`
- `access/CompanySettingsNav.tsx`
Pick a default term (`Sidebar`) and use a prefix for variants (`InstanceSidebar`, `CompanySettingsSidebar`). Reserve `Rail` for genuinely different (narrow-strip) affordances, `Nav` for tab-bar-style navigation. The current split is inconsistent with itself.
### File-naming convention is inconsistent
Most composites are `PascalCase.tsx`. Shadcn primitives are `kebab-case.tsx`. But `agent-config-primitives.tsx` and `agent-config-defaults.ts` sit in the top-level composite directory in kebab-case — they don't belong with the shadcn primitives (not in `ui/`) but also don't match the PascalCase of their neighbors.
Suggested fix: rename `agent-config-primitives.tsx``AgentConfigPrimitives.tsx` (or split into per-component files if the 11 exports warrant it) and `agent-config-defaults.ts``agentConfigDefaults.ts` to match JS convention for non-component modules.
### Prop vocabulary is underspecified
A static scan for conventional variant-shaping props (`variant`, `size`, `intent`, `tone`, `kind`, `state`, `status`, `mode`, `level`, `severity`, `priority`) found them used in just **7** components across the codebase (excluding shadcn primitives where they're well-defined):
| Prop | Component | Type |
|---|---|---|
| `variant` | `IssueChatThread` | `"full" \| "embedded"` |
| `variant` | `PageSkeleton` | `... \| "list"` (opaque) |
| `size` | `Identity` | `IdentitySize` |
| `kind` | `ProjectWorkspaceSummaryCard` | `"project_workspace" \| "execution_workspace"` |
| `status` | `StatusIcon` | `string` (**untyped**) |
| `mode` | `RunTranscriptView` | `TranscriptMode` |
| `priority` | `PriorityIcon` | `string` (**untyped**) |
Observations:
- `status: string` in `StatusIcon` and `priority: string` in `PriorityIcon` should be typed enums (matching the keys of `status-colors.ts`).
- `variant` is used for completely unrelated concepts in different components — that's fine semantically, but reinforces that there's no shared prop-vocabulary convention.
- Shadcn primitives (`button`, `badge`) use `variant`/`size` with well-defined CVA enums — these are the model.
---
## Token non-compliance
(Mirror of the Stage 1 token drift findings, but attributed per-component so Stage 3 reviewers can target the worst offenders.)
### Components that hardcode chart/status colors
From [tokens-review.md §1 and §4](../tokens/tokens-review.md):
| Component | Drift |
|---|---|
| `ActivityCharts.tsx` | 17 hardcoded Tailwind-palette hex values for status/priority chart colors; uses `chart-*` tokens zero times. |
| `OrgChart.tsx` (a page) | 6 hardcoded hex values for agent status dot colors. |
| `StatusIcon.tsx` (14 uses) | Consumes `issueStatusIcon` from `status-colors.ts` — raw Tailwind palette classes (`text-blue-600`, `border-violet-600`, etc.). |
| `StatusBadge.tsx` (19 uses) | Consumes `statusBadge` from `status-colors.ts` — raw Tailwind palette classes. |
| `PriorityIcon.tsx` (5 uses) | Consumes `priorityColor` — raw Tailwind palette. |
| `AgentActionButtons.tsx` | Uses `agentStatusDot` — raw Tailwind palette. |
### Components with heavy raw-palette styling
From Stage 1's 659-hit analysis:
- `AgentDetail.tsx` (75 palette hits) — production page
- `RunTranscriptView.tsx` (47 hits) — production component
- `IssueChatThread.tsx` (22 hits) — production component
### Components with arbitrary radius values
See [tokens-review.md §7](../tokens/tokens-review.md#7-arbitrary-radius-values-bypass-the-scale-18-occurrences) for the full list. Production components in the list:
- `CompanyRail.tsx``rounded-[14px]`, `rounded-[22px]`
- Several UxLab pages (acceptable as prototypes)
### Recommendation
Do not extract per-component detail docs for `StatusIcon`, `StatusBadge`, `PriorityIcon`, `AgentActionButtons` as final specs — their color language is blocked on the signal-token decision from [tokens-review.md §4](../tokens/tokens-review.md#4-status-colorsts-is-a-canonical-semantic-color-catalog-that-bypasses-the-ds). Stage-4 patterns that lean on these (status indicator, priority indicator, agent card with status) will change shape once signal tokens land.
---
## Story coverage gaps
Components with **3+ code uses and no Storybook coverage** — the highest-priority story gaps:
| Component | Uses | Notes |
|---|---|---|
| `StatusIcon` | 14 | Central status-color consumer. Coverage gap tied to §Token non-compliance. |
| `collapsible` (primitive) | 8 | Shadcn primitive. `foundations.stories.tsx` covers many primitives but skips collapsible. |
| `dropdown-menu` (primitive) | 8 | Same. |
| `avatar` (primitive) | 7 | Same — despite Avatar being one of the few components with sub-parts (`AvatarGroup`, `AvatarFallback`, `AvatarBadge`). |
| `skeleton` (primitive) | 6 | Same. |
| `scroll-area` (primitive) | 3 | Same. |
| `ApprovalPayload` | 4 | Feature. |
| `IssueReferencePill` | 4 | Feature. |
| `SidebarNavItem` | 3 | Structural. |
| `RunTranscriptView` | 3 | Feature — the transcript rendering. |
Two categories:
1. **Shadcn primitives missing from `foundations.stories.tsx`.** Small, targeted fix — add them to the existing foundations story.
2. **Production features without a story.** `StatusIcon` is the highest value given its role across 14 call sites.
---
## Unused / low-signal components
### Truly dead (0 imports, no Storybook coverage): 0
None. Every file in `ui/src/components/` either gets imported somewhere or appears in a story.
### Storybook-only (0 imports, appears in a story): 4
These are rendered in a story file but never imported by any page or other component:
| Component | Storybook location (approx.) |
|---|---|
| `AccountingModelCard` | financial/accounting-related story |
| `AgentProperties` | agent-management story |
| `CompanySwitcher` | navigation-layout story |
| `ExecutionParticipantPicker` | (story-referenced; unused in app) |
**Interpretation:** these are either (a) abandoned experiments still living in Storybook, (b) components waiting for the page that uses them, or (c) genuinely unused and should be deleted. Recommend: owner disposition per file. `AgentProperties` is especially surprising given the existence of sibling `IssueProperties`/`GoalProperties`/`ProjectProperties` — possibly a planned-but-not-wired variant of the properties family.
### Below-threshold (12 code uses, no detail file): 76
Not drift — many are legitimately single-use (one-off dialogs, one-off banners). But at 76 out of ~130 it's worth noting: this codebase heavily favors single-use components. A generic-component consolidation pass would likely shrink this group by ~30%. The candidates from §Likely duplicates above are the best places to start.
---
## Plugin SDK hybrid status, prioritization deferred
**Status: RESOLVED as intentional hybrid (2026-04-21).** Not drift. The plugin SDK (`packages/plugins/sdk/src/ui/components.ts`) declares 11 ambient component types; the host implements 2 (`MetricCard`, `StatusBadge`) and leaves the other 9 as contract-only. The 9 unimplemented components now carry a `@status contract-only` JSDoc tag in the SDK source so plugin authors see the status in IDE tooltips at call sites.
Prioritization of which of the 9 to build first is a **separate plugin-SDK roadmap conversation** — not a DS decision and not in scope here. This section captures the current state and the most likely first-implementations when that conversation happens.
### Current state
| SDK contract | Host implementation | Status |
|---|---|---|
| `MetricCard` | [`ui/src/components/MetricCard.tsx`](../../../ui/src/components/MetricCard.tsx) | ✅ implemented |
| `StatusBadge` | [`ui/src/components/StatusBadge.tsx`](../../../ui/src/components/StatusBadge.tsx) | ✅ implemented |
| `DataTable` | — | 🔌 contract-only |
| `TimeseriesChart` | — | 🔌 contract-only |
| `MarkdownBlock` | — | 🔌 contract-only |
| `KeyValueList` | — | 🔌 contract-only |
| `ActionBar` | — | 🔌 contract-only |
| `LogView` | — | 🔌 contract-only |
| `JsonTree` | — | 🔌 contract-only |
| `Spinner` | — | 🔌 contract-only |
| `ErrorBoundary` | — | 🔌 contract-only |
Each of the 9 contract-only entries has a JSDoc block in [`packages/plugins/sdk/src/ui/components.ts`](../../../packages/plugins/sdk/src/ui/components.ts) (lines 253316) that tells plugin authors the runtime will fail and points here.
`PLUGIN_SPEC.md:30` already acknowledged this before the DS extraction: _"The current runtime does not yet ship a real host-provided plugin UI component kit."_
### Implementation notes (for when prioritization happens)
Not prescriptive — candidates surfaced during the 2026-04 extraction:
- **`MarkdownBlock`** — thinnest wrapper around `ui/src/components/MarkdownBody.tsx`. Possibly an alias rather than a new component.
- **`Spinner`** — no matching host component. A ~10-line shadcn-style primitive would be the simplest new build.
- **`KeyValueList`** — patterns exist ad-hoc inside `EntityRow`, `PropertiesPanel` bodies, and `FinanceBillerCard`. Candidate for extraction into a shared primitive.
- **`LogView`** — no counterpart. Transcript rendering is tightly coupled to `RunTranscriptView`; a generic log viewer is a genuine new build.
- **`JsonTree`** — no counterpart. A new build.
- **`ErrorBoundary`** — standard React pattern; a thin wrapper around a React error boundary class.
- **`ActionBar`**, **`DataTable`**, **`TimeseriesChart`** — each is a real component's worth of surface area. Not thin builds.
### Affected files if the prioritization conversation opens
- `packages/plugins/sdk/src/ui/components.ts` (SDK declarations + @status tags)
- `packages/plugins/sdk/src/ui/runtime.ts` (runtime bridge — `renderSdkUiComponent`)
- New host files in `ui/src/components/` for each implemented contract
- [`components/index.md` — Plugin SDK contracts table](./index.md#plugin-sdk-contracts-11) (update implementation column as each lands)

View File

@@ -0,0 +1,228 @@
# Components — Index
- **Generated:** 2026-04-21
- **Repo SHA:** a26e1288b627e82c554445732c7d844648e6b5e1
- **Scope:** `ui/` + plugin SDK contracts
- **Review:** [components-review.md](./components-review.md)
## Counts
- **Total component files:** 135
- **With dedicated detail files (3+ code uses):** 53
- **Below threshold (12 uses):** 76
- **Storybook-only (in stories, 0 code uses):** 4
- **Dead (no uses, no stories):** 0
- **Non-component files (hooks, defaults):** 2
- **Plugin SDK contracts:** 11 (2 implemented by name, 9 contract-only — see §Plugin SDK contracts below)
### By category
| Category | Count |
|---|---|
| composite | 64 |
| primitive | 22 |
| standalone | 47 |
| utility-or-hook | 2 |
**Status markers in the tables below:**
- 📗 **documented** — ≥3 imports, has its own detail file in this directory
- 📘 **below-threshold** — 12 imports, no detail file
- 📙 **storybook-only** — 0 code imports, but appears in a story file
- ☠️ **dead** — 0 imports, 0 stories
- 🔌 **contract-only** — plugin SDK ambient declaration with no matching host implementation
---
## Primitives — `ui/src/components/ui/` (shadcn, 22)
All 22 shadcn primitives, by file name. These are the non-negotiable UI vocabulary — composites should consume these before reaching for custom markup.
| Component | Path | Uses | Pages / Comps | Story |
|---|---|---|---|---|
| 📗 [Button](./Button.md) | `ui/src/components/ui/button.tsx` | 81 | 41 / 38 | ✓ |
| 📗 [Popover](./Popover.md) | `ui/src/components/ui/popover.tsx` | 27 | 6 / 20 | ✓ |
| 📗 [Dialog](./Dialog.md) | `ui/src/components/ui/dialog.tsx` | 21 | 7 / 14 | ✓ |
| 📗 [Input](./Input.md) | `ui/src/components/ui/input.tsx` | 20 | 10 / 10 | ✓ |
| 📗 [Badge](./Badge.md) | `ui/src/components/ui/badge.tsx` | 18 | 11 / 7 | ✓ |
| 📗 [Card](./Card.md) | `ui/src/components/ui/card.tsx` | 18 | 10 / 8 | ✓ |
| 📗 [Tabs](./Tabs.md) | `ui/src/components/ui/tabs.tsx` | 15 | 13 / 2 | ✓ |
| 📗 [Separator](./Separator.md) | `ui/src/components/ui/separator.tsx` | 11 | 7 / 4 | ✓ |
| 📗 [Tooltip](./Tooltip.md) | `ui/src/components/ui/tooltip.tsx` | 11 | 3 / 7 | ✓ |
| 📗 [Select](./Select.md) | `ui/src/components/ui/select.tsx` | 10 | 5 / 5 | ✓ |
| 📗 [Textarea](./Textarea.md) | `ui/src/components/ui/textarea.tsx` | 9 | 4 / 5 | ✓ |
| 📗 [Collapsible](./Collapsible.md) | `ui/src/components/ui/collapsible.tsx` | 8 | 4 / 4 | — |
| 📗 [DropdownMenu](./DropdownMenu.md) | `ui/src/components/ui/dropdown-menu.tsx` | 8 | 3 / 5 | — |
| 📗 [Label](./Label.md) | `ui/src/components/ui/label.tsx` | 8 | 5 / 3 | ✓ |
| 📗 [ToggleSwitch](./ToggleSwitch.md) | `ui/src/components/ui/toggle-switch.tsx` | 8 | 5 / 3 | ✓ |
| 📗 [Avatar](./Avatar.md) | `ui/src/components/ui/avatar.tsx` | 7 | 3 / 4 | — |
| 📗 [Checkbox](./Checkbox.md) | `ui/src/components/ui/checkbox.tsx` | 6 | 4 / 2 | ✓ |
| 📗 [Skeleton](./Skeleton.md) | `ui/src/components/ui/skeleton.tsx` | 6 | 3 / 3 | — |
| 📗 [ScrollArea](./ScrollArea.md) | `ui/src/components/ui/scroll-area.tsx` | 3 | 2 / 1 | — |
| 📘 Breadcrumb | `ui/src/components/ui/breadcrumb.tsx` | 2 | 1 / 1 | — |
| 📘 Command | `ui/src/components/ui/command.tsx` | 2 | 1 / 1 | ✓ |
| 📘 Sheet | `ui/src/components/ui/sheet.tsx` | 2 | 2 / 0 | — |
---
## Composites (64)
Components that import 1+ other component from `@/components/*`. Application-level feature UI.
| Component | Path | Uses | Pages / Comps | Story |
|---|---|---|---|---|
| 📗 [PageSkeleton](./PageSkeleton.md) | `ui/src/components/PageSkeleton.tsx` | 23 | 22 / 1 | ✓ |
| 📗 [EmptyState](./EmptyState.md) | `ui/src/components/EmptyState.tsx` | 20 | 19 / 1 | ✓ |
| 📗 [agent-config-primitives](./agent-config-primitives.md) | `ui/src/components/agent-config-primitives.tsx` | 19 | 4 / 3 | ✓ |
| 📗 [Identity](./Identity.md) | `ui/src/components/Identity.tsx` | 19 | 7 / 12 | ✓ |
| 📗 [StatusIcon](./StatusIcon.md) | `ui/src/components/StatusIcon.tsx` | 14 | 5 / 9 | — |
| 📗 [AgentIconPicker](./AgentIconPicker.md) | `ui/src/components/AgentIconPicker.tsx` | 13 | 4 / 9 | ✓ |
| 📗 [PathInstructionsModal](./PathInstructionsModal.md) | `ui/src/components/PathInstructionsModal.tsx` | 12 | 2 / 3 | ✓ |
| 📗 [PageTabBar](./PageTabBar.md) | `ui/src/components/PageTabBar.tsx` | 10 | 9 / 1 | ✓ |
| 📗 [InlineEntitySelector](./InlineEntitySelector.md) | `ui/src/components/InlineEntitySelector.tsx` | 8 | 2 / 4 | ✓ |
| 📗 [IssuesList](./IssuesList.md) | `ui/src/components/IssuesList.tsx` | 6 | 5 / 1 | ✓ |
| 📗 [AgentConfigForm](./AgentConfigForm.md) | `ui/src/components/AgentConfigForm.tsx` | 5 | 3 / 0 | ✓ |
| 📗 [PriorityIcon](./PriorityIcon.md) | `ui/src/components/PriorityIcon.tsx` | 5 | 2 / 3 | ✓ |
| 📗 [IssueChatThread](./IssueChatThread.md) | `ui/src/components/IssueChatThread.tsx` | 4 | 2 / 2 | ✓ |
| 📗 [ApprovalCard](./ApprovalCard.md) | `ui/src/components/ApprovalCard.tsx` | 3 | 2 / 1 | ✓ |
| 📗 [BudgetPolicyCard](./BudgetPolicyCard.md) | `ui/src/components/BudgetPolicyCard.tsx` | 3 | 3 / 0 | ✓ |
| 📗 [IssueFiltersPopover](./IssueFiltersPopover.md) | `ui/src/components/IssueFiltersPopover.tsx` | 3 | 1 / 2 | ✓ |
| 📗 [IssueLinkQuicklook](./IssueLinkQuicklook.md) | `ui/src/components/IssueLinkQuicklook.tsx` | 3 | 0 / 2 | ✓ |
| 📗 [IssueWorkspaceCard](./IssueWorkspaceCard.md) | `ui/src/components/IssueWorkspaceCard.tsx` | 3 | 1 / 2 | ✓ |
| 📗 [RoutineRunVariablesDialog](./RoutineRunVariablesDialog.md) | `ui/src/components/RoutineRunVariablesDialog.tsx` | 3 | 2 / 1 | ✓ |
| 📗 [WorkspaceRuntimeControls](./WorkspaceRuntimeControls.md) | `ui/src/components/WorkspaceRuntimeControls.tsx` | 3 | 2 / 1 | ✓ |
| 📘 AgentActionButtons | `ui/src/components/AgentActionButtons.tsx` | 2 | 2 / 0 | ✓ |
| 📘 CommandPalette | `ui/src/components/CommandPalette.tsx` | 2 | 0 / 2 | ✓ |
| 📘 IssueColumns | `ui/src/components/IssueColumns.tsx` | 2 | 1 / 1 | ✓ |
| 📘 IssueContinuationHandoff | `ui/src/components/IssueContinuationHandoff.tsx` | 2 | 1 / 1 | ✓ |
| 📘 IssueDocumentsSection | `ui/src/components/IssueDocumentsSection.tsx` | 2 | 1 / 1 | ✓ |
| 📘 IssueProperties | `ui/src/components/IssueProperties.tsx` | 2 | 1 / 1 | ✓ |
| 📘 NewIssueDialog | `ui/src/components/NewIssueDialog.tsx` | 2 | 0 / 2 | ✓ |
| 📘 OutputFeedbackButtons | `ui/src/components/OutputFeedbackButtons.tsx` | 2 | 0 / 2 | — |
| 📘 ProjectWorkspaceSummaryCard | `ui/src/components/ProjectWorkspaceSummaryCard.tsx` | 2 | 0 / 2 | ✓ |
| 📘 ReportsToPicker | `ui/src/components/ReportsToPicker.tsx` | 2 | 1 / 1 | ✓ |
| 📘 RoutineVariablesEditor | `ui/src/components/RoutineVariablesEditor.tsx` | 2 | 2 / 0 | ✓ |
| 📘 Sidebar | `ui/src/components/Sidebar.tsx` | 2 | 0 / 2 | ✓ |
| 📘 SidebarAccountMenu | `ui/src/components/SidebarAccountMenu.tsx` | 2 | 0 / 2 | ✓ |
| 📘 SidebarCompanyMenu | `ui/src/components/SidebarCompanyMenu.tsx` | 2 | 0 / 2 | ✓ |
| 📘 CompanySettingsNav | `ui/src/components/access/CompanySettingsNav.tsx` | 1 | 0 / 1 | — |
| 📘 ModeBadge | `ui/src/components/access/ModeBadge.tsx` | 1 | 1 / 0 | — |
| 📘 BillerSpendCard | `ui/src/components/BillerSpendCard.tsx` | 1 | 1 / 0 | ✓ |
| 📘 BreadcrumbBar | `ui/src/components/BreadcrumbBar.tsx` | 1 | 0 / 1 | ✓ |
| 📘 BudgetIncidentCard | `ui/src/components/BudgetIncidentCard.tsx` | 1 | 1 / 0 | ✓ |
| 📘 CommentThread | `ui/src/components/CommentThread.tsx` | 1 | 0 / 1 | ✓ |
| 📘 CompanyRail | `ui/src/components/CompanyRail.tsx` | 1 | 0 / 1 | ✓ |
| 📘 DocumentDiffModal | `ui/src/components/DocumentDiffModal.tsx` | 1 | 0 / 1 | ✓ |
| 📘 FilterBar | `ui/src/components/FilterBar.tsx` | 1 | 1 / 0 | ✓ |
| 📘 FinanceBillerCard | `ui/src/components/FinanceBillerCard.tsx` | 1 | 1 / 0 | ✓ |
| 📘 FinanceKindCard | `ui/src/components/FinanceKindCard.tsx` | 1 | 1 / 0 | ✓ |
| 📘 FinanceTimelineCard | `ui/src/components/FinanceTimelineCard.tsx` | 1 | 1 / 0 | ✓ |
| 📘 GoalProperties | `ui/src/components/GoalProperties.tsx` | 1 | 1 / 0 | ✓ |
| 📘 IssuesQuicklook | `ui/src/components/IssuesQuicklook.tsx` | 1 | 0 / 1 | ✓ |
| 📘 JsonSchemaForm | `ui/src/components/JsonSchemaForm.tsx` | 1 | 1 / 0 | ✓ |
| 📘 KeyboardShortcutsCheatsheet | `ui/src/components/KeyboardShortcutsCheatsheet.tsx` | 1 | 0 / 1 | ✓ |
| 📘 NewAgentDialog | `ui/src/components/NewAgentDialog.tsx` | 1 | 0 / 1 | ✓ |
| 📘 NewGoalDialog | `ui/src/components/NewGoalDialog.tsx` | 1 | 0 / 1 | ✓ |
| 📘 NewProjectDialog | `ui/src/components/NewProjectDialog.tsx` | 1 | 0 / 1 | ✓ |
| 📘 OnboardingWizard | `ui/src/components/OnboardingWizard.tsx` | 1 | 0 / 0 | ✓ |
| 📘 ProjectProperties | `ui/src/components/ProjectProperties.tsx` | 1 | 1 / 0 | ✓ |
| 📘 PropertiesPanel | `ui/src/components/PropertiesPanel.tsx` | 1 | 0 / 1 | — |
| 📘 ProviderQuotaCard | `ui/src/components/ProviderQuotaCard.tsx` | 1 | 1 / 0 | ✓ |
| 📘 ScheduleEditor | `ui/src/components/ScheduleEditor.tsx` | 1 | 1 / 0 | ✓ |
| 📘 SidebarAgents | `ui/src/components/SidebarAgents.tsx` | 1 | 0 / 1 | — |
| 📘 SidebarProjects | `ui/src/components/SidebarProjects.tsx` | 1 | 0 / 1 | — |
| 📙 AccountingModelCard | `ui/src/components/AccountingModelCard.tsx` | 0 | 0 / 0 | ✓ |
| 📙 AgentProperties | `ui/src/components/AgentProperties.tsx` | 0 | 0 / 0 | ✓ |
| 📙 CompanySwitcher | `ui/src/components/CompanySwitcher.tsx` | 0 | 0 / 0 | ✓ |
| 📙 ExecutionParticipantPicker | `ui/src/components/ExecutionParticipantPicker.tsx` | 0 | 0 / 0 | ✓ |
---
## Standalones (47)
Components that import no other `@/components/*`. Usually: icons, self-contained widgets, components that only depend on radix / lucide / local libs. The fact that they import zero composites or primitives is itself a data point — some of these probably should be using primitives.
| Component | Path | Uses | Pages / Comps | Story |
|---|---|---|---|---|
| 📗 [StatusBadge](./StatusBadge.md) | `ui/src/components/StatusBadge.tsx` | 19 | 12 / 7 | ✓ |
| 📗 [MarkdownEditor](./MarkdownEditor.md) | `ui/src/components/MarkdownEditor.tsx` | 16 | 5 / 9 | ✓ |
| 📗 [MarkdownBody](./MarkdownBody.md) | `ui/src/components/MarkdownBody.tsx` | 11 | 5 / 6 | ✓ |
| 📗 [EntityRow](./EntityRow.md) | `ui/src/components/EntityRow.tsx` | 6 | 6 / 0 | ✓ |
| 📗 [InlineEditor](./InlineEditor.md) | `ui/src/components/InlineEditor.tsx` | 6 | 4 / 2 | ✓ |
| 📗 [ApprovalPayload](./ApprovalPayload.md) | `ui/src/components/ApprovalPayload.tsx` | 4 | 2 / 2 | — |
| 📗 [CompanyPatternIcon](./CompanyPatternIcon.md) | `ui/src/components/CompanyPatternIcon.tsx` | 4 | 3 / 1 | ✓ |
| 📗 [IssueReferencePill](./IssueReferencePill.md) | `ui/src/components/IssueReferencePill.tsx` | 4 | 1 / 3 | — |
| 📗 [ActivityCharts](./ActivityCharts.md) | `ui/src/components/ActivityCharts.tsx` | 3 | 2 / 1 | ✓ |
| 📗 [CopyText](./CopyText.md) | `ui/src/components/CopyText.tsx` | 3 | 2 / 1 | ✓ |
| 📗 [IssueRow](./IssueRow.md) | `ui/src/components/IssueRow.tsx` | 3 | 1 / 2 | ✓ |
| 📗 [PackageFileTree](./PackageFileTree.md) | `ui/src/components/PackageFileTree.tsx` | 3 | 3 / 0 | ✓ |
| 📗 [SidebarNavItem](./SidebarNavItem.md) | `ui/src/components/SidebarNavItem.tsx` | 3 | 0 / 3 | — |
| 📗 [RunTranscriptView](./RunTranscriptView.md) | `ui/src/components/transcript/RunTranscriptView.tsx` | 3 | 2 / 1 | — |
| 📘 ActivityRow | `ui/src/components/ActivityRow.tsx` | 2 | 2 / 0 | ✓ |
| 📘 AsciiArtAnimation | `ui/src/components/AsciiArtAnimation.tsx` | 2 | 1 / 1 | ✓ |
| 📘 BudgetSidebarMarker | `ui/src/components/BudgetSidebarMarker.tsx` | 2 | 0 / 2 | ✓ |
| 📘 CloudAccessGate | `ui/src/components/CloudAccessGate.tsx` | 2 | 0 / 0 | — |
| 📘 CompanySettingsSidebar | `ui/src/components/CompanySettingsSidebar.tsx` | 2 | 0 / 2 | — |
| 📘 EnvVarEditor | `ui/src/components/EnvVarEditor.tsx` | 2 | 0 / 2 | ✓ |
| 📘 ExecutionWorkspaceCloseDialog | `ui/src/components/ExecutionWorkspaceCloseDialog.tsx` | 2 | 1 / 1 | ✓ |
| 📘 GoalTree | `ui/src/components/GoalTree.tsx` | 2 | 2 / 0 | ✓ |
| 📘 IssueGroupHeader | `ui/src/components/IssueGroupHeader.tsx` | 2 | 1 / 1 | ✓ |
| 📘 IssueReferenceActivitySummary | `ui/src/components/IssueReferenceActivitySummary.tsx` | 2 | 1 / 1 | — |
| 📘 IssueRelatedWorkPanel | `ui/src/components/IssueRelatedWorkPanel.tsx` | 2 | 1 / 1 | — |
| 📘 IssueRunLedger | `ui/src/components/IssueRunLedger.tsx` | 2 | 1 / 1 | ✓ |
| 📘 Layout | `ui/src/components/Layout.tsx` | 2 | 0 / 1 | — |
| 📘 MetricCard | `ui/src/components/MetricCard.tsx` | 2 | 2 / 0 | ✓ |
| 📘 OpenCodeLogoIcon | `ui/src/components/OpenCodeLogoIcon.tsx` | 2 | 0 / 1 | — |
| 📘 ProjectWorkspacesContent | `ui/src/components/ProjectWorkspacesContent.tsx` | 2 | 2 / 0 | ✓ |
| 📘 QuotaBar | `ui/src/components/QuotaBar.tsx` | 2 | 0 / 2 | ✓ |
| 📘 RunChatSurface | `ui/src/components/RunChatSurface.tsx` | 2 | 0 / 2 | ✓ |
| 📘 ScrollToBottom | `ui/src/components/ScrollToBottom.tsx` | 2 | 2 / 0 | — |
| 📘 SwipeToArchive | `ui/src/components/SwipeToArchive.tsx` | 2 | 1 / 1 | ✓ |
| 📘 ActiveAgentsPanel | `ui/src/components/ActiveAgentsPanel.tsx` | 1 | 1 / 0 | ✓ |
| 📘 ClaudeSubscriptionPanel | `ui/src/components/ClaudeSubscriptionPanel.tsx` | 1 | 0 / 1 | ✓ |
| 📘 CodexSubscriptionPanel | `ui/src/components/CodexSubscriptionPanel.tsx` | 1 | 0 / 1 | ✓ |
| 📘 DevRestartBanner | `ui/src/components/DevRestartBanner.tsx` | 1 | 0 / 1 | — |
| 📘 HermesIcon | `ui/src/components/HermesIcon.tsx` | 1 | 0 / 0 | — |
| 📘 ImageGalleryModal | `ui/src/components/ImageGalleryModal.tsx` | 1 | 1 / 0 | ✓ |
| 📘 InstanceSidebar | `ui/src/components/InstanceSidebar.tsx` | 1 | 0 / 1 | — |
| 📘 KanbanBoard | `ui/src/components/KanbanBoard.tsx` | 1 | 0 / 1 | ✓ |
| 📘 LiveRunWidget | `ui/src/components/LiveRunWidget.tsx` | 1 | 1 / 0 | ✓ |
| 📘 MobileBottomNav | `ui/src/components/MobileBottomNav.tsx` | 1 | 0 / 1 | ✓ |
| 📘 SidebarSection | `ui/src/components/SidebarSection.tsx` | 1 | 0 / 1 | — |
| 📘 ToastViewport | `ui/src/components/ToastViewport.tsx` | 1 | 0 / 1 | — |
| 📘 WorktreeBanner | `ui/src/components/WorktreeBanner.tsx` | 1 | 0 / 1 | ✓ |
---
## Non-component files (2)
These live in `ui/src/components/` by convention but don't export React components.
| File | Path | Role |
|---|---|---|
| `agent-config-defaults` | `ui/src/components/agent-config-defaults.ts` | module with shared constants/defaults |
| `useLiveRunTranscripts` | `ui/src/components/transcript/useLiveRunTranscripts.ts` | React hook |
---
## Plugin SDK contracts (11)
Ambient component declarations from [`packages/plugins/sdk/src/ui/components.ts`](../../../packages/plugins/sdk/src/ui/components.ts). These are types-only; the host provides implementations at runtime via `renderSdkUiComponent(name, props)`.
> **Hybrid status is intentional (2026-04-21 decision).** Two components are implemented by the host. The other nine are **contract-only** — the types exist so plugin authors can code against them, but rendering today will fail at runtime. The 9 contract-only components carry a `@status contract-only` JSDoc tag in the SDK source, which appears in IDE tooltips at call sites. Prioritization of which to implement first is a separate plugin-SDK roadmap conversation, not a DS decision. See [components-review.md §Plugin SDK hybrid status](./components-review.md#plugin-sdk-hybrid-status-prioritization-deferred).
| SDK Component | Implementation | Status |
|---|---|---|
| `MetricCard` | [`ui/src/components/MetricCard.tsx`](../../../ui/src/components/MetricCard.tsx) | 📗 **implemented** |
| `StatusBadge` | [`ui/src/components/StatusBadge.tsx`](../../../ui/src/components/StatusBadge.tsx) | 📗 **implemented** |
| `DataTable` | — | 🔌 **contract-only** |
| `TimeseriesChart` | — | 🔌 **contract-only** (distinct from `ActivityCharts.tsx`, which has a different API) |
| `MarkdownBlock` | — | 🔌 **contract-only** (`MarkdownBody.tsx` is the host's markdown renderer, name differs) |
| `KeyValueList` | — | 🔌 **contract-only** |
| `ActionBar` | — | 🔌 **contract-only** (`AgentActionButtons.tsx` is role-specific, not a match) |
| `LogView` | — | 🔌 **contract-only** |
| `JsonTree` | — | 🔌 **contract-only** |
| `Spinner` | — | 🔌 **contract-only** |
| `ErrorBoundary` | — | 🔌 **contract-only** |
All 9 contract-only entries carry `@status contract-only` in their JSDoc block (see [`packages/plugins/sdk/src/ui/components.ts`](../../../packages/plugins/sdk/src/ui/components.ts) lines 253316).

View File

@@ -0,0 +1,69 @@
# Detail Page
Full-page layout for viewing and editing a single entity (agent, issue, project, goal, routine, approval, or execution/project workspace).
**Instances: 8.** `AgentDetail`, `IssueDetail`, `ProjectDetail`, `GoalDetail`, `RoutineDetail`, `ApprovalDetail`, `ExecutionWorkspaceDetail`, `ProjectWorkspaceDetail`.
## Composition (shared baseline)
Measured across the 8 instances by import intersection:
- **`button`** — 8/8 (every detail page has actions in its header)
- **`tabs`** — 6/8 (detail pages split sub-views by tab)
- **`PageSkeleton`** — 5/8 (loading state while the entity is being fetched)
- **`StatusBadge`** — 4/8 (status is surfaced in the header area)
- **`separator`** — 4/8
- Breadcrumb context (`useBreadcrumbs`) — all 8 set a breadcrumb trail for the entity.
[INFER] Structural template, from reading AgentDetail and IssueDetail:
```
<PageSkeleton or <ContentLoaded>
<Breadcrumb / back-nav>
<Header>
<Title> — entity name + identifier
<StatusBadge> — where applicable (issue, run, approval, agent)
<Actions> — edit, archive, more-menu
</Header>
<Tabs> — 25 tabs (overview, config, activity, …)
<TabContent>
… entity-specific body (properties, related work, charts, transcripts)
</TabContent>
</Tabs>
</>
```
## Canonical instance
`ui/src/pages/IssueDetail.tsx` is the most mature and most-cross-referenced implementation. `ui/src/pages/AgentDetail.tsx` is second and shows the tab-bar with many sub-surfaces.
## Variance across instances
Observed differences that may be intentional (different entity domain) or may be drift:
| Instance | Breadcrumbs | Tabs | Status element | Loading state | Notes |
|---|---|---|---|---|---|
| `IssueDetail` | yes | yes | `StatusIcon` + `StatusBadge` | custom | largest file; widely-referenced |
| `AgentDetail` | yes | yes | `StatusBadge` + `agentStatusDot` | `PageSkeleton` | composes `AgentConfigForm`, `ActivityCharts`, `PackageFileTree`, `RunTranscriptView` |
| `ProjectDetail` | yes | yes | `StatusBadge` | `PageSkeleton` | |
| `GoalDetail` | yes | (unconfirmed) | `StatusBadge` | `PageSkeleton` | |
| `RoutineDetail` | yes | yes | (unconfirmed) | `PageSkeleton` | |
| `ApprovalDetail` | yes | (none) | tone-coded via `ApprovalCard` | `PageSkeleton` | diverges — simpler shape |
| `ExecutionWorkspaceDetail` | yes | — | — | — | newer; diverges |
| `ProjectWorkspaceDetail` | yes | — | — | — | newer; diverges |
- **3 of 8 don't use `PageSkeleton`** — worth confirming each has an equivalent loading state.
- **`ApprovalDetail` skips tabs** — likely correct for its single-surface nature.
- **Status element is inconsistent** — `StatusBadge` alone, `StatusIcon + StatusBadge`, `StatusBadge + agentStatusDot` (separate dot helper), `ApprovalCard` tone encoding. Tied to [status-display.md](./status-display.md) and [../tokens/tokens-review.md §4](../tokens/tokens-review.md#4-status-colorsts-is-a-canonical-semantic-color-catalog-that-bypasses-the-ds).
## Related components and patterns
- Loading state: [`PageSkeleton`](../components/index.md) (documented as a component — used in 22+ places)
- Empty state: [`EmptyState`](../components/index.md) (mostly used on list pages)
- Status badge: [`StatusBadge`](../components/StatusBadge.md), [`StatusIcon`](../components/index.md) — see [status-display.md](./status-display.md)
- Tab bar: shadcn `tabs`, and a custom [`PageTabBar`](../components/PageTabBar.md) used on some pages
## Open questions / risks
- Whether to codify a `<DetailPageHeader>` composite (title + status + actions block) to reduce per-page drift. Four detail pages already diverge on which status element they use.
- The new `*WorkspaceDetail` pages do not yet share much structure with the older four. Check before Stage-4 pattern extraction runs again in a future quarter.

View File

@@ -0,0 +1,51 @@
# Entity-Creation Dialog
Dialog surface for creating a new entity (agent, goal, issue, project).
**Instances: 4.** `NewAgentDialog`, `NewGoalDialog`, `NewIssueDialog`, `NewProjectDialog`.
> **Extraction-only pass.** This pattern document records the family as it exists today. It does not prescribe a merge into a single generic `NewEntityDialog`. See [components-review.md §Likely duplicates #1](../components/components-review.md#1-entity-creation-dialog-family-newagentdialog--newgoaldialog--newissuedialog--newprojectdialog) for the open-question treatment.
## Instances
| File | Lines | Uses | Opened via |
|---|---|---|---|
| `ui/src/components/NewAgentDialog.tsx` | 210 | 1 | `useDialog().newAgentOpen` |
| `ui/src/components/NewGoalDialog.tsx` | (unread) | 1 | `useDialog()` |
| `ui/src/components/NewIssueDialog.tsx` | 1699 | 2 | `useDialog()` |
| `ui/src/components/NewProjectDialog.tsx` | (unread) | 1 | `useDialog()` |
## Composition (shared)
All four:
- Import `Dialog` + `DialogContent` from `@/components/ui/dialog` (primitive).
- Consume a central `useDialog()` context from `ui/src/context/DialogContext` that exposes open/close flags per entity type.
- Call an entity-specific API on submit (`agentsApi.create`, `issuesApi.create`, …) via `useMutation`.
- Dismiss via `closeNewX()` from the same context.
## Shape divergence
The instances are **not structurally equivalent.** Line counts alone:
- `NewAgentDialog` = 210 lines (adapter picker → create stub)
- `NewIssueDialog` = 1699 lines (rich form: assignees, projects, policies, mentions, dragdrop, advanced panel)
Other divergence indicators from imports:
- `NewIssueDialog` imports `agent-config-primitives` (`DraftInput`, `ChoosePathButton`, etc.), `ToggleSwitch`, `Popover`, large piece of `@dnd-kit`, markdown editors — i.e. a full inline form.
- `NewAgentDialog` imports `Dialog`, `Button`, adapter-registry helpers — a chooser, not a full form.
- `NewGoalDialog` and `NewProjectDialog` not examined in detail here; their size is likely between the two extremes.
## Open questions / risks
- Is `NewIssueDialog` intended to be "the" form and the others are just chooser-stubs that redirect to a detail page? That shape would be load-bearing. Currently unclear from static reading.
- Without a generic base, adding a fifth entity (e.g. `NewRoutineDialog`) means another copy of the dialog-open-context wiring. The `useDialog()` context already carries the per-entity open/close flags — it would be the natural integration point if consolidation is pursued.
## Pattern use in Stage 4 analysis
If Stage 4 were to name a composition "new-entity-dialog" for future reference, the canonical definition would be:
> A `<Dialog>` opened from the `useDialog()` context, closed via a per-entity handler, containing an entity-specific body that submits through the matching API and invalidates the matching `queryKeys` on success.
Not yet codified as code. Documented here only.

View File

@@ -0,0 +1,62 @@
# Entity Properties Panel
Side-panel content that shows an entity's metadata, lets the user edit inline, and drives per-field save state.
**Instances: 4 entity-specific panels + 1 generic panel.**
`AgentProperties`, `GoalProperties`, `IssueProperties`, `ProjectProperties` + `PropertiesPanel`.
> **Extraction-only pass.** This pattern does not prescribe a merge. The open question about whether `PropertiesPanel` composes or duplicates the four entity-specific panels is surfaced — not resolved. See [components-review.md §Likely duplicates #2](../components/components-review.md#2-properties-panel-family-agentproperties--goalproperties--issueproperties--projectproperties--generic-propertiespanel).
## Instances
| Component | Lines | Uses | Role |
|---|---|---|---|
| `PropertiesPanel.tsx` | 29 | 1 | **Generic chrome.** A slide-in `<aside>` that reads `panelContent` from `usePanel()` context and renders whatever the caller has set. |
| `AgentProperties.tsx` | (unread) | **0** | Entity-specific body (currently unused in production — Storybook-only). |
| `GoalProperties.tsx` | (unread) | 1 | Entity-specific body. |
| `IssueProperties.tsx` | 1370 | 2 | Entity-specific body; imports `StatusIcon`, `PriorityIcon`, `Identity`, `IssueReferencePill`, plus form primitives. |
| `ProjectProperties.tsx` | 1140 | 1 | Entity-specific body; imports `StatusBadge`, status-colors consumers, `InlineEditor`, `EnvVarEditor`. |
## Relationship: chrome vs content
Reading `PropertiesPanel.tsx` (29 lines):
```tsx
export function PropertiesPanel() {
const { panelContent, panelVisible, setPanelVisible } = usePanel();
if (!panelContent) return null;
return (
<aside className="… bg-card …">
<div className="… flex flex-col …">
<Header><Button icon="X" onClick={close} /></Header>
<ScrollArea><div className="p-4">{panelContent}</div></ScrollArea>
</div>
</aside>
);
}
```
So `PropertiesPanel` is a **slot**, not a content template. The four `*Properties` components are **contents** that get passed into that slot via `usePanel().setPanelContent(...)`.
## Open question (not resolved here)
Does `PropertiesPanel` already compose the four entity-specific panels, or do the four duplicate work that `PropertiesPanel` could own?
Evidence either way from a static read:
- **For "composes"**: the four entity-specific panels don't render a dialog/drawer wrapper themselves — each emits just the body content. They rely on some parent (either `PropertiesPanel` or a page-owned slot) to provide the outer container.
- **For "duplicates"**: the header layout (`Properties` title + close button) is in `PropertiesPanel`, but each of the four is 1100+ lines of its own scaffolding (section headers, separators, form fields, save-state handling) that *could* be factored into a `<PropertiesPanelBody>` helper.
**Not a call to make in this extraction.** The founder should open `IssueProperties.tsx` and `ProjectProperties.tsx` side-by-side and judge.
## Also noted
- `AgentProperties.tsx` has **0 production uses** (Storybook-only). Either abandoned or waiting for a page. See [components-review.md §Unused components](../components/components-review.md#unused--low-signal-components).
- Each entity-specific panel consumes `status-colors.ts` for status-color rendering (directly or via `StatusBadge`/`StatusIcon`), inheriting the token drift from [tokens-review.md §4](../tokens/tokens-review.md#4-status-colorsts-is-a-canonical-semantic-color-catalog-that-bypasses-the-ds).
- File sizes (11001370 lines each) suggest each panel handles its own save pipeline, field-level error states, mutations, and recent-selection tracking (indirectly observed via imports from `recent-assignees`, `recent-projects`, etc.). Whether that logic is shareable is the real design question underneath the styling question.
## Related components and patterns
- Chrome slot: `PropertiesPanel` and the [`usePanel()` context](../../../ui/src/context/PanelContext.tsx)
- Status coloring: [status-display.md](./status-display.md)
- Inline field editing primitives in [`agent-config-primitives.tsx`](../components/agent-config-primitives.md) (`DraftInput`, `InlineField`, `ToggleField`, `ToggleWithNumber`)

View File

@@ -0,0 +1,80 @@
# Entity Row
Row element for listing items in a scrollable collection (inbox, activity feed, list pages).
**Instances: 3.** `ActivityRow`, `EntityRow`, `IssueRow`.
> **Extraction-only pass.** Documents the family; does not prescribe the merge suggested in components-review. See [components-review.md §Likely duplicates #6](../components/components-review.md#6-row-family-activityrow--entityrow--issuerow).
## Instances
| Component | Lines | Uses | Role |
|---|---|---|---|
| `EntityRow.tsx` | 69 | 6 | Generic slot-based row (`leading` / `identifier` / `title` / `subtitle` / `trailing`) |
| `ActivityRow.tsx` | 92 | 2 | Activity-event-specific — renders an `ActivityEvent` with actor identity + action verb + entity link |
| `IssueRow.tsx` | 168 | 3 | Issue-specific — renders an `Issue` with `StatusIcon`, mobile/desktop slot variants, unread state, archive action |
## Composition
**`EntityRow`** — truly generic:
```tsx
interface EntityRowProps {
leading?: ReactNode;
identifier?: string;
title: string;
subtitle?: string;
trailing?: ReactNode;
selected?: boolean;
to?: string;
onClick?: () => void;
className?: string;
}
```
Renders as `<Link>` or `<div>` depending on click-ability. No status, no unread state, no mobile/desktop split — just slots.
**`ActivityRow`** — imports `Identity`, `IssueReferenceActivitySummary`. Specific to activity events.
**`IssueRow`** — imports `StatusIcon`. Props interface has 15 fields, most of them optional `ReactNode` slots:
```ts
interface IssueRowProps {
issue: Issue;
issueLinkState?: unknown;
selected?: boolean;
mobileLeading?: ReactNode; // slot
desktopMetaLeading?: ReactNode; // slot
desktopLeadingSpacer?: boolean;
mobileMeta?: ReactNode; // slot
desktopTrailing?: ReactNode; // slot
trailingMeta?: ReactNode; // slot
titleSuffix?: ReactNode; // slot
unreadState?: UnreadState | null;
onMarkRead?: () => void;
onArchive?: () => void;
archiveDisabled?: boolean;
className?: string;
}
```
Six slot props plus an untyped `issueLinkState` — this shape is nearly "`<EntityRow>` + issue-specific defaults."
## Observations
- `EntityRow` is used in 6 composites (not examined here — see composition graph), but **not used on any main list page**. Main list pages roll their own row rendering.
- `IssueRow` is only used in 2 places: the `Inbox` page and `SwipeToArchive`. Not used on the `Issues` page (which uses `IssueColumns` + custom row rendering per column).
- The gap is: `EntityRow` covers the "slot-based row" role generically, but the list pages don't adopt it.
## Variance
- **Mobile/desktop split lives only in `IssueRow`.** Whether other pages need it or have their own responsive handling is unknown from static analysis.
- **Unread state lives only in `IssueRow`.** Inbox-specific; would not generalize.
- **Activity-specific text (verb, link target) lives only in `ActivityRow`.** Legitimate domain specialization.
## Open questions
- Could `IssueRow` be expressed as `<EntityRow kind="issue" ... />`? Its slot shape already matches `EntityRow`'s role; the issue-specific bits (StatusIcon, unread state, archive) are add-ons, not structural differences.
- Why do the main list pages (`Issues`, `Agents`, `Projects`, …) avoid `EntityRow`? If there's a good reason it should be documented; if not, adoption would retire a lot of per-page row code.
Answers to these are not required for this extraction. The pattern is noted as documentation-relevant.

View File

@@ -0,0 +1,76 @@
# Finance / Accounting Card
Card surface for summarizing a financial or accounting slice: per-biller spend, per-kind spend, timeline totals, accounting-model totals.
**Instances: 5.** `BillerSpendCard`, `FinanceBillerCard`, `FinanceKindCard`, `FinanceTimelineCard`, `AccountingModelCard`.
> **Extraction-only pass.** Documents the family as it exists. Two specific items are flagged for the founder below; neither is auto-resolved.
## Instances
| Component | Lines | Uses | Data type (inferred) |
|---|---|---|---|
| `BillerSpendCard` | 145 | 1 | `CostByBiller` (+ `CostByProviderModel` breakdown) |
| `FinanceBillerCard` | 44 | 1 | `FinanceByBiller` |
| `FinanceKindCard` | (unread) | 1 | `FinanceByKind` (inferred) |
| `FinanceTimelineCard` | (unread) | 1 | timeline roll-up |
| `AccountingModelCard` | (unread) | **0** | (unknown — unused) |
## Composition (shared)
All five are composites that import the shadcn `Card` family (`Card`, `CardHeader`, `CardTitle`, `CardDescription`, `CardContent`, `CardFooter`) and render a titled card with a body.
`BillerSpendCard` additionally composes `QuotaBar`. That's the richer card in the family — quota visualization + provider breakdown + billing-type breakdown.
`FinanceBillerCard` is a plain summary card with a three-cell metric grid (`debits` / `credits` / `estimated`).
## Flag 1 — Likely true duplicate: `BillerSpendCard` ↔ `FinanceBillerCard`
Per the founder's directive in Stage 3, this pair is flagged for diff review.
- Both have "Biller" in the name.
- Both summarize per-biller financials.
- They consume **different** data models (`CostByBiller` vs `FinanceByBiller`), which suggests either (a) two different reporting concepts the names fail to distinguish, or (b) one of them is a stale parallel implementation of the other.
- Line counts differ significantly (145 vs 44), but that could mean `BillerSpendCard` is the richer one *and* `FinanceBillerCard` is the slimmed-down version of the same concept.
**Action suggested (not taken here):** open both side by side and judge whether they represent two legitimately different reports, or whether one superseded the other and the older survived.
## Flag 2 — `AccountingModelCard` is unused
Zero imports across the codebase. Storybook-only coverage. See [components-review.md §Unused](../components/components-review.md#unused--low-signal-components). Delete or adopt.
## Composition template (common shape)
[INFER] From `FinanceBillerCard` (the cleanest example):
```
<Card>
<CardHeader>
<div flex between>
<div>
<CardTitle>{providerDisplayName(row.biller)}</CardTitle>
<CardDescription>{eventCount}, {kindCount} kinds</CardDescription>
</div>
<div text-right>
<div text-lg tabular-nums>{formatCents(row.netCents)}</div>
<div uppercase tracking-wide muted>net</div>
</div>
</div>
</CardHeader>
<CardContent>
<grid 3-column>
<Cell label="debits" value={formatCents(...)} />
<Cell label="credits" value={formatCents(...)} />
<Cell label="estimated" value={formatCents(...)} />
</grid>
</CardContent>
</Card>
```
The recurring sub-element — a small metric cell with uppercase-tracked muted label + tabular-num value — is a micro-pattern worth noting. It appears here and (less formally) on list pages. Candidate for a `<MetricCell>` helper.
## Variance
- `BillerSpendCard` composes `QuotaBar`; the others don't.
- Different data models (`CostByBiller` vs `FinanceByBiller` vs `FinanceByKind`) — confirm whether the shared shape is intentional convergence or a sign that they should share a common `FinanceCardRow` interface.
- Per-card formatting helpers (`formatCents`, `formatTokens`, `providerDisplayName`) live in `@/lib/utils` — shared. Good.

View File

@@ -0,0 +1,43 @@
# Patterns — Index
- **Generated:** 2026-04-21
- **Repo SHA:** a26e1288b627e82c554445732c7d844648e6b5e1
- **Scope:** `ui/` (@paperclipai/ui) — pages + components
- **Review:** [patterns-review.md](./patterns-review.md)
## What a pattern is
A *pattern* is a composition of components that recurs across pages or across composites — something that has a shape, not just a component name. Pattern documents describe the shape and list the current instances. They do not prescribe refactors.
Patterns were identified by:
1. Reading `_pages.json` and `_composition-graph.json` (Stage 2 scratch).
2. Looking for import-set intersections across pages or composition-graph neighborhoods.
3. Cross-referencing the duplicate families surfaced in [components-review.md §Likely duplicates](../components/components-review.md#likely-duplicates).
4. Checking the Paperclip-domain checklist from the extraction skill (heartbeat, run-transcript row, agent card, approval gate, cost display, metadata grid).
## Pattern inventory
Sorted by instance count. Patterns with ≥3 instances get their own detail doc; pairs below the threshold are included per directive but called out.
| Pattern | Instances | Doc |
|---|---|---|
| [List page](./list-page.md) | 12 | ✓ |
| [Detail page](./detail-page.md) | 8 | ✓ |
| [Sidebar chrome](./sidebar-chrome.md) | 6 outer + 2 menus | ✓ |
| [Finance / accounting card](./finance-card.md) | 5 | ✓ |
| [Entity properties panel](./entity-properties-panel.md) | 4 entity-specific + 1 generic chrome | ✓ |
| [Entity-creation dialog](./entity-creation-dialog.md) | 4 | ✓ |
| [Status display](./status-display.md) | 3 components + 1 catalog | ✓ |
| [Entity row](./entity-row.md) | 3 | ✓ |
| [Subscription panel](./subscription-panel.md) | 2 (below threshold — documented) | ✓ |
| [Quota display](./quota-display.md) | 2 (below threshold — documented) | ✓ |
See [patterns-review.md](./patterns-review.md) for:
- Pattern opportunities that don't yet meet the threshold but are domain-relevant (heartbeat, run-transcript row, agent card, approval gate, cost display, metric cell, severity indicator).
- Variance analysis across patterns.
- Which patterns are safe to codify and which are blocked on upstream token or naming decisions.
## Scope notes
- **Out of this pass:** deep pattern extraction from the UX Lab pages (`IssueChatUxLab`, `RunTranscriptUxLab`, `InviteUxLab`). Those are acknowledged prototypes — pattern work there should follow explicit founder direction, not auto-extraction.
- **Out of this pass:** the plugin SDK contract surface. Patterns emerge from the host `ui/`; if the SDK contract is fulfilled (see [components-review.md §Plugin SDK contract gap](../components/components-review.md#plugin-sdk-contract-gap)), those host implementations become additional pattern instances and this doc will need a re-run.

Some files were not shown because too many files have changed in this diff Show More