@resolvekit/nextjs
v1.0.4
Published
ResolveKit Next.js SDK with runtime parity and browser tool packs
Downloads
530
Readme
@resolvekit/nextjs
Universal Next.js SDK for ResolveKit
Install
npm install @resolvekit/nextjs1) Add Secure Server Routes (App Router)
app/api/resolvekit/client-token/route.ts
import { createResolveKitClientTokenHandler } from "@resolvekit/nextjs/server";
const handler = createResolveKitClientTokenHandler({
agentBaseUrl: process.env.RESOLVEKIT_AGENT_URL ?? "https://agent.resolvekit.app",
resolveApiKey: () => process.env.RESOLVEKIT_API_KEY,
authorizeRequest: async ({ request }) => {
const origin = request.headers.get("origin");
return origin === "https://app.example.com";
},
allowedOrigins: ["https://app.example.com"],
// allowMissingOrigin: true // optional for trusted server-to-server traffic
// forwardOriginHeader: false // optional for local dev if key origin allowlist blocks localhost
});
export const POST = handler;app/api/resolvekit/actions/route.ts
import { createResolveKitActionsProxyHandler } from "@resolvekit/nextjs/server";
const handler = createResolveKitActionsProxyHandler({
targetBaseUrl: process.env.APP_BACKEND_URL ?? "http://localhost:8000",
allowlist: [
{ path: "/v1/workflows/run", methods: ["POST"] },
{ path: "/v1/automation/trigger", methods: ["POST"] }
],
authorizeRequest: async ({ request }) => {
const origin = request.headers.get("origin");
return origin === "https://app.example.com";
},
allowedOrigins: ["https://app.example.com"],
forwardRequestHeaders: ["x-request-id"],
resolveAuthHeader: () => {
const token = process.env.APP_BACKEND_SERVICE_TOKEN;
return token ? `Bearer ${token}` : undefined;
}
});
export const POST = handler;2) Configure Runtime (Client Component)
"use client";
import {
ResolveKitAction,
ResolveKitDevtools,
ResolveKitProvider,
ResolveKitChat,
ResolveKitWidget,
createClientTokenAuthProvider,
createRecommendedBrowserToolsPack,
type ResolveKitConfiguration
} from "@resolvekit/nextjs";
const authProvider = createClientTokenAuthProvider({
endpoint: "/api/resolvekit/client-token"
});
const browserTools = createRecommendedBrowserToolsPack({
apiEndpoint: "/api/resolvekit/actions"
});
const config: ResolveKitConfiguration = {
baseUrl: process.env.NEXT_PUBLIC_RESOLVEKIT_AGENT_URL ?? "https://agent.resolvekit.app",
sdkVersion: "1.0.0",
authProvider,
functions: [
...browserTools.functions,
{
name: "get_local_time",
description: "Returns current local time",
parametersSchema: { type: "object", properties: {}, required: [] },
requiresApproval: true,
invoke: async () => ({ now: new Date().toISOString() })
}
]
};
export default function ResolveKitHost() {
return (
<ResolveKitProvider configuration={config}>
<ResolveKitWidget />
<ResolveKitDevtools
browserToolsOptions={{
discoveryMode: "annotatedOnly",
apiEndpoint: "/api/resolvekit/actions"
}}
/>
{/* Or render the headless/inline component if you need full custom layout */}
{/* <ResolveKitChat /> */}
</ResolveKitProvider>
);
}ResolveKitWidget ships as the default polished chat surface:
- Floating launcher with a Figma-style popup panel
- Theme defaults to
system(inherits OS preference on first load) - Optional single-click toggle to switch to the opposite theme (
light<->dark) - Uses backend chat theme tokens (iOS parity)
- Inherits chat name and message placeholder from backend session/localization
- Shows typing bubbles while waiting for assistant response
- Renders suggested reply chips, richer tool-approval cards, and disabled attachment affordance
- Uses backend-provided tool-call explanations in approval cards
- Renders in a Shadow DOM root to isolate SDK styles from host app global CSS
ResolveKitChat now uses the same visual system as the widget, rendered inline inside its own Shadow DOM host so host page global styles cannot override it.
Key props:
showThemeToggle?: boolean(default:true)launcherLabel?: string(default:Need help?)position?: "bottom-right" | "bottom-left"(default:"bottom-right")
Developer-Friendly Browser Tools
The recommended setup is now:
createRecommendedBrowserToolsPack(...)for secure defaultsResolveKitActionoruseResolveKitActionProps(...)to register UI affordancesResolveKitDevtoolsduring development so you can see what the agent can currently discover
The recommended helper only exposes discovery-based browser actions plus call_backend_api. If you explicitly need raw selector tools like highlight_element, click_element, or navigate_route, use createBrowserToolsPack(...) instead.
React Registration Helpers
Use ResolveKitAction when the SDK owns the element:
<ResolveKitAction
as="button"
actionId="open-billing-modal"
actionRole="dialog-trigger"
description="Open the billing modal"
type="button"
>
Open billing
</ResolveKitAction>Use useResolveKitActionProps(...) when you already have a component and only want props to spread:
const billingLinkProps = useResolveKitActionProps({
actionId: "go-to-billing",
actionRole: "navigation",
description: "Navigate to billing"
});
<Link {...billingLinkProps} href="/billing">
Billing
</Link>Dev Overlay
Use ResolveKitDevtools in development to inspect the currently registered actions and routes:
<ResolveKitDevtools
defaultOpen
browserToolsOptions={{
discoveryMode: "annotatedOnly",
apiEndpoint: "/api/resolvekit/actions"
}}
/>The overlay shows:
- current discovery mode
- current route
- currently discoverable registered actions
- currently discoverable registered routes
Registration Modes
createBrowserToolsPack() supports two discovery modes:
discoveryMode: "open": default. The SDK can discover common visible controls such as buttons, links, and inputs.discoveryMode: "annotatedOnly": hardened mode. The SDK only discovers and acts on elements or routes the app explicitly marks.
Recommended hardened setup:
const browserTools = createRecommendedBrowserToolsPack({
apiEndpoint: "/api/resolvekit/actions",
// discoveryMode defaults to "annotatedOnly"
});In annotatedOnly mode:
discover_page_actionsonly returns marked elements and linksclick_discovered_action,highlight_discovered_action, andnavigate_discovered_routeonly work on discovered ids from those marked elements- raw
click_elementandhighlight_elementreject unannotated selectors - raw
navigate_routerejects routes that are not registered by an annotated link on the current page
Supported annotation attributes:
data-resolvekit-id: stable developer-owned identifier and preferred selector anchordata-resolvekit-role: optional semantic hint for tooling and filteringdata-resolvekit-description: optional human hint
Example:
<button
data-resolvekit-id="open-billing-modal"
data-resolvekit-role="dialog-trigger"
data-resolvekit-description="Open the billing modal"
>
Open billing
</button>
<a
data-resolvekit-id="go-to-billing"
data-resolvekit-role="navigation"
href="/billing"
>
Billing
</a>By default, discovery does not return data-resolvekit-description text back to the model. If you want that richer context, opt in explicitly:
const browserTools = createBrowserToolsPack({
apiEndpoint: "/api/resolvekit/actions",
includeElementDescriptionsInDiscovery: true
});3) Custom UI (Headless)
"use client";
import { ResolveKitProvider, useResolveKit, type ResolveKitConfiguration } from "@resolvekit/nextjs";
function CustomChat() {
const { state, sendMessage, approveToolCallBatch, declineToolCallBatch } = useResolveKit();
return (
<div>
<h2>{state.chatTitle}</h2>
<button onClick={() => void sendMessage("Hello")}>Send</button>
{state.toolCallBatchState === "awaitingApproval" ? (
<>
<button onClick={() => void approveToolCallBatch()}>Approve</button>
<button onClick={() => void declineToolCallBatch()}>Decline</button>
</>
) : null}
</div>
);
}
export function ChatShell({ config }: { config: ResolveKitConfiguration }) {
return (
<ResolveKitProvider configuration={config}>
<CustomChat />
</ResolveKitProvider>
);
}Security Defaults
- API key stays server-side.
- Browser receives short-lived client token from
/api/resolvekit/client-token. - Backend action tool is routed through an allowlisted proxy endpoint.
- Server helpers require
authorizeRequestandallowedOriginsin production. - Browser tools that mutate state remain approval-required; read-only discovery and highlight tools do not.
- Use
discoveryMode: "annotatedOnly"if you want the browser agent limited to developer-registered UI affordances only. createRecommendedBrowserToolsPack(...)makes that hardened mode the easiest starting point.- DOM-derived freeform descriptions are excluded from discovery by default; opt in with
includeElementDescriptionsInDiscovery: trueonly when you explicitly want that extra model context. - Device IDs stay in memory by default. Opt into
deviceIdPersistence: "localStorage"only if you want sticky client identity.
Transport
The SDK now communicates with ResolveKit over an HTTP session stream:
Client -> Server: POST /v1/sessions/{id}/messages | POST /v1/sessions/{id}/tool-results
Server -> Client: GET /v1/sessions/{id}/eventsThe browser runtime and server transport share the same streamed HTTP protocol, so they can use HTTP/3 when the platform and backend negotiate it.
For server or SSR flows that need direct access to the transport layer, use createResolveKitServerTransport from @resolvekit/nextjs/server.
Scripts
npm run test:run
npm run typecheck
npm run buildExample App
See examples/next-app for a complete App Router integration that includes:
ResolveKitActionanduseResolveKitActionProps(...)registration helpersResolveKitDevtoolsshowing the current discoverable action surface- discovery-first browser action demos
data-resolvekit-*annotations for developer-registered browser actions- discovery-based highlighting and clicking against registered actions
- route navigation between
/and/billing - button click automation to open/close modal flows
- backend action calls via
/api/resolvekit/actionsto/api/demo/workflows/run
