diff --git a/.claude/commands/github-manual-stacked-prs.md b/.claude/commands/github-manual-stacked-prs.md
new file mode 100644
index 000000000..91fa11c8d
--- /dev/null
+++ b/.claude/commands/github-manual-stacked-prs.md
@@ -0,0 +1,172 @@
+---
+name: github-manual-stacked-prs
+description: Manage manual stacked pull requests on GitHub without Graphite or other stack tools. Use when the user wants one GitHub PR in review at a time, keeps later branches as draft or local only, and rebases each next branch onto main after the previous PR merges.
+---
+
+# GitHub Manual Stacked PRs
+
+Use this skill when the user wants a GitHub-only stacked PR workflow without stack automation. The rule is simple: build branches in order, keep only one PR ready for review, and rebase the next branch onto `main` after the previous PR merges.
+
+## Defaults
+
+- GitHub only.
+- No Graphite, `gh stack`, `git-town`, or custom stacking tools unless the user asks.
+- Only one PR should be non-draft and actively in review at a time.
+- Later changes can stay as local branches or draft PRs, but they are not review targets yet.
+- Before opening or marking the next PR ready, rebase that branch onto the latest `main`.
+- **Keep each PR under 800–1000 lines of diff (insertions + deletions).** If a proposed slice exceeds this, suggest splitting further before creating branches.
+
+## Start Here
+
+Before proposing commands, confirm:
+
+- trunk branch, usually `main`
+- intended merge order from first PR to last PR
+- whether later branches already have draft PRs or only local branches
+- whether force-push is allowed
+
+If the split is weak, suggest collapsing it before writing commands. If any slice exceeds 1000 LoC, suggest splitting it further before proceeding.
+
+## Branch Model
+
+For work `A -> B -> C`:
+
+- create branch `A` from `main`
+- create branch `B` from `A`
+- create branch `C` from `B`
+- open PR `A` against `main`
+- keep `B` and `C` as draft PRs or local branches until their turn
+
+If draft PRs are used early, base them on their parent branch so the diff stays clean:
+
+- PR `A`: base `main`
+- draft PR `B`: base `A`
+- draft PR `C`: base `B`
+
+When a parent merges, rebase the child branch onto `main`, change the draft PR base to `main`, verify the diff, then mark it ready.
+
+## Fresh Workflow
+
+1. Sync `main`.
+
+```bash
+git checkout main
+git pull --ff-only
+```
+
+2. Create the first branch and commit only the first slice.
+
+```bash
+git checkout -b feat/a
+# edit
+git commit -m "feat(scope): change A"
+```
+
+3. Create each later branch from its direct parent.
+
+```bash
+git checkout -b feat/b
+# edit
+git commit -m "feat(scope): change B"
+
+git checkout -b feat/c
+# edit
+git commit -m "feat(scope): change C"
+```
+
+4. Push the branches.
+
+```bash
+git push -u origin feat/a
+git push -u origin feat/b
+git push -u origin feat/c
+```
+
+5. Open PR `A` against `main`.
+6. Optionally open `B` and `C` as draft PRs against their parent branches. If you do, state clearly that they are placeholders and not ready for review yet.
+
+Use a note like this in every draft PR body:
+
+```md
+Manual stack:
+- depends on: #123
+- status: draft, do not review yet
+- next step: rebase onto `main` after #123 merges
+```
+
+## Promotion Rule
+
+Only promote one PR at a time.
+
+When PR `A` merges:
+
+1. Update local `main`.
+2. Rebase branch `B` onto `main`.
+3. Force-push `B`.
+4. If PR `B` already exists as draft, change its base to `main`.
+5. Verify the diff contains only `B`.
+6. Mark PR `B` ready for review.
+
+Example:
+
+```bash
+git checkout main
+git pull --ff-only
+
+git checkout feat/b
+git rebase main
+git push --force-with-lease
+```
+
+When PR `B` merges, do the same for `C`:
+
+```bash
+git checkout main
+git pull --ff-only
+
+git checkout feat/c
+git rebase main
+git push --force-with-lease
+```
+
+## Review Rules
+
+- Only one PR is reviewable at a time.
+- Later PRs stay draft or unopened until their parent PR merges.
+- Do not ask reviewers to reason about stacked diffs across multiple open review branches.
+- Before marking a draft PR ready, always verify its base is `main` and its diff is clean.
+
+## Recovery Patterns
+
+- Draft PR shows parent commits after a merge: rebase the branch onto `main`, force-push, then set the base to `main`.
+- Diff is still noisy after retargeting: close and reopen the draft PR instead of fighting bad history.
+- Extra commit landed in the wrong branch: move it with `git cherry-pick` or clean it up with `git rebase -i`, then force-push.
+- Conflict storm: rebase from the next branch to be promoted, not from the top of the old stack.
+
+## Response Shape
+
+When helping the user, produce:
+
+- the branch order
+- the current active PR
+- exact git commands in order
+- any force-push step
+- the GitHub action needed next, such as create draft, change base, or mark ready
+
+Default to this format:
+
+```md
+Stack
+- feat/a -> main, ready for review
+- feat/b -> draft until feat/a merges
+- feat/c -> draft until feat/b merges
+
+Commands
+```bash
+# commands here
+```
+
+GitHub
+- Open `feat/a` against `main`.
+- Keep `feat/b` as draft.
+```
diff --git a/apps/apollo-vertex/app/_meta.ts b/apps/apollo-vertex/app/_meta.ts
index 152665709..133fd3f89 100644
--- a/apps/apollo-vertex/app/_meta.ts
+++ b/apps/apollo-vertex/app/_meta.ts
@@ -6,6 +6,7 @@ export default {
templates: "Templates",
guidelines: "Guidelines",
experiment: "Experiment",
+ "vertex-components": "Vertex Components",
"data-querying": "Data Querying",
localization: "Localization",
mcp: "MCP Server",
diff --git a/apps/apollo-vertex/app/patterns/ai-chat/page.mdx b/apps/apollo-vertex/app/patterns/ai-chat/page.mdx
index c4383df18..31afed000 100644
--- a/apps/apollo-vertex/app/patterns/ai-chat/page.mdx
+++ b/apps/apollo-vertex/app/patterns/ai-chat/page.mdx
@@ -1,12 +1,15 @@
-import { AiChatTemplateLazy } from '@/templates/AiChatTemplateLazy';
+import { AiChatTemplate } from '@/templates/AiChatTemplate';
+import { PreviewFullScreen } from '@/app/_components/preview-full-screen';
# AI Chat
-A composable AI chat UI component for Apollo Vertex. Built with React, TypeScript, and Tailwind CSS. Designed to work with [TanStack AI](https://tanstack.com/ai)
+A composable AI chat UI component for Apollo Vertex. Built with React, TypeScript, and Tailwind CSS. Designed to work with [TanStack AI](https://tanstack.com/ai) — you bring `useChat` and a connection adapter, the component handles the chrome (scroll, input, loading, suggestions, errors) while you control how messages and tool calls render.
-
-
-
+
+
+
+
+> **All visual states and sub-components →** [Component Preview](/vertex-components/ai-chat/preview)
## Features
@@ -14,33 +17,20 @@ A composable AI chat UI component for Apollo Vertex. Built with React, TypeScrip
- **Composable** — `AiChat` is the shell, `AiChatMessage` renders messages, you iterate parts and render tools inline
- **Type-Safe Tool Rendering** — Check `part.name` in the parts loop and TypeScript narrows `part.output` automatically
- **AgentHub Adapter** — Built-in adapter for the UiPath AgentHub normalized LLM endpoint (OpenAI + Anthropic models)
-- **Conversational Agent Adapter** — Built-in adapter for a deployed UiPath Conversational Agent, with session management
- **Markdown Rendering** — Renders assistant responses with GitHub Flavored Markdown
- **Data Fabric Table Tool** — Display entity data as filterable tables with list, search, and range filters, and multi-entity joins
- **Data Fabric Distribution Tool** — Render histogram charts for numeric or datetime fields with optional aggregations, filters, and joins
- **Data Fabric Line Chart Tool** — Render time-series line charts over a datetime field with optional aggregations, filters, and joins
-- **Suggestion Buttons** — Interactive choice buttons rendered from tool results
+- **Error Display** — Inline error banner for API and network errors
+- **i18n Support** — Built-in internationalization via react-i18next
+- **Accessible** — WCAG 2.1 compliant with keyboard navigation and ARIA live regions
## Installation
-Apollo Vertex components are published to a custom shadcn registry under the `@uipath` namespace. Before running the `add` command, register the namespace once in your project's `components.json` so shadcn knows where to fetch from (otherwise the CLI will prompt you for a registry URL):
-
-```json
-{
- "registries": {
- "@uipath": "https://apollo-vertex.vercel.app/r/{name}.json"
- }
-}
-```
-
-Then install the component:
-
```bash
npx shadcn@latest add @uipath/ai-chat
```
-> This `registries` setup is a one-time configuration per project — every `@uipath/*` component on this site uses the same alias.
-
## Quick Start
```tsx
@@ -161,204 +151,24 @@ The `model.vendor` field controls wire-format differences:
- The `X-UiPath-LlmGateway-NormalizedApi-ModelName` header is always sent for routing
- Responses are always OpenAI-compatible SSE regardless of the underlying model
----
-
-## Conversational Agent Adapter
-
-The built-in adapter for a deployed **UiPath Conversational Agent**. It opens a session against the agent, forwards the latest user message, and bridges the agent's streaming response back into TanStack AI `StreamChunk` events.
-
-```tsx
-import { useChat } from '@tanstack/ai-react';
-import { UiPath } from '@uipath/uipath-typescript/core';
-import {
- createConversationalAgentConnection,
- type ConversationalAgentAdapterConfig,
-} from '@/components/ui/ai-chat/adapters/conversational-agent/adapter';
-
-const sdk = new UiPath({ /* baseUrl, accessToken, ... */ });
-
-const connection = createConversationalAgentConnection({
- sdk,
- agentId, // number — the deployed agent id
- folderId, // number — the folder the agent lives in
-});
-
-const { messages, sendMessage, isLoading, stop, clear, error } = useChat({
- connection,
-});
-
-// Dispose the session when the connection is no longer needed
-useEffect(() => () => connection.dispose(), [connection]);
-```
-
-Notes:
-- The adapter manages a single session per connection — call `connection.dispose()` when unmounting, or key the component by agent id so a new connection is created on switch.
-- Tools are driven by the agent itself, not by the client — the `tools` option is not used with this adapter.
-- Only the latest user message is sent per turn; prior history is tracked on the agent server-side.
-
----
-
-## Data Fabric Table Tool
-
-The `data_fabric_table` tool renders entity data as interactive tables powered by `@uipath/apollo-dashboarding`. It supports server-side **filtering** — list filters, text search, and numeric ranges — so users can ask for filtered views directly in the chat.
-
-```tsx
-import {
- createDataFabricTableTool,
- dataFabricTableClient,
-} from '@/components/ui/ai-chat/tools/data-fabric-table';
-
-const tableTool = createDataFabricTableTool({
- entities, // Record — entity metadata with field names and types
- accessToken, // Bearer token for Data Fabric API
- dataFabricBaseUrl, // Base URL for Data Fabric proxy
-});
-
-// Use dataFabricTableClient in your tools array, tableTool.toolPrompt in your system prompt,
-// and tableTool.renderTable(part.output, part.id) in your parts loop.
-```
-
-### Filter types
-
-The LLM can pass filters based on the user's request:
-
-- **List filter** — match or exclude specific values: `"show invoices where Status is Pending"`
-- **Search filter** — text pattern matching (contains, startsWith, endsWith): `"find customers starting with A"`
-- **Range filter (numeric)** — numeric min/max: `"show orders over $200"`
-- **Range filter (datetime)** — ISO 8601 min/max: `"show orders from the last 30 days"`. The tool prompt is given today's date so the LLM can resolve relative phrases into absolute ISO dates before calling the tool.
-
-Filters are passed through the table configuration to `@uipath/apollo-dashboarding`, which translates them to Data Fabric query filters server-side.
-
-### Multi-entity joins
-
-The tool can combine data from related entities via the `joins` argument. The `entityName` field is the primary entity, and each join supplies the entity to attach and an `on` clause with `EntityName.FieldName` references:
-
-```json
-{
- "entityName": "Invoice",
- "dimensions": ["Invoice.Number", "Invoice.Total", "Customer.Name"],
- "joins": [
- {
- "type": "LEFT",
- "entity": "Customer",
- "on": { "left": "Invoice.CustomerId", "right": "Customer.Id" }
- }
- ]
-}
-```
-
-When joins are present, dimensions and filter fields must use qualified `EntityName.FieldName` names (using the exact entity names from the Entity Reference — never aliases). The join condition goes in `joins[].on`; don't also add it as a filter.
+## Error Display
----
-
-## Data Fabric Distribution Tool
-
-The `data_fabric_distribution` tool renders a histogram from a Data Fabric entity by binning a single numeric or datetime field. It shares the filter and join system with the table tool, and adds an optional aggregation metric.
-
-```tsx
-import {
- createDataFabricDistributionTool,
- dataFabricDistributionClient,
-} from '@/components/ui/ai-chat/tools/data-fabric-distribution';
-
-const distributionTool = createDataFabricDistributionTool({
- entities, // Record — entity metadata with field names and types
- accessToken, // Bearer token for Data Fabric API
- dataFabricBaseUrl, // Base URL for Data Fabric proxy
-});
-
-// Use dataFabricDistributionClient in your tools array, distributionTool.toolPrompt in your
-// system prompt, and distributionTool.renderDistribution(part.output, part.id) in your parts loop.
-```
-
-### Dimension
-
-The `dimension` is the field used for binning and must be **numeric** or **datetime**:
-
-- Datetime dimensions bin by time (e.g. orders per month).
-- Numeric dimensions bin by value range.
-
-### Metric
-
-Omit `metric` entirely for the default `COUNT` of records per bin. To plot an aggregated numeric field, pass `{ aggregation, field }`:
-
-- `COUNT` — records per bin (default; `field` optional, picks the primary key).
-- `SUM`, `AVG`, `MIN`, `MAX` — applied to a numeric `field`.
-
-```json
-{
- "entityName": "Order",
- "dimension": "OrderDate",
- "metric": { "aggregation": "SUM", "field": "Total" }
-}
-```
-
-### Filters and joins
-
-Filters and joins use the same schemas as the table tool (including the new datetime range filter). When joins are present, the `dimension` and `metric.field` must use qualified `EntityName.FieldName` names.
-
----
-
-## Data Fabric Line Chart Tool
-
-The `data_fabric_line` tool renders a line chart from a Data Fabric entity, plotting a metric over a **datetime** dimension. It shares the metric, filter, and join system with the distribution tool — the only constraint is that the dimension must be a datetime field.
+Pass an `Error` object to show an inline error banner:
```tsx
-import {
- createDataFabricLineTool,
- dataFabricLineClient,
-} from '@/components/ui/ai-chat/tools/data-fabric-line';
-
-const lineTool = createDataFabricLineTool({
- entities, // Record — entity metadata with field names and types
- accessToken, // Bearer token for Data Fabric API
- dataFabricBaseUrl, // Base URL for Data Fabric proxy
-});
-
-// Use dataFabricLineClient in your tools array, lineTool.toolPrompt in your
-// system prompt, and lineTool.renderLine(part.output, part.id) in your parts loop.
+ sendMessage(text)}
+ onStop={stop}
+ error={error}
+>
+ {messages.map((message) => (
+
+ ))}
+
```
-### When to use line vs distribution
-
-- **Line** — trend / time-series questions: *"orders over time"*, *"revenue trend by month"*, *"growth across quarters"*. Always datetime on the X axis.
-- **Distribution** — histogram-style requests: *"distribution of order amount"*, *"histogram of X"*, numeric value-range binning. Either numeric or datetime dimension.
-
-### Metric, filters, and joins
-
-Metric, filter, and join semantics are identical to the distribution tool. Omit `metric` for `COUNT`, or pass `{ aggregation, field }` for `SUM` / `AVG` / `MIN` / `MAX` of a numeric field. When joins are present, `dimension` and `metric.field` must use qualified `EntityName.FieldName` names.
-
----
-
-## Suggestion Buttons
-
-The `presentChoices` tool renders interactive suggestion buttons. Define the tool with a Zod schema, and render choices inline in the parts loop:
-
-```tsx
-import {
- presentChoicesClient,
- renderChoices,
- CHOICES_TOOL_PROMPT,
-} from '@/components/ui/ai-chat/tools/choices';
-
-// Add presentChoicesClient to your tools array and CHOICES_TOOL_PROMPT to your system prompt.
-
-// In your parts loop:
-{message.parts.map((part) => {
- if (part.type === 'tool-call' && part.name === 'presentChoices' && part.output) {
- return (
-
diff --git a/apps/apollo-vertex/templates/ai-chat/AiChatLoginGate.tsx b/apps/apollo-vertex/templates/ai-chat/AiChatLoginGate.tsx
index c14afab29..d3b73a33d 100644
--- a/apps/apollo-vertex/templates/ai-chat/AiChatLoginGate.tsx
+++ b/apps/apollo-vertex/templates/ai-chat/AiChatLoginGate.tsx
@@ -1,9 +1,9 @@
"use client";
+import { useLocalStorage } from "@mantine/hooks";
import { useQuery } from "@tanstack/react-query";
import { jwtDecode } from "jwt-decode";
import { ChevronRight, LogIn, LogOut } from "lucide-react";
-import { useLocalStorage } from "@mantine/hooks";
import type { ReactNode } from "react";
import { z } from "zod";
import { Button } from "@/components/ui/button";
@@ -194,7 +194,7 @@ export function AiChatLoginGate({ children }: AiChatLoginGateProps) {
if (isLoading) {
return (
-
+
Signing in...
);
@@ -202,7 +202,7 @@ export function AiChatLoginGate({ children }: AiChatLoginGateProps) {
if (!isAuthenticated) {
return (
-
+
Sign in to UiPath to use the AI Chat demo
@@ -221,7 +221,7 @@ export function AiChatLoginGate({ children }: AiChatLoginGateProps) {
if (isOrgError) {
return (
-
+
We couldn't load your organization info. Please try signing out
and signing back in.
@@ -233,7 +233,7 @@ export function AiChatLoginGate({ children }: AiChatLoginGateProps) {
if (isOrgLoading || !accessToken) {
return (
-
+
Loading organization info...
);
@@ -241,7 +241,7 @@ export function AiChatLoginGate({ children }: AiChatLoginGateProps) {
if (!orgTenant || !orgInfo) {
return (
-
+
Unable to resolve your organization. Please try signing out and
signing back in.
@@ -252,8 +252,8 @@ export function AiChatLoginGate({ children }: AiChatLoginGateProps) {
}
return (
-