feat(agents): add a frontend component catalog and skill

Closed
RO
3d ago
+503-1

Description

Stacked on #16234. Review that PR first. This PR reads the replacement map from the lint rule that #16234 adds.

This is the second of two PRs that make coding agents reuse Opal and refresh-components, not design UI from scratch. #16234 enforces the rules. This PR fixes the root cause: agents do not know which components exist.

Problem

web/AGENTS.md says "use Opal first, then refresh-components". But an agent that does not know that Opal has Fold, ProgressBar, TagList, IllustrationContent or LineItemButton cannot find a match, so it builds its own. Lint (#16234) catches the obvious cases: raw <button>, lucide-react, replaced refresh imports. Most duplicated UI, though, is valid JSX that lint cannot see: a hand-built card, chip, row or empty state made from divs and token classes.

Finding components today takes a lot of exploration:

  • There are about 60 Opal components and 15 Opal layouts, each with a README in its own folder.
  • There are about 50 refresh-components, some documented with JSDoc and some not.
  • No single place lists them, and agents rarely browse all the READMEs before they start.

There was also a personal skill (writing-frontend-style, in one developer's ~/.claude) with a vague trigger ("Use when writing frontend code"). It reached only that developer's Claude Code, not Codex, Cursor or teammates.

What this PR changes

1. .agents/skills/frontend-components/CATALOG.md (generated)

The catalog is one table per source:

  • Opal components (@opal/components): name, import, one-line purpose and README path.
  • Opal layouts (@opal/layouts): the same columns.
  • refresh-components: name, full module path and one-line purpose. Rows that Opal replaces say "Replaced: use X from @opal/components." in place of the purpose.

It has 138 lines in total, small enough for an agent to read in one call before it starts.

2. .agents/skills/frontend-components/generate_catalog.py

It uses only the Python standard library, with no dependencies.

  • Opal rows: it walks every README.md under lib/opal/src/components and lib/opal/src/layouts. It takes the # Name heading, the **Import:** line and the first sentence of the first paragraph, with markdown links stripped.
    • A README with no **Import:** line falls back to @opal/components or @opal/layouts, but only when the name is exported from that index.ts.
    • Internal components (for example, CardFold, which is "Not exported from @opal/components") are left out, so agents do not import internals.
  • refresh-components rows: it walks every .tsx file (skipping stories and tests). It takes the last JSDoc block above export default function and uses its first sentence, without a leading "X Component" line or @tags. Files with no doc comment say "No doc comment. Read the file." (28 today), so the gap is visible and does not look like an empty cell.
  • Replaced rows: it parses the OPAL_REPLACEMENTS map out of web/tools/oxlint/opal/rules/no-replaced-refresh-import.ts. There is one source of truth: when a row is added to the lint rule, the catalog marks that component Replaced on the next run.
  • --check compares the rendered output with the file on disk. It exits 1 with the regenerate command when they differ.

3. Pre-commit hook frontend-component-catalog

It runs generate_catalog.py --check, next to feature-map-integrity, and fires only when one of these paths changes:

  • web/lib/opal/src/components/
  • web/lib/opal/src/layouts/
  • web/src/refresh-components/
  • web/tools/oxlint/opal/
  • .agents/skills/frontend-components/

This keeps the catalog current without slowing down unrelated commits.

4. Skill .agents/skills/frontend-components/SKILL.md

It lives in .agents/skills/, so the existing .claude/skills, .codex/skills and .cursor/skills symlinks give it to every agent. The description triggers on designing, building or changing any page, view, modal, form, card, list or empty state in web/, and on creating a new .tsx component. The procedure:

  1. Read CATALOG.md.
  2. List the UI pieces the task needs (page shell, header, rows, cards, buttons, inputs, menus, empty states, loaders and text).
  3. Match each piece in order: Opal, then refresh-components (never a Replaced row), then web/src/sections/ and web/src/layouts/ (search with rg, because they are feature composites and not in the catalog).
  4. Read the README of each Opal component it uses. Do not guess props.
  5. Lay pieces out with Section and its props (gap, padding, width, height), never a flex or grid <div>. Section defaults to a centered, full-size column with gap={4}, so set its alignment, size and gap to match the design. This matches the layout rules in #16234.
  6. For design work, write the map from piece to component before writing code.
  7. Build new UI only when nothing matches, say why, and put a reusable component in Opal, not in the feature folder.

It also covers migrate-on-touch (with a link to the #16234 rule and hook) and how to regenerate the catalog.

5. Agent hook .agents/hooks/component_catalog_on_new_file.py

The hook uses the shared agent_hook_io module from #16234, so it works in every agent:

AgentConfigEventHow it finds new files
Claude Code.claude/settings.jsonPreToolUse, WriteThe target file does not exist yet
Cursor.claude/settings.json (imported by default)preToolUse, WriteThe target file does not exist yet. Feedback goes in agent_message with permission: allow
Codex.codex/hooks.jsonPostToolUse, apply_patch*** Add File: lines in the patch

In my live test, Codex 0.163.0 ran the PreToolUse hook and got valid JSON back, but it did not show the additionalContext to the model. Codex does show PostToolUse context to the model. So Codex runs this hook just after the patch, and the agent can rework the new file at once.

What the hook does:

  • It fires only when the target is a .tsx file under web/src/ or web/lib/opal/src/ that does not exist yet (a new component), and not a story or test.
  • It adds context that names CATALOG.md and tells the agent to use an existing component, or to say why none fits.
  • It never blocks or denies the write. This is the moment an agent is about to design something new, so a reminder here costs nothing and is well timed.

6. web/AGENTS.md

"Where components come from" now links the catalog and the skill. Every agent that reads the web standards gets a pointer to them.

How the two PRs fit together

MomentMechanismPR
Agent starts UI workfrontend-components skill triggers, and the agent reads CATALOG.md#16235
Agent creates a new .tsx fileCatalog hook points at the catalog (before the write in Claude Code and Cursor, just after it in Codex)#16235
Agent edits any web fileMigrate hook lints the file and makes the agent migrate replaced refresh imports, raw elements and banned icons#16234
Commit / CIoxlint errors block new violations, and the catalog check blocks a stale catalogboth

Options I rejected

  • A hand-written catalog: it would go stale in weeks. Generating it from the READMEs that Opal already requires costs nothing to maintain.
  • Pointing agents at lib/opal/src/components/index.ts: it lists names but not purposes, so an agent still cannot tell SelectCard from Card or MessageCard without opening each README.
  • Adding web/src/sections/ to the catalog: those are feature composites with many single-use files. Listing them would double the catalog and make it noisier. The skill tells agents to search them with rg.
  • A blocking PreToolUse hook (deny the first Write of a new file): it is more forceful, but it needs per-session state and annoys legitimate new files. Added context is enough when the skill and the lint also apply.

Risks and trade-offs

  • Catalog quality depends on the docs. A weak README first sentence makes a weak catalog row. 28 refresh-components have no doc comment, and the catalog says so.
  • The skill triggers on its description. If agents still skip it, the catalog hook and web/AGENTS.md are the backstops.
  • Cursor was not tested live, because the Cursor CLI is not installed on my machine. I tested the payload and output shapes with sample stdin. Cursor's docs do not confirm that agent_message reaches the model when the hook allows the write.

How to review

  1. .agents/skills/frontend-components/SKILL.md: is the procedure right, and does the description trigger at the right times?
  2. .agents/skills/frontend-components/CATALOG.md: skim it for wrong or misleading rows. It is generated, so fix a bad row in the source README or JSDoc, not here.
  3. generate_catalog.py: the parsing rules, the internal-component filter and the parse of the replacement map.
  4. .agents/hooks/component_catalog_on_new_file.py, with its entries in .claude/settings.json and .codex/hooks.json.
  5. .pre-commit-config.yaml: the new hook and its files regex.

How Has This Been Tested?

  • Generated the catalog and checked it:
    • no rows with an empty import;
    • markdown links stripped (for example, the TextButton row);
    • CardFold (internal) left out;
    • all 6 replacement rows marked Replaced with the same wording as the lint rule.
  • --check: exits 0 on a fresh catalog. After I appended a line to CATALOG.md, it exits 1 with the regenerate message. Then I regenerated the catalog.
  • Pre-commit: frontend-component-catalog passes, and the full pre-commit run passes on all changed files (ruff, ruff format, oxfmt, oxlint, feature-map integrity, typescript-check).
  • Hook, with sample stdin:
    • a new web/src/sections/NewThing.tsx returns the additionalContext JSON with exit 0;
    • an existing web/src/app/craft/components/BuildWelcome.tsx returns nothing with exit 0.
  • Live runs in the real CLIs:
    • claude -p (Claude Code 2.1.303): when it created a new .tsx file, the model quoted the catalog context.
    • codex exec (Codex 0.163.0): when it added a new file with apply_patch, the model quoted the catalog context from the PostToolUse hook.
  • Skill discovery: after the commit, Claude Code listed frontend-components in its available skills through the .claude/skills -> ../.agents/skills symlink.

Additional Options

  • [Optional] Please cherry-pick this PR to the latest release version.

Summary by CodeRabbit

  • Documentation
    • Added a frontend component catalog with descriptions, import guidance, and links to component documentation.
    • Updated frontend development guidance to prioritize Opal components and layouts, identify refresh components that have been replaced, and explain when to document new component choices.
  • Developer Experience
    • New web components receive guidance to consult the catalog. Automated checks flag catalog entries that are missing or out of date.