Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
3a746a6
Add AI Gateway tool calling support
IzumiSy Jul 3, 2026
d05386b
Document AI tool calling internals and APIs
IzumiSy Jul 3, 2026
ce7b518
Document AI tool schema helpers
IzumiSy Jul 3, 2026
4bd438e
refactor(ai): restructure use-ai-chat and tool runtime into clear res…
IzumiSy Jul 6, 2026
126a550
docs: enrich ai-gateway-tools changeset with examples and API reference
IzumiSy Jul 6, 2026
e03b3e1
Format files
IzumiSy Jul 8, 2026
a6eebb0
Merge branch 'main' into izumis/ai-gateway-tool-calling
IzumiSy Sep 17, 2026
6b21958
refactor(ai): remove built-in tool schema helpers
IzumiSy Sep 17, 2026
c74fd8b
Merge branch 'main' into izumis/ai-gateway-tool-calling
IzumiSy Sep 17, 2026
68d4e5b
feat(ai): add browser MCP tool adapters
IzumiSy Sep 18, 2026
cb3293f
Merge branch 'main' into izumis/ai-gateway-tool-calling
IzumiSy Sep 25, 2026
d85febb
Revert "feat(ai): add browser MCP tool adapters"
IzumiSy Sep 25, 2026
4717309
Merge remote-tracking branch 'origin/main' into izumis/ai-gateway-too…
Copilot Oct 1, 2026
4223424
Merge remote-tracking branch 'origin/main' into izumis/ai-gateway-too…
Copilot Oct 2, 2026
bb7b432
Merge branch 'main' into izumis/ai-gateway-tool-calling
IzumiSy Oct 2, 2026
0100b5d
Fix documentation formatting check
Copilot Oct 2, 2026
6669689
Merge remote-tracking branch 'origin/main' into izumis/ai-gateway-too…
Copilot Oct 2, 2026
49402ca
Merge branch 'main' into izumis/ai-gateway-tool-calling
IzumiSy Oct 2, 2026
979ee3d
Merge branch 'main' into izumis/ai-gateway-tool-calling
IzumiSy Oct 2, 2026
d078fc3
Remove unrelated decision file from PR diff
Copilot Oct 2, 2026
ea45e54
Merge remote-tracking branch 'origin/main' into izumis/ai-gateway-too…
Copilot Oct 19, 2026
6bad740
feat(ai-chat): add waiting indicator and transient tool call UI
IzumiSy Oct 19, 2026
79992c0
fix(ai-chat): respect cancellation during tool validation
IzumiSy Oct 19, 2026
a9f7499
fix(ai-chat): allow a final answer after eight tool rounds
IzumiSy Oct 19, 2026
d9ea9f4
fix(ai-chat): accumulate streamed tool name fragments
IzumiSy Oct 19, 2026
875b3d7
fix(ai-chat): preserve safe history after interruption
IzumiSy Oct 19, 2026
3a5897a
docs(ai-chat): note low-level union compatibility
IzumiSy Oct 20, 2026
ec03632
Merge remote-tracking branch 'origin/main' into izumis/ai-gateway-too…
Copilot Oct 20, 2026
4400a5d
Merge branch 'main' into izumis/ai-gateway-tool-calling
IzumiSy Oct 21, 2026
0ec4525
Merge remote-tracking branch 'origin/main' into izumis/ai-gateway-too…
Copilot Oct 26, 2026
a80e950
Merge remote-tracking branch 'origin/main' into izumis/ai-gateway-too…
Copilot Oct 30, 2026
3145627
refactor(ai-chat): limit tool calling to local functions
IzumiSy Oct 30, 2026
0157f31
test(ai-chat): sync tool call snapshot with typography roles
IzumiSy Oct 30, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 79 additions & 0 deletions .changeset/ai-gateway-tools.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
---
"@tailor-platform/app-shell": minor
---

Add local tool support to `useAIChat()` and the AI Gateway transport layer. Streamed function names and arguments are assembled across chunks independently for each tool call.

