Compare commits

..

6 Commits

Author SHA1 Message Date
Devin Foley
0c8b5b852e Remove internal incident reference from paperclip-dev skill
Rewrite the 'Why this Matters' section to remove the PAPA-81 story
since this skill will be open-sourced. The key rationale (agent shell
servers die between heartbeats) is folded into rule #1 instead.
2026-04-19 19:00:32 -07:00
Devin Foley
4d870aec37 docs: add persistent dev server guidance to paperclip-dev skill
Agents launching dev servers with pnpm dev during a heartbeat lose the
process when the heartbeat shell exits. Document using detached tmux
sessions to keep dev servers alive for manual testing, with session
management commands and verification steps. Learned from PAPA-81 where
QA repeatedly found the server dead between heartbeats.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-19 18:07:18 -07:00
Devin Foley
90647f81ff fix: remove hardcoded ~/workspace/paperclip path from paperclip-dev skill
The skill should not assume a specific install location. Replaced with
a generic "repo checkout" reference and changed the worktree example
to use a placeholder path instead of ~/paperclip-my-feature.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-19 09:38:49 -07:00
Devin Foley
f24ca164e4 fix: add hard guardrails to paperclip-dev skill for CLI-only worktree/DB ops
Agents were improvising around CLI failures by manually running postgres
commands, deleting DB directories, and sharing databases between instances.
This caused real damage on PAPA-78. Add explicit "Hard Rules" section that
forbids all manual DB/worktree operations and requires agents to stop and
report when CLI commands fail instead of attempting workarounds.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-16 19:57:39 -07:00
Devin Foley
f025c313be refactor: reference doc/DEVELOPING.md instead of embedding CLI docs in skill
The paperclip-dev skill was duplicating all the CLI command option tables
from doc/DEVELOPING.md. Replaced with MUST-read references to the canonical
doc, keeping only a quick command reference table, workflow overview, and
common mistakes. Same gated-reference pattern used for the PR conventions
section.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-16 19:28:47 -07:00
Devin Foley
d5afd4eacb feat: add paperclip-dev skill with optional bundled skill support
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-16 19:19:41 -07:00
566 changed files with 3855 additions and 116209 deletions

View File

@@ -154,14 +154,6 @@ Each AGENTS.md body should include not just what the agent does, but how they fi
This turns a collection of agents into an organization that actually works together. Without workflow context, agents operate in isolation — they do their job but don't know what happens before or after them.
Add a concise execution contract to every generated working agent:
- Start actionable work in the same heartbeat and do not stop at a plan unless planning was requested.
- Leave durable progress in comments, documents, or work products with the next action.
- Use child issues for long or parallel delegated work instead of polling agents, sessions, or processes.
- Mark blocked work with the unblock owner and action.
- Respect budget, pause/cancel, approval gates, and company boundaries.
### Step 5: Confirm Output Location
Ask the user where to write the package. Common options:

View File

@@ -105,13 +105,6 @@ Your responsibilities:
- Implement features and fix bugs
- Write tests and documentation
- Participate in code reviews
Execution contract:
- Start actionable implementation work in the same heartbeat; do not stop at a plan unless planning was requested.
- Leave durable progress with a clear next action.
- Use child issues for long or parallel delegated work instead of polling agents, sessions, or processes.
- Mark blocked work with the unblock owner and action.
```
## teams/engineering/TEAM.md

View File

@@ -548,7 +548,7 @@ Import from `@paperclipai/adapter-utils/server-utils`:
### Prompt Templates
- Support `promptTemplate` for every run
- Use `renderTemplate()` with the standard variable set
- Default prompt should use `DEFAULT_PAPERCLIP_AGENT_PROMPT_TEMPLATE` from `@paperclipai/adapter-utils/server-utils` so local adapters share Paperclip's execution contract: act in the same heartbeat, avoid planning-only exits unless requested, leave durable progress and a next action, use child issues instead of polling, mark blockers with owner/action, and respect governance boundaries.
- Default prompt: `"You are agent {{agent.id}} ({{agent.name}}). Continue your Paperclip work."`
### Error Handling
- Differentiate timeout vs process error vs parse failure

View File

@@ -2,6 +2,3 @@ DATABASE_URL=postgres://paperclip:paperclip@localhost:5432/paperclip
PORT=3100
SERVE_UI=false
BETTER_AUTH_SECRET=paperclip-dev-secret
# Discord webhook for daily merge digest (scripts/discord-daily-digest.sh)
# DISCORD_WEBHOOK_URL=https://discord.com/api/webhooks/...

View File

@@ -38,8 +38,6 @@
-
> For core feature work, check [`ROADMAP.md`](ROADMAP.md) first and discuss it in `#dev` before opening the PR. Feature PRs that overlap with planned core work may need to be redirected — check the roadmap first. See `CONTRIBUTING.md`.
## Model Used
<!--
@@ -59,7 +57,6 @@
- [ ] I have included a thinking path that traces from project context to this change
- [ ] I have specified the model used (with version and capability details)
- [ ] I have checked ROADMAP.md and confirmed this PR does not duplicate planned core work
- [ ] I have run tests locally and they pass
- [ ] I have added or updated tests where applicable
- [ ] If this change affects the UI, I have included before/after screenshots

4
.gitignore vendored
View File

@@ -1,7 +1,4 @@
node_modules
node_modules/
**/node_modules
**/node_modules/
dist/
.env
*.tsbuildinfo
@@ -35,7 +32,6 @@ server/src/**/*.d.ts
server/src/**/*.d.ts.map
tmp/
feedback-export-*
diagnostics/
# Editor / tool temp files
*.tmp

View File

