655 lines · 42.9 KB · TypeScript
RawDownload
/** * On-demand documentation served through the MCP `skills` tool. * * The long-form how-to for calling integrations used to live in the `execute` * tool description, which every session loads into its prompt up front. That * is pure context bloat: a model that never calls `execute` still pays for the * whole calling-convention essay. Moving it behind a tool lets the `execute` * description carry only the live connection inventory plus a pointer here, and * the model fetches the full guide the moment it actually needs it. * * A skill is a static, named markdown document. The registry is intentionally * tiny and hand-curated, not a plugin surface: add an entry only when a tool * genuinely needs its own how-to behind the same `skills` door. */ export interface Skill {  /** Stable identifier the model passes to the `skills` tool. */  readonly name: string;  /** One-line summary, shown when the `skills` tool lists what is available. */  readonly summary: string;  /** The full markdown body returned when the skill is fetched by name. */  readonly body: string;} // The `execute` how-to. This is the body lifted verbatim out of the old// `buildExecuteDescription` (Workflow + Rules); the description now points// here instead of inlining it.const EXECUTE_SKILL_BODY = [  "# execute",  "",  "Execute TypeScript in a sandboxed runtime with access to configured API tools.",  "",  "## Workflow",  "",  '1. `const { items: matches } = await tools.search({ query: "<intent + key nouns>", limit: 12 });`',  '2. `const path = matches[0]?.path; if (!path) return "No matching tools found.";`',  "3. `const details = await tools.describe.tool({ path });`",  "4. Use `details.inputTypeScript` / `details.outputTypeScript` and `details.typeScriptDefinitions` for compact shapes.",  "5. Use `tools.executor.coreTools.connections.list({})` when you need live saved-connection inventory.",  "6. Call the tool: `const result = await tools.<path>(input);`",  "",  "## Rules",  "",  "- `tools.search()` returns paginated, ranked matches: `{ items, total, hasMore, nextOffset }`. Best-first. Use short intent phrases like `github issues`, `repo details`, or `create calendar event`.",  '- When you already know the namespace, narrow with `tools.search({ namespace: "github", query: "issues" })`.',  "- `tools.executor.coreTools.connections.list({})` returns saved connections with `{ address, integration, owner, name, ... }`. The `address` field includes the leading `tools.` root.",  "- Tool calls return a value union: `{ ok: true, data }` for success or `{ ok: false, error: { code, message, status?, details?, retryable? } }` for expected tool/domain failures. Branch on `result.ok`.",  "- `data` is the upstream payload itself. HTTP-backed tools (OpenAPI) also set `http: { status, headers }` beside `data` — read `result.http?.headers` for pagination (Link) or rate-limit headers.",  "- Use `emit(value)` to append user-visible output. Plain values become MCP text content. MCP content blocks are forwarded as-is. `ToolFile` values are rendered by MIME. Emitting and returning compose: emitted items come first in the tool result, the returned value follows, and the envelope reports an `emitted` count so you can confirm the items landed.",  '- File-returning tools may return `ToolFile` values: `{ _tag: "ToolFile", name?, mimeType, encoding: "base64", data, byteLength }`. A "file-returning tool" includes APIs that return file bytes inside a JSON field, such as Base64-encoded `content`; wrap those payloads in a `ToolFile` and `emit()` them. Emit any attachment with `emit(result.data)`.',  "- Never decode or transcode bytes yourself — the sandbox has no `Buffer`, `atob`, `btoa`, `TextDecoder`, or `TextEncoder`. For base64-encoded bytes in a JSON field, wrap the payload in a `ToolFile` with its `mimeType` and `emit()` it; to forward them to an upload tool, pass the `ToolFile`'s base64 `data` as that tool's `bodyBase64` (both sides speak base64, so nothing is decoded).",  '- To emit MCP-native content directly, pass an MCP content block to `emit(...)`, such as `{ type: "image", data, mimeType }`, `{ type: "audio", data, mimeType }`, `{ type: "text", text }`, `{ type: "resource", resource }`, or `{ type: "resource_link", uri, name, ... }`.',  "- `emit(ToolFile)` is MIME-based: `image/*` becomes MCP image content, `audio/*` becomes MCP audio content, text-like files become decoded text, and other binary files become embedded MCP resources.",  "- `return` is only for ordinary structured data. Returning a `ToolFile`, a `ToolResult`, an MCP content block, or a bare base64 string does not emit content to the MCP client.",  "- Some providers, including Gmail, return attachment bytes as a `ToolFile` with no public URL to hand off — the bytes themselves are the payload, so `emit(result.data)` to display it, or pass its base64 `data` as another tool's `bodyBase64` to forward it.",  "- If `tools.search()` returns `hasMore: true` and you didn't find what you need, fetch the next page: `tools.search({ query, offset: nextOffset, limit })`.",  "- Always use the full address when calling tools: `tools.<integration>.<owner>.<connection>.<tool>(args)`. The `path` returned by `tools.search()` / `tools.describe.tool()` is already the exact path under `tools` — call `tools[path]` rather than guessing segments.",  "- The `tools` object is a lazy proxy — enumerating it (`Object.keys(tools)`, spread, `for...in`) throws. Use `tools.search()` or `tools.executor.coreTools.connections.list({})` instead.",  '- Pass an object to system tools, e.g. `tools.search({ query: "..." })`, `tools.executor.coreTools.connections.list({})`, and `tools.describe.tool({ path })`.',  '- `tools.describe.tool()` returns compact TypeScript shapes. Use `inputTypeScript`, `outputTypeScript`, and `typeScriptDefinitions`. If the path doesn\'t resolve, the result carries `error: { code: "tool_not_found", suggestions }` — use a suggestion instead of retrying the same path.',  "- For tools that return large collections (e.g. `getStates`, `getAll`), filter results in code rather than calling per-item tools.",  "- Do not use `fetch` — all API calls go through `tools.*`.",  "- If execution pauses for interaction, resume it with the returned `resumePayload`.",  "- TypeScript type syntax (`: T`, `as T`, generics, interfaces, type aliases) is stripped before execution — feel free to write idiomatic TypeScript using the shapes from `tools.describe.tool()`. Decorators and `enum` are not supported.",].join("\n"); export const EXECUTE_SKILL: Skill = {  name: "execute",  summary:    "How to call integrations from the execute sandbox: search the catalog, read a tool's shape, call it, emit results, and resume paused runs.",  body: EXECUTE_SKILL_BODY,}; // The `create-artifact` how-to. Same reasoning as `execute`: the discovery-vs-render// protocol, the TanStack rules and the component inventory are a page of prose// that only matters once a model decides to build a UI, so the tool description// stays short and points here.const SHADCN_COMPONENTS =  "Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter, Button, Input, Textarea, Label, Select, SelectTrigger, SelectValue, SelectContent, SelectItem, Checkbox, Switch, Slider, Toggle, Tabs, TabsList, TabsTrigger, TabsContent, Table, TableHeader, TableBody, TableRow, TableHead, TableCell, Badge, Avatar, AvatarFallback, Alert, AlertTitle, AlertDescription, Dialog, Sheet, Popover, Tooltip, Separator, ScrollArea, Skeleton, Progress, Accordion, AccordionItem, AccordionTrigger, AccordionContent, DropdownMenu + sub-components"; const RECHARTS_COMPONENTS =  "BarChart, Bar, LineChart, Line, AreaChart, Area, PieChart, Pie, XAxis, YAxis, CartesianGrid, ResponsiveContainer, Legend, ChartContainer, ChartTooltip, ChartTooltipContent"; const LUCIDE_ICONS =  "Plus, Minus, Check, X, Search, Loader2, AlertCircle, ExternalLink, Copy, Trash2, Edit, Settings, User, Globe, Star, TrendingUp, Activity, Database, Shield, Package, and more"; const CREATE_ARTIFACT_SKILL_BODY = [  "# create-artifact",  "",  "Render an interactive React UI component as an MCP app, and save it as an artifact.",  "",  "This document is the API. For how the result must LOOK — the type scale, the token",  "system, the chart palette, layout density and the anti-patterns — fetch",  '`skills({ name: "artifact-style" })`. Artifacts render inside the Executor console,',  "and an artifact that ignores the design system is visibly not part of the product.",  "",  "Every successful render is persisted under the `title` you supply, so the user can",  "reopen it later and you can find it again with `list-artifacts` / `show-artifact`.",  "Give it a short human-readable name (`Active users dashboard`), and a `description`",  "describing what it shows — that description is what a later request like",  '"show me my active users dashboard" is matched against.',  "",  "## Workflow",  "",  "1. If you need to understand tool names, query syntax, required arguments, response shapes, IDs, mutation inputs, or a list tool's cursor field, first use the regular `execute` tool to inspect them.",  "2. Then call `create-artifact` with a component named `App` in the `code` parameter.",  "3. Recreate every read from the discovery step inside `App` with `useQuery(tools.<integration>.<tool>.queryOptions(args))` so the UI stays live.",  "4. Use `useMutation(tools.<integration>.<tool>.mutationOptions({ onSuccess }))` for user-triggered writes or actions.",  "5. Return only the component code.",  "",  "## Changing An Artifact That Already Exists",  "",  "When the user asks to tweak, fix, extend or restyle a UI you already made, you are",  "UPDATING that artifact, not making another one. A second artifact for a revision",  "leaves the user with two near-identical entries and a stale link to the old one.",  "",  "For a TWEAK — a new column, a fixed label, a restyled section — use `edit-artifact`:",  "exact find-and-replace patches against the stored source, so you send only the",  "changed lines instead of re-emitting the whole component.",  "",  "```",  'artifactId: "art_..."',  "edits: [{ oldText: '<exact current text>', newText: '<replacement>' }]",  "```",  "",  "Each `oldText` must match the current source verbatim (whitespace included) and",  "exactly once — include surrounding lines to disambiguate, or set",  "`replaceAll: true` to change every occurrence. Edits apply in order, each seeing",  "the previous one's result, and the batch is atomic: on any miss nothing is saved",  "and the error returns the current source in `structuredContent.code`, so rebuild",  "the retry from that rather than calling `show-artifact` again. If you don't have",  "the source at all (the artifact came from `list-artifacts`), `show-artifact` once",  "and edit from what it returns — never from memory.",  "",  "For a REWRITE, where most of the code changes, edits would be longer than the code:",  "call `create-artifact` with the same `artifactId` and the complete new component.",  "`code` then fully replaces what was stored — the whole component, never a fragment.",  "",  "Both paths validate and smoke-render the result identically, keep the artifact's id",  "and URL, and re-resolve connection bindings from the resulting code (pass",  "`connections` if it is ambiguous). `title` and `description` are optional on both:",  "leave them out to keep the current ones.",  "",  "Only omit `artifactId` when the user genuinely wants a NEW, separate UI.",  "",  "## Addressing: Integrations, Not Connections",  "",  "This is the one place artifact code differs from `execute` code, and getting it",  "wrong is rejected outright.",  "",  "`execute` addresses a tool by its full five-segment address, because discovery has",  "to say exactly which saved connection it means:",  "",  "```",  "return await tools.linear.org.linearProd.issues.list({ first: 20 })",  "```",  "",  "Artifact code drops the middle two segments and names only the INTEGRATION:",  "",  "```",  "useQuery(tools.linear.issues.list.queryOptions({ first: 20 }))",  "```",  "",  "The connection is bound once, when the artifact is saved, and stored alongside it.",  "That keeps the saved artifact portable — it is not welded to one account — and it",  "is why code containing a `.user.` or `.org.` segment is refused with an error.",  "",  "**Which connection gets bound.** If you have exactly one connection for that",  "integration, it binds automatically and you pass nothing. If you have several,",  "the create fails and lists them; pass `connections` to choose:",  "",  "```",  'connections: { "linear": "linear.org.linearProd" }',  "```",  "",  "The value is the `<integration>.<user|org>.<connection>` triple — exactly the",  "`address` field from `tools.executor.coreTools.connections.list({})`, minus the",  "leading `tools.`. The key is the ROLE, which for a single-account artifact is just",  "the integration slug.",  "",  "**Two accounts of one integration.** Tag each call site with a role of your own",  "choosing — any string, meaningful to a human — and map every role:",  "",  "```",  'const prod = useQuery(tools.linear("prod").issues.list.queryOptions({}));',  'const staging = useQuery(tools.linear("staging").issues.list.queryOptions({}));',  '// connections: { "prod": "linear.org.linearProd", "staging": "linear.user.myLinear" }',  "```",  "",  "Worked example, end to end. Discovery through `execute` shows the full address:",  "",  "```",  "await tools.executor.coreTools.connections.list({})",  '// -> [{ address: "tools.linear.org.linearProd", integration: "linear", ... }]',  "return await tools.linear.org.linearProd.issues.list({ first: 5 })",  "```",  "",  "The artifact you then create says:",  "",  "```",  "code: 'function App(){ const q = useQuery(tools.linear.issues.list.queryOptions({ first: 20 })); ... }'",  'connections: { "linear": "linear.org.linearProd" }   // optional if linearProd is your only linear connection',  "```",  "",  "System tools keep their usual paths and need no binding: `tools.search(...)`,",  "`tools.describe.tool(...)`, `tools.executor.coreTools.*`.",  "",  "## The Contract: tools.* Only",  "",  "Artifact code is purely declarative. Every read and every write goes through the",  "`tools` proxy's TanStack helpers — there is no way to run arbitrary code from an",  "artifact, and no `run(...)` function. Two rules follow, and both are enforced:",  "",  "- **Never hand-roll `useQuery({ queryKey, queryFn })`.** Always pass the proxy's options object: `useQuery(tools.<integration>.<tool>.queryOptions(args))`. A hand-written `queryKey` is invisible to `queryFilter`/`pathFilter`, so mutations silently stop refreshing the UI, and it hides which tool the artifact uses from artifact analysis.",  "- **Never fetch in a loop by hand.** Cursor pagination is declarative — see below.",  "",  "## Using Execute For Discovery",  "",  "- `execute` is for exploration: list datasets, inspect schemas, test a query, fetch one small sample row, or learn the exact mutation input shape.",  "- `create-artifact` is for the final interactive surface. Do not paste discovery results into JSX as literal rows, cards, summaries, metrics, or chart series.",  "- After discovering an API call with `execute`, put the same call in TanStack Query options inside the generated component.",  "- Example discovery: call `execute` with `return await tools.axiom_mcp.querydataset({ ... })` to confirm columns, then call `create-artifact` with `useQuery(tools.axiom_mcp.querydataset.queryOptions({ ... }))`.",  "- Use discovered result shapes exactly. If a sample or schema returns `{ renew, expiresAt }`, read `data?.renew`, not `data?.domain?.renew`.",  "- Keep discovery small. Use limits, narrow time ranges, or schema/list tools when possible.",  "",  "## TanStack Query State",  "",  "- The component is already wrapped in a `QueryClientProvider`; do not create your own.",  "- Use `const queryClient = useQueryClient()` when a mutation changes data shown by a query.",  "- For simple writes, invalidate with `queryClient.invalidateQueries(tools.<integration>.<queryTool>.queryFilter(args))` in `onSuccess` or `onSettled`.",  "- For toggles and switches, pass the new checked value into `mutate`: `onCheckedChange={(checked) => mutation.mutate({ body: { enabled: checked } })}`.",  "- For optimistic UI, use `onMutate` to `cancelQueries`, snapshot `getQueryData`, and `setQueryData`; return the snapshot, restore it in `onError`, and invalidate in `onSettled`.",  "- Tool proxy helpers are TanStack-native: `.queryOptions(args, options)`, `.infiniteQueryOptions(args, options)`, `.mutationOptions(options)`, `.queryKey(args)`, `.queryFilter(args, filters)`, `.infiniteQueryKey(args)`, `.infiniteQueryFilter(args, filters)`, `.pathKey()`, `.pathFilter(filters)`, and `.mutationKey()`.",  "- `.pathFilter()` matches BOTH a tool's plain and infinite queries, so one `queryClient.invalidateQueries(tools.<integration>.<tool>.pathFilter())` after a mutation refreshes every view of that tool.",  "",  "## Paginated Reads",  "",  "When a tool returns one page plus a cursor, use `useInfiniteQuery` with",  "`.infiniteQueryOptions(args, options)`. You supply TanStack's standard",  "`initialPageParam` and `getNextPageParam`; the proxy merges each page param",  "into the tool input for you and mints the query key.",  "",  "- `getNextPageParam(lastPage, allPages)` reads the cursor out of the tool's own response. Return `undefined` (or `null`) when there are no more pages — that is what stops the paging.",  "- `initialPageParam` defaults to `null`, which means the FIRST request carries no cursor at all. Set it only when a tool requires an explicit starting value (e.g. `initialPageParam: 1` for page numbers).",  '- `cursorKey` says where the page param lands in the tool input. It defaults to `"cursor"`. Use a dotted path for nested inputs — `cursorKey: "query.since"` writes `{ query: { since: <pageParam> } }`. Read the tool\'s input shape with `execute` first; do not guess the field name.',  "- Render `data.pages` (an array of tool results, newest page last) and drive further loading from `hasNextPage` / `fetchNextPage` / `isFetchingNextPage`. Do not call `fetchNextPage` in a loop on mount — let the user pull more, or paginate deliberately with a bounded `useEffect`.",  "",  "**Never chain `useQuery` calls in a loop to page through a cursor.** This is",  "the most common way to get it wrong, and the server rejects it:",  "",  "```jsx",  "// DON'T — a hook per page. The hook count changes between renders (a",  "// rules-of-hooks violation), and MAX_PAGES silently truncates the data.",  "function App() {",  "  const pages = [];",  "  let cursor = null;",  "  for (let i = 0; i < MAX_PAGES; i++) {",  "    const page = useQuery(",  "      tools.vercel.getDomains.queryOptions({ limit: 100, since: cursor }),",  "      { enabled: i === 0 || cursor != null },",  "    );",  "    cursor = page.data?.data?.pagination?.next ?? null;",  "    pages.push(page);",  "  }",  "  // …",  "}",  "```",  "",  "One `useInfiniteQuery` replaces the whole loop — a single hook, paging until",  "`getNextPageParam` returns `undefined`, with one query key that invalidation",  "can match. The same domain list, done right (the tool takes `{ limit, since }`",  "and returns `{ domains, pagination: { next } }`):",  "",  "```jsx",  "function App() {",  "  const domains = useInfiniteQuery(",  "    tools.vercel.getDomains.infiniteQueryOptions(",  "      { limit: 100 },",  "      {",  '        cursorKey: "since",',  "        getNextPageParam: (lastPage) => lastPage?.data?.pagination?.next ?? undefined,",  "      },",  "    ),",  "  );",  "",  '  if (domains.isLoading) return <Skeleton className="h-24 w-full" />;',  '  if (domains.error) return <Alert variant="destructive"><AlertDescription>{domains.error.message}</AlertDescription></Alert>;',  "",  "  const rows = (domains.data?.pages ?? []).flatMap((page) => page?.data?.domains ?? []);",  "",  "  return (",  "    <Card>",  "      <CardHeader><CardTitle>Domains ({rows.length})</CardTitle></CardHeader>",  "      <CardContent>",  "        <Table>",  "          <TableBody>",  "            {rows.map((domain) => (",  "              <TableRow key={domain.id}><TableCell>{domain.name}</TableCell></TableRow>",  "            ))}",  "          </TableBody>",  "        </Table>",  "        {domains.hasNextPage && (",  "          <Button onClick={() => domains.fetchNextPage()} disabled={domains.isFetchingNextPage}>",  '            {domains.isFetchingNextPage ? "Loading…" : "Load more"}',  "          </Button>",  "        )}",  "      </CardContent>",  "    </Card>",  "  );",  "}",  "```",  "",  "## What Is Already In Scope",  "",  "**Write no imports.** Everything below is bound before your code runs:",  "",  "- React: `useState`, `useEffect`, `useRef`, `useCallback`, `useMemo`, `useContext`, `createContext`, `Fragment`.",  "- TanStack Query v5: `useQuery`, `useInfiniteQuery`, `useMutation`, `useQueryClient`, `queryOptions`, `infiniteQueryOptions`, `mutationOptions`, `skipToken`.",  "- The tool proxy `tools`. It is the only way to reach an integration; there is no `run()` and no other escape hatch.",  `- shadcn/ui components available by name: ${SHADCN_COMPONENTS}`,  `- Recharts components available by name: ${RECHARTS_COMPONENTS}`,  `- Lucide icons available by name: ${LUCIDE_ICONS}`,  "- The artifact state components `ArtifactLoading`, `ArtifactEmpty` and `ArtifactError` — the house-style loading skeleton, empty state and error. Use them rather than writing your own spinner or error box.",  "- The class-name helper `cn`.",  "",  "## Rules",  "",  "- Use this tool instead of `execute` whenever the output should be an interactive UI.",  "- Export a component named `App`. A top-level `const config = { maxHeight }` caps the frame height where the artifact is embedded in a scrolling page; it is ignored where the artifact has been given the whole viewport.",  '- Lay the artifact out as an APP, not a document: root `flex h-full flex-col`, headers and filters as ordinary children, and the one long list or table as `flex-1 min-h-0 overflow-auto` so it scrolls under a header that stays put. See `skills({ name: "artifact-style" })`.',  "- Do not call API tools first and paste returned data into JSX.",  "- Do not embed tool response rows, API results, summaries, dashboard data, or copied query output as literals. Fetch them with `useQuery` so the UI stays live; only hardcode display constants like labels, tab names, and chart configuration.",  "- Always render the loading and error states from `useQuery` / `useInfiniteQuery` / `useMutation`; do not replace them with hardcoded fallback data. `ArtifactLoading` / `ArtifactEmpty` / `ArtifactError` are in scope for exactly this.",  '- Style with the design system, not by taste: tokens only (`text-muted-foreground`, `border-border`), never a hex literal or a Tailwind palette class, and never a page-level `p-6` — the shell owns the outer padding. See `skills({ name: "artifact-style" })`.',  "- Never write `useQuery({ queryKey: [...], queryFn: ... })` by hand. Only `tools.<integration>.<tool>.queryOptions(...)` / `.infiniteQueryOptions(...)` produce keys the invalidation helpers can match.",  "- Never call a hook inside a `for` / `while` / `do` body. Hooks run unconditionally at the top level of the component, in the same order every render — a loop makes the hook count vary and React breaks. The server rejects it.",  "- Do not redeclare or destructure provided globals. `const { useState } = React` and `const Card = ...` are rejected by the server before the UI reaches the iframe — use them directly.",  "- `fetch`, `XMLHttpRequest`, `WebSocket`, `EventSource` and workers are blocked inside the frame. Every read and write goes through `tools`.",  "- Give `title` a short human-readable name and `description` enough detail that you can find the artifact again later.",  "",  "## Retrieving Saved Artifacts",  "",  "- `list-artifacts` returns every saved artifact's id, title, description and last-updated time.",  "- `show-artifact({ id })` re-renders one and returns its source. Match the user's phrasing against the titles and descriptions from `list-artifacts` rather than guessing an id.",  '- To change one, patch it with `edit-artifact` (or rewrite it with `create-artifact` + `artifactId`) — see "Changing An Artifact That Already Exists" above.',  "- Clients that cannot display MCP apps get a link to the artifact in the web app instead; pass that URL on to the user verbatim.",].join("\n"); export const CREATE_ARTIFACT_SKILL: Skill = {  name: "create-artifact",  summary:    "How to write a React component for the create-artifact tool: discover data with execute, keep it live with TanStack Query (including cursor pagination), and what is already in scope.",  body: CREATE_ARTIFACT_SKILL_BODY,}; // The design system, kept SEPARATE from the capability manifest above.//// They answer different questions — "what can I call" versus "what should it// look like" — and a model that already knows the API should not re-read the// TanStack rules to check a type size. Splitting them also means this document// can be dense with visual specifics without pushing the calling convention out// of the model's attention.//// The content is executor's own design system (`design.md`, serialized from// `packages/react/src/styles/globals.css`), narrowed to what a model writing an// artifact actually decides. Artifacts render INSIDE the console, so the target// is not "a beautiful page" but "indistinguishable from the product around it".const ARTIFACT_STYLE_SKILL_BODY = [  "# artifact-style",  "",  "How an artifact must look. Artifacts render inside the Executor console, so the",  "bar is not *beautiful*, it is *native*: a user should not be able to tell where",  "the product ends and your component begins.",  "",  "**The thesis: hairlines over shadows, grayscale over color, restraint over",  "decoration, and mono as the voice of metadata.** Hierarchy comes from tone and",  "1px borders, never from hue, weight or depth. When a choice is open, take the",  "quieter one — `too cluttered` is the most common thing wrong with generated UI.",  "",  "## Type",  "",  "Two families, already set as the defaults: Geist (`font-sans`) for everything",  "human, Geist Mono (`font-mono`) for everything machine.",  "",  "- Headings: `text-lg`/`text-xl` (18-20px) at `font-semibold`, `tracking-tight`. An artifact's own title is the biggest thing on it; nothing goes above `text-2xl`.",  "- Body and labels: `text-sm` (14px) at `font-normal`, `font-medium` for a label. `text-xs` (12px) for secondary detail.",  "- Never below 11px, and never more than two weights in one view (400 and 500; 600 for a heading).",  "- Mono carries metadata: counts, IDs, tool slugs (`github.search_issues`), durations, timestamps, keyboard shortcuts, and index numerals.",  "- Section labels are the signature: `font-mono text-[11px] uppercase tracking-[0.08em] text-muted-foreground`. Use them instead of a bold heading over a group.",  "- Title Case for buttons, tabs and column headers; sentence case for body, helper text and descriptions.",  "- Do not bold mid-sentence, and do not restate a number in prose that a metric already shows.",  "",  "## Color",  "",  "**Only tokens. Never a hex literal, never a Tailwind palette class** (`text-gray-500`,",  "`bg-blue-50`, `border-slate-200` are all wrong). The tokens invert between light",  "and dark; a literal does not, so a hardcoded `#333` is invisible on a near-black",  "background — and dark mode is not optional here, the host switches it under you.",  "",  "The mental test before you ship a color: **if the background were near-black,",  "would every text element still be readable?** If you cannot answer, you used a",  "literal.",  "",  "- `bg-background` page, `bg-card` for containers on it, `bg-muted`/`bg-accent` for quiet fills and hover only — never as a page background.",  "- `text-foreground` primary, `text-muted-foreground` secondary and metadata. That pair does almost all the ranking work.",  "- `border-border` is the hairline. It is how two things are separated.",  '- `bg-primary` is a near-black (white in dark) solid fill reserved for the ONE most important action on the view. Every sibling button is `variant="outline"` or `variant="ghost"`.',  '- `text-destructive` / `variant="destructive"` is the only hue in the interface, reserved for errors and irreversible actions.',  "- There is no brand color and no second accent. If a design seems to need another color, it needs another step on the gray ramp.",  "- Status never reads through color alone. Pair it with a word or an icon: `Verified`, `Pending`, `Failed`.",  "",  "## Restraint",  "",  "- At most one filled button per view. Prefer `ghost` for anything repeated per row.",  "- Dense lists are bordered rows (`divide-y divide-border` inside one bordered container), NOT a grid of rounded cards. A card per list item is the single most common way generated UI looks generic.",  "- Do not wrap a single value in a Card just to have a Card. A label and a number is fine.",  "- Avoid disabled buttons; hide the action, or explain what would enable it.",  "- No icon next to every label. Icons are functional, not decorative.",  "",  "## Layout and density",  "",  "- **The shell owns the outer padding.** The frame already applies 24px and a max width. Do not add a page-level `p-6`/`p-8` or your own `max-w-*` wrapper on the root element — it doubles. Pad INSIDE cards, not around the artifact.",  "- Spacing is a 4px rhythm: 8px inside a group, 16px between groups, 24-32px between sections. `gap-2` / `gap-4` / `gap-8`.",  "- Grid columns use `minmax(0, 1fr)`, never bare `1fr` — bare `1fr` refuses to shrink and a long cell blows the layout out horizontally.",  "- At most 4 metric cards per row, and at most 2 columns below ~400px wide. `grid-cols-2 md:grid-cols-4`.",  "- Constrained tables use `table-fixed` with explicit column widths and `truncate` on the text cells.",  "- Radii are tight and consistent: `rounded-md` (6px) controls, `rounded-lg` (8px) cards. Never mix rounded and square corners in one view.",  "",  "## Build It Like An App, Not A Document",  "",  "Your artifact may be given a BOUNDED viewport — on its own page in the console",  "it is the whole screen below the title bar, while inside a chat it is a block in",  "a scrolling thread. Write one layout that is right in both, and the rule is the",  "same either way: **the artifact's own chrome stays put, and only the data",  "scrolls.** A user filtering 143 rows should never lose the search box they are",  "typing in.",  "",  "Three parts, and all three are needed — a missing `min-h-0` is the usual reason",  "a scroll region silently grows instead of scrolling:",  "",  "1. The root element is `flex h-full flex-col`.",  "2. Headers, toolbars, filters and summary rows are ordinary children — they take their natural height and stay.",  "3. The one data region that should scroll is `flex-1 min-h-0 overflow-auto`.",  "",  "```jsx",  "return (",  '  <div className="flex h-full flex-col gap-4">',  "    {/* Stays put: title, search, filters, metrics. */}",  '    <div className="flex shrink-0 items-center justify-between gap-3">',  '      <h2 className="text-lg font-semibold tracking-tight">Projects</h2>',  '      <Input value={q} onChange={(e) => setQ(e.target.value)} placeholder="Search projects" className="h-8 max-w-xs" />',  "    </div>",  "",  "    {/* Scrolls: the rows, and nothing else. */}",  '    <div className="min-h-0 flex-1 overflow-auto rounded-lg border border-border">',  '      <table className="w-full table-fixed">',  '        <thead className="sticky top-0 z-10 bg-background">',  '          <tr className="border-b border-border">',  '            <th className="px-4 py-2 text-left text-xs font-medium">Name</th>',  "          </tr>",  "        </thead>",  '        <tbody className="divide-y divide-border">',  "          {rows.map((row) => (",  '            <tr key={row.id}><td className="truncate px-4 py-2 text-sm">{row.name}</td></tr>',  "          ))}",  "        </tbody>",  "      </table>",  "    </div>",  "  </div>",  ");",  "```",  "",  "- A table header inside the scroll region is `sticky top-0` with a background — without the background the rows show through it. This is the one sanctioned use of `sticky`; do not use `fixed`, which escapes the frame.",  "- **Never rely on the page scrolling for you.** Anything you want visible while the user reads a long list has to live outside the scrolling child.",  "- Only ONE region scrolls. Two scroll boxes on a screen means the wheel does something different depending on where the pointer is.",  "- Do not give a scroll region a fixed height (`h-[440px]`, `max-h-96`) or a `ScrollArea` sized in pixels. A fixed height is a guess about a viewport you cannot see: too short and it wastes half the screen, too tall and it clips. `flex-1 min-h-0` fills whatever room there actually is.",  "- `h-full` only works if every ancestor is height-bounded, which is why the root must be `flex h-full flex-col` and each intermediate wrapper `flex-1 min-h-0`. A wrapper that forgets `min-h-0` has a content-height floor and the scroll region grows past its parent instead of scrolling.",  "- Short artifacts need none of this. A handful of metric cards can be plain flow; reach for the pattern when a list, table or log could outgrow the screen.",  "",  "## Recipes",  "",  "A metric — mono uppercase label above, large tabular number below:",  "",  "```jsx",  '<div className="rounded-lg border border-border p-4">',  '  <div className="font-mono text-[11px] uppercase tracking-[0.08em] text-muted-foreground">Active users</div>',  '  <div className="mt-2 text-2xl font-medium tabular-nums">{count.toLocaleString()}</div>',  '  <div className="mt-1 text-xs text-muted-foreground">+12 this week</div>',  "</div>",  "```",  "",  "A dense row list — one border around the group, hairlines between:",  "",  "```jsx",  '<div className="divide-y divide-border rounded-lg border border-border">',  "  {rows.map((row) => (",  '    <div key={row.id} className="flex items-center justify-between px-4 py-3">',  '      <span className="truncate text-sm">{row.name}</span>',  '      <span className="font-mono text-xs text-muted-foreground">{row.id}</span>',  "    </div>",

Lines 1–500 of 655.