You can now register **local tools** via the `tools` option on `useAIChat()`. Tool-call and tool-result protocol messages stay internal to the hook — the public `messages` array remains user/assistant text only. Provider-native tools are not supported.

Stopped or failed requests retain user messages, partial assistant text, and completed tool exchanges in subsequent request history. Unanswered tool calls are excluded. Stopping does not undo tool side effects or guarantee safe retries of tools that were already running.

**Low-level API compatibility:** `AIGatewayChatMessage` now includes `role: "tool"`, and `AIChatCompletionEvent` now includes `type: "tool-call"`. Exhaustive handlers for these exported unions must handle the new cases, even when tools are not enabled. The high-level `useAIChat().messages` role union remains `"user" | "assistant"`.

## Local tools

Define tools that run inside AppShell using `defineAIChatTool`. The model decides when to call them, AppShell validates arguments with the schema, executes the handler, and feeds results back into the next model turn automatically.

```ts
import { defineAIChatTool } from "@tailor-platform/app-shell";
import { z } from "zod/v4";

const lookupCustomer = defineAIChatTool({
description: "Look up a customer by ID and return their profile",
schema: z.object({
customerId: z.string().describe("Customer ID"),
includeInactive: z.boolean().optional(),
}),
async execute({ customerId, includeInactive }, { signal }) {
const res = await fetch(`/api/customers/${customerId}?inactive=${includeInactive ?? false}`, {
signal,
});
return res.json();
},
});
```

