feat(agents): add a frontend component catalog and skill
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.mdunderlib/opal/src/componentsandlib/opal/src/layouts. It takes the# Nameheading, 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/componentsor@opal/layouts, but only when the name is exported from thatindex.ts. - Internal components (for example,
CardFold, which is "Not exported from@opal/components") are left out, so agents do not import internals.
- A README with no
- refresh-components rows: it walks every
.tsxfile (skipping stories and tests). It takes the last JSDoc block aboveexport default functionand 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_REPLACEMENTSmap out ofweb/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. --checkcompares 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:
- Read
CATALOG.md. - List the UI pieces the task needs (page shell, header, rows, cards, buttons, inputs, menus, empty states, loaders and text).
- Match each piece in order: Opal, then refresh-components (never a Replaced row), then
web/src/sections/andweb/src/layouts/(search withrg, because they are feature composites and not in the catalog). - Read the README of each Opal component it uses. Do not guess props.
- Lay pieces out with
Sectionand its props (gap,padding,width,height), never a flex or grid<div>. Section defaults to a centered, full-size column withgap={4}, so set its alignment, size and gap to match the design. This matches the layout rules in #16234. - For design work, write the map from piece to component before writing code.
- 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:
| Agent | Config | Event | How it finds new files |
|---|---|---|---|
| Claude Code | .claude/settings.json | PreToolUse, Write | The target file does not exist yet |
| Cursor | .claude/settings.json (imported by default) | preToolUse, Write | The target file does not exist yet. Feedback goes in agent_message with permission: allow |
| Codex | .codex/hooks.json | PostToolUse, 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
.tsxfile underweb/src/orweb/lib/opal/src/that does not exist yet (a new component), and not a story or test. - It adds context that names
CATALOG.mdand 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
| Moment | Mechanism | PR |
|---|---|---|
| Agent starts UI work | frontend-components skill triggers, and the agent reads CATALOG.md | #16235 |
Agent creates a new .tsx file | Catalog hook points at the catalog (before the write in Claude Code and Cursor, just after it in Codex) | #16235 |
| Agent edits any web file | Migrate hook lints the file and makes the agent migrate replaced refresh imports, raw elements and banned icons | #16234 |
| Commit / CI | oxlint errors block new violations, and the catalog check blocks a stale catalog | both |
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 tellSelectCardfromCardorMessageCardwithout 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 withrg. - 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.mdare 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_messagereaches the model when the hook allows the write.
How to review
.agents/skills/frontend-components/SKILL.md: is the procedure right, and does the description trigger at the right times?.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.generate_catalog.py: the parsing rules, the internal-component filter and the parse of the replacement map..agents/hooks/component_catalog_on_new_file.py, with its entries in.claude/settings.jsonand.codex/hooks.json..pre-commit-config.yaml: the new hook and itsfilesregex.
How Has This Been Tested?
- Generated the catalog and checked it:
- no rows with an empty import;
- markdown links stripped (for example, the
TextButtonrow); 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 toCATALOG.md, it exits 1 with the regenerate message. Then I regenerated the catalog.- Pre-commit:
frontend-component-catalogpasses, 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.tsxreturns theadditionalContextJSON with exit 0; - an existing
web/src/app/craft/components/BuildWelcome.tsxreturns nothing with exit 0.
- a new
- Live runs in the real CLIs:
claude -p(Claude Code 2.1.303): when it created a new.tsxfile, the model quoted the catalog context.codex exec(Codex 0.163.0): when it added a new file withapply_patch, the model quoted the catalog context from the PostToolUse hook.
- Skill discovery: after the commit, Claude Code listed
frontend-componentsin its available skills through the.claude/skills -> ../.agents/skillssymlink.
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.