/** * 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: "", 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.(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....(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...queryOptions(args))` so the UI stays live.", "4. Use `useMutation(tools...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: '', newText: '' }]", "```", "", "Each `oldText` must match the current source verbatim (whitespace included) and", "exactly once — include surrounding lines to disambiguate, or set", "`replaceAll: true` to change every occurrence. Edits apply in order, each seeing", "the previous one's result, and the batch is atomic: on any miss nothing is saved", "and the error returns the current source in `structuredContent.code`, so rebuild", "the retry from that rather than calling `show-artifact` again. If you don't have", "the source at all (the artifact came from `list-artifacts`), `show-artifact` once", "and edit from what it returns — never from memory.", "", "For a REWRITE, where most of the code changes, edits would be longer than the code:", "call `create-artifact` with the same `artifactId` and the complete new component.", "`code` then fully replaces what was stored — the whole component, never a fragment.", "", "Both paths validate and smoke-render the result identically, keep the artifact's id", "and URL, and re-resolve connection bindings from the resulting code (pass", "`connections` if it is ambiguous). `title` and `description` are optional on both:", "leave them out to keep the current ones.", "", "Only omit `artifactId` when the user genuinely wants a NEW, separate UI.", "", "## Addressing: Integrations, Not Connections", "", "This is the one place artifact code differs from `execute` code, and getting it", "wrong is rejected outright.", "", "`execute` addresses a tool by its full five-segment address, because discovery has", "to say exactly which saved connection it means:", "", "```", "return await tools.linear.org.linearProd.issues.list({ first: 20 })", "```", "", "Artifact code drops the middle two segments and names only the INTEGRATION:", "", "```", "useQuery(tools.linear.issues.list.queryOptions({ first: 20 }))", "```", "", "The connection is bound once, when the artifact is saved, and stored alongside it.", "That keeps the saved artifact portable — it is not welded to one account — and it", "is why code containing a `.user.` or `.org.` segment is refused with an error.", "", "**Which connection gets bound.** If you have exactly one connection for that", "integration, it binds automatically and you pass nothing. If you have several,", "the create fails and lists them; pass `connections` to choose:", "", "```", 'connections: { "linear": "linear.org.linearProd" }', "```", "", "The value is the `..` 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...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...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...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: } }`. 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 ;', ' if (domains.error) return {domains.error.message};', "", " const rows = (domains.data?.pages ?? []).flatMap((page) => page?.data?.domains ?? []);", "", " return (", " ", " Domains ({rows.length})", " ", " ", " ", " {rows.map((domain) => (", " {domain.name}", " ))}", " ", "
", " {domains.hasNextPage && (", " ", " )}", "
", "
", " );", "}", "```", "", "## 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...queryOptions(...)` / `.infiniteQueryOptions(...)` produce keys the invalidation helpers can match.", "- Never call a hook inside a `for` / `while` / `do` body. Hooks run unconditionally at the top level of the component, in the same order every render — a loop makes the hook count vary and React breaks. The server rejects it.", "- Do not redeclare or destructure provided globals. `const { useState } = React` and `const Card = ...` are rejected by the server before the UI reaches the iframe — use them directly.", "- `fetch`, `XMLHttpRequest`, `WebSocket`, `EventSource` and workers are blocked inside the frame. Every read and write goes through `tools`.", "- Give `title` a short human-readable name and `description` enough detail that you can find the artifact again later.", "", "## Retrieving Saved Artifacts", "", "- `list-artifacts` returns every saved artifact's id, title, description and last-updated time.", "- `show-artifact({ id })` re-renders one and returns its source. Match the user's phrasing against the titles and descriptions from `list-artifacts` rather than guessing an id.", '- To change one, patch it with `edit-artifact` (or rewrite it with `create-artifact` + `artifactId`) — see "Changing An Artifact That Already Exists" above.', "- Clients that cannot display MCP apps get a link to the artifact in the web app instead; pass that URL on to the user verbatim.", ].join("\n"); export const CREATE_ARTIFACT_SKILL: Skill = { name: "create-artifact", summary: "How to write a React component for the create-artifact tool: discover data with execute, keep it live with TanStack Query (including cursor pagination), and what is already in scope.", body: CREATE_ARTIFACT_SKILL_BODY, }; // The design system, kept SEPARATE from the capability manifest above. // // They answer different questions — "what can I call" versus "what should it // look like" — and a model that already knows the API should not re-read the // TanStack rules to check a type size. Splitting them also means this document // can be dense with visual specifics without pushing the calling convention out // of the model's attention. // // The content is executor's own design system (`design.md`, serialized from // `packages/react/src/styles/globals.css`), narrowed to what a model writing an // artifact actually decides. Artifacts render INSIDE the console, so the target // is not "a beautiful page" but "indistinguishable from the product around it". const ARTIFACT_STYLE_SKILL_BODY = [ "# artifact-style", "", "How an artifact must look. Artifacts render inside the Executor console, so the", "bar is not *beautiful*, it is *native*: a user should not be able to tell where", "the product ends and your component begins.", "", "**The thesis: hairlines over shadows, grayscale over color, restraint over", "decoration, and mono as the voice of metadata.** Hierarchy comes from tone and", "1px borders, never from hue, weight or depth. When a choice is open, take the", "quieter one — `too cluttered` is the most common thing wrong with generated UI.", "", "## Type", "", "Two families, already set as the defaults: Geist (`font-sans`) for everything", "human, Geist Mono (`font-mono`) for everything machine.", "", "- Headings: `text-lg`/`text-xl` (18-20px) at `font-semibold`, `tracking-tight`. An artifact's own title is the biggest thing on it; nothing goes above `text-2xl`.", "- Body and labels: `text-sm` (14px) at `font-normal`, `font-medium` for a label. `text-xs` (12px) for secondary detail.", "- Never below 11px, and never more than two weights in one view (400 and 500; 600 for a heading).", "- Mono carries metadata: counts, IDs, tool slugs (`github.search_issues`), durations, timestamps, keyboard shortcuts, and index numerals.", "- Section labels are the signature: `font-mono text-[11px] uppercase tracking-[0.08em] text-muted-foreground`. Use them instead of a bold heading over a group.", "- Title Case for buttons, tabs and column headers; sentence case for body, helper text and descriptions.", "- Do not bold mid-sentence, and do not restate a number in prose that a metric already shows.", "", "## Color", "", "**Only tokens. Never a hex literal, never a Tailwind palette class** (`text-gray-500`,", "`bg-blue-50`, `border-slate-200` are all wrong). The tokens invert between light", "and dark; a literal does not, so a hardcoded `#333` is invisible on a near-black", "background — and dark mode is not optional here, the host switches it under you.", "", "The mental test before you ship a color: **if the background were near-black,", "would every text element still be readable?** If you cannot answer, you used a", "literal.", "", "- `bg-background` page, `bg-card` for containers on it, `bg-muted`/`bg-accent` for quiet fills and hover only — never as a page background.", "- `text-foreground` primary, `text-muted-foreground` secondary and metadata. That pair does almost all the ranking work.", "- `border-border` is the hairline. It is how two things are separated.", '- `bg-primary` is a near-black (white in dark) solid fill reserved for the ONE most important action on the view. Every sibling button is `variant="outline"` or `variant="ghost"`.', '- `text-destructive` / `variant="destructive"` is the only hue in the interface, reserved for errors and irreversible actions.', "- There is no brand color and no second accent. If a design seems to need another color, it needs another step on the gray ramp.", "- Status never reads through color alone. Pair it with a word or an icon: `Verified`, `Pending`, `Failed`.", "", "## Restraint", "", "- At most one filled button per view. Prefer `ghost` for anything repeated per row.", "- Dense lists are bordered rows (`divide-y divide-border` inside one bordered container), NOT a grid of rounded cards. A card per list item is the single most common way generated UI looks generic.", "- Do not wrap a single value in a Card just to have a Card. A label and a number is fine.", "- Avoid disabled buttons; hide the action, or explain what would enable it.", "- No icon next to every label. Icons are functional, not decorative.", "", "## Layout and density", "", "- **The shell owns the outer padding.** The frame already applies 24px and a max width. Do not add a page-level `p-6`/`p-8` or your own `max-w-*` wrapper on the root element — it doubles. Pad INSIDE cards, not around the artifact.", "- Spacing is a 4px rhythm: 8px inside a group, 16px between groups, 24-32px between sections. `gap-2` / `gap-4` / `gap-8`.", "- Grid columns use `minmax(0, 1fr)`, never bare `1fr` — bare `1fr` refuses to shrink and a long cell blows the layout out horizontally.", "- At most 4 metric cards per row, and at most 2 columns below ~400px wide. `grid-cols-2 md:grid-cols-4`.", "- Constrained tables use `table-fixed` with explicit column widths and `truncate` on the text cells.", "- Radii are tight and consistent: `rounded-md` (6px) controls, `rounded-lg` (8px) cards. Never mix rounded and square corners in one view.", "", "## Build It Like An App, Not A Document", "", "Your artifact may be given a BOUNDED viewport — on its own page in the console", "it is the whole screen below the title bar, while inside a chat it is a block in", "a scrolling thread. Write one layout that is right in both, and the rule is the", "same either way: **the artifact's own chrome stays put, and only the data", "scrolls.** A user filtering 143 rows should never lose the search box they are", "typing in.", "", "Three parts, and all three are needed — a missing `min-h-0` is the usual reason", "a scroll region silently grows instead of scrolling:", "", "1. The root element is `flex h-full flex-col`.", "2. Headers, toolbars, filters and summary rows are ordinary children — they take their natural height and stay.", "3. The one data region that should scroll is `flex-1 min-h-0 overflow-auto`.", "", "```jsx", "return (", '
', " {/* Stays put: title, search, filters, metrics. */}", '
', '

