Skip to main content

intentbar-setup

Installs and wires up intentbar in this codebase — there is no npm package, so it copies the resolver in from the source that ships with this skill, adds a ready shadcn/ui command palette when the app has none, reads the app's routes, writes a deliberately small sitemap with real descriptions, fills each page's intents from the app's own navigation labels and translation files so common phrases resolve instantly and free, mounts the server handler, and connects the hook to the existing command palette. Use when the user asks to set up, install, configure or fix intentbar, or wants natural-language / AI navigation in their command palette.

Informações da origem

Repositório
enesozturk/intentbar
Última atividade na origem
17 de outubro de 2026 às 22:51
Idioma detectado do SKILL.md
inglês
Estrelas
0
Forks
0

Opções de instalação

Por padrão, está selecionado o prompt que primeiro revisa a origem. Você pode mudar para um comando direto ou baixar uma cópia local.

Revise os arquivos de origem

Leia o SKILL.md e os arquivos complementares exibidos pelo SkillsMP antes de decidir se vai instalar.

Exibindo SKILL.md

SKILL.md
Instruções da origem · Visualização somente leitura
name
intentbar-setup
description
Installs and wires up intentbar in this codebase — there is no npm package, so it copies the resolver in from the source that ships with this skill, adds a ready shadcn/ui command palette when the app has none, reads the app's routes, writes a deliberately small sitemap with real descriptions, fills each page's intents from the app's own navigation labels and translation files so common phrases resolve instantly and free, mounts the server handler, and connects the hook to the existing command palette. Use when the user asks to set up, install, configure or fix intentbar, or wants natural-language / AI navigation in their command palette.
# Installing intentbar Your job is to leave the user with a working palette row, not with a config file they still have to fill in. The package ships a resolver; **the sitemap is the product**, and writing it is the part only someone who can read this codebase can do. Work in this order. Do not skip step 1 — a sitemap written from route paths alone performs badly, and that is the single most common reason this feature feels broken. What this skill sets up is a default stack, not a requirement: shadcn/ui's `command` for the palette, a React hook on the client, one route handler on the server, and `fetch` to the Decisions API for the model call. Where the project already does one of these differently — another palette, the AI SDK or a gateway for model calls, a framework that is not React — follow the project and say what you changed. Installing a package the project would have reached for anyway is fine; only the resolver's shape is fixed, and the model call is one file behind the `provider` option. Two things in step 2 are **defaults, not options**: keeping the sitemap small (2a) and filling the intents list from the app's own vocabulary (2b). They are the only two levers on what this costs and how fast it feels, so do both without being asked, and report the measured result in step 7. ## 0. Copy the code in intentbar is not published to npm. The code travels with this skill: the directory holding this `SKILL.md` also holds `src/`, `templates/` and `docs/`. Nothing is cloned, downloaded or installed — you copy the source in, the way you would any file in this codebase: ```bash cp -R <skill-dir>/src <project>/src/lib/intentbar ``` The files import each other relatively and extensionlessly and need no build step, so a plain copy compiles as it lands. Choose the destination from the project's own layout — `src/lib/`, `lib/`, `app/lib/`, a `packages/*` workspace — and note the alias that resolves there (`tsconfig.json` `paths`, or `components.json` `aliases`). Everything after this imports from `<alias>`, `<alias>/server` and `<alias>/react`; the examples below write `@/lib/intentbar`, so substitute yours. The copied files are the project's from that moment. If its conventions differ from what you copied — import ordering, a lint rule, a `.d.ts` style — fix them there rather than leaving code that stands out as foreign. To take a newer version later, the user runs `npx skills update intentbar-setup` and you copy again; the diff shows up in their own version control. ## 1. Learn the app before writing anything Find the routes and understand what each page is *for*: - **Next.js App Router**: `app/**/page.tsx`. Dynamic segments (`[id]`) are not destinations on their own; a detail page is only worth listing if it can be reached without an id. - **Next.js Pages Router**: `pages/**/*.tsx`, minus `_app`, `_document`, `api/`. - **React Router / TanStack Router**: the route definition object or the `<Route>` tree. Follow `lazy`/`element` imports to the component. - **Anything else**: ask. Guessing the router produces a sitemap of paths that do not exist. For each candidate, open the component (or its heading, empty state and primary button) and answer: what does a person come here to DO? That sentence is the `description`. Do not paraphrase the path. **Only read-only destinations belong in the sitemap.** No API routes, no create/delete flows, no checkout, no logout, no anything that acts when opened. The palette navigates on Enter, sometimes on a near-miss, so a destructive destination is a footgun. Leave auth pages out too. ### Collect the app's own vocabulary while you are in there You need the words users type, and this codebase already contains most of them. Gather, as you go: - **Translation files** — `locales/`, `messages/`, `*.json` under an `i18n` directory, `next-intl` / `react-i18next` resources. This is the single richest source: a localized app already states every nav label, page title and menu item **in every language it ships**. Mine the keys that correspond to the pages you are listing. - **The navigation itself** — sidebar, menu, tab and breadcrumb labels. These are what the user has been reading, so they are what they type. - **Page headings, empty states and primary buttons** — "You have no invoices yet", "Invite teammate" give you the nouns and verbs of the page. - **Help, FAQ or docs content** in the repo, if any. Those headings are written in user language by definition. Note which languages the product ships in. You will write intents in all of them, not only English. ## 2. Write the sitemap Create `intentbar.sitemap.ts` (or wherever the project keeps constants): ```ts import { defineSitemap } from "@/lib/intentbar"; export const sitemap = defineSitemap([ { id: "settings-billing", path: "/settings/billing", breadcrumb: ["Settings", "Billing"], description: "Invoices, the current plan, and the payment method on file.", intents: ["billing", "invoices", "change my plan", "payment method"], }, ]); ``` Rules that decide whether this works: - `description` says what the user DOES there, in their words, not the component's name. "Invite teammates and change their roles" beats "Members table". - `breadcrumb` is what the palette row shows. Match the app's own navigation labels, so the row looks like somewhere the user has been. - Give adjacent pages *distinct* descriptions. "Orders" and "Order reports" that read alike will split probability and abstain — the most confusing failure mode, because both answers were nearly right. The two rules below are not polish. They are the only two levers on what this feature costs and how fast it feels, so do both by default — do not ask whether to, and do not leave them for later. ### 2a. Keep the sitemap small — a budget, not a preference Every destination — id, breadcrumb, description and all its intents — is sent on every lookup, so **the sitemap IS the request**. Measured against the live API: | sitemap | input tokens | per 1,000 lookups | | --- | --- | --- | | 7 lean destinations | 388 | $0.04 | | 15 lean | 682 | $0.07 | | 25 lean | 1,048 | $0.11 | | 25 real ones, intents in two languages, both optional questions | 1,793 | $0.18 | | 50 lean | 1,957 | $0.20 | About 130 tokens of framing plus 37–51 per destination — the spread is how long your descriptions and intents are. And there is **no caching discount**: identical repeated requests still bill full input, so this is a per-lookup cost forever, not a warm-up. **Target 15–30 destinations. Treat 40 as the ceiling** and say so to the user if you are pushing against it. Size is not only money: a long list makes every answer less confident, because near-duplicates split probability and the resolver abstains. To choose, rank candidate pages by *how often someone would ask for them* and cut from the bottom: - Keep: the pages in the nav, the ones support links to, the ones with their own menu entry. - Cut: anything reachable in one click from a page you kept and rarely asked for by name — sub-tabs, per-item detail views, admin-only internals, duplicate report variants of a page already listed. - Cut: pages whose description you cannot write without repeating another entry's. Two pages that need the same sentence should be one entry pointing at the better landing place. Report the final count and the resulting per-1,000-lookup cost when you hand over. ### 2b. Enrich `intents` — this is the free path An exact match against `intents` **skips the model entirely**: 0 ms, $0, no network. So the intents list is a cache you write by hand, and filling it is what makes the common phrases instant. **It is not free, though, and do not tell the user it is.** Those phrases ride in every request that *does* reach the model, at roughly 4 tokens each. On a 25-destination sitemap, adding 6 intents per page costs ~600 tokens per lookup (+34%), which pays for itself only once it converts enough traffic to the free path: | intents added per destination | lookup grows by | breaks even at an exact-hit rate of | | --- | --- | --- | | 2 | +200 tokens | 10% | | 4 | +400 | 18% | | 6 | +600 | 25% | | 10 | +1,000 | 36% | Nav labels and page titles alone usually clear that bar, which is why they go in first. But the real argument is latency, and it holds either way: an exact hit is **0 ms with no network**, against 200–1000 ms for a model call. Enrich for the speed; expect the money to be roughly a wash. So: aim for **6–12 intents per destination**, ordered by how likely they are to be typed, built from the vocabulary you collected in step 1 rather than invented. Padding the list past that spends tokens on phrasings nobody types. 1. **The nav label and page title**, exactly as the app shows them, in every shipped language. From the translation files — these are free wins, because the user is typing what they just read in the sidebar. 2. **The thing itself, as a bare noun**: "invoices", "orders", "members". 3. **The task, as the user would say it**: "change my plan", "invite someone", "refund an order", "reset a password". 4. **The obvious synonym** your UI does *not* use: if the page is "Members", users type "team", "colleagues", "users". 5. **Abbreviations and the words support uses** on the phone: "2FA", "MFA", "SSO", "shipping". Two things that decide whether these actually hit: - **Matching is whole-phrase and case-insensitive, not stemmed.** "invoice" does not match "invoices", so write both: `["invoice", "invoices"]`. In a heavily inflected language (Turkish, Finnish, Hungarian) one noun can take a dozen forms — write the two or three users actually type, in every language the app ships. - **Do not add misspellings.** The model handles typos — measured, "dashbard" resolves at 1.00 — so a list of typos is noise that costs tokens on every lookup. Keep each intent a short phrase. A whole sentence will never be typed verbatim, so it buys nothing in the exact path and only enlarges the request. If the project has search or analytics logs for its own palette or help centre (a `search_queries` table, a PostHog/Amplitude event, support macros), read the top queries and put the real ones in. Ask the user whether such a source exists rather than assuming it does not — it is the difference between guessed vocabulary and theirs. ## 3. Mount the handler The API key must stay on the server, so there is exactly one endpoint to add. Match the project's framework: ```ts // Next.js App Router — app/api/intentbar/route.ts import { createHandler } from "@/lib/intentbar/server"; import { sitemap } from "@/intentbar.sitemap"; export const POST = createHandler({ sitemap, appContext: "<one sentence: what this app is, so the model knows what it is routing inside>", // Required in any app with a login. Without it this endpoint is open to // anyone who can reach it, and it spends the API budget. authorize: async (request) => Boolean(await getSession(request)), }); ``` Use the project's existing auth helper — find it, do not invent one. If the codebase wraps route handlers (error capture, rate limiting, logging), wrap this one the same way; follow the convention already in the repo. That is the whole endpoint for most apps. **If, and only if, the URLs carry a workspace, team, org or project**, intentbar can also resolve "acme billing" to Billing inside Acme; that is one extra option on this same handler, documented in `docs/multi-tenant.md` next to this file. Nothing else in this skill changes, so leave it out unless the app actually has tenants in the URL. For a server without Web-standard `Request`/`Response` (Express, older Node), adapt at the edge — `createHandler` returns `(Request) => Promise<Response>` and nothing else. ## 4. Set the key Add `OPENAI_API_KEY` to the server environment: `.env.local` for local work, plus wherever production secrets live (check for a deploy workflow, a `wrangler.jsonc`, a Dockerfile, a Vercel/Netlify config — and tell the user exactly which one still needs it). Never add it to a `NEXT_PUBLIC_*` variable or any client-visible config; that key would be in the bundle. If the project already has `OPENAI_API_KEY`, reuse it. Say so rather than adding a second one. ## 5. The palette There are two routes here and they are not interchangeable. **Look first:** search for `cmdk`, `CommandDialog`, "command palette", a ⌘K key handler. - **The app already has a palette** → 5b. Add rows to theirs. Never replace a palette someone already built and styled. - **It has none** → 5a. Copy in the ready one. ### 5a. Copy in the palette (no palette exists yet) This skill ships `templates/command-palette.tsx`, written against shadcn/ui's current `command` and `dialog` components. It is copied into the project, the way shadcn distributes components, so the team owns and can restyle it. **Check the prerequisites before copying anything.** This template is Tailwind + shadcn/ui. Dropping it into a project without them produces an unstyled, broken-looking bar, which is worse than no feature: 1. **Tailwind** — a `tailwind.config.*`, or a `@import "tailwindcss"` in the global stylesheet for v4. **If there is no Tailwind, stop and say so.** Offer the alternatives: wire intentbar into whatever UI they do have with the headless hook (5b), or let them add Tailwind first. Do not copy the file and hope. 2. **shadcn/ui** — a `components.json`, and `cn` in `@/lib/utils`. If shadcn is set up but `command` or `dialog` is missing, add them with the project's own CLI: `npx shadcn@latest add command dialog`. 3. The alias the project actually uses. `components.json` has `aliases.ui` — it is often `@/components/ui`, but not always, and the template's imports must match. Fix them while copying, not after. Then copy it in and point its one placeholder import at the code you copied in step 0: ```bash cp <skill-dir>/templates/command-palette.tsx <project>/src/components/intentbar/command-palette.tsx ``` ```diff - import { useIntentbar } from "intentbar/react"; + import { useIntentbar } from "@/lib/intentbar/react"; ``` Fix the `@/components/ui/*` imports if this project's `aliases.ui` differs, install `cmdk` and `lucide-react` if `shadcn add` did not, and mount it where the app's layout lives: ```tsx const [open, setOpen] = React.useState(false); const router = useRouter(); <CommandPalette open={open} onOpenChange={setOpen} onNavigate={(path) => router.push(path)} scopeId={currentWorkspaceSlug} // multi-tenant apps only; omit otherwise commands={[ { id: "settings", label: "Settings", perform: () => router.push("/settings") }, ]} />; ``` It binds ⌘K/Ctrl+K itself. Pass the app's existing palette entries (or its main nav items) as `commands` so it is a real palette rather than only an AI row — typing one of those labels exactly is also what stops a known command from costing a model call. Two things in that file worth knowing before you edit it: - It composes `Dialog` + `Command` **instead of** shadcn's `CommandDialog`, deliberately. `CommandDialog` spreads its props onto `Dialog` and hands the cmdk root a fixed `className`, so there is no way through it to pass the controlled `value` and the custom `filter`. Both are required: the resolved rows do not contain the typed text, so cmdk's default scorer hides them. - The resolved rows' values are prefixed `intentbar-`, and `paletteFilter` returns 1 for that prefix. Keep the prefix if you rename things. ### 5b. Wire the existing palette Find the palette — `cmdk`, a Radix dialog, a custom one — and add rows to it. Match what is there, including its row markup, icon sizing and selected-row styling. The `templates/command-palette.tsx` beside this file is a working reference for the behaviour even when you are not copying it. ```tsx const { resolution, isResolving, error } = useIntentbar(search, { scopeId: currentWorkspaceSlug, // multi-tenant apps only; omit otherwise // Typing a command the palette already lists must not cost a model call. enabled: !isExistingCommand, }); const match = resolution?.status === "matched" ? resolution : null; ``` Then: - Render the match as the first group, and **auto-focus it** so Enter navigates. If the palette is a `cmdk` list with a custom `filter`, give these rows a value prefix and return `1` for it, or the filter will hide them (they do not contain the typed text). - Show the three states: resolving, no match, and lookup failed. Silence on failure makes the feature look dead. - Navigate with the app's own router to `match.path`. It is already a complete in-app path; do not prefix it again. - Multi-tenant apps only: if `resolution.scope` came back with `isCurrent: false`, say so in the row ("Switch to Acme"). Moving someone between tenants without telling them is disorienting. ## 6. The fallback row (optional, but this is the good part) If the app has an assistant, a chat, or a search page that accepts a query, configure `fallback`. Text like "why did my revenue drop last week" is not a destination, and without this the palette answers it with "no matching page". ```ts fallback: { path: (query) => `/chat?prompt=${encodeURIComponent(query)}`, // add the tenant here if the app has one }, ``` Check how the target page accepts a prefilled query **before** writing this — many already support a `?prompt=`/`?q=` param for deep links, and reusing it means no change on that side. If it does not, that is a separate change the user should agree to. In the UI, when `resolution.fallback` is set, give it its own row and let it take Enter: the user wrote a question, not a destination. Hide the page row if it points at that same page — with the query prefilled, opening it empty is a worse version of the same row. A confident page match and a strong fallback often both appear, because both answers are right ("how did last week go" matches a Dashboard at 0.73 and scores 0.97 as a question). Show the question first and the page below it, and say so to the user — it is a product decision they may want inverted if their pages answer questions better than their assistant does. ## 7. Verify against the real API, then show the numbers Do not hand this over on "it compiles". Write a throwaway script that calls `resolveIntent` with the real provider for 10–15 phrases the user's customers would type — in every language they use — and print the chosen id and confidence: ```ts const result = await resolveIntent(phrase, { sitemap, provider: decisionsProvider() }); ``` Include cases that *should* abstain (off-topic text, gibberish). Print `result.method` alongside the id — `"exact"` means the intents list answered and no request was made. Then measure the two levers from step 2, because "I enriched the intents" is not a result: 1. **Exact-hit rate.** Take the nav label and page title of every destination, plus the phrasings you expect to be most common, and run them. Every one of those *should* come back `method: "exact"`. Any that calls the model is a phrasing missing from that page's `intents` — add it and re-run. This is the
Ver no GitHub
Este SKILL.md e muito grande, entao o SkillsMP mostra aqui apenas a primeira secao. Ver no GitHub