Tool schemas must implement both [Standard Schema](https://standardschema.dev/) validation and Standard JSON Schema generation; Zod 4 does this directly.

### Tool context

The `execute` function receives a second argument with:

- `signal` — the `AbortSignal` for the in-flight chat request
- `messages` — the public user/assistant message history at the time of execution

Stopping a request while tool arguments are being validated prevents the tool from starting. Cancellation is propagated rather than returned to the model as a tool error.

## Registering tools with `useAIChat`

Pass tools as a record to the `tools` option. The key becomes the tool name sent to the model.

```tsx
import { useAIChat, defineAIChatTool } from "@tailor-platform/app-shell";
import { z } from "zod/v4";

const lookupCustomer = defineAIChatTool({
description: "Look up a customer by ID",
schema: z.object({
customerId: z.string(),
}),
async execute({ customerId }) {
return { customerId, name: "Acme Corp", plan: "enterprise" };
},
});

function ChatPanel({ client }) {
const { messages, status, sendMessage } = useAIChat({
client,
model: "gpt-5-mini",
tools: {
lookupCustomer,
},
});

// messages only contains user/assistant text — tool calls are handled internally
// status transitions: "submitted" → "streaming" → ("submitted" during tool rounds) → "ready"
}
```

The hook runs up to 8 tool rounds per user message, then allows a final model response using the eighth round's results. If the model requests a ninth tool batch, the request fails before those tools execute.
7 changes: 7 additions & 0 deletions .changeset/chat-progress-tool-names.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@tailor-platform/app-shell": minor
---

Show a localized waiting indicator in `AIChat.Conversation` while a response or tool round is pending.

Expose requested tool names in a transient `toolCalls` list from `useAIChat()`. Render them with `toolCalls.map(call => <AIChat.ToolCall key={call.id} call={call} />)`; the localized labels stack vertically and the list clears on completion, stop, or error without changing the message history. Call metadata does not imply successful execution, and tool arguments/results remain internal.
25 changes: 13 additions & 12 deletions docs-manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -74,16 +74,17 @@
"AIChatConversationProps",
"AIChatHeaderProps",
"AIChatProps",
"AIChatToolCallProps",
"ChainOfThoughtStepStatus",
"ChatHistoryGroupData",
"ChatHistoryItemData",
"ToolState"
],
"hashes": {
"typeSurface": "7cb8a34c1c36eb02",
"outline": "797e012e34847b80",
"snapshot": "a21f0e323ea4774e",
"outputMd": "824d1aa36bb7f194",
"typeSurface": "932ff7674cc6df99",
"outline": "0d84350ee1e55312",
"snapshot": "39edd53bc6562561",
"outputMd": "82313d5b6fe140d2",
"examples": null
}
},
Expand Down Expand Up @@ -326,9 +327,9 @@
"symbols": [],
"hashes": {
"typeSurface": null,
"outline": "171ba8a5b6eda15b",
"outline": "43a13487e8f3e6bd",
"snapshot": null,
"outputMd": "7eaff97521a14ab0",
"outputMd": "ed0db6372d25e2f9",
"examples": null
}
},
Expand Down Expand Up @@ -1534,9 +1535,9 @@
"symbols": [],
"hashes": {
"typeSurface": null,
"outline": "4cf294ec6ae1492f",
"outline": "807ee6ee2b33d5de",
"snapshot": null,
"outputMd": "f4a88f636000fd60",
"outputMd": "3bcaceab3290ca92",
"examples": null
}
},
Expand Down Expand Up @@ -1773,7 +1774,7 @@
"packages/core/skills/app-shell-patterns/references/concepts/styling-theming.md": "1ed3e6d92e582e26",
"packages/core/skills/app-shell-patterns/references/components/action-panel.md": "dc861082aa9610af",
"packages/core/skills/app-shell-patterns/references/components/activity-card.md": "4f386e49380fb0b0",
"packages/core/skills/app-shell-patterns/references/components/ai-chat.md": "824d1aa36bb7f194",
"packages/core/skills/app-shell-patterns/references/components/ai-chat.md": "82313d5b6fe140d2",
"packages/core/skills/app-shell-patterns/references/components/alert.md": "db2c4388ca783a75",
"packages/core/skills/app-shell-patterns/references/components/app-shell.md": "081e4c7bb61db191",
"packages/core/skills/app-shell-patterns/references/components/appearance-switcher.md": "b55f1d538651d467",
Expand Down Expand Up @@ -1819,7 +1820,7 @@
"packages/core/skills/app-shell-patterns/references/components/tooltip.md": "113c95b2995917ed",
"packages/core/skills/app-shell-patterns/references/components/with-guard.md": "d79aaf40cb97073f",
"packages/core/skills/app-shell-patterns/references/api/aggregations.md": "daf6c93e7285ee88",
"packages/core/skills/app-shell-patterns/references/api/create-ai-gateway-client.md": "7eaff97521a14ab0",
"packages/core/skills/app-shell-patterns/references/api/create-ai-gateway-client.md": "ed0db6372d25e2f9",
"packages/core/skills/app-shell-patterns/references/api/create-typed-paths.md": "4c3a858d3eb02a2a",
"packages/core/skills/app-shell-patterns/references/api/define-i18n-labels.md": "3035dea9ac31b7f5",
"packages/core/skills/app-shell-patterns/references/api/define-module.md": "199295d8e7a9f4fa",
Expand All @@ -1831,7 +1832,7 @@
"packages/core/skills/app-shell-patterns/references/api/router/use-location.md": "ddb44cc51af636cc",
"packages/core/skills/app-shell-patterns/references/api/router/use-navigate.md": "d018cb13a7ddc38b",
"packages/core/skills/app-shell-patterns/references/api/router/use-params.md": "535f701e77dfc4e6",
"packages/core/skills/app-shell-patterns/references/api/use-ai-chat.md": "f4a88f636000fd60",
"packages/core/skills/app-shell-patterns/references/api/use-ai-chat.md": "3bcaceab3290ca92",
"packages/core/skills/app-shell-patterns/references/api/use-app-shell.md": "ce989e06790aade8",
"packages/core/skills/app-shell-patterns/references/api/use-app-shell-config.md": "1a0a937f3a412c13",
"packages/core/skills/app-shell-patterns/references/api/use-app-shell-data.md": "2d2eaf5aa04d95d3",
Expand All @@ -1856,6 +1857,6 @@
"packages/core/skills/app-shell-patterns/references/patterns/list-dense-scan.md": "59c93617dc135033",
"packages/core/skills/app-shell-patterns/references/pages/document-detail.md": "5a6612349cb50ef1",
"packages/core/skills/app-shell-patterns/references/migrations.md": "e0286979718e2769",
"packages/core/skills/app-shell-patterns/SKILL.md": "de250e21921fc3e8"
"packages/core/skills/app-shell-patterns/SKILL.md": "2c900e3b4d203b31"
}
}
40 changes: 37 additions & 3 deletions docs-src/components/ai-chat.docs.outline.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,17 @@ Other props spread onto the root `<div>`.

