agent-kit

Give a coding agent tradecn's rules, a way to find the item that fits, and a check it can run on the screen it built. The install puts a skill, an instructions file and a review prompt under .github/, where GitHub Copilot loads them on its own, and adds checkContract in lib/agent-kit.ts.

import { cn } from "cn"
import { useEffect, useRef, useState } from "react"
import { Button } from "@/components/ui/button"
import { CONTRACT_RULES, checkContract, type ContractReport } from "@/lib/agent-kit"
import { NUMERIC_CLASS } from "@/lib/format"

export default function AgentKitDemo() {
  const [cue, setCue] = useState(true)
  const [named, setNamed] = useState(true)
  const [report, setReport] = useState<ContractReport | null>(null)
  const row = useRef<HTMLDivElement>(null)

  // Check the row once each change is on screen.
  useEffect(() => {
    const frame = requestAnimationFrame(() => {
      if (row.current) setReport(checkContract({ root: row.current }))
    })
    return () => cancelAnimationFrame(frame)
  }, [cue, named])

  return (
    <div className="flex w-full max-w-md flex-col gap-3 text-sm">
      <div data-demo-controls className="flex flex-wrap gap-4 text-xs">
        <label className="flex items-center gap-1.5">
          <input type="checkbox" checked={cue} onChange={(event) => setCue(event.target.checked)} />
          Direction cue
        </label>
        <label className="flex items-center gap-1.5">
          <input type="checkbox" checked={named} onChange={(event) => setNamed(event.target.checked)} />
          Refresh has a name
        </label>
      </div>
      <div ref={row} className="grid grid-cols-[1fr_auto_auto_auto_auto] items-center gap-x-4 rounded-md border border-border px-3 py-2">
        <span>UST 10Y</span>
        <span data-numeric="" className={NUMERIC_CLASS}>99-16+</span>
        <span data-numeric="" className={NUMERIC_CLASS}>99-17</span>
        <span data-numeric="" data-direction={cue ? "up" : undefined} className={cn(NUMERIC_CLASS, "text-up")}>
          {cue ? "+0-01" : "0-01"}
        </span>
        <Button type="button" variant="ghost" size="icon" aria-label={named ? "Refresh" : undefined}>
          <svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
            <path d="M20 11a8 8 0 1 0-2.3 5.7M20 5v6h-6" />
          </svg>
        </Button>
      </div>
      {report && (
        <div className="flex flex-col gap-1 text-xs">
          <p className={cn("text-muted-foreground", NUMERIC_CLASS)}>Checked {CONTRACT_RULES.map((rule) => `${rule} ${report.checked[rule]}`).join(", ")}</p>
          {report.findings.length ? (
            <ul className="flex flex-col gap-0.5">
              {report.findings.map((finding) => (
                <li key={`${finding.rule} ${finding.where}`}>
                  <span className="font-medium">{finding.rule}</span> {finding.detail}
                </li>
              ))}
            </ul>
          ) : (
            <p>No findings.</p>
          )}
        </div>
      )}
    </div>
  )
}

Installation

pnpm dlx shadcn@latest add tradecn/ui/agent-kit#v2.0.0
npx shadcn@latest add tradecn/ui/agent-kit#v2.0.0
yarn dlx shadcn@latest add tradecn/ui/agent-kit#v2.0.0
bunx --bun shadcn@latest add tradecn/ui/agent-kit#v2.0.0

Usage

The preview checks a quote row in the page. Turn off the direction cue and the check reports a value colored by direction with nothing else saying which way. Take the refresh button's name away and it reports a control with no accessible name.

Run the same check in an end-to-end test, on every screen a change touched:

import { expect, test } from "@playwright/test"
import { checkContract } from "../src/lib/agent-kit"

test("the blotter keeps the contract", async ({ page }) => {
  await page.goto("/")
  const report = await page.evaluate(checkContract, { root: "[data-slot='tradecn-blotter']" })
  expect(report.findings).toEqual([])
  expect(report.checked.numeric).toBeGreaterThan(0)
})

checkContract uses nothing outside its own body, so page.evaluate sends it to the page as it is. Point the import at wherever the CLI put lib/agent-kit.ts.

What the agent reads

Installed at For
.github/skills/tradecn/SKILL.md The skill: find the item first, write the screen down, the ten rules, the check and the review. Copilot reads its description and loads the rest when a task matches.
.github/skills/tradecn/references/items.md Every item at the tag by kind, with what it's for and what it exports.
.github/skills/tradecn/references/review.md The walk to finish UI work with, and the change order to write when someone else makes the fixes.
.github/instructions/tradecn.instructions.md Ten rules Copilot applies to every .tsx file it works on.
.github/prompts/tradecn-review.prompt.md /tradecn-review: the walk, with the findings written as a change order.

The CLI puts these at the project root, where components.json is. Copilot reads them from the repository root, so in a monorepo move .github/ there. They're plain Markdown in the Agent Skills layout, so an agent that reads skills from another folder can use them from there.

