692 lines · 44.4 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. For live saved-connection inventory, call `tools.executor.coreTools.connections.list({})`; after checking `result.ok`, read `result.data.connections`.",  "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.',  "- When `outputTypeScriptNote` is present, the `data` type was observed from live responses rather than declared by the provider: the listed fields are reliable, but the shape may be incomplete — prefer optional access for anything not listed.",  "- 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 = [  "## Build the 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. Follow the discovery workflow above to inspect tool names, input schemas, response shapes, and pagination before writing the artifact.",  "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",  "",  "Artifact code differs from the full tool IDs used during discovery. Getting it",  "wrong is rejected outright.",  "",  "A tool ID includes its full five-segment address, because discovery has",  "to say exactly which saved connection it means:",  "",  "```",  "tools.linear.org.linearProd.issues.list",  "```",  "",  "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.",  "Use the integration, owner, and connection from discovery. Omit 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" }',  "```",  "",  "For example, discovery identifies the account and its operation:",  "",  "```",  "Integration: linear; owner: org; connection: linearProd",  "Tool ID: tools.linear.org.linearProd.issues.list; input: { 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',  "```",  "",  "Keep tool discovery outside the component. The component uses integration operations.",  "",  "## 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.",  "",  "## Keep Discovered Data Live",  "",  "- Use the discovery workflow above to inspect schemas, test a query, fetch a small sample, or learn mutation inputs.",  "- `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, put that integration operation in TanStack Query options inside the generated component.",  "- For example, inspect a dataset query to confirm columns, then use its integration operation with `.queryOptions(...)` in the component.",  "- 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 schema during discovery 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 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"); const createArtifactSkillBody = (searchAndInvoke: boolean): string =>  [    "# create-artifact",    "",    "## Discover accounts and data",    "",    ...(searchAndInvoke      ? [          "1. Call `integrations({})` to choose an account. Its `integration`, `owner`, and `connection` fields form the artifact binding, joined with dots.",          '2. Call `search({ query: "list issues", integration: "linear", owner: "org", connection: "linearProd" })` using that account. Read the returned inputSchema.',          "3. Call `invoke({ tool: <exact returned id>, arguments: <JSON input> })` to inspect a small sample response. Use search and invoke, not general code execution, to learn the data shape.",        ]      : [          '1. Use `execute` to call `tools.search({ query: "list issues" })`, then `tools.describe.tool({ path })` to inspect the matched tool.',          "2. Use `tools.executor.coreTools.connections.list({})` for saved accounts. The connection address without its leading `tools.` is the artifact binding.",          "3. Call the discovered tool in `execute` to inspect a small sample response.",        ]),    "",    CREATE_ARTIFACT_SKILL_BODY,  ].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: createArtifactSkillBody(false),}; // 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",

Lines 1–500 of 692.