# Home (/) # IntentCtrl [#intentctrl] Embedded AI runtime for React applications. Learn how to install, configure, and use IntentCtrl in your app. Get started with no backend setup required. Reusable components for building chat interfaces. # Apps (/changelogs/apps-changelogs) No releases yet. # Overview (/changelogs) Release history for packages. Release history for apps. # Packages (/changelogs/packages-changelogs) No releases yet. # Getting Started (/docs/getting-started) ## Quick Start [#quick-start] ### Install [#install] {/* prettier-ignore */} npm pnpm yarn bun ```bash npm install @intentctrl/react ``` ```bash pnpm add @intentctrl/react ``` ```bash yarn add @intentctrl/react ``` ```bash bun add @intentctrl/react ``` Make sure `react`, `react-dom`, and `zod` are already in your project. ### Add the provider [#add-the-provider] Wrap your application (or the part that needs AI) in `IntentCtrlProvider`. You need the URL of an AI backend and an API key. Use the [cloud platform](http://app.intentctrl.com) directly, or [self-host the backend](https://github.com/intentctrl) for free. ```tsx title="app/layout.tsx" import { IntentCtrlProvider } from "@intentctrl/react"; export default function RootLayout({ children }) { return ( {children} ); } ``` ### Start a chat [#start-a-chat] Inside any child component, use `useIntentCtrl()` to send messages and display responses. ```tsx title="components/chat-panel.tsx" import { useIntentCtrl } from "@intentctrl/react"; export function ChatPanel() { const { messages, sendMessage, status } = useIntentCtrl(); return (
{messages.map((msg) => (
{msg.role}: {msg.content}
))} { if (e.key === "Enter" && e.currentTarget.value) { sendMessage(e.currentTarget.value); e.currentTarget.value = ""; } }} /> {status === "streaming" && Thinking...}
); } ``` That's it. The AI sees the current page, knows about any tools you've registered, and can respond with text or take actions.
## What's next [#whats-next] * [Register custom tools](/docs/tools/custom) so the AI can interact with your app * [Control permissions](/docs/configuration/permissions) to restrict which actions the AI can take * [Learn about built-in tools](/docs/tools/built-in) the AI already has # Overview (/docs) An embedded AI runtime for React applications. It connects your app to an AI assistant that can understand what's on screen and take action through tools. ## How it works [#how-it-works] Wrap your application in ``, and every component beneath it gets access to the AI chat via `useIntentCtrl()`. When the user sends a message, the current page is captured as structured content, bundled with your registered tools, and sent to your AI backend. The AI can respond naturally and call tools — both built-in DOM actions and your own custom functions. ## What you get [#what-you-get] * **Streaming chat** — real-time AI responses with a single hook * **Page-aware AI** — the assistant sees the page the user is looking at * **Built-in tools** — navigate, click, type, scroll, highlight, and extract page content * **Custom tools** — register your own functions with typed inputs * **Permissions** — control which tools the AI may use, per user * **Approval workflows** — require user confirmation before sensitive actions * **Session management** — persistent conversations across page visits ## Requirements [#requirements] Runs in the browser and requires: * **React 19** and **React DOM 19** * **Zod 4** — used to define tool input schemas * **An AI backend** — use the [cloud platform](http://app.intentctrl.com) with no setup required, or self-host for free with unlimited usage. The cloud platform provides session history, analytics, and memory. ## One package [#one-package] {/* prettier-ignore */} npm pnpm yarn bun ```bash npm install @intentctrl/react ``` ```bash pnpm add @intentctrl/react ``` ```bash yarn add @intentctrl/react ``` ```bash bun add @intentctrl/react ``` That's all you need. Everything comes in a single dependency. # Coming Soon (/ui) # UI Components [#ui-components] Coming soon. Reusable UI components for building chat interfaces with IntentCtrl will be documented here. # Approvals (/docs/chat/approvals) Some tools should not run without the user's explicit consent. An approval workflow is provided for these cases. ## How it works [#how-it-works] When the AI calls a tool that has `needsApproval: true`, the execution pauses. The tool call appears in the message parts with state `"approval-requested"`. Your UI can detect this state and prompt the user. The user either approves or denies the call: ```tsx const { approveToolCall, denyToolCall } = useIntentCtrl(); await approveToolCall(toolCallId); // tool executes denyToolCall(toolCallId); // tool is cancelled ``` ## Tool state lifecycle [#tool-state-lifecycle] Each tool invocation goes through a series of states as it progresses: | state | Meaning | | -------------------- | --------------------------------- | | `input-streaming` | Tool arguments are being streamed | | `input-available` | Arguments are fully received | | `approval-requested` | Waiting for user approval | | `approval-responded` | User has approved or denied | | `output-available` | Tool executed successfully | | `output-error` | Tool execution failed | | `output-denied` | User denied the call | ## Detecting tool calls [#detecting-tool-calls] Messages contain a `parts` array. Each part has a `type` field: | type | Represents | | ---------------- | ------------------------------- | | `"text"` | Text content | | `"reasoning"` | AI reasoning (chain of thought) | | `"dynamic-tool"` | A tool call (dynamic name) | | `"tool-"` | A tool call for a specific tool | Tool parts include `toolCallId`, `toolName`, `state`, `input`, `output`, and `errorText`. ## Usage [#usage] Call `approveToolCall(toolCallId)` to allow the tool to execute, or `denyToolCall(toolCallId)` to cancel it. ## When to use approvals [#when-to-use-approvals] Enable approvals for actions that modify data, navigate the user, or perform irreversible operations. Tools that only read or highlight content typically don't need approval. # Chat Interface (/docs/chat) The chat interface is the primary way users interact with the AI. All functionality is available through the `useIntentCtrl()` hook. ## Hook reference [#hook-reference] ```tsx const { messages, // Array of messages in the current conversation sendMessage, // Send a new user message status, // Current state of the chat stop, // Abort an in-progress response error, // Latest error message, if any } = useIntentCtrl(); ``` ### messages [#messages] An array of `UIMessage` objects. Each message has: * **id** — unique identifier * **role** — `"user"` or `"assistant"` * **content** — text content of the message * **parts** — structured parts — text, reasoning, tool calls, files, sources Each part has a `type` field. The types you will encounter: | type | Represents | | ---------------- | ------------------------------------- | | `"text"` | Text content | | `"reasoning"` | AI chain-of-thought reasoning | | `"dynamic-tool"` | A tool call (dynamic name) | | `"tool-"` | A tool call for a specific known tool | Tool parts also carry `state` (e.g. `"approval-requested"`, `"output-available"`), `toolCallId`, `input`, `output`, and `errorText`. Messages update in real-time as the AI streams its response. ### status [#status] The chat lifecycle has four states: | Status | Meaning | | ----------- | ------------------------------------------------------- | | `submitted` | Message sent, waiting for the AI to begin responding | | `streaming` | AI is responding — content appears in real-time | | `ready` | Response is complete, waiting for the next user message | | `error` | Something went wrong — check the `error` field | ### sendMessage [#sendmessage] ```ts sendMessage("Show me the sales report"); ``` Sends the user's message along with the current page content, registered tools, permissions, and data context. Everything the AI needs is bundled automatically. ### stop [#stop] Interrupts a response that's currently streaming. The response will be cut off at whatever point it's reached. ## Example [#example] ```tsx title="components/chat-window.tsx" export function ChatWindow() { const { messages, sendMessage, status, stop, error } = useIntentCtrl(); return (
{messages.map((message) => (
{message.content}
))} {status === "streaming" && } {error &&
{error}
}
{ e.preventDefault(); const data = new FormData(e.currentTarget); const text = data.get("input") as string; if (text.trim()) sendMessage(text); }} >
); } ``` # Sessions (/docs/chat/sessions) Conversations are organized into sessions. A session persists the chat history so users can pick up where they left off, even after closing the page. ## Session lifecycle [#session-lifecycle] Sessions are created lazily. The first time a user sends a message, a new session is created automatically. Subsequent visits restore the most recent active session. All session data is persisted locally via IndexedDB and synced to your backend through REST API calls. ## Switching sessions [#switching-sessions] Users can switch between conversations: ```tsx const { session, switchSession, newSession, refreshSessions } = useIntentCtrl(); // Switch to a past conversation await switchSession("session-id-123"); // Start a fresh conversation await newSession(); // Refresh the session list (e.g., after opening a sidebar) await refreshSessions(); ``` ## Session state [#session-state] ```tsx session.activeSessionId; // string | null — the currently active session session.sessions; // { items, rowCount, pageCount, pageIndex, pageSize } session.initState; // "idle" | "loading" | "ready" | "error" ``` The session list is paginated. Each item in `items` represents a conversation with its id, timestamps, and associated user information. ## Example: Session sidebar [#example-session-sidebar] ```tsx title="components/session-sidebar.tsx" export function SessionSidebar() { const { session, switchSession, newSession } = useIntentCtrl(); return (
    {session.sessions.items.map((s) => (
  • switchSession(s.id)} className={s.id === session.activeSessionId ? "active" : ""} > {s.id.slice(0, 8)}...
  • ))}
); } ``` # Configuration (/docs/configuration) Several options are available to control how the AI behaves in your application. ## Permissions [#permissions] Restrict which built-in tools the AI can use — navigate, click, type, and more. Permissions can be set globally on the provider or updated dynamically per user. ## Router Integration [#router-integration] For the `navigate` tool to work with client-side routing (Next.js, React Router), register your router so the AI can move between pages without full reloads. # Permissions (/docs/configuration/permissions) Permissions let you restrict the AI's ability to use tools on a per-user or per-session basis. This is useful when different users have different levels of access, or when you want to prevent the AI from taking certain actions entirely. ## Setting permissions [#setting-permissions] Pass permissions to the provider: ```tsx ``` ## How it works [#how-it-works] Each built-in tool has three possible permission states: | Value | Behavior | | ----------- | ------------------------------------------------------------------------------ | | `undefined` | Tool is allowed but requires user approval if it has the approval flag enabled | | `true` | Tool is allowed and skips the approval prompt entirely | | `false` | Tool is denied — the AI cannot call it | Custom tools are always permitted. Their approval behavior is controlled by the `needsApproval` flag on the tool definition. ## Reactive permissions [#reactive-permissions] You can update permissions dynamically using `usePermissions()`: ```tsx title="components/permission-panel.tsx" import { usePermissions } from "@intentctrl/react"; export function PermissionPanel() { const { permissions, setPermissions } = usePermissions(); return (
); } ``` Changes take effect on the next message sent to the AI. ## Use cases [#use-cases] * **Read-only mode** — deny `navigate`, `click`, and `type` so the AI can only read and highlight * **Guest users** — deny all built-in tools, allow only custom tools * **Admin mode** — set everything to `true` to bypass approval prompts * **Feature gates** — tie permissions to user roles or subscription tiers # Router Integration (/docs/configuration/router-integration) For the `navigate` tool to work with client-side routing (rather than full page reloads), IntentCtrl needs to know about your router. ## Next.js App Router [#nextjs-app-router] ```tsx title="components/router-bridge.tsx" "use client"; import { setRouter } from "@intentctrl/react"; import { useRouter } from "next/navigation"; import { useEffect } from "react"; export function RouterBridge() { const router = useRouter(); useEffect(() => { setRouter({ push: (path) => router.push(path) }); }, [router]); return null; } ``` Place this component somewhere inside your `IntentCtrlProvider`. ## React Router [#react-router] ```tsx title="components/router-bridge.tsx" import { setRouter } from "@intentctrl/react"; import { useNavigate } from "react-router-dom"; import { useEffect } from "react"; export function RouterBridge() { const navigate = useNavigate(); useEffect(() => { setRouter({ push: (path) => navigate(path) }); }, [navigate]); return null; } ``` React Router DOM v6+ is also auto-detected via a global variable, but explicit setup is more reliable. ## Other frameworks [#other-frameworks] Any router with a `push` method works: ```ts setRouter({ push: (path: string) => { // Your custom navigation logic }, }); ``` ## What happens without a router [#what-happens-without-a-router] If no router is configured, the `navigate` tool falls back to `window.location.href`, which causes a full page reload. This works but loses client-side state. # API Reference (/docs/reference) ## Components [#components] ### IntentCtrlProvider [#intentctrlprovider] The root provider component. Must wrap any component that uses IntentCtrl hooks. *** ## Hooks [#hooks] ### useIntentCtrl [#useintentctrl] Returns the full chat interface. Throws if used outside `IntentCtrlProvider`. ### useTool [#usetool] Registers a custom tool for the AI to call. The tool is available while the component is mounted. ### usePermissions [#usepermissions] Reads and updates the current permission state reactively. ### useDataContext [#usedatacontext] Injects contextual data that is sent with every chat request. Data is cleaned up when the component unmounts. *** ## Functions [#functions] ### setRouter [#setrouter] Registers a client-side router for the `navigate` tool. *** ## Types [#types] ### RuntimePermissions [#runtimepermissions] Controls which built-in tools are available to the AI. * `undefined` — allowed, approval may be required * `true` — allowed, bypasses approval * `false` — denied ### SerializedTool [#serializedtool] A tool definition serialized for transport. ### ChatRequest [#chatrequest] The payload sent to your backend with every user message. ### SessionState [#sessionstate] ### PaginatedChatSessionsResponse [#paginatedchatsessionsresponse] ### ChatSessionResponse [#chatsessionresponse] ### ApiResponse [#apiresponse] ### UIMessage [#uimessage] Re-exported from the Vercel AI SDK. Represents a chat message with structured parts. # Built-in Tools (/docs/tools/built-in) Built-in tools give the AI the ability to interact with the page the same way a user would — clicking buttons, filling forms, navigating routes, and reading content. ## How the AI targets elements [#how-the-ai-targets-elements] When the AI receives the page content, every interactive element is annotated with selector information. The annotations let the AI reference specific elements by their location on the page. You can help the AI identify important elements by adding data attributes: | Attribute | Used by | Purpose | | ------------------- | ----------------- | --------------------------------------------------------------- | | `data-ai-field` | type, extract | Marks input fields, textareas, and selects | | `data-ai-action` | click | Marks clickable elements | | `data-ai-region` | highlight, scroll | Marks areas or containers | | `data-ai-highlight` | — | Set automatically by the highlight tool on the targeted element | Adding these attributes is optional but makes targeting more reliable. You can style highlighted elements with CSS: ```css title="app/globals.css" [data-ai-highlight] { outline: 3px solid #3b82f6; outline-offset: 2px; border-radius: 4px; transition: outline-color 0.2s; } ``` The attribute is present for three seconds while the flash animation plays, then removed automatically. ## Tool reference [#tool-reference] ### navigate [#navigate] Navigates the user to a different route. ```json { "target": "/dashboard/reports" } ``` Works with client-side routers (Next.js, React Router) and falls back to standard browser navigation. External URLs, `javascript:` links, and `data:` URIs are rejected for security. ### click [#click] Clicks an interactive element. ```json { "label": "Submit" } ``` Matches by `data-ai-action`, `aria-label`, placeholder, or visible text. Triggers the element's native click handler. ### type [#type] Types a value into a text field. ```json { "field": "Email", "value": "user@example.com" } ``` Supports all input types, textareas, and contenteditable elements. Works with framework-controlled inputs by triggering the appropriate change events. ### highlight [#highlight] Briefly flashes an element to draw the user's attention. ```json { "region": "Error summary" } ``` The element is highlighted for three seconds. No approval needed — purely visual feedback. ### scroll [#scroll] Scrolls an element into the viewport. ```json { "target": "Pricing section" } ``` Smooth scrolls to the element and centers it on screen. ### extract [#extract] Reads the current value or text content of an element. ```json { "field": "Search input" } ``` Returns the element's value (for inputs), text content (for general elements), or inner text (for contenteditable regions). ## Controlling availability [#controlling-availability] See [Permissions](/docs/configuration/permissions) to learn how to restrict which built-in tools the AI can use. # Custom Tools (/docs/tools/custom) Custom tools let you expose any function in your application to the AI. If your app can do it — search a database, fetch from an API, trigger a workflow — you can make it a tool. ## The useTool hook [#the-usetool-hook] ```tsx title="components/search-widget.tsx" import { useTool } from "@intentctrl/react"; import { z } from "zod"; export function SearchWidget() { useTool({ id: "search_knowledge_base", description: "Search the knowledge base for relevant articles", inputSchema: z.object({ query: z.string(), maxResults: z.number().min(1).max(20).optional(), }), handler: async ({ query, maxResults }) => { const results = await searchApi(query, maxResults ?? 5); return results; }, }); return
Search widget
; } ``` Every time the user sends a message, all registered tools are sent to the AI as available functions. ## Tool contract [#tool-contract] ## Writing good descriptions [#writing-good-descriptions] The description is the most important part of a custom tool. It's how the AI decides whether to use it and how to fill in the parameters. Bad: `"Search function"`\ Good: `"Search the product catalog by name or SKU. Returns matching products with their price, stock level, and category."` Bad: `"Get user data"`\ Good: `"Retrieve user profile information by user ID. Use this when the user asks about their account details, settings, or personal information."` ## Lifecycle [#lifecycle] Tools registered with `useTool()` are scoped to the component that registers them: * **Mounted** — tool is available to the AI * **Unmounted** — tool is removed automatically * **Multiple instances** — if two components register the same tool ID, it stays active until both are unmounted This means a tool for a settings panel only exists while the settings panel is open. A tool for a dashboard widget only exists while that widget is visible. ## Example: Fetching data [#example-fetching-data] ```tsx title="components/order-lookup.tsx" import { useTool } from "@intentctrl/react"; import { z } from "zod"; export function OrderLookup() { useTool({ id: "get_order_status", description: "Get the current status of an order by order ID. Returns delivery date, tracking number, and current status.", inputSchema: z.object({ orderId: z.string(), }), handler: async ({ orderId }) => { const response = await fetch(`/api/orders/${orderId}`); return response.json(); }, }); return null; } ``` # Tools Overview (/docs/tools) Tools are the mechanism through which the AI takes action. When the AI decides it needs to do something — navigate to a page, fill in a form, call an API — it invokes a tool. There are two categories of tools: ## Built-in tools [#built-in-tools] Six tools that let the AI interact with the DOM directly: * **navigate** — change the current route * **click** — click an interactive element * **type** — enter text into a field * **highlight** — flash a region to draw attention * **scroll** — scroll an element into view * **extract** — read a field's value or text These tools are always available unless explicitly restricted. They work by using CSS and XPath selectors derived from the page's annotated content. ## Custom tools [#custom-tools] Tools you define yourself. Any function your app can perform can be exposed as a tool — searching a database, fetching data from an API, submitting a form, or triggering a workflow. Custom tools are registered using `useTool()` and are scoped to the component's lifecycle: they exist while the component is mounted and are cleaned up automatically. ## Tool execution flow [#tool-execution-flow] 1. The user sends a message 2. The AI receives the message along with the page content and tool definitions 3. The AI may request to call one or more tools 4. Each tool call is validated — input is checked against its schema 5. If the tool requires approval, execution pauses for user confirmation 6. The tool runs and its result is sent back to the AI 7. The AI continues its response with the tool result in context