Other props spread onto the region's outer `<div>`.

While the root's `status` is `"submitted"`, the conversation automatically appends a [`Spinner`](./spinner.md) and localized “Thinking…” status at the end. This covers the initial reply wait, tool execution, and the next reply wait after a tool result. The indicator disappears when text starts streaming, or when the request finishes, fails, or is stopped. It participates in the conversation's normal auto-scroll behavior; no separate loader state is needed.

### `AIChat.ToolCall`

A compact label for one model-requested tool call. Each label occupies its own row, so multiple calls stack vertically. It does not imply execution success, render tool arguments/results, or manage history.

| Prop | Type | Default | Description |
| ----------- | ------------------------------ | -------- | ------------------------------------------------ |
| `call` | `{ id: string; name: string }` | required | A `toolCalls` element returned by `useAIChat()`. |
| `className` | `string` | - | Merged onto the outer row, not the badge. |

### `AIChat.Composer`

| Prop | Type | Default | Description |
Expand Down Expand Up @@ -114,17 +125,18 @@ Most props stay inside their region. These `AIChat.Composer` props land on **ano

`onValueChange` is wrapped rather than forwarded — it is called from the `Textarea`'s `onChange`, and again with `""` after a submit. `defaultValue` seeds the composer's own state and is never forwarded. Nothing on `AIChat`, `AIChat.Header`, or `AIChat.Conversation` reaches another AppShell component.

Three attached parts wrap an AppShell component. They expose only the props the part's job needs rather than inheriting the wrapped component's surface, so its visual treatment is fixed and not yours to change:
Four attached parts wrap an AppShell component. They expose only the props the part's job needs rather than inheriting the wrapped component's surface, so its visual treatment is fixed and not yours to change:

| Part | Wraps | Accepts | Fixed |
| ----------------------------------- | ---------------------------------------------------------- | ------------------------------------------------------------- | ---------------------------------- |
| `AIChat.Suggestion` | [`Button`](./button.md) | `suggestion`, `onSelect`, `className`, `disabled`, `children` | `variant="secondary"`, `size="sm"` |
| `AIChat.Action` | [`Button`](./button.md) inside a [`Tooltip`](./tooltip.md) | `label`, `onClick`, `className`, `disabled`, `children` | `variant="ghost"`, `size="icon"` |
| `AIChat.ChainOfThoughtSearchResult` | [`Badge`](./badge.md) | `className`, `children`, `variant` | — |
| `AIChat.ToolCall` | [`Badge`](./badge.md) | `call`, `className` | `variant="neutral"` |

`variant` stays open on `ChainOfThoughtSearchResult` because conveying status is what a `Badge` is for. `AIChat.Action`'s `label` becomes both the accessible name and the tooltip text.

One thing to know about `className` on these: it is merged with `cn()`, which is not configured for AppShell's internal prefix, so it cannot reliably override a base utility from the wrapped component. Add properties the component doesn't set, and reach for the wrapped component's own props or variants to change ones it does — see [Styling AppShell components](../concepts/styling-theming.md#styling-appshell-components).
`ToolCall` applies `className` to its outer row, not the inner badge. For `Suggestion`, `Action`, and `ChainOfThoughtSearchResult`, `className` is merged with `cn()`, which is not configured for AppShell's internal prefix, so it cannot reliably override a base utility from the wrapped component. Add properties the component doesn't set, and reach for the wrapped component's own props or variants to change ones it does — see [Styling AppShell components](../concepts/styling-theming.md#styling-appshell-components).