Projects

', ' setQ(e.target.value)} placeholder="Search projects" className="h-8 max-w-xs" />', "
", "", " {/* Scrolls: the rows, and nothing else. */}", '
', ' ', ' ', ' ', ' ', " ", " ", ' ', " {rows.map((row) => (", ' ', " ))}", " ", "
Name
{row.name}
", "
", "
", ");", "```", "", "- 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", '
', '
Active users
', '
{count.toLocaleString()}
', '
+12 this week
', "
", "```", "", "A dense row list — one border around the group, hairlines between:", "", "```jsx", '
', " {rows.map((row) => (", '
', ' {row.name}', ' {row.id}', "
", " ))}", "
", "```", "", "The three states are already in scope — use them instead of inventing a spinner:", "", "```jsx", 'if (query.isLoading) return ;', "if (query.error) return ;", 'if (!rows.length) return ;', "", '// variant: "list" | "table" | "cards" | "chart" — match the shape of what is loading.', "```", "", "## Charts", "", "**Every chart is wrapped in ``.** It is not optional", "styling — `ChartTooltip`, `ChartTooltipContent`, `ChartLegend` and", "`ChartLegendContent` read a React context that only `ChartContainer` provides, so", "one of them outside it throws `useChart must be used within a `", "on the first render and the user sees an error instead of a chart. The server", "rejects a create that does this.", "", "`config` maps each series key to its label and color, and is what the tooltip and", "legend read to name a series. `ChartContainer` is also the sizing element, so give", 'it the height (`className="h-[240px] w-full"`) rather than wrapping it in a', "`ResponsiveContainer` of your own — it renders one for you.", "", "The whole shape, end to end:", "", "```jsx", "", " Deployments per day", " ", " ", " ", ' ', " ", ' ', " } />", ' ', " ", " ", " ", "", "```", "", "Series colors are an ordered 8-slot palette. Use them **in order**, and let color", 'follow the entity, never its rank — slot 3 means "the third series in this chart",', 'not "the third-largest value". Slot 1 is a neutral slate, so a single-series chart', "stays grayscale like the rest of the product.", "", "`var(--chart-1)` slate · `--chart-2` blue · `--chart-3` clay · `--chart-4` teal ·", "`--chart-5` violet · `--chart-6` terracotta · `--chart-7` moss · `--chart-8` mauve.", "", "Status colors are reserved and never used as series colors: `var(--positive)`,", "`var(--caution)`, `var(--negative)`.", "", "- One y-axis, ever. A second axis invents a correlation the data does not have — use two stacked charts.", "- Lines: `strokeWidth={2}`, `dot={false}`, round line caps. Areas: the same stroke over a ~10% opacity fill.", "- Bars: max ~24px thick, `radius={[4, 4, 0, 0]}` — rounded at the data end, square on the baseline.", '- Gridlines recessive and horizontal only: ``. Never vertical gridlines on a time axis.', '- Axis ticks, labels and legends are ink, never a series color: `tick={{ fill: "var(--muted-foreground)", fontSize: 11 }}`, and `tickLine={false} axisLine={false}`.', "- Sequential data is one hue at varying lightness; diverging data is two hues with a gray midpoint. Never a rainbow.", "- A hero stat above a chart is `text-4xl`/`text-5xl`, `font-medium`, tabular.", "- Size through `ChartContainer`'s `className` (`h-[240px] w-full`); never a fixed pixel width, and never a second `ResponsiveContainer` inside it.", "", "## Numbers", "", "**Round every number that reaches the screen.** `0.1 + 0.2` renders as", "`0.30000000000000004` and instantly reads as broken. Every displayed value goes", "through `toFixed(n)`, `Math.round`, or `Intl.NumberFormat` — percentages to 1", "decimal, currency to 2, counts as integers with thousands separators", "(`value.toLocaleString()`). Format bytes and durations into units rather than", "printing raw. Numbers are already `tabular-nums` by default so columns line up;", "do not override that.", "", "## Voice", "", "- Sentence case for prose. No exclamation marks. No `please`. Never `successfully`.", "- Name actions with a verb and a noun (`Add Integration`, `Revoke Token`), never `Confirm`, `OK` or a bare verb.", "- Errors are what happened plus what to do: `Couldn't reach the integration. Check the server is running, then retry.`", "- Empty states point at the first action rather than announcing absence.", "- In-progress states are a present participle with an ellipsis: `Syncing...`.", "- Use numerals (`3 tools`), and skip superlatives — powerful, seamless, robust, unlock.", "", "## Never", "", "- Gradients, mesh backgrounds, noise, glows, `blur`, `backdrop-blur`, or decorative drop shadows. Depth is a border and a tonal step.", "- Emoji, anywhere. No hand-drawn `` illustrations — use a Lucide icon or nothing.", "- Neon or fully saturated color, and any color outside the tokens above.", "- A colored or tinted background behind the whole artifact.", "- `position: fixed` — you are in a frame the host resizes, and a fixed element detaches and floats over the console. `sticky top-0` inside your own scroll region is fine and is how a table header stays put.", '- A scroll container with a HARDCODED height (`overflow-auto` plus `h-[440px]` or `max-h-96`). Size it with `flex-1 min-h-0` instead, so it fits the room it is actually given — see "Build It Like An App".', "- `100vh` / `100vw` sizing. The viewport those refer to is the frame's, not the window's, so they either overflow or collapse. Use `h-full` against a bounded parent.", "- Rounded corners on an element with a border on only one side.", "- Icons larger than 20px inline (16px is the default, `size-4`); 24px is the ceiling for a genuinely decorative one.", ].join("\n"); export const ARTIFACT_STYLE_SKILL: Skill = { name: "artifact-style", summary: "How an artifact must LOOK to read as native Executor UI: the type scale, the grayscale token system, the ordered chart palette, layout density, number formatting, voice, and the anti-patterns.", body: ARTIFACT_STYLE_SKILL_BODY, }; /** The full skill catalog. Hand-curated; keep it small. */ export const SKILLS: readonly Skill[] = [ EXECUTE_SKILL, CREATE_ARTIFACT_SKILL, ARTIFACT_STYLE_SKILL, ]; /** The skills that only make sense when the session serves artifacts. A * connection that opted out of artifacts (`?artifacts=false`) has no tool * to apply them to, so they are dropped from its catalog rather than served as * guidance for a surface it cannot reach. */ const ARTIFACT_SKILLS: ReadonlySet = new Set([CREATE_ARTIFACT_SKILL, ARTIFACT_STYLE_SKILL]); /** * The catalog a single session sees. Artifacts off (the default) drops the * artifact skills so the index never advertises a doc for tools this connection * does not have; a session that opted in gets the full {@link SKILLS} list. */ export const skillCatalogFor = (options: { readonly artifacts: boolean }): readonly Skill[] => options.artifacts ? SKILLS : SKILLS.filter((skill) => !ARTIFACT_SKILLS.has(skill)); /** Look up a skill by its exact name within a session's catalog. */ export const findSkill = (name: string, catalog: readonly Skill[] = SKILLS): Skill | undefined => catalog.find((skill) => skill.name === name); /** The index the `skills` tool returns when called without a name (or with an * unknown one): every skill's name and one-line summary, plus how to fetch * the body. * * It leads with what the catalog IS. A host without a skill tool of its own * (pi, for one) reads `skills` as the general skill reader it is missing and * asks it for the user's or the harness's skills; the index is where that * model lands, so it has to say plainly that this list is the whole of it and * covers only Executor's own tools. */ export const renderSkillsIndex = (catalog: readonly Skill[] = SKILLS): string => [ "How-to docs for Executor's own tools — this is the complete list, and there is nothing else to fetch.", 'Fetch one with `skills({ name: "" })`. Names outside this list, file paths, and skills belonging to your harness or the user are not served here.', "", ...catalog.map((skill) => `- \`${skill.name}\` — ${skill.summary}`), ].join("\n");