/** * 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 and return `undefined`. Plain values become MCP text content. MCP content blocks are forwarded as-is. `ToolFile` values are rendered by MIME. Emitted output goes to the user, not back to you; the result envelope reports an `emitted` count so you can confirm it landed, but to read a value yourself, `return` it.", '- File-returning tools may return `ToolFile` values: `{ _tag: "ToolFile", name?, mimeType, encoding: "base64", data, byteLength }`. Emit any attachment with `emit(result.data)`.', '- 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 without a public URL. To send that attachment to another API from code, decode `ToolFile.data` from base64 and pass the bytes to that API's upload/file input.", "- 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.", "", "1. `show-artifact({ id })` to get the current source back. Do not rewrite from memory.", "2. Edit that source.", "3. `create-artifact` with the same `artifactId` and the complete edited component.", "", "```", "code: '<the whole edited component>'", 'artifactId: "art_..."', "```", "", "`code` fully replaces what was stored — send the whole component, never a fragment", "or a diff. `title` and `description` are optional on an update: leave them out to", "keep the current ones, pass them to change them. Connection bindings are", "re-resolved from the new code, so a call to a new integration binds on the update", "the same way it would on a create (and pass `connections` if it is ambiguous).", "The artifact keeps its id and its URL, so anyone holding the link sees the new", "version.", "", "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, edit that source and call `create-artifact` with its `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>", " ))}", "</div>", "```", "", "The three states are already in scope — use them instead of inventing a spinner:", "", "```jsx", 'if (query.isLoading) return <ArtifactLoading variant="table" rows={5} />;', "if (query.error) return <ArtifactError error={query.error} onRetry={query.refetch} />;", 'if (!rows.length) return <ArtifactEmpty title="No deployments yet" description="Deployments appear here once a project builds." />;', "",Lines 1–500 of 637. Load more
/** * 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 and return `undefined`. Plain values become MCP text content. MCP content blocks are forwarded as-is. `ToolFile` values are rendered by MIME. Emitted output goes to the user, not back to you; the result envelope reports an `emitted` count so you can confirm it landed, but to read a value yourself, `return` it.", '- File-returning tools may return `ToolFile` values: `{ _tag: "ToolFile", name?, mimeType, encoding: "base64", data, byteLength }`. Emit any attachment with `emit(result.data)`.', '- 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 without a public URL. To send that attachment to another API from code, decode `ToolFile.data` from base64 and pass the bytes to that API's upload/file input.", "- 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.", "", "1. `show-artifact({ id })` to get the current source back. Do not rewrite from memory.", "2. Edit that source.", "3. `create-artifact` with the same `artifactId` and the complete edited component.", "", "```", "code: '<the whole edited component>'", 'artifactId: "art_..."', "```", "", "`code` fully replaces what was stored — send the whole component, never a fragment", "or a diff. `title` and `description` are optional on an update: leave them out to", "keep the current ones, pass them to change them. Connection bindings are", "re-resolved from the new code, so a call to a new integration binds on the update", "the same way it would on a create (and pass `connections` if it is ambiguous).", "The artifact keeps its id and its URL, so anyone holding the link sees the new", "version.", "", "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, edit that source and call `create-artifact` with its `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>", " ))}", "</div>", "```", "", "The three states are already in scope — use them instead of inventing a spinner:", "", "```jsx", 'if (query.isLoading) return <ArtifactLoading variant="table" rows={5} />;', "if (query.error) return <ArtifactError error={query.error} onRetry={query.refetch} />;", 'if (!rows.length) return <ArtifactEmpty title="No deployments yet" description="Deployments appear here once a project builds." />;', "",