## Filling the page

Expand Down Expand Up @@ -263,6 +275,28 @@ Structured step timeline for agentic turns — the discrete plan being executed,

`status` is `"complete" | "active" | "pending"`.

### Tool calls during the active flow

`useAIChat()` returns a transient `toolCalls` list separately from `messages`. Render it with `AIChat.ToolCall` after the message history inside `AIChat.Conversation`:

```tsx
{
toolCalls.length > 0 ? (
<AIChat.Message from="assistant">
{toolCalls.map((call) => (
<AIChat.ToolCall key={call.id} call={call} />
))}
</AIChat.Message>
) : null;
}
```

Each call renders on its own row, in the order supplied. Labels are localized automatically.

The list clears when the response finishes, is stopped, or fails. It does not create tool messages or persist in the conversation log.

These names record model requests, not execution success. Tool arguments and results stay internal. Do not infer a `ToolHeader` success or running state from name-only metadata.

### Tool

Collapsible tool-call card. Maps 1:1 to the AI SDK's tool-part lifecycle, so a streaming loop can update a call in place as parts arrive.
Expand All @@ -281,7 +315,7 @@ Collapsible tool-call card. Maps 1:1 to the AI SDK's tool-part lifecycle, so a s

### Sources

Compact "Used N sources" citation trigger under a grounded answer.
Compact "Used N sources" citation trigger under a grounded answer. Supply source data from your application; `useAIChat()` does not return citation metadata.

```tsx
<AIChat.Sources>
Expand Down
34 changes: 32 additions & 2 deletions docs-src/hooks/create-ai-gateway-client.docs.outline.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ description: Create a low-level AI Gateway client that reuses AppShell authentic

# createAIGatewayClient

Creates a small AI Gateway transport client for text-only chat completions.
Creates a small AI Gateway transport client for chat completions and local function tool calls.

## Signature

Expand Down Expand Up @@ -43,6 +43,7 @@ interface AIGatewayClient {
The iterable yields completion events:

- `text-delta` — append `event.text` to build the assistant response
- `tool-call` — a local function tool call requested by the model
- `done` — terminal event with an optional `finishReason`

## Related Types
Expand All @@ -56,11 +57,33 @@ type AIGatewayChatMessage =
| {
role: "assistant";
content?: string;
toolCalls?: AIGatewayToolCall[];
}
| {
role: "tool";
toolCallId: string;
content: string;
};

interface AIGatewayToolCall {
id: string;
name: string;
argumentsText: string;
}

interface AIGatewayFunctionTool {
type: "function";
function: {
name: string;
description?: string;
parameters: Record<string, unknown>;
};
}

interface AIGatewayChatRequest {
model: string;
messages: AIGatewayChatMessage[];
tools?: AIGatewayFunctionTool[];
signal?: AbortSignal;
}

Expand All @@ -69,6 +92,12 @@ type AIChatCompletionEvent =
type: "text-delta";
text: string;
}
| {
type: "tool-call";
toolCallId: string;
toolName: string;
argumentsText: string;
}
| {
type: "done";
finishReason?: string;
Expand Down Expand Up @@ -106,7 +135,8 @@ console.log(text);
## Notes

- AppShell chooses the appropriate AI Gateway transport automatically
- The low-level API is intentionally narrow: text deltas plus completion metadata
- Local tools are sent as normalized `type: "function"` definitions; provider-native tools are not supported
- The low-level API stays event-based so hooks can layer streaming UI and tool loops on top
- `request.signal` is passed through so callers can abort in-flight work

## Related
Expand Down
Loading
Loading