API Reference

checkContract

checkContract(options?: ContractOptions): ContractReport checks a rendered subtree and returns what breaks the contract. It throws when root names nothing, so a mistyped selector fails instead of passing.

Option Type Default Description
root ParentNode | string document The subtree, as an element, a shadow root or a selector.
rules readonly ContractRule[] All four The rules to run.
floorPx number --tradecn-text-size-grid-min, else 12 The smallest text size, in px. The install adds the token when it is missing, since the check reads it.
ignore string "[data-contract-ignore]" A selector for subtrees to leave out. An empty string leaves nothing out.
visibleCue boolean false Direction needs a leading sign, an arrow or a word a reader can see.

The report has findings, each with its rule, where (the nearest tradecn slot, then the tag and its data attributes), text and detail. A password reads as ••••, and a file picker is measured without being read, so no finding carries a password or a file's name. It also has checked, how many elements each rule looked at. A rule that looked at nothing shows zero, so a test can tell an empty check from a pass.

The rules

Rule What it checks
floor Drawn text, and fields showing a value or a placeholder, at the floor size or larger. Screen-reader-only, faded-out and transparent text isn't drawn, so it isn't held to it. An empty field's placeholder is measured in its own ::placeholder style, and SVG paint shows only with some opacity, a stroke only with some width. SVG text is measured as drawn, through its viewBox. A select shows its chosen option, or every option in a list box. A file picker draws the chosen file's name in its own style and its button's word in ::file-selector-button's, so both are measured. Rule 14 of the item contract.
numeric Digits under a tradecn slot, fields holding a number there, and every [data-numeric] node, set in lining-nums tabular-nums. Rule 14, read the way the site and the browser matrix read it.
direction A value with a number in it, in text or in a field, drawn in the up or down token or sitting on one of their fills, also carries a leading sign, an arrow, a data-direction of up or down or a data-side of buy, sell, bid, ask, offer, long or short on the value, its colored run, or the cell or row that holds it, or an accessible label or description that states it, on the value, its colored run, or the cell or row that holds it: its own, one it points to, or a native label. A row's aria-labelledby says nothing for the cells it points at inside that row, or for what those cells hold, since a screen reader reads those cells one by one; a label of the row's own that is no cell still counts, as does a target outside the row and any other element's own child that labels it. A label counts only where the element takes a name, never on a plain span or div, which ARIA doesn't let a screen reader announce; a description counts anywhere. The colored run is the value and up to three wrappers around it that share its color, the checked root among them. A grid rule's highlight in the up or down tone is the rule's color rather than a direction, so the description of the rule that paints the value counts for it when the rule's tone is the direction the value is painted in. A hidden sign says nothing, and under visibleCue neither does a screen-reader-only or transparent one. Under aria-hidden, which a screen reader skips, only a cue a sighted reader sees counts: a data-direction or data-side, a label or a description there says nothing. SVG text is drawn in its fill and stroke, so those are what count there. Text with no number in it isn't held to it, and a word that states the direction (up, down, buy, sell, bid, ask and their kin) is a cue of its own, and an element's edge ends a word, so Up and 10 in two spans read as a word and a number. A sign is read as drawn, so 99 and -16 in two spans stay one price, with no sign. Units excuse nothing: 10 lots in the up color needs a cue like any number. Rule 15.
name Every visible control outside an aria-hidden or inert subtree, a disclosure's summary, a tree item and a radio group among them, has an accessible name: aria-labelledby, aria-label, a label, a title, a text field's placeholder, an image input's alt, or the text it shows, where a labelled icon inside names its button. A reference or a label is read the way a screen reader reads it: an image by its alt, a part by its own label, nothing hidden, and an ID only in the element's own tree, so a reference inside a shadow root never reaches the document. A field, a select, a combobox or an editable region takes no name from what it holds, even through a label or a reference that wraps it, unless its aria-labelledby points at itself, a grid or a list box none from its cells, a radio group none from its radios, and hidden text names nothing. A role is read the way a browser reads a list of them, by the first one it knows in any case, so role="switch checkbox" is a switch and a cell or a row is found by its role before its tag.

CONTRACT_RULES lists them in that order.

What it does not do

It reads computed styles, so it runs in a browser or in a DOM that computes them. It doesn't check contrast, which scripts/themes.test.ts holds the themes to, or visible focus, layout and clipping, which the review walks by eye. It doesn't look at shapes without text for direction: a dot, a bar or a line needs a label of its own. A color inside another theme's scope, or mixed with transparency, isn't matched to a direction token, and a fill outside the checked root isn't read, even when the value sits on it. HTML text is measured at its computed size, so a CSS transform that shrinks it goes unseen, and text faded out mid-transition isn't read until it shows. Direction words are read in English, so a value that says its direction in another language wants a sign or data-direction as well. The skill and the review ask for what the check can't see.

The item index

references/items.md is written from registry.json by just agent-kit, and a test fails when a change to the registry leaves it stale.

This page is docs/agent-kit.md at v2.0.0.