@@ -51,21 +51,6 @@ All tests must pass before a PR can be merged. Run them locally first and verify
We use [Greptile](https://greptile.com) for automated code review. Your PR must achieve a **5/5 Greptile score** with **all Greptile comments addressed** before it can be merged. If Greptile leaves comments, fix or respond to each one and request a re-review.
## Feature Contributions
We actively manage the core Paperclip feature roadmap.
Uncoordinated feature PRs against the core product may be closed, even when the implementation is thoughtful and high quality. That is about roadmap ownership, product coherence, and long-term maintenance commitment, not a judgment about the effort.
If you want to contribute a feature:
- Check [ROADMAP.md](ROADMAP.md) first
- Start the discussion in Discord -> `#dev` before writing code
- If the idea fits as an extension, prefer building it with the [plugin system](doc/plugins/PLUGIN_SPEC.md)
- If you want to show a possible direction, reference implementations are welcome as feedback, but they generally will not be merged directly into core
Bugs, docs improvements, and small targeted improvements are still the easiest path to getting merged, and we really do appreciate them.
## General Rules (both paths)
- Write clear commit messages

View File

@@ -2,7 +2,15 @@ FROM node:lts-trixie-slim AS base
ARG USER_UID=1000
ARG USER_GID=1000
RUN apt-get update \
&& apt-get install -y --no-install-recommends ca-certificates gosu curl gh git wget ripgrep python3 \
&& apt-get install -y --no-install-recommends ca-certificates gosu curl git wget ripgrep python3 \
&& mkdir -p -m 755 /etc/apt/keyrings \
&& wget -nv -O/etc/apt/keyrings/githubcli-archive-keyring.gpg https://cli.github.com/packages/githubcli-archive-keyring.gpg \
&& echo "20e0125d6f6e077a9ad46f03371bc26d90b04939fb95170f5a1905099cc6bcc0 /etc/apt/keyrings/githubcli-archive-keyring.gpg" | sha256sum -c - \
&& chmod go+r /etc/apt/keyrings/githubcli-archive-keyring.gpg \
&& mkdir -p -m 755 /etc/apt/sources.list.d \
&& echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" > /etc/apt/sources.list.d/github-cli.list \
&& apt-get update \
&& apt-get install -y --no-install-recommends gh \
&& rm -rf /var/lib/apt/lists/* \
&& corepack enable
@@ -48,9 +56,6 @@ ARG USER_GID=1000
WORKDIR /app
COPY --chown=node:node --from=build /app /app
RUN npm install --global --omit=dev @anthropic-ai/claude-code@latest @openai/codex@latest opencode-ai \
&& apt-get update \
&& apt-get install -y --no-install-recommends openssh-client jq \
&& rm -rf /var/lib/apt/lists/* \
&& mkdir -p /paperclip \
&& chown node:node /paperclip

View File

@@ -256,10 +256,10 @@ See [doc/DEVELOPING.md](doc/DEVELOPING.md) for the full development guide.
- ✅ Scheduled Routines
- ✅ Better Budgeting
- ✅ Agent Reviews and Approvals
- Multiple Human Users
- Multiple Human Users
- ⚪ Cloud / Sandbox agents (e.g. Cursor / e2b agents)
- ⚪ Artifacts & Work Products
- ⚪ Memory / Knowledge
- ⚪ Memory & Knowledge
- ⚪ Enforced Outcomes
- ⚪ MAXIMIZER MODE
- ⚪ Deep Planning
@@ -270,8 +270,6 @@ See [doc/DEVELOPING.md](doc/DEVELOPING.md) for the full development guide.
- ⚪ Cloud deployments
- ⚪ Desktop App
This is the short roadmap preview. See the full roadmap in [ROADMAP.md](ROADMAP.md).
<br/>
## Community & Plugins

View File

@@ -1,97 +0,0 @@
# Roadmap
This document expands the roadmap preview in `README.md`.
Paperclip is still moving quickly. The list below is directional, not promised, and priorities may shift as we learn from users and from operating real AI companies with the product.
We value community involvement and want to make sure contributor energy goes toward areas where it can land.
We may accept contributions in the areas below, but if you want to work on roadmap-level core features, please coordinate with us first in Discord (`#dev`) before writing code. Bugs, docs, polish, and tightly scoped improvements are still the easiest contributions to merge.
If you want to extend Paperclip today, the best path is often the [plugin system](doc/plugins/PLUGIN_SPEC.md). Community reference implementations are also useful feedback even when they are not merged directly into core.
## Milestones
### ✅ Plugin system
Paperclip should keep a thin core and rich edges. Plugins are the path for optional capabilities like knowledge bases, custom tracing, queues, doc editors, and other product-specific surfaces that do not need to live in the control plane itself.
### ✅ Get OpenClaw / claw-style agent employees
Paperclip should be able to hire and manage real claw-style agent workers, not just a narrow built-in runtime. This is part of the larger "bring your own agent" story and keeps the control plane useful across different agent ecosystems.
### ✅ companies.sh - import and export entire organizations
Reusable companies matter. Import/export is the foundation for moving org structures, agent definitions, and reusable company setups between environments and eventually for broader company-template distribution.
### ✅ Easy AGENTS.md configurations
Agent setup should feel repo-native and legible. Simple `AGENTS.md`-style configuration lowers the barrier to getting an agent team running and makes it easier for contributors to understand how a company is wired together.
### ✅ Skills Manager
Agents need a practical way to discover, install, and use skills without every setup becoming bespoke. The skills layer is part of making Paperclip companies more reusable and easier to operate.
### ✅ Scheduled Routines
Recurring work should be native. Routine tasks like reports, reviews, and other periodic work need first-class scheduling so the company keeps operating even when no human is manually kicking work off.
### ✅ Better Budgeting
Budgets are a core control-plane feature, not an afterthought. Better budgeting means clearer spend visibility, safer hard stops, and better operator control over how autonomy turns into real cost.
### ✅ Agent Reviews and Approvals
Paperclip should support explicit review and approval stages as first-class workflow steps, not just ad hoc comments. That means reviewer routing, approval gates, change requests, and durable audit trails that fit the same task model as the rest of the control plane.
### ✅ Multiple Human Users
Paperclip needs a clearer path from solo operator to real human teams. That means shared board access, safer collaboration, and a better model for several humans supervising the same autonomous company.
### ⚪ Cloud / Sandbox agents (e.g. Cursor / e2b agents)
We want agents to run in more remote and sandboxed environments while preserving the same Paperclip control-plane model. This makes the system safer, more flexible, and more useful outside a single trusted local machine.
### ⚪ Artifacts & Work Products
Paperclip should make outputs first-class. That means generated artifacts, previews, deployable outputs, and the handoff from "agent did work" to "here is the result" should become more visible and easier to operate.
### ⚪ Memory / Knowledge
We want a stronger memory and knowledge surface for companies, agents, and projects. That includes durable memory, better recall of prior decisions and context, and a clearer path for knowledge-style capabilities without turning Paperclip into a generic chat app.
### ⚪ Enforced Outcomes
Paperclip should get stricter about what counts as finished work. Tasks, approvals, and execution flows should resolve to clear outcomes like merged code, published artifacts, shipped docs, or explicit decisions instead of stopping at vague status updates.
### ⚪ MAXIMIZER MODE
This is the direction for higher-autonomy execution: more aggressive delegation, deeper follow-through, and stronger operating loops with clear budgets, visibility, and governance. The point is not hidden autonomy; the point is more output per human supervisor.
### ⚪ Deep Planning
Some work needs more than a task description before execution starts. Deeper planning means stronger issue documents, revisionable plans, and clearer review loops for strategy-heavy work before agents begin execution.
### ⚪ Work Queues
Paperclip should support queue-style work streams for repeatable inputs like support, triage, review, and backlog intake. That would make it easier to route work continuously without turning every system into a one-off workflow.
### ⚪ Self-Organization
As companies grow, agents should be able to propose useful structural changes such as role adjustments, delegation changes, and new recurring routines. The goal is adaptive organizations that still stay within governance and approval boundaries.
### ⚪ Automatic Organizational Learning
Paperclip should get better at turning completed work into reusable organizational knowledge. That includes capturing playbooks, recurring fixes, and decision patterns so future work starts from what the company has already learned.
### ⚪ CEO Chat
We want a lighter-weight way to talk to leadership agents, but those conversations should still resolve to real work objects like plans, issues, approvals, or decisions. This should improve interaction without changing the core task-and-comments model.
### ⚪ Cloud deployments
Local-first remains important, but Paperclip also needs a cleaner shared deployment story. Teams should be able to run the same product in hosted or semi-hosted environments without changing the mental model.
### ⚪ Desktop App
A desktop app can make Paperclip feel more accessible and persistent for day-to-day operators. The goal is easier access, better local ergonomics, and a smoother default experience for users who want the control plane always close at hand.

View File

@@ -12,7 +12,7 @@
<p align="center">
<a href="https://github.com/paperclipai/paperclip/blob/master/LICENSE"><img src="https://img.shields.io/badge/license-MIT-blue" alt="MIT License" /></a>
<a href="https://github.com/paperclipai/paperclip/stargazers"><img src="https://img.shields.io/github/stars/paperclipai/paperclip?style=flat" alt="Stars" /></a>
<a href="https://discord.gg/m4HZY7xNG3"><img src="https://img.shields.io/discord/000000000?label=discord" alt="Discord" /></a>
<a href="https://discord.gg/m4HZY7xNG3"><img src="https://img.shields.io/badge/discord-join%20chat-5865F2?logo=discord&logoColor=white" alt="Discord" /></a>
</p>
<br/>
@@ -258,7 +258,7 @@ See [doc/DEVELOPING.md](https://github.com/paperclipai/paperclip/blob/master/doc
- ⚪ Artifacts & Deployments
- ⚪ CEO Chat
- ⚪ MAXIMIZER MODE
- Multiple Human Users
- Multiple Human Users
- ⚪ Cloud / Sandbox agents (e.g. Cursor / e2b agents)
- ⚪ Cloud deployments
- ⚪ Desktop App

View File

@@ -287,11 +287,6 @@ describeEmbeddedPostgres("paperclipai company import/export e2e", () => {
headers: { "content-type": "application/json" },
body: JSON.stringify({ name: `CLI Export Source ${Date.now()}` }),
});
await api(apiBase, `/api/companies/${sourceCompany.id}`, {
method: "PATCH",
headers: { "content-type": "application/json" },
body: JSON.stringify({ requireBoardApprovalForNewAgents: false }),
});
const sourceAgent = await api<{ id: string; name: string }>(
apiBase,

View File

@@ -3,15 +3,11 @@ import os from "node:os";
import path from "node:path";
import { execFileSync } from "node:child_process";
import { randomUUID } from "node:crypto";
import { eq } from "drizzle-orm";
import { afterEach, describe, expect, it, vi } from "vitest";
import {
agents,
authUsers,
companies,
createDb,
issueComments,
issues,
projects,
routines,
routineTriggers,
@@ -20,7 +16,6 @@ import {
copyGitHooksToWorktreeGitDir,
copySeededSecretsKey,
pauseSeededScheduledRoutines,
quarantineSeededWorktreeExecutionState,
readSourceAttachmentBody,
rebindWorkspaceCwd,
resolveSourceConfigPath,
@@ -52,7 +47,6 @@ import {
const ORIGINAL_CWD = process.cwd();
const ORIGINAL_ENV = { ...process.env };
const embeddedPostgresSupport = await getEmbeddedPostgresTestSupport();
const itEmbeddedPostgres = embeddedPostgresSupport.supported ? it : it.skip;
const describeEmbeddedPostgres = embeddedPostgresSupport.supported ? describe : describe.skip;
if (!embeddedPostgresSupport.supported) {
@@ -286,138 +280,6 @@ describe("worktree helpers", () => {
expect(full.nullifyColumns).toEqual({});
});
itEmbeddedPostgres("quarantines copied live execution state in seeded worktree databases", async () => {
const tempDb = await startEmbeddedPostgresTestDatabase("paperclip-worktree-quarantine-");
const db = createDb(tempDb.connectionString);
const companyId = randomUUID();
const agentId = randomUUID();
const idleAgentId = randomUUID();
const inProgressIssueId = randomUUID();
const todoIssueId = randomUUID();
const reviewIssueId = randomUUID();
const userIssueId = randomUUID();
try {
await db.insert(companies).values({
id: companyId,
name: "Paperclip",
issuePrefix: "WTQ",
requireBoardApprovalForNewAgents: false,
});
await db.insert(agents).values([
{
id: agentId,
companyId,
name: "CodexCoder",
role: "engineer",
status: "running",
adapterType: "codex_local",
adapterConfig: {},
runtimeConfig: {
heartbeat: { enabled: true, intervalSec: 60 },
wakeOnDemand: true,
},
permissions: {},
},
{
id: idleAgentId,
companyId,
name: "Reviewer",
role: "reviewer",
status: "idle",
adapterType: "codex_local",
adapterConfig: {},
runtimeConfig: { heartbeat: { enabled: false, intervalSec: 300 } },
permissions: {},
},
]);
await db.insert(issues).values([
{
id: inProgressIssueId,
companyId,
title: "Copied in-flight issue",
status: "in_progress",
priority: "medium",
assigneeAgentId: agentId,
issueNumber: 1,
identifier: "WTQ-1",
executionAgentNameKey: "codexcoder",
executionLockedAt: new Date("2026-04-18T00:00:00.000Z"),
},
{
id: todoIssueId,
companyId,
title: "Copied assigned todo issue",
status: "todo",
priority: "medium",
assigneeAgentId: agentId,
issueNumber: 2,
identifier: "WTQ-2",
},
{
id: reviewIssueId,
companyId,
title: "Copied assigned review issue",
status: "in_review",
priority: "medium",
assigneeAgentId: idleAgentId,
issueNumber: 3,
identifier: "WTQ-3",
},
{
id: userIssueId,
companyId,
title: "Copied user issue",
status: "todo",
priority: "medium",
assigneeUserId: "user-1",
issueNumber: 4,
identifier: "WTQ-4",
},
]);
await expect(quarantineSeededWorktreeExecutionState(tempDb.connectionString)).resolves.toEqual({
disabledTimerHeartbeats: 1,
resetRunningAgents: 1,
quarantinedInProgressIssues: 1,
unassignedTodoIssues: 1,
unassignedReviewIssues: 1,
});
const [quarantinedAgent] = await db.select().from(agents).where(eq(agents.id, agentId));
expect(quarantinedAgent?.status).toBe("idle");
expect(quarantinedAgent?.runtimeConfig).toMatchObject({
heartbeat: { enabled: false, intervalSec: 60 },
wakeOnDemand: true,
});
const [inProgressIssue] = await db.select().from(issues).where(eq(issues.id, inProgressIssueId));
expect(inProgressIssue?.status).toBe("blocked");
expect(inProgressIssue?.assigneeAgentId).toBeNull();
expect(inProgressIssue?.executionAgentNameKey).toBeNull();
expect(inProgressIssue?.executionLockedAt).toBeNull();
const [todoIssue] = await db.select().from(issues).where(eq(issues.id, todoIssueId));
expect(todoIssue?.status).toBe("todo");
expect(todoIssue?.assigneeAgentId).toBeNull();
const [reviewIssue] = await db.select().from(issues).where(eq(issues.id, reviewIssueId));
expect(reviewIssue?.status).toBe("in_review");
expect(reviewIssue?.assigneeAgentId).toBeNull();
const [userIssue] = await db.select().from(issues).where(eq(issues.id, userIssueId));
expect(userIssue?.status).toBe("todo");
expect(userIssue?.assigneeUserId).toBe("user-1");
const comments = await db.select().from(issueComments).where(eq(issueComments.issueId, inProgressIssueId));
expect(comments).toHaveLength(1);
expect(comments[0]?.body).toContain("Quarantined during worktree seed");
} finally {
await db.$client?.end?.({ timeout: 5 }).catch(() => undefined);
await tempDb.cleanup();
}
}, 20_000);
it("copies the source local_encrypted secrets key into the seeded worktree instance", () => {
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "paperclip-worktree-secrets-"));
const originalInlineMasterKey = process.env.PAPERCLIP_SECRETS_MASTER_KEY;
@@ -511,97 +373,6 @@ describe("worktree helpers", () => {
}
});
itEmbeddedPostgres(
"seeds authenticated users into minimally cloned worktree instances",
async () => {
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "paperclip-worktree-auth-seed-"));
const worktreeRoot = path.join(tempRoot, "PAP-999-auth-seed");
const sourceHome = path.join(tempRoot, "source-home");
const sourceConfigDir = path.join(sourceHome, "instances", "source");
const sourceConfigPath = path.join(sourceConfigDir, "config.json");
const sourceEnvPath = path.join(sourceConfigDir, ".env");
const sourceKeyPath = path.join(sourceConfigDir, "secrets", "master.key");
const worktreeHome = path.join(tempRoot, ".paperclip-worktrees");
const originalCwd = process.cwd();
const sourceDb = await startEmbeddedPostgresTestDatabase("paperclip-worktree-auth-source-");
try {
const sourceDbClient = createDb(sourceDb.connectionString);
await sourceDbClient.insert(authUsers).values({
id: "user-existing",
email: "existing@paperclip.ing",
name: "Existing User",
emailVerified: true,
createdAt: new Date(),
updatedAt: new Date(),
});
fs.mkdirSync(path.dirname(sourceKeyPath), { recursive: true });
fs.mkdirSync(worktreeRoot, { recursive: true });
const sourceConfig = buildSourceConfig();
sourceConfig.database = {
mode: "postgres",
embeddedPostgresDataDir: path.join(sourceConfigDir, "db"),
embeddedPostgresPort: 54329,
backup: {
enabled: true,
intervalMinutes: 60,
retentionDays: 30,
dir: path.join(sourceConfigDir, "backups"),
},
connectionString: sourceDb.connectionString,
};
sourceConfig.logging.logDir = path.join(sourceConfigDir, "logs");
sourceConfig.storage.localDisk.baseDir = path.join(sourceConfigDir, "storage");
sourceConfig.secrets.localEncrypted.keyFilePath = sourceKeyPath;
fs.writeFileSync(sourceConfigPath, JSON.stringify(sourceConfig, null, 2) + "\n", "utf8");
fs.writeFileSync(sourceEnvPath, "", "utf8");
fs.writeFileSync(sourceKeyPath, "source-master-key", "utf8");
process.chdir(worktreeRoot);
await worktreeInitCommand({
name: "PAP-999-auth-seed",
home: worktreeHome,
fromConfig: sourceConfigPath,
force: true,
});
const targetConfig = JSON.parse(
fs.readFileSync(path.join(worktreeRoot, ".paperclip", "config.json"), "utf8"),
) as PaperclipConfig;
const { default: EmbeddedPostgres } = await import("embedded-postgres");
const targetPg = new EmbeddedPostgres({
databaseDir: targetConfig.database.embeddedPostgresDataDir,
user: "paperclip",
password: "paperclip",
port: targetConfig.database.embeddedPostgresPort,
persistent: true,
initdbFlags: ["--encoding=UTF8", "--locale=C", "--lc-messages=C"],
onLog: () => {},
onError: () => {},
});
await targetPg.start();
try {
const targetDb = createDb(
`postgres://paperclip:paperclip@127.0.0.1:${targetConfig.database.embeddedPostgresPort}/paperclip`,
);
const seededUsers = await targetDb.select().from(authUsers);
expect(seededUsers.some((row) => row.email === "existing@paperclip.ing")).toBe(true);
} finally {
await targetPg.stop();
}
} finally {
process.chdir(originalCwd);
await sourceDb.cleanup();
fs.rmSync(tempRoot, { recursive: true, force: true });
}
},
20000,
);
it("avoids ports already claimed by sibling worktree instance configs", async () => {
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "paperclip-worktree-claimed-ports-"));
const repoRoot = path.join(tempRoot, "repo");

View File

@@ -93,7 +93,6 @@ type WorktreeInitOptions = {
dbPort?: number;
seed?: boolean;
seedMode?: string;
preserveLiveWork?: boolean;
force?: boolean;
};
@@ -127,7 +126,6 @@ type WorktreeReseedOptions = {
fromDataDir?: string;
fromInstance?: string;
seedMode?: string;
preserveLiveWork?: boolean;
yes?: boolean;
allowLiveTarget?: boolean;
};
@@ -139,7 +137,6 @@ type WorktreeRepairOptions = {
fromDataDir?: string;
fromInstance?: string;
seedMode?: string;
preserveLiveWork?: boolean;
noSeed?: boolean;
allowLiveTarget?: boolean;
};
@@ -182,8 +179,6 @@ type CopiedGitHooksResult = {
type SeedWorktreeDatabaseResult = {
backupSummary: string;
pausedScheduledRoutines: number;
executionQuarantine: SeededWorktreeExecutionQuarantineSummary;
reboundWorkspaces: Array<{
name: string;
fromCwd: string;
@@ -191,14 +186,6 @@ type SeedWorktreeDatabaseResult = {
}>;
};
export type SeededWorktreeExecutionQuarantineSummary = {
disabledTimerHeartbeats: number;
resetRunningAgents: number;
quarantinedInProgressIssues: number;
unassignedTodoIssues: number;
unassignedReviewIssues: number;
};
function nonEmpty(value: string | null | undefined): string | null {
return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
}
@@ -211,18 +198,6 @@ function isCurrentSourceConfigPath(sourceConfigPath: string): boolean {
return path.resolve(currentConfigPath) === path.resolve(sourceConfigPath);
}
function formatSeededWorktreeExecutionQuarantineSummary(
summary: SeededWorktreeExecutionQuarantineSummary,
): string {
return [
`disabled timer heartbeats: ${summary.disabledTimerHeartbeats}`,
`reset running agents: ${summary.resetRunningAgents}`,
`quarantined in-progress issues: ${summary.quarantinedInProgressIssues}`,
`unassigned todo issues: ${summary.unassignedTodoIssues}`,
`unassigned review issues: ${summary.unassignedReviewIssues}`,
].join(", ");
}
const WORKTREE_NAME_PREFIX = "paperclip-";
function resolveWorktreeMakeName(name: string): string {
@@ -1144,133 +1119,6 @@ export async function pauseSeededScheduledRoutines(connectionString: string): Pr
}
}
const EMPTY_SEEDED_WORKTREE_EXECUTION_QUARANTINE_SUMMARY: SeededWorktreeExecutionQuarantineSummary = {
disabledTimerHeartbeats: 0,
resetRunningAgents: 0,
quarantinedInProgressIssues: 0,
unassignedTodoIssues: 0,
unassignedReviewIssues: 0,
};
function isRecord(value: unknown): value is Record<string, unknown> {
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
}
function isEnabledValue(value: unknown): boolean {
return value === true || value === "true" || value === 1 || value === "1";
}
function normalizeWorktreeRuntimeConfig(runtimeConfig: unknown): {
runtimeConfig: Record<string, unknown>;
disabledTimerHeartbeat: boolean;
changed: boolean;
} {
const nextRuntimeConfig = isRecord(runtimeConfig) ? { ...runtimeConfig } : {};
const heartbeat = isRecord(nextRuntimeConfig.heartbeat) ? { ...nextRuntimeConfig.heartbeat } : null;
if (!heartbeat) {
return { runtimeConfig: nextRuntimeConfig, disabledTimerHeartbeat: false, changed: false };
}
const disabledTimerHeartbeat = isEnabledValue(heartbeat.enabled);
if (heartbeat.enabled !== false) {
heartbeat.enabled = false;
nextRuntimeConfig.heartbeat = heartbeat;
return { runtimeConfig: nextRuntimeConfig, disabledTimerHeartbeat, changed: true };
}
return { runtimeConfig: nextRuntimeConfig, disabledTimerHeartbeat: false, changed: false };
}
export async function quarantineSeededWorktreeExecutionState(
connectionString: string,
): Promise<SeededWorktreeExecutionQuarantineSummary> {
const db = createDb(connectionString);
const summary = { ...EMPTY_SEEDED_WORKTREE_EXECUTION_QUARANTINE_SUMMARY };
try {
await db.transaction(async (tx) => {
const seededAgents = await tx
.select({
id: agents.id,
status: agents.status,
runtimeConfig: agents.runtimeConfig,
})
.from(agents);
for (const agent of seededAgents) {
const normalized = normalizeWorktreeRuntimeConfig(agent.runtimeConfig);
const nextStatus = agent.status === "running" ? "idle" : agent.status;
if (normalized.disabledTimerHeartbeat) {
summary.disabledTimerHeartbeats += 1;
}
if (agent.status === "running") {
summary.resetRunningAgents += 1;
}
if (normalized.changed || nextStatus !== agent.status) {
await tx
.update(agents)
.set({
runtimeConfig: normalized.runtimeConfig,
status: nextStatus,
updatedAt: new Date(),
})
.where(eq(agents.id, agent.id));
}
}
const affectedIssues = await tx
.select({
id: issues.id,
companyId: issues.companyId,
status: issues.status,
})
.from(issues)
.where(
and(
sql`${issues.assigneeAgentId} is not null`,
sql`${issues.assigneeUserId} is null`,
inArray(issues.status, ["todo", "in_progress", "in_review"]),
),
);
for (const issue of affectedIssues) {
const nextStatus = issue.status === "in_progress" ? "blocked" : issue.status;
await tx
.update(issues)
.set({
status: nextStatus,
assigneeAgentId: null,
checkoutRunId: null,
executionRunId: null,
executionAgentNameKey: null,
executionLockedAt: null,
executionWorkspaceId: null,
updatedAt: new Date(),
})
.where(eq(issues.id, issue.id));
if (issue.status === "in_progress") {
summary.quarantinedInProgressIssues += 1;
await tx.insert(issueComments).values({
companyId: issue.companyId,
issueId: issue.id,
body:
"Quarantined during worktree seed so copied in-flight work does not auto-run in this isolated instance. " +
"Reassign or unblock here only if you intentionally want the worktree instance to own this task.",
});
} else if (issue.status === "todo") {
summary.unassignedTodoIssues += 1;
} else if (issue.status === "in_review") {
summary.unassignedReviewIssues += 1;
}
}
});
return summary;
} finally {
await db.$client?.end?.({ timeout: 5 }).catch(() => undefined);
}
}
async function seedWorktreeDatabase(input: {
sourceConfigPath: string;
sourceConfig: PaperclipConfig;
@@ -1278,7 +1126,6 @@ async function seedWorktreeDatabase(input: {
targetPaths: WorktreeLocalPaths;
instanceId: string;
seedMode: WorktreeSeedMode;
preserveLiveWork?: boolean;
}): Promise<SeedWorktreeDatabaseResult> {
const seedPlan = resolveWorktreeSeedPlan(input.seedMode);
const sourceEnvFile = resolvePaperclipEnvFile(input.sourceConfigPath);
@@ -1329,10 +1176,7 @@ async function seedWorktreeDatabase(input: {
backupFile: backup.backupFile,
});
await applyPendingMigrations(targetConnectionString);
const executionQuarantine = input.preserveLiveWork
? { ...EMPTY_SEEDED_WORKTREE_EXECUTION_QUARANTINE_SUMMARY }
: await quarantineSeededWorktreeExecutionState(targetConnectionString);
const pausedScheduledRoutines = await pauseSeededScheduledRoutines(targetConnectionString);
await pauseSeededScheduledRoutines(targetConnectionString);
const reboundWorkspaces = await rebindSeededProjectWorkspaces({
targetConnectionString,
currentCwd: input.targetPaths.cwd,
@@ -1340,8 +1184,6 @@ async function seedWorktreeDatabase(input: {
return {
backupSummary: formatDatabaseBackupResult(backup),
pausedScheduledRoutines,
executionQuarantine,
reboundWorkspaces,
};
} finally {
@@ -1420,8 +1262,6 @@ async function runWorktreeInit(opts: WorktreeInitOptions): Promise<void> {
const copiedGitHooks = copyGitHooksToWorktreeGitDir(cwd);
let seedSummary: string | null = null;
let seedExecutionQuarantineSummary: SeededWorktreeExecutionQuarantineSummary | null = null;
let pausedScheduledRoutineCount: number | null = null;
let reboundWorkspaceSummary: SeedWorktreeDatabaseResult["reboundWorkspaces"] = [];
if (opts.seed !== false) {
if (!sourceConfig) {
@@ -1439,11 +1279,8 @@ async function runWorktreeInit(opts: WorktreeInitOptions): Promise<void> {
targetPaths: paths,
instanceId,
seedMode,
preserveLiveWork: opts.preserveLiveWork,
});
seedSummary = seeded.backupSummary;
seedExecutionQuarantineSummary = seeded.executionQuarantine;
pausedScheduledRoutineCount = seeded.pausedScheduledRoutines;
reboundWorkspaceSummary = seeded.reboundWorkspaces;
spinner.stop(`Seeded isolated worktree database (${seedMode}).`);
} catch (error) {
@@ -1466,16 +1303,6 @@ async function runWorktreeInit(opts: WorktreeInitOptions): Promise<void> {
if (seedSummary) {
p.log.message(pc.dim(`Seed mode: ${seedMode}`));
p.log.message(pc.dim(`Seed snapshot: ${seedSummary}`));
if (opts.preserveLiveWork) {
p.log.warning("Preserved copied live work; this worktree instance may auto-run source-instance assignments.");
} else if (seedExecutionQuarantineSummary) {
p.log.message(
pc.dim(`Seed execution quarantine: ${formatSeededWorktreeExecutionQuarantineSummary(seedExecutionQuarantineSummary)}`),
);
}
if (pausedScheduledRoutineCount != null) {
p.log.message(pc.dim(`Paused scheduled routines: ${pausedScheduledRoutineCount}`));
}
for (const rebound of reboundWorkspaceSummary) {
p.log.message(
pc.dim(`Rebound workspace ${rebound.name}: ${rebound.fromCwd} -> ${rebound.toCwd}`),
@@ -3120,20 +2947,11 @@ async function runWorktreeReseed(opts: WorktreeReseedOptions): Promise<void> {
targetPaths,
instanceId: targetPaths.instanceId,
seedMode,
preserveLiveWork: opts.preserveLiveWork,
});
spinner.stop(`Reseeded ${targetEndpoint.label} (${seedMode}).`);
p.log.message(pc.dim(`Source: ${source.configPath}`));
p.log.message(pc.dim(`Target: ${targetEndpoint.configPath}`));
p.log.message(pc.dim(`Seed snapshot: ${seeded.backupSummary}`));
if (opts.preserveLiveWork) {
p.log.warning("Preserved copied live work; this worktree instance may auto-run source-instance assignments.");
} else {
p.log.message(
pc.dim(`Seed execution quarantine: ${formatSeededWorktreeExecutionQuarantineSummary(seeded.executionQuarantine)}`),
);
}
p.log.message(pc.dim(`Paused scheduled routines: ${seeded.pausedScheduledRoutines}`));
for (const rebound of seeded.reboundWorkspaces) {
p.log.message(
pc.dim(`Rebound workspace ${rebound.name}: ${rebound.fromCwd} -> ${rebound.toCwd}`),
@@ -3197,7 +3015,6 @@ export async function worktreeRepairCommand(opts: WorktreeRepairOptions): Promis
fromConfig: source.configPath,
to: target.rootPath,
seedMode,
preserveLiveWork: opts.preserveLiveWork,
yes: true,
allowLiveTarget: opts.allowLiveTarget,
});
@@ -3230,7 +3047,6 @@ export async function worktreeRepairCommand(opts: WorktreeRepairOptions): Promis
fromInstance: opts.fromInstance,
seed: opts.noSeed ? false : true,
seedMode,
preserveLiveWork: opts.preserveLiveWork,
force: true,
});
} finally {
@@ -3254,7 +3070,6 @@ export function registerWorktreeCommands(program: Command): void {
.option("--server-port <port>", "Preferred server port", (value) => Number(value))
.option("--db-port <port>", "Preferred embedded Postgres port", (value) => Number(value))
.option("--seed-mode <mode>", "Seed profile: minimal or full (default: minimal)", "minimal")
.option("--preserve-live-work", "Do not quarantine copied agent timers or assigned open issues in the seeded worktree", false)
.option("--no-seed", "Skip database seeding from the source instance")
.option("--force", "Replace existing repo-local config and isolated instance data", false)
.action(worktreeMakeCommand);
@@ -3271,7 +3086,6 @@ export function registerWorktreeCommands(program: Command): void {
.option("--server-port <port>", "Preferred server port", (value) => Number(value))
.option("--db-port <port>", "Preferred embedded Postgres port", (value) => Number(value))
.option("--seed-mode <mode>", "Seed profile: minimal or full (default: minimal)", "minimal")
.option("--preserve-live-work", "Do not quarantine copied agent timers or assigned open issues in the seeded worktree", false)
.option("--no-seed", "Skip database seeding from the source instance")
.option("--force", "Replace existing repo-local config and isolated instance data", false)
.action(worktreeInitCommand);
@@ -3311,7 +3125,6 @@ export function registerWorktreeCommands(program: Command): void {
.option("--from-data-dir <path>", "Source PAPERCLIP_HOME used when deriving the source config")
.option("--from-instance <id>", "Source instance id when deriving the source config")
.option("--seed-mode <mode>", "Seed profile: minimal or full (default: full)", "full")
.option("--preserve-live-work", "Do not quarantine copied agent timers or assigned open issues in the seeded worktree", false)
.option("--yes", "Skip the destructive confirmation prompt", false)
.option("--allow-live-target", "Override the guard that requires the target worktree DB to be stopped first", false)
.action(worktreeReseedCommand);
@@ -3325,7 +3138,6 @@ export function registerWorktreeCommands(program: Command): void {
.option("--from-data-dir <path>", "Source PAPERCLIP_HOME used when deriving the source config")
.option("--from-instance <id>", "Source instance id when deriving the source config (default: default)")
.option("--seed-mode <mode>", "Seed profile: minimal or full (default: minimal)", "minimal")
.option("--preserve-live-work", "Do not quarantine copied agent timers or assigned open issues in the seeded worktree", false)
.option("--no-seed", "Repair metadata only and skip reseeding when bootstrapping a missing worktree config", false)
.option("--allow-live-target", "Override the guard that requires the target worktree DB to be stopped first", false)
.action(worktreeRepairCommand);

View File

@@ -27,18 +27,6 @@ pnpm db:migrate
When `DATABASE_URL` is unset, this command targets the current embedded PostgreSQL instance for your active Paperclip config/instance.
Issue reference mentions follow the normal migration path: the schema migration creates the tracking table, but it does not backfill historical issue titles, descriptions, comments, or documents automatically.
To backfill existing content manually after migrating, run:
```sh
pnpm issue-references:backfill
# optional: limit to one company
pnpm issue-references:backfill -- --company <company-id>
```
Future issue, comment, and document writes sync references automatically without running the backfill command.
This mode is ideal for local development and one-command installs.
Docker note: the Docker quickstart image also uses embedded PostgreSQL by default. Persist `/paperclip` to keep DB state across container restarts (see `doc/DOCKER.md`).
@@ -106,16 +94,6 @@ Set `DATABASE_URL` in your `.env`:
DATABASE_URL=postgres://postgres.[PROJECT-REF]:[PASSWORD]@aws-0-[REGION].pooler.supabase.com:6543/postgres
```
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 using connection pooling (port 6543), the `postgres` client must disable prepared statements. Update `packages/db/src/client.ts`:
```ts

View File

@@ -142,4 +142,3 @@ This prevents lockout when a user migrates from long-running local trusted usage
- implementation plan: `doc/plans/deployment-auth-mode-consolidation.md`
- V1 contract: `doc/SPEC-implementation.md`
- operator workflows: `doc/DEVELOPING.md` and `doc/CLI.md`
- invite/join state map: `doc/spec/invite-flow.md`

View File

@@ -43,17 +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.
## Storybook
The board UI Storybook keeps stories and Storybook config under `ui/storybook/` so component review files stay out of the app source routes.
```sh
pnpm storybook
pnpm build-storybook
```
These run the `@paperclipai/ui` Storybook on port `6006` and build the static output to `ui/storybook-static/`.
Inspect or stop the current repo's managed dev runner:
```sh
@@ -220,8 +209,6 @@ Seed modes:
- `full` makes a full logical clone of the source instance
- `--no-seed` creates an empty isolated instance
Seeded worktree instances quarantine copied live execution by default for both `minimal` and `full` seeds. During restore, Paperclip disables copied agent timer heartbeats, resets copied `running` agents to `idle`, blocks and unassigns copied agent-owned `in_progress` issues, and unassigns copied agent-owned `todo`/`in_review` issues. This keeps a freshly booted worktree from starting agents for work already owned by the source instance. Pass `--preserve-live-work` only when you intentionally want the isolated worktree to resume copied assignments.
After `worktree init`, both the server and the CLI auto-load the repo-local `.paperclip/.env` when run inside that worktree, so normal commands like `pnpm dev`, `paperclipai doctor`, and `paperclipai db:backup` stay scoped to the worktree instance.
`pnpm dev` now fails fast in a linked git worktree when `.paperclip/.env` is missing, instead of silently booting against the default instance/port. If that happens, run `paperclipai worktree init` in the worktree first.
@@ -235,8 +222,6 @@ That repo-local env also sets:
- `PAPERCLIP_WORKTREE_COLOR=<hex-color>`
The server/UI use those values for worktree-specific branding such as the top banner and dynamically colored favicon.
Authenticated worktree servers also use the `PAPERCLIP_INSTANCE_ID` value to scope Better Auth cookie names.
Browser cookies are shared by host rather than port, so this prevents logging into one `127.0.0.1:<port>` worktree from replacing another worktree server's session cookie.
Print shell exports explicitly when needed:

View File

@@ -115,6 +115,38 @@ If the first real publish returns npm `E404`, check npm-side prerequisites befor
- The initial publish must include `--access public` for a public scoped package.
- npm also requires either account 2FA for publishing or a granular token that is allowed to bypass 2FA.
### Manual first publish for `@paperclipai/mcp-server`
If you need to publish only the MCP server package once by hand, use:
- `@paperclipai/mcp-server`
Recommended flow from the repo root:
```bash
# optional sanity check: this 404s until the first publish exists
npm view @paperclipai/mcp-server version
# make sure the build output is fresh
pnpm --filter @paperclipai/mcp-server build
# confirm your local npm auth before the real publish
npm whoami
# safe preview of the exact publish payload
cd packages/mcp-server
pnpm publish --dry-run --no-git-checks --access public
# real publish
pnpm publish --no-git-checks --access public
```
Notes:
- Publish from `packages/mcp-server/`, not the repo root.
- If `npm view @paperclipai/mcp-server version` already returns the same version that is in [`packages/mcp-server/package.json`](../packages/mcp-server/package.json), do not republish. Bump the version or use the normal repo-wide release flow in [`scripts/release.sh`](../scripts/release.sh).
- The same npm-side prerequisites apply as above: valid npm auth, permission to publish to the `@paperclipai` scope, `--access public`, and the required publish auth/2FA policy.
## Version formats
Paperclip uses calendar versions:

View File

@@ -619,7 +619,7 @@ Per-agent schedule fields in `adapter_config`:
- `enabled` boolean
- `intervalSec` integer (minimum 30)
- `maxConcurrentRuns` integer; new agents default to `5`
- `maxConcurrentRuns` fixed at `1` for V1
Scheduler must skip invocation when:

View File

@@ -1,122 +0,0 @@
# 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

@@ -1,229 +0,0 @@
{
"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

@@ -1,646 +0,0 @@
{
"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

@@ -1,338 +0,0 @@
{
"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

@@ -1,21 +0,0 @@
# 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

@@ -1,40 +0,0 @@
# 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

@@ -1,38 +0,0 @@
# 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

@@ -1,24 +0,0 @@
# 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

@@ -1,21 +0,0 @@
# 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

@@ -1,22 +0,0 @@
# 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

@@ -1,26 +0,0 @@
# 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

@@ -1,24 +0,0 @@
# 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

@@ -1,27 +0,0 @@
# 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

@@ -1,22 +0,0 @@
# 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

@@ -1,21 +0,0 @@
# 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

@@ -1,22 +0,0 @@
# 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

@@ -1,28 +0,0 @@
# 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

@@ -1,32 +0,0 @@
# 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

@@ -1,26 +0,0 @@
# 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

@@ -1,22 +0,0 @@
# 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

@@ -1,31 +0,0 @@
# 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

@@ -1,32 +0,0 @@
# 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

@@ -1,32 +0,0 @@
# 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

@@ -1,34 +0,0 @@
# 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

@@ -1,43 +0,0 @@
# 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

@@ -1,21 +0,0 @@
# 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

@@ -1,73 +0,0 @@
# 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

@@ -1,24 +0,0 @@
# 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

@@ -1,25 +0,0 @@
# 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

@@ -1,20 +0,0 @@
# 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

@@ -1,38 +0,0 @@
# 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

@@ -1,41 +0,0 @@
# 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

@@ -1,45 +0,0 @@
# 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

@@ -1,21 +0,0 @@
# 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

@@ -1,32 +0,0 @@
# 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

@@ -1,39 +0,0 @@
# 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

@@ -1,21 +0,0 @@
# 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

@@ -1,36 +0,0 @@
# 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

@@ -1,32 +0,0 @@
# 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

@@ -1,30 +0,0 @@
# 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

@@ -1,22 +0,0 @@
# 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

@@ -1,31 +0,0 @@
# 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

@@ -1,24 +0,0 @@
# 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

@@ -1,32 +0,0 @@
# 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

@@ -1,22 +0,0 @@
# 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

@@ -1,22 +0,0 @@
# 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

@@ -1,21 +0,0 @@
# 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

@@ -1,33 +0,0 @@
# 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

@@ -1,21 +0,0 @@
# 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

@@ -1,20 +0,0 @@
# 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

@@ -1,32 +0,0 @@
# 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

@@ -1,27 +0,0 @@
# 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

@@ -1,21 +0,0 @@
# 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

@@ -1,21 +0,0 @@
# 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

@@ -1,22 +0,0 @@
# 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

@@ -1,38 +0,0 @@
# 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

@@ -1,25 +0,0 @@
# 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

@@ -1,393 +0,0 @@
# 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

@@ -1,228 +0,0 @@
# 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

@@ -1,69 +0,0 @@
# 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

@@ -1,51 +0,0 @@
# 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

@@ -1,62 +0,0 @@
# 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

@@ -1,80 +0,0 @@
# 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

@@ -1,76 +0,0 @@
# 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

@@ -1,43 +0,0 @@
# 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.

View File

@@ -1,60 +0,0 @@
# List Page
Full-page layout for browsing a collection of entities (agents, issues, projects, goals, routines, approvals, workspaces).
**Instances: 12.** `Activity`, `Agents`, `Approvals`, `Companies`, `Goals`, `Inbox`, `Issues`, `MyIssues`, `Projects`, `Routines`, `Workspaces`, `JoinRequestQueue`.
## Composition (shared baseline)
Measured across 12 instances by import intersection:
- **`PageSkeleton`** — 9/12 (the loading shell)
- **`EmptyState`** — 8/12 (shown when the collection is empty)
- **`button`** — 7/12 (action buttons: new-X, filters, view-toggle)
- **`PageTabBar`** — 4/12 (views split into named lists)
- **`tabs`** — 4/12
[INFER] Structural template, from reading Issues / Agents / Projects:
```
<Page>
<Header>
<Title> — "Issues", "Agents", …
<FilterOrTabBar> — PageTabBar or tabs for sub-collections
<ActionButton> — "New Issue", "Invite", etc.
</Header>
<Body>
{loading ? <PageSkeleton />
empty ? <EmptyState title message action />
: <List / Grid / Kanban>
{items.map(item =>
<IssueRow or EntityRow or domain-specific row />)}
</>
}
</Body>
</Page>
```
## Canonical instance
No single clean instance — `Issues.tsx` and `Agents.tsx` are both representative but each mixes in substantial custom logic. `Agents.tsx` shows the pattern with the least domain-specific clutter.
## Variance across instances
- **Row rendering diverges.** `Issues` uses `IssueRow`, `Agents` builds a grid of cards, `Approvals` uses a list of cards, `Goals` uses `GoalTree`. Different collections legitimately need different affordances, but none of them use the generic `EntityRow` — which is 6 uses spread across feature subsurfaces, not pages.
- **Empty-state content is ad-hoc.** No shared "empty collection" copy or illustration; each page passes its own strings to `EmptyState`.
- **Sorting / filtering surface is not shared.** Some pages have a `FilterBar`, some have in-header popovers, some have `IssueFiltersPopover` specifically for Issues. See also [components-review.md §Naming inconsistencies](../components/components-review.md#naming-inconsistencies) on `*Bar` vs popover filters.
- **`Activity` is borderline.** It's a chronological feed rather than a collection — composes `ActivityRow` only.
- **Loading state absent on 3 pages** — confirm each has its own mechanism.
## Related components and patterns
- [`EmptyState`](../components/index.md) (used in 19 places across the app)
- [`PageSkeleton`](../components/index.md) (22 places)
- [`PageTabBar`](../components/PageTabBar.md) (10 uses)
- [entity-row pattern](./entity-row.md) — generic row, currently underused on list pages
## Open questions / risks
- `EntityRow` (6 uses, generic) is never used on the main list pages. The main list pages roll their own row. Worth asking whether that's by choice or by miss. See also [entity-row.md](./entity-row.md).
- The mobile list experience is handled per-page; no shared mobile list pattern found. See [`SwipeToArchive.tsx`](../components/index.md) which only `Inbox` uses.

View File

@@ -1,138 +0,0 @@
# Patterns Review
Cross-cutting findings from Stage 4. Ordered by expected human value.
- **Generated:** 2026-04-21
- **Inventory:** [index.md](./index.md)
- **Upstream dependencies:** [../tokens/tokens-review.md](../tokens/tokens-review.md), [../components/components-review.md](../components/components-review.md)
---
## Variance across documented patterns (what's inconsistent between instances)
### Status-element variance in detail pages ([detail-page.md](./detail-page.md))
Four different ways detail pages render the entity's status:
| Detail page | Status treatment |
|---|---|
| `IssueDetail` | `StatusIcon` + `StatusBadge` |
| `AgentDetail` | `StatusBadge` + `agentStatusDot` (a helper in `status-colors.ts`, not a component) |
| `ProjectDetail`, `GoalDetail` | `StatusBadge` alone |
| `ApprovalDetail` | Tone encoding inside `ApprovalCard`, no badge |
That's four variations across eight pages. Tied to the broader [status-display.md](./status-display.md) and [tokens-review.md §4](../tokens/tokens-review.md#4-status-colorsts-is-a-canonical-semantic-color-catalog-that-bypasses-the-ds) issues.
### Loading-state variance across list and detail pages
`PageSkeleton` is used in 22 places, but 3 of 8 detail pages and 3 of 12 list pages skip it. Each of those pages has its own ad-hoc loading handling (or doesn't render until data arrives). Worth auditing for consistency.
### Row variance across list pages ([list-page.md](./list-page.md) + [entity-row.md](./entity-row.md))
`EntityRow` (generic, 6 uses) is never used on any main list page. Every list page rolls its own row: `IssueRow` for Inbox/Swipe, grid cards for Agents, Kanban columns for Issues, `GoalTree` for Goals, etc. Either the list pages should adopt `EntityRow` or `EntityRow` has a gap that keeps it from fitting (status/unread/mobile-responsive).
---
## Paperclip-domain patterns worth calling out (opportunities, not ratified patterns)
These are the checklist patterns from the extraction skill — run-transcript row, heartbeat indicator, agent card, cost display, approval gate, metric cell. Each is examined for whether it currently exists as a 3+ pattern, a thinner-than-threshold pattern, or a clear opportunity.
### 1. Run transcript row
**Status:** a single parent component (`RunTranscriptView.tsx`, 3 uses) owns the rendering. Not a cross-cutting pattern at the instance level.
- `RunTranscriptView` lives in `ui/src/components/transcript/`.
- Used by `AgentDetail`, `IssueDetail`, `RunTranscriptUxLab`.
- Has its own test (`RunTranscriptView.test.tsx`) and a sibling hook (`useLiveRunTranscripts.ts`).
- No Storybook story — see [components-review.md §Story coverage gaps](../components/components-review.md#story-coverage-gaps).
**Opportunity:** if transcript rows (agent action + timestamp + tool call summary) were exposed as a `<TranscriptRow>` primitive, downstream surfaces (embedded transcripts, search results, activity feed) could reuse it. Currently the rendering is internal to `RunTranscriptView`.
### 2. Heartbeat / liveness indicator
**Status:** one component (`LiveRunWidget`, 1 use) plus scattered inline usage in `AgentDetail` (imports `heartbeatsApi`). Not a pattern.
- `LiveRunWidget.tsx` is a "live" indicator with cyan border glow; 90 lines; only used on Dashboard.
- `AgentDetail` renders its own live-status dot (`border-cyan-500/30 shadow-[0_0_12px_rgba(6,182,212,0.08)]` hardcoded).
- `ActiveAgentsPanel` also renders live markers.
**Opportunity:** extract a `<Heartbeat live={bool} lastSeenAt={Date}>` primitive. Today the three surfaces each reinvent the visual treatment.
### 3. Agent card
**Status:** fragmented across `ActiveAgentsPanel` (1 use, composite), `Agents` page (rolls its own), and `SidebarAgents` (1 use). No shared `AgentCard`.
**Opportunity:** unify into a single card pattern if Stage 3's "Card vs Panel vs Widget" naming question gets resolved.
### 4. Approval gate UI
**Status:** `ApprovalCard` (3 uses), `ApprovalPayload` (4 uses), `OutputFeedbackButtons` (2 uses). Three components, all in the approval flow, but each a single surface — not repeated across three independent contexts.
- `ApprovalCard` and `ApprovalPayload` appear in `ApprovalDetail` page and in `Approvals` list. `OutputFeedbackButtons` appears in run detail surfaces.
**Status call:** not enough cross-cutting instance count to formalize as a pattern today. The trio is **already** the approval-gate pattern — just not repeated enough to name.
### 5. Cost / budget display
**Status:** six related components (`BudgetPolicyCard`, `BudgetIncidentCard`, `BudgetSidebarMarker`, `QuotaBar`, `ProviderQuotaCard`, `BillerSpendCard`) span two adjacent pattern docs ([finance-card.md](./finance-card.md), [quota-display.md](./quota-display.md)).
**Not merged here** — the finance / accounting slice documents the "reporting card" shape; the quota slice documents the "used vs budget" shape; the budget slice hand-rolls its own 3-level severity indicator (see §6). These three related but distinct domains would benefit from a shared glossary before pattern consolidation.
### 6. Severity indicator (3-level health display) — **pattern opportunity**
[Not documented as a dedicated pattern file in this pass; surfaced here as the most impactful opportunity.]
The app has **four distinct systems** for encoding "healthy / warning / critical":
| System | Location | Example |
|---|---|---|
| `status-colors.ts` | `issueStatusText.todo = "text-blue-600 dark:text-blue-400"` etc. | 11 hues, -600/-400 weights |
| `ActivityCharts.tsx` hardcoded hex | `critical: "#ef4444"` | Raw hex, no dark-mode variant |
| `BudgetPolicyCard` / `BudgetIncidentCard` | `"text-red-300 border-red-500/30 bg-red-500/10"` | 3-level with 300/500/10% alpha |
| `BudgetSidebarMarker` | `"bg-emerald-500/90 text-white"` | 3-level with 500/90% alpha |
| `QuotaBar` | `"bg-red-400"` etc. | 3-level solid 400 |
Each encodes the same *concept* (red/amber/green severity), each chooses different *Tailwind utility classes*. None of them reference the DS tokens — because there are no DS tokens for this. See [tokens-review.md §4 — status-colors.ts is a canonical semantic-color catalog that bypasses the DS](../tokens/tokens-review.md#4-status-colorsts-is-a-canonical-semantic-color-catalog-that-bypasses-the-ds).
**Prerequisite:** a `--signal-*` token family (`--signal-success`, `--signal-warning`, `--signal-error`, each with `-bg`/`-text`/`-border`/`-subtle` variants). Once tokens exist, a `<SeverityIndicator level="ok|warn|critical">` primitive becomes writable and all four sites collapse onto it.
**Why this is the highest-leverage pattern opportunity:** it sits downstream of Stage 1's biggest finding (§4) and unblocks at least three patterns at once (status-display, quota-display, cost-display). Do not try to codify those three separately first.
### 7. Metric cell (small uppercase label + tabular-num value)
Recurring micro-pattern spotted in `FinanceBillerCard`:
```tsx
<div className="border border-border p-3">
<div className="text-[11px] uppercase tracking-[0.14em] text-muted-foreground">debits</div>
<div className="mt-1 font-medium tabular-nums">{formatCents(...)}</div>
</div>
```
Appears inline in `FinanceBillerCard`, `BillerSpendCard`, `ProviderQuotaCard`, and (with small variations) in some detail-page headers and dashboard surfaces. Used ad hoc rather than as a reusable `<MetricCell>` primitive. Pattern exists informally; could be extracted to ~15 lines.
### 8. Metadata grid (label-value pairs, common in detail views)
Per the skill checklist. Not surfaced as a recurring shape in the current codebase's detail pages — each detail page renders its metadata inline. `KeyValueList` is contracted in the plugin SDK but not implemented. Weak pattern; opportunity.
---
## Candidates for future documentation (below threshold today)
Patterns glimpsed at 12 instances. If usage grows, they'd warrant dedicated docs:
- **Toolbar / filter bar** — `FilterBar` (1), `IssueFiltersPopover` (2), inline filter popovers on several list pages. No shared shape yet.
- **Inline editor** — `InlineEditor` (6 uses) plus the `DraftInput` / `DraftTextarea` primitives from `agent-config-primitives`. The inline-edit interaction has a shape; not yet abstracted as a named pattern.
- **Modal image / file viewer** — `ImageGalleryModal`, `DocumentDiffModal` (and `PathInstructionsModal`). Three modals with different content, possibly sharing chrome.
- **Kanban column** — `KanbanBoard` (2 uses) + `IssueColumns` (4 uses) + `IssueGroupHeader` (1). Issue-specific; pattern lives inside the issue domain.
---
## What to resolve before Stage 4 would run cleanly on re-extraction
Pattern extraction is limited by upstream ambiguity. The following decisions, if made, would let the next pattern re-run produce materially tighter docs:
1. **Signal token family** ([tokens-review.md §4](../tokens/tokens-review.md#4-status-colorsts-is-a-canonical-semantic-color-catalog-that-bypasses-the-ds)). Blocks [status-display.md](./status-display.md), part of [quota-display.md](./quota-display.md), and §6 above.
2. **Radius scale decision** ([tokens-review.md §Radius scale](../tokens/tokens-review.md#radius-scale--under-founder-review)). Once `rounded-lg` / `rounded-xl` behave predictably, the UxLab pages can be brought into the main DS conversation instead of ignored.
3. **Plugin SDK contract disposition** ([components-review.md §Plugin SDK contract gap](../components/components-review.md#plugin-sdk-contract-gap)). Dictates whether `KeyValueList` / `LogView` / `ActionBar` / `Spinner` / `DataTable` / `TimeseriesChart` become host patterns or stay aspirational.
4. **Naming vocabulary** ([components-review.md §Naming inconsistencies](../components/components-review.md#naming-inconsistencies)). Not strictly required for patterns to exist, but the docs here have to use neutral language (_"dialog pattern — some implementations named *Modal, same primitive"_) because the vocabulary is unresolved. A canonical decision would tighten pattern prose.

View File

@@ -1,71 +0,0 @@
# Quota Display
Visual representation of "used vs budget" for provider quotas, subscription quotas, or billing windows.
**Instances: 2 (below the 3+ threshold, documented per directive).**
`QuotaBar` (2 uses), `ProviderQuotaCard` (1 use).
> **Extraction-only pass.** Documents the pair; does not prescribe a merge. See [components-review.md §Likely duplicates #9](../components/components-review.md#9-quota-display-providerquotacard--quotabar).
## Instances
| Component | Lines | Uses | Role |
|---|---|---|---|
| `QuotaBar.tsx` | 65 | 2 | Single horizontal bar — % used with optional deficit notch |
| `ProviderQuotaCard.tsx` | 416 | 1 | Full per-provider card — composes `QuotaBar` + `ClaudeSubscriptionPanel`/`CodexSubscriptionPanel` |
## Composition
`QuotaBar`:
```ts
interface QuotaBarProps {
label: string;
percentUsed: number; // 0100
leftLabel: string;
rightLabel?: string;
showDeficitNotch?: boolean;
className?: string;
}
```
- Single horizontal bar with a filled portion.
- Fill color is computed from a threshold function:
- `>90% → bg-red-400`
- `>70% → bg-yellow-400`
- `else → bg-green-400`
`ProviderQuotaCard`:
- Composes `QuotaBar` (multiple times for different windows: 5h / 24h / 7d rolling + period budget), plus `ClaudeSubscriptionPanel` / `CodexSubscriptionPanel` for vendor-native quota windows.
- Three-level wrapper around `QuotaBar`.
## Token drift inherent to this pattern
`QuotaBar` hardcodes `bg-red-400`, `bg-yellow-400`, `bg-green-400` — raw Tailwind palette. This is a **fourth** place where the app encodes three-level severity:
1. `status-colors.ts` — red-600/500/400, amber-400, yellow-400, etc.
2. `ActivityCharts.tsx` — direct hex values
3. `BudgetPolicyCard` / `BudgetIncidentCard` / `BudgetSidebarMarker` — 3-level severity with 500/90% alpha
4. `QuotaBar` — 3-level severity with solid 400s
All four encode "health / warn / hard-stop" but none share a token. See [tokens-review.md §1, §4](../tokens/tokens-review.md) for the token-drift picture and [patterns-review.md](./patterns-review.md) for the severity-indicator pattern opportunity.
## Different affordances, not a duplicate
The pair is **not** a duplicate in the literal sense — `QuotaBar` is a rendering primitive, `ProviderQuotaCard` is a composer. The naming suggests parallels (`-Bar` vs `-Card`) but their roles are distinct:
- Use `QuotaBar` for a single measured ratio.
- Use `ProviderQuotaCard` for a full provider's worth of quotas.
What the name spread **does** flag: there's no shared concept of "quota display primitive" — `QuotaBar` is one, but the finance/accounting cards use their own bar-less metric cells (`<Cell label value />` triples), and `BudgetPolicyCard` uses hand-rolled borders. The family would benefit from either promoting `QuotaBar` into a shared DS primitive for all "used vs budget" visualizations, or acknowledging that each surface wants a different look.
## Scale caveat
Only 2 instances — the pattern is thinly attested. Listed here per founder directive; a real "quota display" DS pattern would need a third independent caller before it's real.
## Related
- [finance-card.md](./finance-card.md) — `BillerSpendCard` also composes `QuotaBar`.
- [subscription-panel.md](./subscription-panel.md) — `ProviderQuotaCard` composes both subscription panels.
- [patterns-review.md §Severity indicator](./patterns-review.md) — the wider family of 3-level severity displays.

View File

@@ -1,63 +0,0 @@
# Sidebar Chrome
Left-rail and settings-sidebar UI that wraps the main app surface.
**Instances: 6 "outer" + 2 sidebar menus = 8 components in the family.**
`Sidebar`, `InstanceSidebar`, `CompanySettingsSidebar`, `CompanyRail`, `access/CompanySettingsNav`, `MobileBottomNav` + `SidebarAccountMenu`, `SidebarCompanyMenu`.
> **Extraction-only pass.** Does not resolve the naming inconsistencies (Sidebar vs Rail vs Nav). See [components-review.md §Naming inconsistencies — Sidebar / Rail / Nav](../components/components-review.md#sidebar--rail--nav) and [§Likely duplicates #4 and #7](../components/components-review.md#4-sidebar-menu-pair-sidebaraccountmenu--sidebarcompanymenu).
## Instances
**Outer chrome (6):**
| Component | Lines | Uses | Role (inferred) |
|---|---|---|---|
| `Sidebar.tsx` | (unread) | ≥3 | Main app navigation sidebar |
| `InstanceSidebar.tsx` | (unread) | 1 | Instance-settings scope |
| `CompanySettingsSidebar.tsx` | (unread) | 2 | Company-settings scope |
| `CompanyRail.tsx` | 260 | 1 | Narrow vertical rail of sortable companies (dnd-kit) |
| `access/CompanySettingsNav.tsx` | (unread) | 1 | Tab-bar-style nav at the top of a settings surface |
| `MobileBottomNav.tsx` | (unread) | ≥3 | Mobile-bottom-tab alternative to the desktop sidebar |
**Sidebar menus (2):**
| Component | Uses | Role (inferred) |
|---|---|---|
| `SidebarAccountMenu.tsx` | 2 | Account dropdown anchored to the sidebar |
| `SidebarCompanyMenu.tsx` | 2 | Company dropdown anchored to the sidebar |
## Composition
Each of the six outer-chrome components solves a different surface problem (main nav vs settings nav vs rail vs bottom-bar), so the set is not simply a duplicate family. What they share is:
- They all attach to an app-level layout slot (`Layout.tsx`).
- They all render navigation items (`SidebarNavItem` is a shared primitive, 3 uses).
- Most compose `SidebarSection` as a grouping primitive.
- `Sidebar` + `InstanceSidebar` + `CompanySettingsSidebar` share the "aside" shape; `CompanyRail` is a narrower 3rd-dimension variant; `MobileBottomNav` is a horizontal mobile variant; `CompanySettingsNav` is a top-of-page tab bar and arguably belongs in a different family ("page tab nav") rather than "sidebar chrome."
## Token note — none of these consume `sidebar-*` tokens
From [tokens-review.md §3](../tokens/tokens-review.md#3-sidebar--tokens-are-dead): the 8 `sidebar-*` color tokens defined in `index.css` (`--sidebar`, `--sidebar-foreground`, `--sidebar-primary`, `--sidebar-primary-foreground`, `--sidebar-accent`, `--sidebar-accent-foreground`, `--sidebar-border`, `--sidebar-ring`) have **0 code usages**. All six chrome components consume the general semantic tokens (`background`, `accent`, `border`) directly.
Either the `sidebar-*` family should be adopted (to enable theming the sidebar independently of the main surface), or deleted (since nothing uses it).
## The sidebar-menu pair
`SidebarAccountMenu` and `SidebarCompanyMenu` are the dropdown menus triggered from the sidebar (account actions, company switching). Both used exactly twice. Per the duplicate directive, documented as a pair here, not auto-merged. If merged, the natural shape would be `<SidebarMenu kind="account" | "company">` or `<SidebarMenu>` with `children` slots.
## Vocabulary observation (not resolved)
The chrome family uses three different names for the same category of affordance:
- **Sidebar** (3 of the 6): the persistent rail
- **Rail** (1): `CompanyRail` — narrower / sortable
- **Nav** (2): `MobileBottomNav` + `CompanySettingsNav`
Whether these reflect real category distinctions or casual naming is a call for the founder. Pattern extraction records the observation and moves on.
## Related components and patterns
- `SidebarNavItem` — shared nav-item primitive (3 uses; no story — see [components-review.md §Story gaps](../components/components-review.md#story-coverage-gaps))
- `SidebarSection` — shared grouping primitive (1 use; below detail-file threshold)
- [entity-row.md](./entity-row.md) — tangential, since the item in a sidebar is closer to a nav-item than an entity row

View File

@@ -1,105 +0,0 @@
# Status Display
How the app renders entity status (for issues, runs, agents, goals, approvals, projects) and priority (for issues).
**Instances: 3 components + 1 module.**
`StatusIcon` (14 uses), `StatusBadge` (19 uses), `PriorityIcon` (5 uses), and `status-colors.ts` (the canonical catalog that all three consume).
> **Pattern shape pending signal-token scoping** — see [tokens-review.md §4](../tokens/tokens-review.md#4-status-colorsts-is-a-canonical-semantic-color-catalog-that-bypasses-the-ds). Any pattern codification that fixes the current prop shape or color-binding will likely change once the signal-token family lands.
## Instances
### `StatusIcon` (14 uses, `ui/src/components/StatusIcon.tsx`)
Small circular status indicator for issue status.
```ts
interface StatusIconProps {
status: string; // untyped — see Open questions
onChange?: (status: string) => void;
className?: string;
showLabel?: boolean;
}
```
- Renders a `<span>` shaped as a circle with `border-2` and a hue from `issueStatusIcon[status]`.
- Includes a special "done" variant that fills the circle.
- If `onChange` is provided, wraps in a `<Popover>` with a picker of all statuses.
- Used across issue lists, properties panels, inbox, issue detail.
### `StatusBadge` (19 uses, `ui/src/components/StatusBadge.tsx`)
Pill-shaped status badge for any entity type.
```ts
interface Props {
status: string; // untyped — see Open questions
}
```
- 15-line component — wraps a `<span>` with `statusBadge[status]` classes.
- Uses `rounded-full px-2.5 py-0.5 text-xs` — a fixed shape across all statuses.
- `status` prop accepts an open string. The `statusBadge` record in `status-colors.ts` has 24 known keys covering agent / goal / run / approval / issue domains.
### `PriorityIcon` (5 uses, `ui/src/components/PriorityIcon.tsx`)
Small priority indicator (up/down/flat arrow or warning triangle).
```ts
interface PriorityIconProps {
priority: string; // untyped — see Open questions
onChange?: (priority: string) => void;
className?: string;
showLabel?: boolean;
}
```
- Renders one of four lucide icons (`ArrowUp`, `ArrowDown`, `Minus`, `AlertTriangle`) per priority level.
- Same popover-when-onChange pattern as `StatusIcon`.
- Priority values: `critical`, `high`, `medium`, `low`.
### `status-colors.ts` (`ui/src/lib/status-colors.ts`)
Canonical status-and-priority color catalog:
```ts
export const issueStatusIcon: Record<string, string> = { };
export const issueStatusText: Record<string, string> = { };
export const statusBadge: Record<string, string> = { };
export const agentStatusDot: Record<string, string> = { };
export const priorityColor: Record<string, string> = { };
```
The file's header says _"Every component that renders a status indicator should import from here so colors stay consistent."_ — the three components above do. So does `AgentActionButtons`, `ProjectProperties`, `AgentDetail`, and a handful of pages.
## Composition (shared pattern)
```
<statusValue-prop> → <record lookup in status-colors.ts> → <raw Tailwind-palette classes> → <render>
```
All three components follow this template. The pattern is correct in spirit — there is a central catalog. But the catalog contains raw Tailwind palette values (`text-blue-600 dark:text-blue-400`, `bg-green-100 text-green-700`, etc.), not DS tokens.
## Untokenized colors
Per [tokens-review.md §4](../tokens/tokens-review.md#4-status-colorsts-is-a-canonical-semantic-color-catalog-that-bypasses-the-ds), this entire pattern bypasses the DS token layer. The 11 hues currently in play (`blue`, `cyan`, `sky`, `green`, `emerald`, `amber`, `orange`, `yellow`, `violet`, `red`, `neutral`) across 24+ status keys could consolidate into a `--signal-*` token family:
- `--signal-success`, `--signal-warning`, `--signal-error`, `--signal-info`, `--signal-in-progress`, `--signal-in-review`, `--signal-neutral` — each with `-bg`, `-text`, `-border` variants.
Not proposed as a concrete change here. Flagged as the token gap that blocks this pattern from being codifiable.
## Props are untyped strings
Both `StatusIcon.status: string` and `PriorityIcon.priority: string` are open strings — TypeScript doesn't prevent callers from passing `"banana"`. The records in `status-colors.ts` fall through to a `*Default` class when a key is missing, so bad input degrades silently.
Once signal tokens exist, these props should be typed enums whose members match the signal-token keys. Any callers passing arbitrary strings will then light up at compile time.
## Related patterns and token drift
- Page-level: [detail-page.md](./detail-page.md) (status appears in detail-page headers)
- The "severity indicator" family — `BudgetPolicyCard`, `BudgetIncidentCard`, `BudgetSidebarMarker`, `QuotaBar` — uses a **third** color system (hand-picked red/amber/emerald at varying opacities), distinct from `status-colors.ts` and from the dead `--chart-*` tokens. See [patterns-review.md §Candidates](./patterns-review.md) for notes.
- `AgentActionButtons` and `ActivityCharts` also consume `status-colors.ts` / hardcoded hex — see [components-review.md §Token non-compliance](../components/components-review.md#token-non-compliance).
## Do-not-codify (yet)
Do not propose concrete refactors of `StatusIcon` / `StatusBadge` / `PriorityIcon` shape before the signal-token work. The prop API (typed enums), class naming, and default-fallback behavior all depend on what signal tokens look like.

View File

@@ -1,56 +0,0 @@
# Subscription Panel
Panel summarizing a per-vendor subscription quota (rolling windows, reset times, session vs weekly).
**Instances: 2 (below the 3+ threshold, documented per directive).**
`ClaudeSubscriptionPanel`, `CodexSubscriptionPanel`.
> **Extraction-only pass.** Documents the pair as it exists; does not prescribe a collapse to a single `SubscriptionPanel({ vendor })`. See [components-review.md §Likely duplicates #3](../components/components-review.md#3-subscription-panel-pair-claudesubscriptionpanel--codexsubscriptionpanel).
## Instances
| Component | Lines | Uses | Vendor |
|---|---|---|---|
| `ClaudeSubscriptionPanel.tsx` | 140 | 1 | Anthropic Claude |
| `CodexSubscriptionPanel.tsx` | (unread) | 1 | OpenAI Codex |
## Composition
`ClaudeSubscriptionPanel` signature:
```ts
interface ClaudeSubscriptionPanelProps {
windows: QuotaWindow[];
source?: string | null;
error?: string | null;
}
```
- Takes an array of `QuotaWindow` (from `@paperclipai/shared`).
- Renders ordered windows (session, week-all-models, week-sonnet-only, week-opus-only, extra-usage).
- Shows a reset timestamp per window using `toLocaleString`.
`CodexSubscriptionPanel` is parallel — same shape of inputs, same conceptual layout, different window ordering and different label normalization presumably.
Both are composed by `ProviderQuotaCard` — the host that decides "this is a Claude provider, render `ClaudeSubscriptionPanel`; this is a Codex provider, render `CodexSubscriptionPanel`."
## Variance
- **Window keys differ** (`currentsession`, `currentweekallmodels`, `currentweeksonnetonly`, … in Claude; likely a different set in Codex).
- **Label rules differ** — each has its own `normalizeLabel`.
- **Reset-time rendering is identical pattern** (`window.resetsAt → toLocaleString`).
- **Error surface is the same prop**.
## Scale caveat
Only 2 instances. The "pattern" shape is strong because the components look parallel, but there's no third point to triangulate from. If a third vendor is added (Gemini? Cursor?), the pattern becomes real; until then, two parallel files that share ~60% structure.
## Open questions
- Should `ProviderQuotaCard` own the vendor-specific rendering directly, or should the pair stay as separate files the card dispatches to?
- Are the two files diffable down to a `windowsConfig` data shape + a shared renderer? This would be the path if consolidation is pursued.
## Related
- [finance-card.md](./finance-card.md) — `BillerSpendCard` and `ProviderQuotaCard` are the orchestrator cards that consume these subscription panels.
- [tokens-review.md §4](../tokens/tokens-review.md) — subscription panels may consume status-color logic for "over-quota" state; confirm during any future consolidation.

View File

@@ -1,402 +0,0 @@
# Token Review
This is the high-value artifact from Stage 1. It lists what looks wrong, inconsistent, or underdetermined about the token set. Ordered by expected human value, not by stage.
- **Generated:** 2026-04-21
- **Repo SHA:** a26e1288b627e82c554445732c7d844648e6b5e1
- **Scope:** `ui/` (`@paperclipai/ui`)
- **Inventory:** [tokens.md](./tokens.md), [tokens.json](./tokens.json)
---
## High-confidence drift (likely should be fixed)
### 1. `chart-*` tokens — reserved
**Status: RECLASSIFIED (2026-04-21).** Tokens preserved as **reserved** for the future chart-tokenization project. Not considered drift; do not consume today. Comment added above the `@theme` block in `ui/src/index.css`. Original finding retained below for history.
**Original finding:** `--chart-1` through `--chart-5` have **0 code usages** (excluding their definitions and the `DesignGuide.tsx` swatch showcase). Yet the app renders charts — `ui/src/components/ActivityCharts.tsx` and `ui/src/pages/OrgChart.tsx` carry the chart color logic.
**What they use instead:** raw Tailwind-palette hex values, typed directly into TSX.
```
ui/src/components/ActivityCharts.tsx:125-128
critical: "#ef4444", high: "#f97316", medium: "#eab308", low: "#6b7280",
ui/src/components/ActivityCharts.tsx:178-184
todo: "#3b82f6", in_progress: "#8b5cf6", in_review: "#a855f7",
done: "#10b981", blocked: "#ef4444", cancelled: "#6b7280", backlog: "#64748b",
ui/src/components/ActivityCharts.tsx:258
color = rate >= 0.8 ? "#10b981" : rate >= 0.5 ? "#eab308" : "#ef4444";
ui/src/pages/OrgChart.tsx:162-169
running: "#22d3ee", active: "#4ade80", paused: "#facc15",
idle: "#facc15", error: "#f87171", terminated: "#a3a3a3",
```
**Why it matters:** `--chart-1..5` were reserved by the shadcn default theme for "data visualization tokens", but the actual chart layer never migrated onto them. Chart color is currently a parallel system, bound to Tailwind palette + status semantics rather than to the DS.
**Suggested direction (not decided here):** Replace `chart-1..5` with a semantic status-token family (see §4) and have `ActivityCharts` / `OrgChart` consume those. Or: delete `chart-1..5` if there's no chart design language yet and note the gap.
### 2. `destructive-foreground` has a wrong light-mode value AND is unused
**Status: RESOLVED (2026-04-21).** Light-mode value corrected to `oklch(0.985 0 0)` to match the dark-mode pattern. `ui/src/index.css:63`. Zero-risk change (0 production consumers confirmed). Original finding retained below for history.
**Original finding:** The light-mode value of `--destructive-foreground` was `oklch(0.577 0.245 27.325)` — the same red as `--destructive` itself. That means light-mode destructive text rendered over a destructive background would be invisible. The dark-mode value `oklch(0.985 0 0)` (white) is correct.
```
ui/src/index.css:62 --destructive: oklch(0.577 0.245 27.325);
ui/src/index.css:63 --destructive-foreground: oklch(0.577 0.245 27.325); ← likely bug
ui/src/index.css:97 --destructive: oklch(0.637 0.237 25.331);
ui/src/index.css:98 --destructive-foreground: oklch(0.985 0 0); ← correct
```
**Why no one has noticed:** `--destructive-foreground` has **0 code usages**. Components using destructive color use `bg-destructive` with `text-destructive` or implicit foreground, never `text-destructive-foreground` on a destructive-filled surface. So the bug is masked by non-adoption.
**Suggested direction:** Either delete the token, or fix its light value (likely `oklch(0.985 0 0)`) and actually use it on destructive buttons / filled badges.
### 3. `sidebar-*` tokens — reserved
**Status: RECLASSIFIED (2026-04-21).** Tokens preserved as **reserved** for shadcn sidebar primitive compatibility in case that primitive is reintroduced. Not considered drift; do not consume today. Comment added above the `@theme` block in `ui/src/index.css`. Original finding retained below for history.
**Original finding:** All 8 sidebar tokens (`sidebar`, `sidebar-foreground`, `sidebar-primary`, `sidebar-primary-foreground`, `sidebar-accent`, `sidebar-accent-foreground`, `sidebar-border`, `sidebar-ring`) have **0 code usages**. Only references are the `@theme inline` alias block and the `DesignGuide.tsx` swatch showcase.
```
rg -P "bg-sidebar|text-sidebar|border-sidebar|--sidebar" ui/src
→ only hits in ui/pages/DesignGuide.tsx and ui/src/index.css
```
**Why:** The shadcn `Sidebar` primitive was not installed (no `ui/src/components/ui/sidebar.tsx`). The app has a custom `ui/src/components/Sidebar.tsx` that consumes the general semantic tokens (`background`, `accent`, `border`, etc.) directly. The `sidebar-*` family came from the shadcn default `components.json` theme generation and was never adopted.
**Suggested direction:** Either (a) delete the `sidebar-*` family from `index.css`, or (b) refactor `Sidebar.tsx` to consume them so the sidebar can be themed independently of the main surface. Status quo is dead code.
Note: in light mode, every `sidebar-*` value equals a semantic-surface value except the first — `sidebar` = `oklch(0.985 0 0)` vs `background` = `oklch(1 0 0)`. So consolidation would be nearly free if option (a) is chosen.
### 4. `status-colors.ts` is a canonical semantic-color catalog that bypasses the DS
**Status: PARTIALLY ADDRESSED (2026-04-21).** Two new action-severity tokens added — `--signal-success` and `--signal-success-foreground` — paired with the existing `--destructive` / `--destructive-foreground` as the DS's solid-accent severity vocabulary. `status-colors.ts` itself is **not touched** in this pass; tokenizing its entity-state coloring as a `--status-*` family is a deferred future project. `--signal-warning` and `--signal-info` are intentionally not added (see §Deferred signal variants below). Original finding retained below for history.
**Separation of concerns now documented.** `destructive` / `signal-success` = **action severity** (solid buttons, toasts). `status-colors.ts` = **entity state** (issue status, agent status, priority) — stays as a TypeScript catalog. `--chart-*` and `--sidebar-*` remain reserved (see §1, §3).
**Original finding:**
**Finding:** `ui/src/lib/status-colors.ts` defines the color language for all entity statuses and priorities (issues, agents, goals, runs, approvals) and then renders them with **raw Tailwind palette classes**. Every one of ~24 status/priority entries looks like:
```ts
// ui/src/lib/status-colors.ts (excerpt)
issueStatusIcon.todo = "text-blue-600 border-blue-600 dark:text-blue-400 dark:border-blue-400"
statusBadge.running = "bg-cyan-100 text-cyan-700 dark:bg-cyan-900/50 dark:text-cyan-300"
statusBadge.pending_approval = "bg-amber-100 text-amber-700 dark:bg-amber-900/50 dark:text-amber-300"
priorityColor.critical = "text-red-600 dark:text-red-400"
agentStatusDot.running = "bg-cyan-400 animate-pulse"
```
The file header says _"Every component that renders a status indicator … should import from here so colors stay consistent"_ — i.e. status color IS treated as part of the design system, but it lives in TypeScript, not in CSS tokens, and uses raw Tailwind scales rather than semantic tokens.
**Hues in active status use:** blue, cyan, sky, green, emerald, amber, orange, yellow, violet, red, neutral. That's 11 hues — more than the shadcn default palette anticipates.
**Why it matters:** This is the single largest DS gap found in Stage 1. A token family like `--signal-success`, `--signal-warning`, `--signal-error`, `--signal-info`, `--signal-in-progress`, `--signal-review`, `--signal-neutral` (each with `-bg`, `-text`, `-border` variants) would unify the status color language, make dark-mode pairing automatic, and close the drift feeding into `ActivityCharts.tsx` (§1).
**Suggested direction:** Design a signal/status token family and migrate `status-colors.ts` onto it. Do this **before** Stage 3 extracts `StatusBadge`, `StatusIcon`, `PriorityIcon`, `AgentStatusDot` — otherwise those component docs will bake in the raw-palette drift.
### 5. Theme-color meta tag uses hardcoded hex
```
ui/src/context/ThemeContext.tsx:20 const DARK_THEME_COLOR = "#18181b";
ui/src/context/ThemeContext.tsx:21 const LIGHT_THEME_COLOR = "#ffffff";
```
These drive the browser's `<meta name="theme-color">` tag so the mobile browser chrome matches the app. They're sort-of the inverse of `--background`:
- Light: `--background = oklch(1 0 0) = #ffffff` — the hardcoded value matches.
- Dark: `--background = oklch(0.145 0 0) ≈ #252525`, but the hardcoded is `#18181b` (zinc-900). **Mismatch.**
**Suggested direction:** Compute from `--background` at runtime, or keep the hardcoded values but make them match `--background` exactly. Currently the mobile chrome is a different dark than the app's background.
---
## Medium-confidence drift
### 6. Raw Tailwind palette usage: 659 occurrences across 83 files
The codebase bypasses semantic tokens for a large number of styling decisions:
```
rg -P "\b(bg|text|border|...)-(neutral|gray|zinc|slate|stone|red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose)-(50|100|…|950)\b" ui/src
→ 659 occurrences across 83 files
```
**Heavy offenders (ranked):**
| File | Hits | Notes |
|---|---|---|
| `ui/src/pages/AgentDetail.tsx` | 75 | Production page — worth migrating. |
| `ui/src/pages/InviteLanding.tsx` | 58 | Auth surface with zinc-based dark palette; deliberately different visual language than the app. |
| `ui/src/pages/InviteUxLab.tsx` | 55 | UX lab prototype. Acceptable scratch. |
| `ui/src/lib/status-colors.ts` | 47 | See §4. |
| `ui/src/components/transcript/RunTranscriptView.tsx` | 47 | Feature uses palette for agent action differentiation — tokenize. |
| `ui/src/components/IssueChatThread.tsx` | 22 | Production component. |
| `ui/src/pages/Inbox.tsx` | 16 | |
| `ui/src/pages/DesignGuide.tsx` | 13 | Swatch page — presentational, acceptable. |
| `ui/src/pages/IssueChatUxLab.tsx` | 6 | UX lab. |
| `ui/src/pages/RunTranscriptUxLab.tsx` | 10 | UX lab. |
**Buckets:**
- **Clearly intentional scratch / auth surface:** UX Labs (`IssueChatUxLab`, `InviteUxLab`, `RunTranscriptUxLab`), `InviteLanding` — probably should stay.
- **Status color catalog:** `status-colors.ts` + `ActivityCharts.tsx` — covered by §4, §1.
- **Production surfaces that silently diverge:** `AgentDetail.tsx`, `RunTranscriptView.tsx`, `IssueChatThread.tsx`, `Inbox.tsx` — highest ROI to migrate.
### 7. Arbitrary radius values bypass the scale (18 occurrences)
```
ui/src/pages/InviteUxLab.tsx rounded-[28px], rounded-[24px], rounded-[32px]
ui/src/pages/IssueChatUxLab.tsx rounded-[28px], rounded-[32px]
ui/src/pages/RunTranscriptUxLab.tsx rounded-xl (via theme), rounded-2xl (via TW default)
ui/src/pages/ProfileSettings.tsx rounded-[28px], rounded-[24px]
ui/src/pages/CompanySettings.tsx rounded-[14px]
ui/src/components/CompanyRail.tsx rounded-[14px], rounded-[22px]
```
These are almost all in pages that already opt out of the default radius scale. See §Radius scale below for the likely reason.
### 8. Project/label color fallbacks fragmented across 10+ files
Two default fallback hexes for user-picked project/label colors:
- `#6366f1` (indigo-500) — in `IssueProperties.tsx`, `SidebarProjects.tsx`, `NewIssueDialog.tsx`, `ProjectDetail.tsx`, `CompanySettings.tsx`
- `#64748b` (slate-500) — in `IssueColumns.tsx`, `RoutineRunVariablesDialog.tsx`, `RoutineDetail.tsx`, `Routines.tsx`, `MarkdownEditor.tsx`
Not strictly drift — these are fallbacks for a user-supplied field, not the DS. But they disagree with each other, and every caller has duplicated the literal. A single `const DEFAULT_PROJECT_COLOR` (or better, a DS token + utility) would fix it.
### 9. Arbitrary shadow values in production surfaces
```
shadow-[0_24px_60px_rgba(15,23,42,0.08)] UxLab pages, ProfileSettings
shadow-[0_20px_80px_-40px_rgba(0,0,0,0.55)] BudgetPolicyCard
shadow-[0_0_12px_rgba(6,182,212,0.08)] AgentDetail (live indicator)
shadow-[0_18px_50px_rgba(6,182,212,0.08)] LiveRunWidget
```
No `--shadow-*` tokens exist, so there's nothing to migrate _to_, but these are the signal that a shadow/elevation token family would pay for itself quickly.
---
## Low-confidence drift (candidates for new tokens)
### 10. Code block theme is a hardcoded Catppuccin Mocha palette
```
ui/src/index.css:553 background: #1e1e2e; ← code bg
ui/src/index.css:554 color: #cdd6f4; ← code fg
ui/src/index.css:567 background-color: #181825; ← gutter bg
ui/src/index.css:568 color: #585b70; ← gutter fg
ui/src/index.css:569 border-right: 1px solid #313244;
ui/src/index.css:614 background-color: #313244; ← language selector bg
ui/src/index.css:616 border-color: #45475a;
ui/src/index.css:586 color-mix(…, #89b4fa 25%, transparent) ← selection
```
This is a deliberate choice (Catppuccin Mocha is a widely-loved dev theme and it's consistent across MDXEditor + rendered markdown + CodeMirror). But the choice is buried in selectors and spread across 30+ lines of `index.css`. Candidate `--code-bg`, `--code-fg`, `--code-gutter-bg`, `--code-gutter-fg`, `--code-border`, `--code-selection` tokens would:
- Make the choice legible.
- Make "port to Catppuccin Latte for light mode" a one-line change.
- Let MDXEditor and the `.paperclip-markdown` renderer reference the same tokens rather than copy the values.
### 11. GitHub link colors hardcoded
```
ui/src/index.css:429 color: color-mix(in oklab, var(--foreground) 76%, #0969da 24%);
ui/src/index.css:436 color: color-mix(in oklab, var(--foreground) 80%, #58a6ff 20%); (.dark)
ui/src/index.css:756 (duplicated for .paperclip-markdown)
ui/src/index.css:772 (duplicated for .paperclip-markdown dark)
```
Candidate `--link` / `--link-foreground` tokens.
### 12. MDXEditor editor text colors hardcoded
The CodeMirror-inside-MDXEditor rules (`index.css:560618`) hardcode `#cdd6f4`, `#89b4fa`, `#313244` etc. for cursor, selection, gutter. Covered by §10 if `--code-*` tokens are added.
---
## Reserved & near-dead tokens
Summary of tokens whose production usage is at or near zero. **As of 2026-04-21, the 13 previously-flagged "dead" tokens are reclassified as Reserved.** They remain defined in `ui/src/index.css` with explanatory comments; they are not a consolidation opportunity.
| Token | Code usage | Disposition |
|---|---|---|
| `chart-1..5` | 0 each | **Reserved** for future chart tokenization (see §1) |
| `sidebar`, `sidebar-foreground`, `sidebar-primary`, `sidebar-primary-foreground`, `sidebar-accent`, `sidebar-accent-foreground`, `sidebar-border`, `sidebar-ring` | 0 each | **Reserved** for shadcn sidebar compatibility (see §3) |
| `destructive-foreground` | 0 | Unused but light value corrected on 2026-04-21 (see §2). Paired with `--destructive` and with the new `--signal-success-foreground`; kept as part of the action-severity vocabulary. |
| `card-foreground` | 1 | Near-dead. Not necessarily wrong — `card-foreground` equals `foreground` in both modes, so `text-foreground` inside a card is sufficient. |
| `secondary-foreground` | 2 | Near-dead |
| `secondary` | 3 | Near-dead |
| `popover-foreground` | 5 | Low use — likely limited to `popover.tsx` primitive |
No longer flagged as a consolidation opportunity. The 34-token color set is the intended surface.
---
## Duplicate values
These groups of tokens share identical values. Most are classic shadcn-default structural overlaps (intentional, so `*-foreground` patterns match across surfaces). A few are suspicious.
**Light mode:**
| Value | Tokens |
|---|---|
| `oklch(1 0 0)` | `background`, `card`, `popover` |
| `oklch(0.145 0 0)` | `foreground`, `card-foreground`, `popover-foreground`, `sidebar-foreground` |
| `oklch(0.205 0 0)` | `primary`, `secondary-foreground`, `accent-foreground`, `sidebar-primary` |
| `oklch(0.985 0 0)` | `primary-foreground`, `sidebar`, `sidebar-primary-foreground`, `sidebar-accent-foreground` |
| `oklch(0.97 0 0)` | `secondary`, `muted`, `accent`, `sidebar-accent` |
| `oklch(0.922 0 0)` | `border`, `input`, `sidebar-border` |
| `oklch(0.708 0 0)` | `ring`, `sidebar-ring` |
| `oklch(0.577 0.245 27.325)` | `destructive`, `destructive-foreground` ⚠ (see §2) |
**Dark mode** (analogous structure — all sidebar tokens match their non-sidebar counterparts except `sidebar-primary = oklch(0.488 0.243 264.376)`, a distinct blue).
**Takeaway:** In light mode, every `sidebar-*` value equals its non-sidebar counterpart; in dark mode, `sidebar-primary` drifts (blue) from `primary` (white). Per the 2026-04-21 decision, the sidebar family is **preserved as reserved** (see §3) rather than collapsed — the identity with non-sidebar tokens is fine; the value of reserving is keeping the option of theming the sidebar independently if that need emerges.
---
## Non-semantic color usage
(Covered by §6 above and enumerated there with offender files and hit counts.)
---
## Radius scale
**Status: RESOLVED (2026-04-21).** Scale restored to monotonic `sm=6px, md=8px, lg=10px, xl=12px`. 226 call sites in `ui/src/` migrated from `rounded-lg` / `rounded-xl``rounded-none` to preserve the existing flat-Swiss aesthetic on dashboard surfaces. Shadcn primitives (`ui/src/components/ui/**`) excluded from migration — `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` (`rounded-lg``rounded-none`).
Original Stage 1 observations retained below for history.
### What's defined
```
ui/src/index.css:39 --radius-sm: 0.375rem; (6px) @theme inline
ui/src/index.css:40 --radius-md: 0.5rem; (8px) @theme inline
ui/src/index.css:41 --radius-lg: 0px; @theme inline
ui/src/index.css:42 --radius-xl: 0px; @theme inline
ui/src/index.css:47 --radius: 0; :root (not @theme)
```
### What's non-standard
1. **Non-monotonic progression.** Almost every design-system radius scale is `sm ≤ md ≤ lg ≤ xl`. Here it's `sm=6, md=8, lg=0, xl=0`. Two possibilities:
- Intentional flattening at the outer scale (keep small things rounded for friendliness, keep large surfaces square for an editorial look).
- Mid-migration artifact from flipping the scale to 0 from the shadcn defaults (`lg=0.5rem`, `xl=0.75rem`) and not yet reconciling.
2. **Two coexisting radius bases.** `--radius` (value 0) lives at `:root` and is referenced by `index.css`'s own `calc(var(--radius) - 2px)` expressions and by the MDXEditor bridge (`--baseRadius: var(--radius)`). But the Tailwind-visible scale is the `@theme`-scoped `--radius-sm/md/lg/xl`. These never cross-reference. An editor reading the file might reasonably expect `--radius-md` to be derived from `--radius`, but they're independent.
3. **Heavy use of utilities that resolve to 0.**
| Utility | Value | Uses |
|---|---|---|
| `rounded-sm` | 0.375rem | 49 |
| `rounded-md` | 0.5rem | 310 |
| `rounded-lg` | **0px** | **146** |
| `rounded-xl` | **0px** | **81** |
| `rounded-2xl` | (Tailwind default, not themed) | 21 |
| `rounded-full` | full | 208 |
| `rounded-none` | 0 | 36 |
| `rounded` (no suffix) | (Tailwind default) | 127 |
So **227 places** in the codebase write `rounded-lg` or `rounded-xl` and get square corners. Without context on whether that's the intended look, this is ambiguous — if intentional, all those sites are fine; if unintentional, every one of them is a small visual bug.
4. **Arbitrary radius values (see §7).** 18 occurrences of `rounded-[14px]`, `rounded-[22px]`, `rounded-[24px]`, `rounded-[28px]`, `rounded-[32px]` — concentrated in pages that want actually-rounded corners. If `rounded-lg` resolved to ~12px, some of these would go away.
### Open question for the founder
_Is the radius scale intentionally flattening at the top (lg/xl = 0), or is that a stale state from earlier shadcn defaults that should be replaced with a monotonic scale?_
**Resolved 2026-04-21.** Hybrid: dashboard surfaces stay flat (`rounded-none`), the scale is restored monotonically for newer surfaces and for the `dialog.tsx` primitive. 226 host-code call sites migrated.
---
### Radius workaround audit: 18 occurrences, all retained
Per the decision to audit each `rounded-[Npx]` workaround: none match the new scale (6 / 8 / 10 / 12 px). All are intentional. Table below captures the disposition.
| File | Line | Value | Context | Disposition |
|---|---|---|---|---|
| `ui/src/pages/IssueChatUxLab.tsx` | 52 | `rounded-[28px]` | Chat-surface outer container (prototype) | **Keep** — editorial pill > xl scale; UX Lab prototype |
| `ui/src/pages/IssueChatUxLab.tsx` | 139 | `rounded-[32px]` | Hero container with layered gradients (prototype) | **Keep** — ditto |
| `ui/src/pages/InviteUxLab.tsx` | 112 | `rounded-[28px]` | Invite-panel outer (prototype) | **Keep** — UX Lab |
| `ui/src/pages/InviteUxLab.tsx` | 149 | `rounded-[24px]` | Tone card (prototype) | **Keep** — UX Lab |
| `ui/src/pages/InviteUxLab.tsx` | 171 | `rounded-[28px]` | Dark invite capsule (prototype) | **Keep** — UX Lab |
| `ui/src/pages/InviteUxLab.tsx` | 454 | `rounded-[28px]` | Alt invite panel (prototype) | **Keep** — UX Lab |
| `ui/src/pages/InviteUxLab.tsx` | 529 | `rounded-[28px]` | Tone card variant (prototype) | **Keep** — UX Lab |
| `ui/src/pages/InviteUxLab.tsx` | 612 | `rounded-[28px]` | Tone card variant (prototype) | **Keep** — UX Lab |
| `ui/src/pages/InviteUxLab.tsx` | 700 | `rounded-[32px]` | Hero gradient container (prototype) | **Keep** — UX Lab |
| `ui/src/pages/ProfileSettings.tsx` | 159 | `rounded-[28px]` | Profile header hero card | **Keep** — intentional oversize hero radius |
| `ui/src/pages/ProfileSettings.tsx` | 163 | `rounded-[24px]` | Profile identity panel | **Keep** — matches hero language |
| `ui/src/pages/CompanySettings.tsx` | 296 | `rounded-[14px]` | Brand-color preview swatch | **Keep** — matches CompanyRail icon scale |
| `ui/src/components/CompanyRail.tsx` | 99 | `rounded-[14px]` | Company icon (selected state) | **Keep** — iOS-icon-scale, morphing-radius pattern |
| `ui/src/components/CompanyRail.tsx` | 100 | `rounded-[22px]` | Company icon (unselected state) | **Keep** — part of the hover-morph pair |
| `ui/src/components/CompanyRail.tsx` | 247 | `rounded-[22px]``[14px]` on hover | "New company" button | **Keep** — same morph pattern |
| `ui/src/components/ui/checkbox.tsx` | 17 | `rounded-[4px]` | Checkbox control | **Keep** — shadcn primitive; excluded from migration |
| `ui/src/components/ui/tooltip.tsx` | 51 | `rounded-[2px]` | Tooltip arrow | **Keep** — shadcn primitive; excluded from migration |
| `ui/src/components/ui/scroll-area.tsx` | 19 | `rounded-[inherit]` | Scroll viewport | **Keep** — non-numeric special value; can't match the scale by construction |
Summary:
- **9 in UX Lab pages** (`InviteUxLab`, `IssueChatUxLab`) — prototypes, not pursued.
- **2 in ProfileSettings** — intentional editorial hero radii (24px / 28px).
- **1 in CompanySettings** — brand-swatch match to the rail.
- **3 in CompanyRail** — morphing-radius iOS-icon pattern (22px ↔ 14px on hover).
- **3 in shadcn primitives** — out of scope per the migration rule.
No rewrites. No consolidations. All values sit outside the new 6-8-10-12 scale either because they're editorial (20-30px range), because they're part of a morphing-radius interaction (14/22), or because they're shadcn-internal micro-radii (2/4 px, or `inherit`).
---
## Deferred signal variants
Captured here so they don't get forgotten. **Intentionally not added** in the 2026-04-21 pass:
- **`--signal-success-soft` / `--signal-success-subtle`** — a lower-weight variant for use on toast surfaces and inline success banners. Current soft-success rendering uses `bg-emerald-50 text-emerald-900` (light) and `bg-emerald-950/60 text-emerald-100` (dark) in [`ToastViewport.tsx`](../../../ui/src/components/ToastViewport.tsx) — a different hue family (emerald) than the new solid `--signal-success` (green-700/600). A soft variant would let the toast adopt the DS token without shifting visual weight. Add when a concrete refactor needs it.
- **`--signal-warning` / `--signal-info`** — not defined. No current consumer that couldn't go on using its local palette. Add when a real use case appears (e.g., a standardized warning toast variant).
- **Green vs emerald reconciliation.** Approve buttons use `green-700/600`; soft success surfaces use `emerald-50/900`. `--signal-success` sourced from the button family. If a soft-variant lands, the emerald surfaces become the migration candidate — visible as a small hue shift on the toast success background. Document the shift when migrating.
## Integration layer (not drift)
### MDXEditor CSS-variable bridge
**Lines:** `ui/src/index.css:332361`. **24 variables**, scoped to `.paperclip-mdxeditor-scope, .paperclip-mdxeditor`.
These are not tokens. They are an explicit bridge between Paperclip's DS tokens and MDXEditor's internal token vocabulary. Every one of the 24 values is a `var(--host-token)` reference or a `color-mix()` over host tokens — no hardcoded color values. Documenting the mapping for traceability:
| MDXEditor var | Maps to |
|---|---|
| `--baseBase` | `var(--background)` |
| `--baseBg` | `transparent` |
| `--baseBgSubtle` | `color-mix(in oklab, var(--accent) 35%, transparent)` |
| `--baseLine` | `var(--border)` |
| `--baseSolid` | `var(--muted-foreground)` |
| `--baseSolidHover` | `var(--foreground)` |
| `--baseText` | `var(--muted-foreground)` |
| `--baseBorderColor` | `var(--border)` |
| `--baseBorder` | `var(--border)` |
| `--baseBorderHover` | `var(--ring)` |
| `--baseTextContrast` | `var(--foreground)` |
| `--baseTextContrastMuted` | `var(--muted-foreground)` |
| `--baseTextEmphasis` | `var(--foreground)` |
| `--basePageBg` | `var(--background)` |
| `--baseRadius` | `var(--radius)` (the `:root`-scoped 0-value, not a `@theme` radius) |
| `--baseLineHeight` | `1.5` (hardcoded numeric — the only non-bridge value in the block) |
| `--accentBorder` | `color-mix(in oklab, var(--primary) 35%, var(--border))` |
| `--accentSolid` | `var(--primary)` |
| `--accentSolidHover` | `var(--primary)` |
| `--accentLine` | `color-mix(in oklab, var(--primary) 20%, transparent)` |
| `--accentBg` | `var(--accent)` |
| `--accentBgHover` | `color-mix(in oklab, var(--accent) 80%, var(--background))` |
| `--accentBgActive` | `color-mix(in oklab, var(--accent) 72%, var(--background))` |
| `--accentText` | `var(--accent-foreground)` |
The bridge is sound. If DS tokens move, the editor moves with them. Nothing to do.
### Scrollbar oklch values
`index.css:172219`. Hand-picked greys for light/dark scrollbar tracks and thumbs. Not tokens, but candidates for tokenization if scrollbar theming becomes a DS concern.

View File

@@ -1,765 +0,0 @@
{
"generated_at": "2026-04-21T00:00:00Z",
"repo_sha": "a26e1288b627e82c554445732c7d844648e6b5e1",
"scope": "ui/",
"authoritative_source": "ui/src/index.css",
"tailwind_version": "v4",
"usage_count_method": "Sum of (a) Tailwind utility occurrences \\b(bg|text|border|ring|fill|stroke|from|to|via|outline|decoration|placeholder|caret|accent|shadow|divide)-<token>(?![\\w-]) and (b) literal var(--<token>) references, both EXCLUDING the definition file ui/src/index.css. Story coverage is deferred to Stage 2 (covered_by_story=null until then).",
"tokens": [
{
"name": "background",
"category": "color",
"value": "oklch(1 0 0)",
"dark_value": "oklch(0.145 0 0)",
"defined_at": "ui/src/index.css:57 (light) / ui/src/index.css:96 (dark)",
"tailwind_alias_at": "ui/src/index.css:7",
"usage_count": 183,
"used_in_components": 36,
"used_in_pages": 20,
"covered_by_story": null,
"aliases": [
"--background",
"--color-background",
"bg-background",
"text-background",
"border-background"
]
},
{
"name": "foreground",
"category": "color",
"value": "oklch(0.145 0 0)",
"dark_value": "oklch(0.985 0 0)",
"defined_at": "ui/src/index.css:58 (light) / ui/src/index.css:97 (dark)",
"tailwind_alias_at": "ui/src/index.css:8",
"usage_count": 372,
"used_in_components": 57,
"used_in_pages": 31,
"covered_by_story": null,
"aliases": [
"--foreground",
"--color-foreground"
]
},
{
"name": "card",
"category": "color",
"value": "oklch(1 0 0)",
"dark_value": "oklch(0.205 0 0)",
"defined_at": "ui/src/index.css:59 (light) / ui/src/index.css:98 (dark)",
"tailwind_alias_at": "ui/src/index.css:9",
"usage_count": 52,
"used_in_components": 9,
"used_in_pages": 17,
"covered_by_story": null,
"aliases": [
"--card",
"--color-card"
]
},
{
"name": "card-foreground",
"category": "color",
"value": "oklch(0.145 0 0)",
"dark_value": "oklch(0.985 0 0)",
"defined_at": "ui/src/index.css:60 (light) / ui/src/index.css:99 (dark)",
"tailwind_alias_at": "ui/src/index.css:10",
"usage_count": 1,
"used_in_components": 1,
"used_in_pages": 0,
"covered_by_story": null,
"aliases": [
"--card-foreground",
"--color-card-foreground"
]
},
{
"name": "popover",
"category": "color",
"value": "oklch(1 0 0)",
"dark_value": "oklch(0.205 0 0)",
"defined_at": "ui/src/index.css:61 (light) / ui/src/index.css:100 (dark)",
"tailwind_alias_at": "ui/src/index.css:11",
"usage_count": 11,
"used_in_components": 7,
"used_in_pages": 2,
"covered_by_story": null,
"aliases": [
"--popover",
"--color-popover"
]
},
{
"name": "popover-foreground",
"category": "color",
"value": "oklch(0.145 0 0)",
"dark_value": "oklch(0.985 0 0)",
"defined_at": "ui/src/index.css:62 (light) / ui/src/index.css:101 (dark)",
"tailwind_alias_at": "ui/src/index.css:12",
"usage_count": 5,
"used_in_components": 4,
"used_in_pages": 0,
"covered_by_story": null,
"aliases": [
"--popover-foreground",
"--color-popover-foreground"
]
},
{
"name": "primary",
"category": "color",
"value": "oklch(0.205 0 0)",
"dark_value": "oklch(0.985 0 0)",
"defined_at": "ui/src/index.css:63 (light) / ui/src/index.css:102 (dark)",
"tailwind_alias_at": "ui/src/index.css:13",
"usage_count": 35,
"used_in_components": 19,
"used_in_pages": 5,
"covered_by_story": null,
"aliases": [
"--primary",
"--color-primary"
]
},
{
"name": "primary-foreground",
"category": "color",
"value": "oklch(0.985 0 0)",
"dark_value": "oklch(0.205 0 0)",
"defined_at": "ui/src/index.css:64 (light) / ui/src/index.css:103 (dark)",
"tailwind_alias_at": "ui/src/index.css:14",
"usage_count": 10,
"used_in_components": 8,
"used_in_pages": 2,
"covered_by_story": null,
"aliases": [
"--primary-foreground",
"--color-primary-foreground"
]
},
{
"name": "secondary",
"category": "color",
"value": "oklch(0.97 0 0)",
"dark_value": "oklch(0.269 0 0)",
"defined_at": "ui/src/index.css:65 (light) / ui/src/index.css:104 (dark)",
"tailwind_alias_at": "ui/src/index.css:15",
"usage_count": 3,
"used_in_components": 3,
"used_in_pages": 0,
"covered_by_story": null,
"aliases": [
"--secondary",
"--color-secondary"
]
},
{
"name": "secondary-foreground",
"category": "color",
"value": "oklch(0.205 0 0)",
"dark_value": "oklch(0.985 0 0)",
"defined_at": "ui/src/index.css:66 (light) / ui/src/index.css:105 (dark)",
"tailwind_alias_at": "ui/src/index.css:16",
"usage_count": 2,
"used_in_components": 2,
"used_in_pages": 0,
"covered_by_story": null,
"aliases": [
"--secondary-foreground",
"--color-secondary-foreground"
]
},
{
"name": "muted",
"category": "color",
"value": "oklch(0.97 0 0)",
"dark_value": "oklch(0.269 0 0)",
"defined_at": "ui/src/index.css:67 (light) / ui/src/index.css:106 (dark)",
"tailwind_alias_at": "ui/src/index.css:17",
"usage_count": 90,
"used_in_components": 31,
"used_in_pages": 15,
"covered_by_story": null,
"aliases": [
"--muted",
"--color-muted"
]
},
{
"name": "muted-foreground",
"category": "color",
"value": "oklch(0.556 0 0)",
"dark_value": "oklch(0.708 0 0)",
"defined_at": "ui/src/index.css:68 (light) / ui/src/index.css:107 (dark)",
"tailwind_alias_at": "ui/src/index.css:18",
"usage_count": 1540,
"used_in_components": 98,
"used_in_pages": 48,
"covered_by_story": null,
"aliases": [
"--muted-foreground",
"--color-muted-foreground"
]
},
{
"name": "accent",
"category": "color",
"value": "oklch(0.97 0 0)",
"dark_value": "oklch(0.269 0 0)",
"defined_at": "ui/src/index.css:69 (light) / ui/src/index.css:108 (dark)",
"tailwind_alias_at": "ui/src/index.css:19",
"usage_count": 340,
"used_in_components": 57,
"used_in_pages": 21,
"covered_by_story": null,
"aliases": [
"--accent",
"--color-accent"
]
},
{
"name": "accent-foreground",
"category": "color",
"value": "oklch(0.205 0 0)",
"dark_value": "oklch(0.985 0 0)",
"defined_at": "ui/src/index.css:70 (light) / ui/src/index.css:109 (dark)",
"tailwind_alias_at": "ui/src/index.css:20",
"usage_count": 14,
"used_in_components": 5,
"used_in_pages": 1,
"covered_by_story": null,
"aliases": [
"--accent-foreground",
"--color-accent-foreground"
]
},
{
"name": "destructive",
"category": "color",
"value": "oklch(0.577 0.245 27.325)",
"dark_value": "oklch(0.637 0.237 25.331)",
"defined_at": "ui/src/index.css:71 (light) / ui/src/index.css:110 (dark)",
"tailwind_alias_at": "ui/src/index.css:21",
"usage_count": 160,
"used_in_components": 28,
"used_in_pages": 43,
"covered_by_story": null,
"aliases": [
"--destructive",
"--color-destructive"
]
},
{
"name": "destructive-foreground",
"category": "color",
"value": "oklch(0.985 0 0)",
"dark_value": "oklch(0.985 0 0)",
"defined_at": "ui/src/index.css:72 (light) / ui/src/index.css:111 (dark)",
"tailwind_alias_at": "ui/src/index.css:22",
"usage_count": 0,
"used_in_components": 0,
"used_in_pages": 0,
"covered_by_story": null,
"aliases": [
"--destructive-foreground",
"--color-destructive-foreground"
],
"review_flag": null,
"history": "Light-mode value corrected from oklch(0.577 0.245 27.325) (equalled --destructive) to oklch(0.985 0 0) on 2026-04-21."
},
{
"name": "signal-success",
"category": "color",
"value": "oklch(0.527 0.154 150.069)",
"dark_value": "oklch(0.627 0.194 149.214)",
"defined_at": "ui/src/index.css:73 (light) / ui/src/index.css:112 (dark)",
"tailwind_alias_at": "ui/src/index.css:23",
"usage_count": 0,
"used_in_components": 0,
"used_in_pages": 0,
"covered_by_story": null,
"aliases": [
"--signal-success",
"--color-signal-success"
],
"review_flag": null,
"notes": "Added 2026-04-21. Action-severity token paired with --destructive. Sourced from the approve-button treatment (bg-green-700 / bg-green-600) in ApprovalCard, ApprovalDetail, Inbox. No call sites migrated yet."
},
{
"name": "signal-success-foreground",
"category": "color",
"value": "oklch(0.985 0 0)",
"dark_value": "oklch(0.985 0 0)",
"defined_at": "ui/src/index.css:74 (light) / ui/src/index.css:113 (dark)",
"tailwind_alias_at": "ui/src/index.css:24",
"usage_count": 0,
"used_in_components": 0,
"used_in_pages": 0,
"covered_by_story": null,
"aliases": [
"--signal-success-foreground",
"--color-signal-success-foreground"
],
"review_flag": null,
"notes": "Added 2026-04-21. Text-on-signal-success-surface (white)."
},
{
"name": "border",
"category": "color",
"value": "oklch(0.922 0 0)",
"dark_value": "oklch(0.269 0 0)",
"defined_at": "ui/src/index.css:75 (light) / ui/src/index.css:114 (dark)",
"tailwind_alias_at": "ui/src/index.css:25",
"usage_count": 701,
"used_in_components": 72,
"used_in_pages": 43,
"covered_by_story": null,
"aliases": [
"--border",
"--color-border"
]
},
{
"name": "input",
"category": "color",
"value": "oklch(0.922 0 0)",
"dark_value": "oklch(0.269 0 0)",
"defined_at": "ui/src/index.css:76 (light) / ui/src/index.css:115 (dark)",
"tailwind_alias_at": "ui/src/index.css:26",
"usage_count": 8,
"used_in_components": 6,
"used_in_pages": 1,
"covered_by_story": null,
"aliases": [
"--input",
"--color-input"
]
},
{
"name": "ring",
"category": "color",
"value": "oklch(0.708 0 0)",
"dark_value": "oklch(0.439 0 0)",
"defined_at": "ui/src/index.css:77 (light) / ui/src/index.css:116 (dark)",
"tailwind_alias_at": "ui/src/index.css:27",
"usage_count": 26,
"used_in_components": 17,
"used_in_pages": 2,
"covered_by_story": null,
"aliases": [
"--ring",
"--color-ring"
]
},
{
"name": "chart-1",
"category": "color",
"value": "oklch(0.646 0.222 41.116)",
"dark_value": "oklch(0.488 0.243 264.376)",
"defined_at": "ui/src/index.css:79 (light) / ui/src/index.css:118 (dark)",
"tailwind_alias_at": "ui/src/index.css:32",
"usage_count": 0,
"used_in_components": 0,
"used_in_pages": 0,
"covered_by_story": null,
"aliases": [
"--chart-1",
"--color-chart-1"
],
"review_flag": "RESERVED \u2014 reserved for future chart tokenization"
},
{
"name": "chart-2",
"category": "color",
"value": "oklch(0.6 0.118 184.704)",
"dark_value": "oklch(0.696 0.17 162.48)",
"defined_at": "ui/src/index.css:80 (light) / ui/src/index.css:119 (dark)",
"tailwind_alias_at": "ui/src/index.css:33",
"usage_count": 0,
"used_in_components": 0,
"used_in_pages": 0,
"covered_by_story": null,
"aliases": [
"--chart-2",
"--color-chart-2"
],
"review_flag": "RESERVED \u2014 reserved for future chart tokenization"
},
{
"name": "chart-3",
"category": "color",
"value": "oklch(0.398 0.07 227.392)",
"dark_value": "oklch(0.769 0.188 70.08)",
"defined_at": "ui/src/index.css:81 (light) / ui/src/index.css:120 (dark)",
"tailwind_alias_at": "ui/src/index.css:34",
"usage_count": 0,
"used_in_components": 0,
"used_in_pages": 0,
"covered_by_story": null,
"aliases": [
"--chart-3",
"--color-chart-3"
],
"review_flag": "RESERVED \u2014 reserved for future chart tokenization"
},
{
"name": "chart-4",
"category": "color",
"value": "oklch(0.828 0.189 84.429)",
"dark_value": "oklch(0.627 0.265 303.9)",
"defined_at": "ui/src/index.css:82 (light) / ui/src/index.css:121 (dark)",
"tailwind_alias_at": "ui/src/index.css:35",
"usage_count": 0,
"used_in_components": 0,
"used_in_pages": 0,
"covered_by_story": null,
"aliases": [
"--chart-4",
"--color-chart-4"
],
"review_flag": "RESERVED \u2014 reserved for future chart tokenization"
},
{
"name": "chart-5",
"category": "color",
"value": "oklch(0.769 0.188 70.08)",
"dark_value": "oklch(0.645 0.246 16.439)",
"defined_at": "ui/src/index.css:83 (light) / ui/src/index.css:122 (dark)",
"tailwind_alias_at": "ui/src/index.css:36",
"usage_count": 0,
"used_in_components": 0,
"used_in_pages": 0,
"covered_by_story": null,
"aliases": [
"--chart-5",
"--color-chart-5"
],
"review_flag": "RESERVED \u2014 reserved for future chart tokenization"
},
{
"name": "sidebar",
"category": "color",
"value": "oklch(0.985 0 0)",
"dark_value": "oklch(0.145 0 0)",
"defined_at": "ui/src/index.css:85 (light) / ui/src/index.css:124 (dark)",
"tailwind_alias_at": "ui/src/index.css:40",
"usage_count": 0,
"used_in_components": 0,
"used_in_pages": 0,
"covered_by_story": null,
"aliases": [
"--sidebar",
"--color-sidebar"
],
"review_flag": "RESERVED \u2014 reserved for shadcn sidebar primitive compatibility"
},
{
"name": "sidebar-foreground",
"category": "color",
"value": "oklch(0.145 0 0)",
"dark_value": "oklch(0.985 0 0)",
"defined_at": "ui/src/index.css:86 (light) / ui/src/index.css:125 (dark)",
"tailwind_alias_at": "ui/src/index.css:41",
"usage_count": 0,
"used_in_components": 0,
"used_in_pages": 0,
"covered_by_story": null,
"aliases": [
"--sidebar-foreground",
"--color-sidebar-foreground"
],
"review_flag": "RESERVED \u2014 reserved for shadcn sidebar primitive compatibility"
},
{
"name": "sidebar-primary",
"category": "color",
"value": "oklch(0.205 0 0)",
"dark_value": "oklch(0.488 0.243 264.376)",
"defined_at": "ui/src/index.css:87 (light) / ui/src/index.css:126 (dark)",
"tailwind_alias_at": "ui/src/index.css:42",
"usage_count": 0,
"used_in_components": 0,
"used_in_pages": 0,
"covered_by_story": null,
"aliases": [
"--sidebar-primary",
"--color-sidebar-primary"
],
"review_flag": "RESERVED \u2014 reserved for shadcn sidebar primitive compatibility"
},
{
"name": "sidebar-primary-foreground",
"category": "color",
"value": "oklch(0.985 0 0)",
"dark_value": "oklch(0.985 0 0)",
"defined_at": "ui/src/index.css:88 (light) / ui/src/index.css:127 (dark)",
"tailwind_alias_at": "ui/src/index.css:43",
"usage_count": 0,
"used_in_components": 0,
"used_in_pages": 0,
"covered_by_story": null,
"aliases": [
"--sidebar-primary-foreground",
"--color-sidebar-primary-foreground"
],
"review_flag": "RESERVED \u2014 reserved for shadcn sidebar primitive compatibility"
},
{
"name": "sidebar-accent",
"category": "color",
"value": "oklch(0.97 0 0)",
"dark_value": "oklch(0.269 0 0)",
"defined_at": "ui/src/index.css:89 (light) / ui/src/index.css:128 (dark)",
"tailwind_alias_at": "ui/src/index.css:44",
"usage_count": 0,
"used_in_components": 0,
"used_in_pages": 0,
"covered_by_story": null,
"aliases": [
"--sidebar-accent",
"--color-sidebar-accent"
],
"review_flag": "RESERVED \u2014 reserved for shadcn sidebar primitive compatibility"
},
{
"name": "sidebar-accent-foreground",
"category": "color",
"value": "oklch(0.205 0 0)",
"dark_value": "oklch(0.985 0 0)",
"defined_at": "ui/src/index.css:90 (light) / ui/src/index.css:129 (dark)",
"tailwind_alias_at": "ui/src/index.css:45",
"usage_count": 0,
"used_in_components": 0,
"used_in_pages": 0,
"covered_by_story": null,
"aliases": [
"--sidebar-accent-foreground",
"--color-sidebar-accent-foreground"
],
"review_flag": "RESERVED \u2014 reserved for shadcn sidebar primitive compatibility"
},
{
"name": "sidebar-border",
"category": "color",
"value": "oklch(0.922 0 0)",
"dark_value": "oklch(0.269 0 0)",
"defined_at": "ui/src/index.css:91 (light) / ui/src/index.css:130 (dark)",
"tailwind_alias_at": "ui/src/index.css:46",
"usage_count": 0,
"used_in_components": 0,
"used_in_pages": 0,
"covered_by_story": null,
"aliases": [
"--sidebar-border",
"--color-sidebar-border"
],
"review_flag": "RESERVED \u2014 reserved for shadcn sidebar primitive compatibility"
},
{
"name": "sidebar-ring",
"category": "color",
"value": "oklch(0.708 0 0)",
"dark_value": "oklch(0.439 0 0)",
"defined_at": "ui/src/index.css:92 (light) / ui/src/index.css:131 (dark)",
"tailwind_alias_at": "ui/src/index.css:47",
"usage_count": 0,
"used_in_components": 0,
"used_in_pages": 0,
"covered_by_story": null,
"aliases": [
"--sidebar-ring",
"--color-sidebar-ring"
],
"review_flag": "RESERVED \u2014 reserved for shadcn sidebar primitive compatibility"
},
{
"name": "radius",
"category": "radius",
"value": "0",
"dark_value": null,
"defined_at": "ui/src/index.css:56",
"tailwind_alias_at": null,
"scope": ":root (not in @theme)",
"usage_count": 127,
"used_in_components": 24,
"used_in_pages": 11,
"covered_by_story": null,
"aliases": [
"--radius"
],
"review_flag": null,
"usage_note": "Tailwind `rounded` utility (no suffix) resolves via Tailwind defaults. This :root token is also consumed by index.css calc() expressions and the MDXEditor bridge --baseRadius."
},
{
"name": "radius-sm",
"category": "radius",
"value": "0.375rem",
"dark_value": null,
"defined_at": "ui/src/index.css:48",
"tailwind_alias_at": "ui/src/index.css:48",
"scope": "@theme inline",
"usage_count": 49,
"used_in_components": 20,
"used_in_pages": 5,
"covered_by_story": null,
"aliases": [
"--radius-sm",
"rounded-sm"
],
"review_flag": null
},
{
"name": "radius-md",
"category": "radius",
"value": "0.5rem",
"dark_value": null,
"defined_at": "ui/src/index.css:49",
"tailwind_alias_at": "ui/src/index.css:49",
"scope": "@theme inline",
"usage_count": 310,
"used_in_components": 43,
"used_in_pages": 31,
"covered_by_story": null,
"aliases": [
"--radius-md",
"rounded-md"
],
"review_flag": null
},
{
"name": "radius-lg",
"category": "radius",
"value": "0.625rem",
"dark_value": null,
"defined_at": "ui/src/index.css:50",
"tailwind_alias_at": "ui/src/index.css:50",
"scope": "@theme inline",
"usage_count": 1,
"used_in_components": 1,
"used_in_pages": 0,
"covered_by_story": null,
"aliases": [
"--radius-lg",
"rounded-lg"
],
"review_flag": null,
"history": "Value restored from 0px to 0.625rem (10px) on 2026-04-21 as part of the monotonic scale restore."
},
{
"name": "radius-xl",
"category": "radius",
"value": "0.75rem",
"dark_value": null,
"defined_at": "ui/src/index.css:51",
"tailwind_alias_at": "ui/src/index.css:51",
"scope": "@theme inline",
"usage_count": 0,
"used_in_components": 0,
"used_in_pages": 0,
"covered_by_story": null,
"aliases": [
"--radius-xl",
"rounded-xl"
],
"review_flag": null,
"history": "Value restored from 0px to 0.75rem (12px) on 2026-04-21. No code consumers; opt-in for new surfaces."
}
],
"tokens_by_category_count": {
"color": 34,
"radius": 5,
"spacing": 0,
"type": 0,
"motion": 0,
"elevation": 0
},
"excluded": {
"mdxeditor_bridge": {
"scope_selector": ".paperclip-mdxeditor-scope, .paperclip-mdxeditor",
"line_range": "ui/src/index.css:332-361",
"variable_count": 24,
"reason": "Integration-layer aliases that map host DS tokens to MDXEditor internal token names. Every value is `var(--host-token)` or color-mix() over host tokens. Not authoritative DS tokens. Documented as a bridge in tokens-review.md, not as drift.",
"bridge_mapping_sample": [
{
"mdx_var": "--baseBase",
"maps_to": "var(--background)"
},
{
"mdx_var": "--baseLine",
"maps_to": "var(--border)"
},
{
"mdx_var": "--baseSolid",
"maps_to": "var(--muted-foreground)"
},
{
"mdx_var": "--baseText",
"maps_to": "var(--muted-foreground)"
},
{
"mdx_var": "--baseBorderHover",
"maps_to": "var(--ring)"
},
{
"mdx_var": "--baseTextContrast",
"maps_to": "var(--foreground)"
},
{
"mdx_var": "--baseRadius",
"maps_to": "var(--radius)"
},
{
"mdx_var": "--accentSolid",
"maps_to": "var(--primary)"
},
{
"mdx_var": "--accentBg",
"maps_to": "var(--accent)"
},
{
"mdx_var": "--accentText",
"maps_to": "var(--accent-foreground)"
}
]
},
"scrollbar_oklch": {
"line_range": "ui/src/index.css:172-219",
"reason": "Intentional scrollbar styling using raw oklch values (oklch(0.205 0 0), oklch(0.4 0 0), oklch(0.5 0 0), oklch(0.92 0 0), oklch(0.7 0 0), oklch(0.6 0 0)). Candidate for tokenization but currently out of scope."
},
"shimmer_component_vars": {
"scope_selector": ".shimmer-text",
"line_range": "ui/src/index.css:305-306",
"reason": "Component-local CSS variables (--shimmer-base, --shimmer-highlight)."
},
"code_block_hex_palette": {
"reason": "Hardcoded Catppuccin Mocha hex values for code-block theming (#1e1e2e, #cdd6f4, #181825, #585b70, #313244, #45475a, #89b4fa). Flagged as drift candidate in tokens-review.md; not extracted as tokens yet."
}
},
"keyframes": [
{
"name": "dashboard-activity-enter",
"defined_at": "ui/src/index.css:228",
"used_by": ".activity-row-enter (520ms cubic-bezier(0.16, 1, 0.3, 1))"
},
{
"name": "dashboard-activity-highlight",
"defined_at": "ui/src/index.css:246",
"used_by": ".activity-row-enter (920ms cubic-bezier(0.16, 1, 0.3, 1))"
},
{
"name": "cot-line-slide-in",
"defined_at": "ui/src/index.css:272",
"used_by": ".cot-line-enter (300ms cubic-bezier(0.4, 0, 0.2, 1))"
},
{
"name": "cot-line-slide-out",
"defined_at": "ui/src/index.css:277",
"used_by": ".cot-line-exit (300ms cubic-bezier(0.4, 0, 0.2, 1))"
},
{
"name": "shimmer-text-slide",
"defined_at": "ui/src/index.css:298",
"used_by": ".shimmer-text (2.5s linear infinite)"
}
]
}

View File

@@ -1,157 +0,0 @@
# Paperclip Design System — Tokens
- **Generated:** 2026-04-21
- **Repo SHA:** a26e1288b627e82c554445732c7d844648e6b5e1
- **Scope:** `ui/` (`@paperclipai/ui`)
- **Authoritative source:** [`ui/src/index.css`](../../../ui/src/index.css)
- **Review doc (drift & open questions):** [tokens-review.md](./tokens-review.md)
## Source & conventions
- **Tailwind v4** (CSS-first config). No `tailwind.config.*` file.
- **shadcn/ui** (`new-york` style, `neutral` base, `cssVariables: true`, `iconLibrary: lucide`, non-RSC).
- **Color space:** `oklch()` throughout.
- **Theme scoping:** `:root` for light mode; `.dark` for dark overrides. Dark mode is opt-in via the `dark` class through the custom variant `@custom-variant dark (&:is(.dark *))`.
- **Tailwind alias layer:** `@theme inline { --color-*: var(--<token>); }` exposes every semantic token as a Tailwind utility (`bg-background`, `text-foreground`, …). That's the mechanism — the `--<token>` at `:root` is authoritative.
## Token counts
| Category | Count | Notes |
|------------|-------|-------|
| Color | 34 | 19 semantic surfaces + 2 signal + 5 chart + 8 sidebar |
| Radius | 5 | Scale under review — non-monotonic values. See [tokens-review.md](./tokens-review.md#radius-scale--under-founder-review). |
| Spacing | 0 | Tailwind v4 defaults only. |
| Type | 0 | Tailwind v4 defaults + `@tailwindcss/typography`. Markdown styling is hand-rolled via `.paperclip-markdown` / `.paperclip-mdxeditor-content` classes in `index.css`. |
| Motion | 0 | No `--motion-*` / `--duration-*` variables. 5 named `@keyframes`; easing and duration expressed inline at use sites. |
| Elevation | 0 | No `--shadow-*` tokens. Project uses borders + background shifts for elevation; occasional arbitrary `shadow-[…]` in UxLab / polished surfaces. |
## Color (34)
Each token has a light value (`:root`) and a dark value (`.dark`). Tailwind alias is `bg-<name>`, `text-<name>`, etc.
> **Separation of concerns.** `destructive` / `destructive-foreground` and `signal-success` / `signal-success-foreground` are **action-severity** tokens — used on buttons, toasts, and solid accent fills where the intent is "this click is destructive/successful." They are paired in `{base, -foreground}` form for solid-bg + contrasting-text use. They are **not** status-indicator colors for entity state (issue status, agent status, priority). Entity-state coloring lives in [`ui/src/lib/status-colors.ts`](../../../ui/src/lib/status-colors.ts) as a TypeScript catalog using raw Tailwind palette classes — deliberately separate from DS tokens. Tokenizing `status-colors.ts` as a `--status-*` family is a deferred future project; see [tokens-review.md §4](./tokens-review.md#4-status-colorsts-is-a-canonical-semantic-color-catalog-that-bypasses-the-ds).
### Semantic surfaces (19)
| Token | Light | Dark | Uses |
|---|---|---|---|
| `background` | `oklch(1 0 0)` | `oklch(0.145 0 0)` | 183 |
| `foreground` | `oklch(0.145 0 0)` | `oklch(0.985 0 0)` | 372 |
| `card` | `oklch(1 0 0)` | `oklch(0.205 0 0)` | 52 |
| `card-foreground` | `oklch(0.145 0 0)` | `oklch(0.985 0 0)` | 1 ⚠ |
| `popover` | `oklch(1 0 0)` | `oklch(0.205 0 0)` | 11 |
| `popover-foreground` | `oklch(0.145 0 0)` | `oklch(0.985 0 0)` | 5 |
| `primary` | `oklch(0.205 0 0)` | `oklch(0.985 0 0)` | 35 |
| `primary-foreground` | `oklch(0.985 0 0)` | `oklch(0.205 0 0)` | 10 |
| `secondary` | `oklch(0.97 0 0)` | `oklch(0.269 0 0)` | 3 ⚠ |
| `secondary-foreground` | `oklch(0.205 0 0)` | `oklch(0.985 0 0)` | 2 ⚠ |
| `muted` | `oklch(0.97 0 0)` | `oklch(0.269 0 0)` | 90 |
| `muted-foreground` | `oklch(0.556 0 0)` | `oklch(0.708 0 0)` | 1540 |
| `accent` | `oklch(0.97 0 0)` | `oklch(0.269 0 0)` | 340 |
| `accent-foreground` | `oklch(0.205 0 0)` | `oklch(0.985 0 0)` | 14 |
| `destructive` | `oklch(0.577 0.245 27.325)` | `oklch(0.637 0.237 25.331)` | 160 |
| `destructive-foreground` | `oklch(0.985 0 0)` | `oklch(0.985 0 0)` | 0 |
| `border` | `oklch(0.922 0 0)` | `oklch(0.269 0 0)` | 701 |
| `input` | `oklch(0.922 0 0)` | `oklch(0.269 0 0)` | 8 |
| `ring` | `oklch(0.708 0 0)` | `oklch(0.439 0 0)` | 26 |
⚠ Flagged for review. See [tokens-review.md](./tokens-review.md).
### Signal (2)
Action-severity tokens paired with `destructive`. Intended for solid-accent fills on buttons, toasts, and confirmation surfaces where the action is semantically "success" (approve, confirm, ship).
| Token | Light | Dark | Uses |
|---|---|---|---|
| `signal-success` | `oklch(0.527 0.154 150.069)` | `oklch(0.627 0.194 149.214)` | 0 (new) |
| `signal-success-foreground` | `oklch(0.985 0 0)` | `oklch(0.985 0 0)` | 0 (new) |
Sourced from the canonical approve-action button treatment (`bg-green-700 hover:bg-green-600 text-white` across `ApprovalCard`, `ApprovalDetail`, `Inbox`). Tailwind aliases are `bg-signal-success`, `text-signal-success-foreground`, etc. No call sites migrated yet — tokens land as primitives for opt-in adoption.
`--signal-warning` and `--signal-info` are intentionally **not** defined — defer until a real use case appears. See [tokens-review.md §Deferred variants](./tokens-review.md#deferred-signal-variants).
### Chart (5) — Reserved
> **Status: Reserved.** These five tokens are preserved in `ui/src/index.css` for the future chart-tokenization project. **Do not consume them today.** Current chart implementations (`ActivityCharts.tsx`, `OrgChart.tsx`) use hardcoded Tailwind-palette hex values directly; those call sites will migrate onto the chart tokens (or onto a `--status-*` family) in a separate future project. Keeping the tokens here is a deliberate placeholder. See [tokens-review.md §Chart tokens — reserved](./tokens-review.md#1-chart--tokens-are-dead).
| Token | Light | Dark | Status |
|---|---|---|---|
| `chart-1` | `oklch(0.646 0.222 41.116)` | `oklch(0.488 0.243 264.376)` | Reserved |
| `chart-2` | `oklch(0.6 0.118 184.704)` | `oklch(0.696 0.17 162.48)` | Reserved |
| `chart-3` | `oklch(0.398 0.07 227.392)` | `oklch(0.769 0.188 70.08)` | Reserved |
| `chart-4` | `oklch(0.828 0.189 84.429)` | `oklch(0.627 0.265 303.9)` | Reserved |
| `chart-5` | `oklch(0.769 0.188 70.08)` | `oklch(0.645 0.246 16.439)` | Reserved |
### Sidebar (8) — Reserved
> **Status: Reserved.** Preserved for shadcn `Sidebar` primitive compatibility in case that primitive is reintroduced. The current custom `Sidebar.tsx` consumes the semantic surface tokens (`background`, `foreground`, `accent`, `border`) directly. **Do not consume these tokens today** unless a sidebar variant that needs its own theming is explicitly being built. See [tokens-review.md §Sidebar tokens — reserved](./tokens-review.md#3-sidebar--tokens-are-dead).
| Token | Light | Dark | Status |
|---|---|---|---|
| `sidebar` | `oklch(0.985 0 0)` | `oklch(0.145 0 0)` | Reserved |
| `sidebar-foreground` | `oklch(0.145 0 0)` | `oklch(0.985 0 0)` | Reserved |
| `sidebar-primary` | `oklch(0.205 0 0)` | `oklch(0.488 0.243 264.376)` | Reserved |
| `sidebar-primary-foreground` | `oklch(0.985 0 0)` | `oklch(0.985 0 0)` | Reserved |
| `sidebar-accent` | `oklch(0.97 0 0)` | `oklch(0.269 0 0)` | Reserved |
| `sidebar-accent-foreground` | `oklch(0.205 0 0)` | `oklch(0.985 0 0)` | Reserved |
| `sidebar-border` | `oklch(0.922 0 0)` | `oklch(0.269 0 0)` | Reserved |
| `sidebar-ring` | `oklch(0.708 0 0)` | `oklch(0.439 0 0)` | Reserved |
## Radius (5)
> **Scale resolved 2026-04-21.** Existing dashboard surfaces use `rounded-none` by intent (sharp, Swiss aesthetic) — 226 call sites in `ui/src/` migrated off `rounded-lg` / `rounded-xl`. The `lg` and `xl` values are restored to a monotonic scale for newer surfaces (dialogs, chat bubbles, card explorations) and for the one shadcn primitive that consumes them (`dialog.tsx`, which now renders with 10px-rounded corners).
| Token | Value | Scope | Maps to Tailwind | Uses (post-migration) |
|---|---|---|---|---|
| `--radius` | `0` | `:root` | `rounded` (no suffix, via default) | 127 |
| `--radius-sm` | `0.375rem` (6px) | `@theme inline` | `rounded-sm` | 49 |
| `--radius-md` | `0.5rem` (8px) | `@theme inline` | `rounded-md` | 310 |
| `--radius-lg` | `0.625rem` (10px) | `@theme inline` | `rounded-lg` | 1 (dialog.tsx primitive) |
| `--radius-xl` | `0.75rem` (12px) | `@theme inline` | `rounded-xl` | 0 (opt-in for new surfaces) |
`--radius` (`:root`, = 0) is the legacy shadcn base; it's consumed by the MDXEditor bridge (`--baseRadius`) and by a few `calc(var(--radius) ± Npx)` expressions inside `index.css`. The `@theme` scale is what Tailwind utilities resolve to.
### 18 arbitrary `rounded-[Npx]` exceptions — all intentional, preserved
None of the 18 values below match the new scale (6 / 8 / 10 / 12 px). Kept as-is per design intent. See [tokens-review.md §Radius workaround audit](./tokens-review.md#radius-workaround-audit-18-occurrences-all-retained) for the per-file disposition.
## Spacing (0)
No project-local spacing tokens. Uses the Tailwind v4 default scale (`p-1`, `gap-4`, etc., driven by the Tailwind-shipped `--spacing` base of 0.25rem).
## Type (0)
No project-local font-family, font-size, or line-height tokens. Typography sources:
- Tailwind v4 defaults (the `--text-*` family ships with `tailwindcss`).
- `@tailwindcss/typography` plugin (prose).
- Hand-authored rules on `.paperclip-markdown` and `.paperclip-mdxeditor-content` in `ui/src/index.css` (hardcoded `font-size`, `line-height`, `margin`). These are per-surface overrides — not tokens.
- Code blocks use a hardcoded Catppuccin-Mocha palette in `index.css`. Flagged as drift candidate in [tokens-review.md — §Low-confidence drift](./tokens-review.md#low-confidence-drift-candidates-for-new-tokens).
## Motion (0)
No `--motion-*` or `--duration-*` variables. Motion is expressed as per-feature `@keyframes` with inline duration and `cubic-bezier()`.
**Keyframes defined in `ui/src/index.css`:**
| Name | Used by | Duration / easing |
|---|---|---|
| `dashboard-activity-enter` | `.activity-row-enter` | 520ms `cubic-bezier(0.16, 1, 0.3, 1)` |
| `dashboard-activity-highlight` | `.activity-row-enter` | 920ms `cubic-bezier(0.16, 1, 0.3, 1)` |
| `cot-line-slide-in` | `.cot-line-enter` | 300ms `cubic-bezier(0.4, 0, 0.2, 1)` |
| `cot-line-slide-out` | `.cot-line-exit` | 300ms `cubic-bezier(0.4, 0, 0.2, 1)` |
| `shimmer-text-slide` | `.shimmer-text` | 2.5s linear infinite |
Two easing curves recur (`cubic-bezier(0.16, 1, 0.3, 1)` and `cubic-bezier(0.4, 0, 0.2, 1)`). No duration pattern repeats. All animations honor `prefers-reduced-motion`.
## Elevation (0)
No `--shadow-*` tokens. The project's default visual elevation is border-based. Arbitrary `shadow-[…]` values appear in polished surfaces and UxLab prototypes; see [tokens-review.md — §Medium-confidence drift](./tokens-review.md#medium-confidence-drift).
## Out of scope / excluded from tokens.json
- **MDXEditor theme bridge** — 24 variables at `.paperclip-mdxeditor-scope, .paperclip-mdxeditor` (`index.css:332361`). Every value is `var(--host-token)` or `color-mix(in oklab, var(--host-token) N%, …)` — a deliberate integration-layer alias, not a hardcoded theme. Documented as a bridge in [tokens-review.md — §Integration layer](./tokens-review.md#integration-layer-not-drift).
- **Scrollbar oklch values** — `index.css:172219`. Hand-picked greys for light/dark scrollbar track/thumb. Candidate for tokenization; not done yet.
- **Component-local vars** — `.shimmer-text` (`--shimmer-base`, `--shimmer-highlight`) and `.paperclip-mermaid*` classes.
- **Hardcoded chart hex palette** in `ActivityCharts.tsx` / `OrgChart.tsx` — treated as drift, not as tokens. See [tokens-review.md](./tokens-review.md).
- **Raw Tailwind palette usage** across the codebase — treated as non-semantic drift. See [tokens-review.md — §Non-semantic color usage](./tokens-review.md#non-semantic-color-usage).

View File

@@ -146,8 +146,6 @@ Use it for:
- explicit waiting relationships
- automatic wakeups when all blockers resolve
Blocked issues should stay idle while blockers remain unresolved. Paperclip should not create a queued heartbeat run for that issue until the final blocker is done and the `issue_blockers_resolved` wake can start real work.
If a parent is truly waiting on a child, model that with blockers. Do not rely on the parent/child relationship alone.
## 7. Consistent Execution Path Rules

View File

@@ -10,9 +10,6 @@ It is intentionally narrower than [PLUGIN_SPEC.md](./PLUGIN_SPEC.md). The spec i
- Plugin UI runs as same-origin JavaScript inside the main Paperclip app.
- Worker-side host APIs are capability-gated.
- Plugin UI is not sandboxed by manifest capabilities.
- Plugin database migrations are restricted to a host-derived plugin namespace.
- Plugin-owned JSON API routes must be declared in the manifest and are mounted
only under `/api/plugins/:pluginId/api/*`.
- There is no host-provided shared React component kit for plugins yet.
- `ctx.assets` is not supported in the current runtime.
@@ -80,12 +77,10 @@ Worker:
- secrets
- activity
- state
- database namespace via `ctx.db`
- scoped JSON API routes declared with `apiRoutes`
- entities
- projects and project workspaces
- companies
- issues, comments, namespaced `plugin:<pluginKey>` origins, blocker relations, checkout assertions, assignment wakeups, and orchestration summaries
- issues and comments
- agents and agent sessions
- goals
- data/actions
@@ -94,55 +89,6 @@ Worker:
- metrics
- logger
### Plugin database declarations
First-party or otherwise trusted orchestration plugins can declare:
```ts
database: {
migrationsDir: "migrations",
coreReadTables: ["issues"],
}
```
Required capabilities are `database.namespace.migrate` and
`database.namespace.read`; add `database.namespace.write` for runtime mutations.
The host derives `ctx.db.namespace`, runs SQL files in filename order before the
worker starts, records checksums in `plugin_migrations`, and rejects changed
already-applied migrations.
Migration SQL may create or alter objects only inside `ctx.db.namespace`. It may
reference whitelisted `public` core tables for foreign keys or read-only views,
but may not mutate/alter/drop/truncate public tables, create extensions,
triggers, untrusted languages, or runtime multi-statement SQL. Runtime
`ctx.db.query()` is restricted to `SELECT`; runtime `ctx.db.execute()` is
restricted to namespace-local `INSERT`, `UPDATE`, and `DELETE`.
### Scoped plugin API routes
Plugins can expose JSON-only routes under their own namespace:
```ts
apiRoutes: [
{
routeKey: "initialize",
method: "POST",
path: "/issues/:issueId/smoke",
auth: "board-or-agent",
capability: "api.routes.register",
checkoutPolicy: "required-for-agent-in-progress",
companyResolution: { from: "issue", param: "issueId" },
},
]
```
The host resolves the plugin, checks that it is ready, enforces
`api.routes.register`, matches the declared method/path, resolves company access,
and applies checkout policy before dispatching to the worker's `onApiRequest`
handler. The worker receives sanitized headers, route params, query, parsed JSON
body, actor context, and company id. Do not use plugin routes to claim core
paths; they always remain under `/api/plugins/:pluginId/api/*`.
UI:
- `usePluginData`

View File

@@ -28,9 +28,6 @@ Current limitations to keep in mind:
- The repo example plugins under `packages/plugins/examples/` are development conveniences. They work from a source checkout and should not be assumed to exist in a generic published build unless they are explicitly shipped with that build.
- Dynamic plugin install is not yet cloud-ready for horizontally scaled or ephemeral deployments. There is no shared artifact store, install coordination, or cross-node distribution layer yet.
- The current runtime does not yet ship a real host-provided plugin UI component kit, and it does not support plugin asset uploads/reads. Treat those as future-scope ideas in this spec, not current implementation promises.
- Scoped plugin API routes are JSON-only and must be declared in `apiRoutes`.
They mount under `/api/plugins/:pluginId/api/*`; plugins cannot shadow core
API routes.
In practice, that means the current implementation is a good fit for local development and self-hosted persistent deployments, but not yet for multi-instance cloud plugin distribution.
@@ -627,46 +624,7 @@ Required SDK clients:
Plugins that need filesystem, git, terminal, or process operations handle those directly using standard Node APIs or libraries. The host provides project workspace metadata through `ctx.projects` so plugins can resolve workspace paths, but the host does not proxy low-level OS operations.
## 14.1 Issue Orchestration APIs
Trusted orchestration plugins can create and update Paperclip issues through `ctx.issues` instead of importing server internals. The public issue contract includes parent/project/goal links, board or agent assignees, blocker IDs, labels, billing code, request depth, execution workspace inheritance, and plugin origin metadata.
Origin rules:
- Built-in core issues keep built-in origins such as `manual` and `routine_execution`.
- Plugin-managed issues use `plugin:<pluginKey>` or a sub-kind such as `plugin:<pluginKey>:feature`.
- The host derives the default plugin origin from the installed plugin key and rejects attempts to set `plugin:<otherPluginKey>` origins.
- `originId` is plugin-defined and should be stable for idempotent generated work.
Relation and read helpers:
- `ctx.issues.relations.get(issueId, companyId)`
- `ctx.issues.relations.setBlockedBy(issueId, blockerIssueIds, companyId)`
- `ctx.issues.relations.addBlockers(issueId, blockerIssueIds, companyId)`
- `ctx.issues.relations.removeBlockers(issueId, blockerIssueIds, companyId)`
- `ctx.issues.getSubtree(issueId, companyId, options)`
- `ctx.issues.summaries.getOrchestration({ issueId, companyId, includeSubtree, billingCode })`
Governance helpers:
- `ctx.issues.assertCheckoutOwner({ issueId, companyId, actorAgentId, actorRunId })` lets plugin actions preserve agent-run checkout ownership.
- `ctx.issues.requestWakeup(issueId, companyId, options)` requests assignment wakeups through host heartbeat semantics, including terminal-status, blocker, assignee, and budget hard-stop checks.
- `ctx.issues.requestWakeups(issueIds, companyId, options)` applies the same host-owned wakeup semantics to a batch and may use an idempotency key prefix for stable coordinator retries.
Plugin-originated issue, relation, document, comment, and wakeup mutations must write activity entries with `actorType: "plugin"` and details fields for `sourcePluginId`, `sourcePluginKey`, `initiatingActorType`, `initiatingActorId`, and `initiatingRunId` when a user or agent run initiated the plugin work.
Scoped API routes:
- `apiRoutes[]` declares `routeKey`, `method`, plugin-local `path`, `auth`,
`capability`, optional checkout policy, and company resolution.
- The host enforces auth, company access, `api.routes.register`, route matching,
and checkout policy before worker dispatch.
- The worker implements `onApiRequest(input)` and returns a JSON response shape
`{ status?, headers?, body? }`.
- Only safe request headers are forwarded; auth/cookie headers are never passed
to the worker.
## 14.2 Example SDK Shape
## 14.1 Example SDK Shape
```ts
/** Top-level helper for defining a plugin with type checking */
@@ -738,24 +696,16 @@ The host enforces capabilities in the SDK layer and refuses calls outside the gr
- `project.workspaces.read`
- `issues.read`
- `issue.comments.read`
- `issue.documents.read`
- `issue.relations.read`
- `issue.subtree.read`
- `agents.read`
- `goals.read`
- `activity.read`
- `costs.read`
- `issues.orchestration.read`
### Data Write
- `issues.create`
- `issues.update`
- `issue.comments.create`
- `issue.documents.write`
- `issue.relations.write`
- `issues.checkout`
- `issues.wakeup`
- `assets.write`
- `assets.read`
- `activity.log.write`
@@ -822,13 +772,6 @@ Minimum event set:
- `issue.created`
- `issue.updated`
- `issue.comment.created`
- `issue.document.created`
- `issue.document.updated`
- `issue.document.deleted`
- `issue.relations.updated`
- `issue.checked_out`
- `issue.released`
- `issue.assignment_wakeup_requested`
- `agent.created`
- `agent.updated`
- `agent.status_changed`
@@ -838,8 +781,6 @@ Minimum event set:
- `agent.run.cancelled`
- `approval.created`
- `approval.decided`
- `budget.incident.opened`
- `budget.incident.resolved`
- `cost_event.created`
- `activity.logged`
@@ -1297,8 +1238,6 @@ Plugin-originated mutations should write:
- `actor_type = plugin`
- `actor_id = <plugin-id>`
- details include `sourcePluginId` and `sourcePluginKey`
- details include `initiatingActorType`, `initiatingActorId`, and `initiatingRunId` when a user or agent run triggered the plugin work
## 21.5 Plugin Migrations

View File

@@ -114,14 +114,14 @@ If the connection drops, the UI reconnects automatically.
1. Enable timer wakeups (for example every 300s)
2. Keep assignment wakeups on
3. Use a focused prompt template that tells agents to act in the same heartbeat, leave durable progress, and mark blocked work with an owner/action
3. Use a focused prompt template
4. Watch run logs and adjust prompt/config over time
## 7.2 Event-driven loop (less constant polling)
1. Disable timer or set a long interval
2. Keep wake-on-assignment enabled
3. Use child issues, comments, and on-demand wakeups for handoffs instead of loops that poll agents, sessions, or processes
3. Use on-demand wakeups for manual nudges
## 7.3 Safety-first loop

View File

@@ -1,299 +0,0 @@
# Invite Flow State Map
Status: Current implementation map
Date: 2026-04-13
This document maps the current invite creation and acceptance states implemented in:
- `ui/src/pages/CompanyInvites.tsx`
- `ui/src/pages/CompanySettings.tsx`
- `ui/src/pages/InviteLanding.tsx`
- `server/src/routes/access.ts`
- `server/src/lib/join-request-dedupe.ts`
## State Legend
- Invite state: `active`, `revoked`, `accepted`, `expired`
- Join request status: `pending_approval`, `approved`, `rejected`
- Claim secret state for agent joins: `available`, `consumed`, `expired`
- Invite type: `company_join` or `bootstrap_ceo`
- Join type: `human`, `agent`, or `both`
## Entity Lifecycle
```mermaid
flowchart TD
Board[Board user on invite screen]
HumanInvite[Create human company invite]
OpenClawInvite[Generate OpenClaw invite prompt]
Active[Invite state: active]
Revoked[Invite state: revoked]
Expired[Invite state: expired]
Accepted[Invite state: accepted]
BootstrapDone[Bootstrap accepted<br/>no join request]
HumanReuse{Matching human join request<br/>already exists for same user/email?}
HumanPending[Join request<br/>pending_approval]
HumanApproved[Join request<br/>approved]
HumanRejected[Join request<br/>rejected]
AgentPending[Agent join request<br/>pending_approval<br/>+ optional claim secret]
AgentApproved[Agent join request<br/>approved]
AgentRejected[Agent join request<br/>rejected]
ClaimAvailable[Claim secret available]
ClaimConsumed[Claim secret consumed]
ClaimExpired[Claim secret expired]
OpenClawReplay[Special replay path:<br/>accepted invite can be POSTed again<br/>for openclaw_gateway only]
Board --> HumanInvite --> Active
Board --> OpenClawInvite --> Active
Active --> Revoked: revoke
Active --> Expired: expiresAt passes
Active --> BootstrapDone: bootstrap_ceo accept
BootstrapDone --> Accepted
Active --> HumanReuse: human accept
HumanReuse --> HumanPending: reuse existing pending request
HumanReuse --> HumanApproved: reuse existing approved request
HumanReuse --> HumanPending: no reusable request<br/>create new request
HumanPending --> HumanApproved: board approves
HumanPending --> HumanRejected: board rejects
HumanPending --> Accepted
HumanApproved --> Accepted
Active --> AgentPending: agent accept
AgentPending --> Accepted
AgentPending --> AgentApproved: board approves
AgentPending --> AgentRejected: board rejects
AgentApproved --> ClaimAvailable: createdAgentId + claimSecretHash
ClaimAvailable --> ClaimConsumed: POST claim-api-key succeeds
ClaimAvailable --> ClaimExpired: secret expires
Accepted --> OpenClawReplay
OpenClawReplay --> AgentPending
OpenClawReplay --> AgentApproved
```
## Board-Side Screen States
```mermaid
stateDiagram-v2
[*] --> CompanySelection
CompanySelection --> NoCompany: no company selected
CompanySelection --> LoadingHistory: selectedCompanyId present
LoadingHistory --> HistoryError: listInvites failed
LoadingHistory --> Ready: listInvites succeeded
state Ready {
[*] --> EmptyHistory
EmptyHistory --> PopulatedHistory: invites exist
PopulatedHistory --> LoadingMore: View more
LoadingMore --> PopulatedHistory: next page loaded
PopulatedHistory --> RevokePending: Revoke active invite
RevokePending --> PopulatedHistory: revoke succeeded
RevokePending --> PopulatedHistory: revoke failed
EmptyHistory --> CreatePending: Create invite
PopulatedHistory --> CreatePending: Create invite
CreatePending --> LatestInviteVisible: create succeeded
CreatePending --> Ready: create failed
LatestInviteVisible --> CopiedToast: clipboard copy succeeded
LatestInviteVisible --> Ready: navigate away or refresh
}
CompanySelection --> OpenClawPromptReady: Company settings prompt generator
OpenClawPromptReady --> OpenClawPromptPending: Generate OpenClaw Invite Prompt
OpenClawPromptPending --> OpenClawSnippetVisible: prompt generated
OpenClawPromptPending --> OpenClawPromptReady: generation failed
```
## Invite Landing Screen States
```mermaid
stateDiagram-v2
[*] --> TokenGate
TokenGate --> InvalidToken: token missing
TokenGate --> Loading: token present
Loading --> InviteUnavailable: invite fetch failed or invite not returned
Loading --> CheckingAccess: signed-in session and invite.companyId
Loading --> InviteResolved: invite loaded without membership check
Loading --> AcceptedInviteSummary: invite already consumed<br/>but linked join request still exists
CheckingAccess --> RedirectToBoard: current user already belongs to company
CheckingAccess --> InviteResolved: membership check finished and no join-request summary state is active
CheckingAccess --> AcceptedInviteSummary: membership check finished and invite has joinRequestStatus
state InviteResolved {
[*] --> Branch
Branch --> AgentForm: company_join + allowedJoinTypes=agent
Branch --> InlineAuth: authenticated mode + no session + join is not agent-only
Branch --> AcceptReady: bootstrap invite or human-ready session/local_trusted
InlineAuth --> InlineAuth: toggle sign-up/sign-in
InlineAuth --> InlineAuth: auth validation or auth error message
InlineAuth --> RedirectToBoard: auth succeeded and company membership already exists
InlineAuth --> AcceptPending: auth succeeded and invite still needs acceptance
AgentForm --> AcceptPending: submit request
AgentForm --> AgentForm: validation or accept error
AcceptReady --> AcceptPending: Accept invite
AcceptReady --> AcceptReady: accept error
}
AcceptPending --> BootstrapComplete: bootstrapAccepted=true
AcceptPending --> RedirectToBoard: join status=approved
AcceptPending --> PendingApprovalResult: join status=pending_approval
AcceptPending --> RejectedResult: join status=rejected
state AcceptedInviteSummary {
[*] --> SummaryBranch
SummaryBranch --> PendingApprovalReload: joinRequestStatus=pending_approval
SummaryBranch --> OpeningCompany: joinRequestStatus=approved<br/>and human invite user is now a member
SummaryBranch --> RejectedReload: joinRequestStatus=rejected
SummaryBranch --> ConsumedReload: approved agent invite or other consumed state
}
PendingApprovalResult --> PendingApprovalReload: reload after submit
RejectedResult --> RejectedReload: reload after board rejects
RedirectToBoard --> OpeningCompany: brief pre-navigation render when approved membership is detected
OpeningCompany --> RedirectToBoard: navigate to board
```
## Sequence Diagrams
### Human Invite Creation And First Acceptance
```mermaid
sequenceDiagram
autonumber
actor Board as Board user
participant Settings as Company Invites UI
participant API as Access routes
participant Invites as invites table
actor Invitee as Invite recipient
participant Landing as Invite landing UI
participant Auth as Auth session
participant Join as join_requests table
Board->>Settings: Choose role and click Create invite
Settings->>API: POST /api/companies/:companyId/invites
API->>Invites: Insert active invite
API-->>Settings: inviteUrl + metadata
Invitee->>Landing: Open invite URL
Landing->>API: GET /api/invites/:token
API->>Invites: Load active invite
API-->>Landing: Invite summary
alt Authenticated mode and no session
Landing->>Auth: Sign up or sign in
Auth-->>Landing: Session established
end
Landing->>API: POST /api/invites/:token/accept (requestType=human)
API->>Join: Look for reusable human join request
alt Reusable pending or approved request exists
API->>Invites: Mark invite accepted
API-->>Landing: Existing join request status
else No reusable request exists
API->>Invites: Mark invite accepted
API->>Join: Insert pending_approval join request
API-->>Landing: New pending_approval join request
end
```
### Human Approval And Reload Path
```mermaid
sequenceDiagram
autonumber
actor Invitee as Invite recipient
participant Landing as Invite landing UI
participant API as Access routes
participant Join as join_requests table
actor Approver as Company admin
participant Queue as Access queue UI
participant Membership as company_memberships + grants
Invitee->>Landing: Reload consumed invite URL
Landing->>API: GET /api/invites/:token
API->>Join: Load join request by inviteId
API-->>Landing: joinRequestStatus + joinRequestType
alt joinRequestStatus = pending_approval
Landing-->>Invitee: Show waiting-for-approval panel
Approver->>Queue: Review request in Company Settings -> Access
Queue->>API: POST /companies/:companyId/join-requests/:requestId/approve
API->>Membership: Ensure membership and grants
API->>Join: Mark join request approved
Invitee->>Landing: Refresh after approval
Landing->>API: GET /api/invites/:token
API->>Join: Reload approved join request
API-->>Landing: approved status
Landing-->>Invitee: Opening company and redirect
else joinRequestStatus = rejected
Landing-->>Invitee: Show rejected error panel
else joinRequestStatus = approved but membership missing
Landing-->>Invitee: Fall through to consumed/unavailable state
end
```
### Agent Invite Approval, Claim, And Replay
```mermaid
sequenceDiagram
autonumber
actor Board as Board user
participant Settings as Company Settings UI
participant API as Access routes
participant Invites as invites table
actor Gateway as OpenClaw gateway agent
participant Join as join_requests table
actor Approver as Company admin
participant Agents as agents table
participant Keys as agent_api_keys table
Board->>Settings: Generate OpenClaw invite prompt
Settings->>API: POST /api/companies/:companyId/openclaw-invite-prompt
API->>Invites: Insert active agent invite
API-->>Settings: Prompt text + invite token
Gateway->>API: POST /api/invites/:token/accept (agent, openclaw_gateway)
API->>Invites: Mark invite accepted
API->>Join: Insert pending_approval join request + claimSecretHash
API-->>Gateway: requestId + claimSecret + claimApiKeyPath
Approver->>API: POST /companies/:companyId/join-requests/:requestId/approve
API->>Agents: Create agent + membership + grants
API->>Join: Mark request approved and store createdAgentId
Gateway->>API: POST /api/join-requests/:requestId/claim-api-key (claimSecret)
API->>Keys: Create initial API key
API->>Join: Mark claim secret consumed
API-->>Gateway: Plaintext Paperclip API key
opt Replay accepted invite for updated gateway defaults
Gateway->>API: POST /api/invites/:token/accept again
API->>Join: Reuse existing approved or pending request
API->>Agents: Update approved agent adapter config when applicable
API-->>Gateway: Updated join request payload
end
```
## Notes
- `GET /api/invites/:token` treats `revoked` and `expired` invites as unavailable. Accepted invites remain resolvable when they already have a linked join request, and the summary now includes `joinRequestStatus` plus `joinRequestType`.
- Human acceptance consumes the invite immediately and then either creates a new join request or reuses an existing `pending_approval` or `approved` human join request for the same user/email.
- The landing page has two layers of post-accept UI:
- immediate mutation-result UI from `POST /api/invites/:token/accept`
- reload-time summary UI from `GET /api/invites/:token` once the invite has already been consumed
- Reload behavior for accepted company invites is now status-sensitive:
- `pending_approval` re-renders the waiting-for-approval panel
- `rejected` renders the "This join request was not approved." error panel
- `approved` only becomes a success path for human invites after membership is visible to the current session; otherwise the page falls through to the generic consumed/unavailable state
- `GET /api/invites/:token/logo` still rejects accepted invites, so accepted-invite reload states may fall back to the generated company icon even though the summary payload still carries `companyLogoUrl`.
- The only accepted-invite replay path in the current implementation is `POST /api/invites/:token/accept` for `agent` requests with `adapterType=openclaw_gateway`, and only when the existing join request is still `pending_approval` or already `approved`.
- `bootstrap_ceo` invites are one-time and do not create join requests.

View File

@@ -124,14 +124,14 @@ If the connection drops, the UI reconnects automatically.
1. Enable timer wakeups (for example every 300s)
2. Keep assignment wakeups on
3. Use a focused prompt template that tells agents to act in the same heartbeat, leave durable progress, and mark blocked work with an owner/action
3. Use a focused prompt template
4. Watch run logs and adjust prompt/config over time
## 7.2 Event-driven loop (less constant polling)
1. Disable timer or set a long interval
2. Keep wake-on-assignment enabled
3. Use child issues, comments, and on-demand wakeups for handoffs instead of loops that poll agents, sessions, or processes
3. Use on-demand wakeups for manual nudges
## 7.3 Safety-first loop

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