From 218bc98478da49aa5cda6ee268eb21c0de39603e Mon Sep 17 00:00:00 2001 From: Alem Tuzlak Date: Thu, 30 Jul 2026 18:51:06 +0200 Subject: [PATCH 1/4] feat(ai): position landing page as the headless agent framework Reframe the TanStack AI landing copy around the agent loop and a bring-your-own-stack story, grounded in what the tanstack/ai repo actually ships rather than the old generic "AI SDK" framing. - Hero and library descriptions lead with the headless agent framework positioning: the loop, its parts, and the absence of a hosted gateway or proprietary wire format. - Retitle the tools section to the agent loop and cover stop strategies and needsApproval interrupts that resume on a stateless server. - Name all eleven official provider adapters plus openaiCompatible, and describe per-model type narrowing concretely. - Add a section for the rest of the agent stack: Code Mode isolates, coding-agent harnesses, MCP and MCP Apps, memory and persistence. - Expand the modality rail with summarize, music, diarization, and the async video job lifecycle; generalize ModalityRail into FeatureRail so both rails share one component. - Hero graph: eight client bindings on a 4x2 grid, a shuffling server/runtime row so Python and Go read as first-class AG-UI targets, and the runtime tile follows it. - Use white text on a darkened accent fill for the TanStack pills and user chat bubbles; the shared --landing-accent-ink is pure black and read badly on orange in light mode. Register the Angular bindings, which ship upstream as @tanstack/ai-angular (published, 0.3.1) but were missing from the site config, and update ai-framework-doc-links to match. Octane is named in prose only: its bindings ship as @octanejs/tanstack-ai rather than a @tanstack package, so the framework picker would need a logo asset and a Framework union entry. Prose avoids em-dashes throughout. --- src/components/landing/AiLanding.tsx | 236 ++++++++++++++++++--------- src/libraries/ai.tsx | 37 +++-- src/libraries/libraries.ts | 17 +- tests/ai-framework-doc-links.test.ts | 3 + 4 files changed, 198 insertions(+), 95 deletions(-) diff --git a/src/components/landing/AiLanding.tsx b/src/components/landing/AiLanding.tsx index 4fdda9f87..92dc76dd3 100644 --- a/src/components/landing/AiLanding.tsx +++ b/src/components/landing/AiLanding.tsx @@ -1,10 +1,14 @@ import * as React from 'react' import { BracketsCurly, + Code, + Cube, + Database, Microphone, Plug, Radio, Robot, + Terminal, Waveform, type Icon, } from '@phosphor-icons/react' @@ -17,12 +21,18 @@ import { } from './LibraryLanding' const aiPrompt = [ - 'Build a TanStack AI feature for a TypeScript app.', - 'Use the headless client or framework adapter, AG-UI-compatible request and event streams, provider adapters, typed client and server tools, structured output, and observable runtime state without requiring a hosted gateway.', - 'Show provider-specific capabilities honestly, make tool approval explicit, and include media or realtime primitives only where the selected model supports them.', + 'Build an agent with TanStack AI, the headless agent framework for TypeScript.', + 'Drive the agent loop with chat(): isomorphic tools via toolDefinition().server() / .client(), composable (state) => boolean stop strategies, needsApproval interrupts resolved on the client, and native AG-UI request and event streams consumed by the headless client or a framework adapter.', + 'Reach for the rest of the stack only when the task needs it: Code Mode in an isolate for multi-tool orchestration, a sandboxed coding-agent harness, @tanstack/ai-mcp for MCP servers, memoryMiddleware for cross-session recall, @tanstack/ai-persistence for durable threads and resumable streams.', + 'Never introduce a hosted gateway, a prescribed UI kit, or a provider-specific wire format. Keep provider capabilities honest: model options, tool support, and modality-specific results stay typed at the adapter boundary, and media or realtime primitives appear only where the selected model supports them.', ].join(' ') const providers = [ + { + name: 'OpenRouter', + model: 'any of 300+ models', + capabilities: ['text', 'reasoning', 'tools', 'image'], + }, { name: 'OpenAI', model: 'gpt-5', @@ -65,27 +75,35 @@ type GraphPoint = { y: number } -const aiHeroClients = ['Vanilla', 'React', 'Vue', 'Solid', 'Svelte', 'Preact'] +const aiHeroClients = [ + 'Vanilla', + 'React', + 'Vue', + 'Solid', + 'Svelte', + 'Preact', + 'Angular', + 'Octane', +] const aiHeroServers: Array = [ - { label: 'TanStack AI', detail: 'TypeScript', kind: 'tanstack' }, + { label: 'TanStack AI', detail: 'Server', kind: 'tanstack' }, { label: 'Python', dotted: true }, { label: 'Go', dotted: true }, { label: 'PHP', dotted: true }, ] const aiHeroProviders = ['OpenRouter', 'OpenAI', 'Anthropic', 'Gemini'] +// ponytail: 8 clients on a fixed 4x2 grid; recompute the columns if the list changes length const graphClientNodes = aiHeroClients.map((label, index) => ({ label, - x: [34, 154, 274][index % 3] ?? 154, - y: index < 3 ? 44 : 90, - width: 86, - height: 34, + x: [10, 112, 214, 316][index % 4] ?? 112, + y: index < 4 ? 36 : 84, + width: 94, + height: 36, })) const graphAgUiNode: GraphNodePosition & { - detail: string kind: 'tanstack' } = { label: 'TanStack AI Client', - detail: 'AG-UI', kind: 'tanstack', x: 142, y: 138, @@ -108,22 +126,27 @@ const graphProviderNodes = aiHeroProviders.map((label, index) => ({ })) const aiHeroMessages = [ { - user: 'Build the invoice assistant on our stack.', + user: 'Build the invoice agent on our stack, not yours.', assistant: - 'Using TanStack AI for TypeScript, AG-UI events, a server tool for invoices, and OpenRouter for model routing.', + 'Done. Headless client in your app, the agent loop on your server, AG-UI between them. No gateway, no hosted state.', }, { - user: 'Ask before it charges a card.', + user: 'It should ask before it charges a card.', assistant: - 'Tool approval added. The UI will pause on chargeCard until your app confirms it.', + 'chargeCard is marked needsApproval, so the run ends as an interrupt. Resolve it and the loop continues from that exact step.', }, { - user: 'Can we switch providers later?', + user: 'And if we move off this provider?', assistant: - 'Yes. The provider stays behind an adapter; the app keeps the same event stream and typed tools.', + 'Swap the adapter. Your tools, events, and UI never learn the difference.', }, ] +// ponytail: the shared --landing-accent-ink is pure black, which reads badly on the +// orange accent fill. Darken the fill instead and use white text on it. +const accentFillClass = + 'bg-[linear-gradient(135deg,color-mix(in_srgb,var(--landing-accent)_84%,black),color-mix(in_srgb,var(--landing-accent)_52%,black))] text-white' + type AiHeroChatMessage = { assistant: string id: string @@ -135,8 +158,8 @@ export default function AiLanding() { return ( } prompt={aiPrompt} promptLabel="Copy AI prompt" @@ -144,10 +167,10 @@ export default function AiLanding() {
@@ -159,8 +182,8 @@ export default function AiLanding() {
@@ -170,21 +193,33 @@ export default function AiLanding() { centered eyebrow="AG-UI both ways" icon={
@@ -193,12 +228,13 @@ export default function AiLanding() { function AiGraphChatHero() { const [activeClient, setActiveClient] = React.useState(0) + const [activeServer, setActiveServer] = React.useState(0) const [activeProvider, setActiveProvider] = React.useState(0) const [chatMessages, setChatMessages] = React.useState< Array >([]) const [typingUserMessage, setTypingUserMessage] = React.useState('') - const primaryServerNode = graphServerNodes[0] + const activeServerNode = graphServerNodes[activeServer] ?? graphServerNodes[0] const chatScrollRef = React.useRef(null) const chatLockedToBottomRef = React.useRef(true) @@ -210,12 +246,16 @@ function AiGraphChatHero() { const clientIntervalId = window.setInterval(() => { setActiveClient((current) => (current + 1) % aiHeroClients.length) }, 2300) + const serverIntervalId = window.setInterval(() => { + setActiveServer((current) => (current + 1) % aiHeroServers.length) + }, 3300) const providerIntervalId = window.setInterval(() => { setActiveProvider((current) => (current + 1) % aiHeroProviders.length) }, 4100) return () => { window.clearInterval(clientIntervalId) + window.clearInterval(serverIntervalId) window.clearInterval(providerIntervalId) } }, []) @@ -379,9 +419,9 @@ function AiGraphChatHero() { return (
- A client graph shows six UI adapters converging on the TanStack AI - Client over AG-UI, then reaching a TypeScript runtime and - interchangeable model providers. + A client graph shows eight UI adapters converging on the TanStack AI + Client over AG-UI, then reaching an agent runtime in TypeScript, Python, + Go, or PHP, and interchangeable model providers. @@ -408,7 +448,7 @@ function AiGraphChatHero() { {graphServerNodes.map((node, index) => ( ( {chatMessages.map((message) => ( -
+
{message.user}
{message.assistant || message.isStreaming ? ( @@ -506,7 +547,10 @@ function AiGraphChatHero() { ['event', 'text content'], ['tool', 'approval gate'], ['provider', aiHeroProviders[activeProvider]], - ['runtime', 'TanStack AI'], + [ + 'runtime', + aiHeroServers[activeServer]?.label ?? 'TanStack AI', + ], ].map(([label, value]) => (
{boundary === 'client' - ? 'Runs beside the UI and can update local application state.' - : 'Runs behind your server boundary with private credentials and data.'} + ? 'Runs beside the UI and can update local application state. The loop waits for it and feeds the result back to the model.' + : 'Runs behind your server boundary with private credentials and data. The model never sees them.'}

@@ -760,8 +804,9 @@ function ProviderWorkbench() { )}

- Adapter-specific types expose the options and outputs available for - this model. + Types narrow to this exact model: its options, its capabilities, its + input modalities. Pass an image to a text-only model and it fails at + compile time, not in production.

@@ -773,7 +818,7 @@ function ProtocolMap() { const nodes = [ ['UI', 'headless client'], ['AG-UI', 'request + events'], - ['Runtime', 'your server'], + ['Agent loop', 'your server'], ['Provider', 'typed adapter'], ] @@ -799,49 +844,88 @@ function ProtocolMap() { ) } -function ModalityRail() { - const modalities: Array<{ detail: string; icon: Icon; label: string }> = [ - { - label: 'Text + objects', - detail: 'chat · outputSchema', - icon: Robot, - }, - { - label: 'Speech + transcription', - detail: 'generateSpeech · generateTranscription', - icon: Microphone, - }, - { - label: 'Realtime voice', - detail: 'realtimeToken · RealtimeClient', - icon: Waveform, - }, - { - label: 'Images + video', - detail: 'generateImage · generateVideo', - icon: Radio, - }, - ] +type RailItem = { + body: string + detail: string + icon: Icon + label: string +} + +const agentStack: Array = [ + { + label: 'Code Mode', + detail: '@tanstack/ai-code-mode', + body: 'The model writes one TypeScript program that calls your tools with loops and Promise.all, instead of a round trip per call. It runs in a V8 isolate, QuickJS WASM, or a Cloudflare Worker, with no host filesystem, network, or process.', + icon: Code, + }, + { + label: 'Coding-agent harnesses', + detail: '@tanstack/ai-sandbox', + body: 'Run Claude Code, Codex, OpenCode, Grok Build, or any ACP agent as a chat backend, inside a local process, Docker, Daytona, Vercel, Sprites, or Cloudflare sandbox. Their tool activity streams back as AG-UI events your UI already renders.', + icon: Terminal, + }, + { + label: 'MCP + MCP Apps', + detail: '@tanstack/ai-mcp', + body: 'A host-side MCP client with a type-generating CLI, provider-routed mcpTool(), and interactive ui:// widgets rendered from tool results across multiple servers.', + icon: Cube, + }, + { + label: 'Memory + persistence', + detail: '@tanstack/ai-memory · -persistence', + body: 'memoryMiddleware recalls across sessions through Redis, mem0, Honcho, or Hindsight adapters. Persistence keeps an authoritative server thread, resumes a stream through a dropped connection, and survives a reload.', + icon: Database, + }, +] +const modalities: Array = [ + { + label: 'Text, objects, reasoning', + detail: 'chat · outputSchema · summarize', + body: 'Structured output streams as a typed message part beside tool calls and is preserved per turn in history, not a separate one-shot call.', + icon: Robot, + }, + { + label: 'Speech, transcription, music', + detail: 'generateSpeech · generateTranscription · generateAudio', + body: 'Six speech formats with speed control, transcription with word timestamps and diarization, plus music and sound effects.', + icon: Microphone, + }, + { + label: 'Realtime voice', + detail: 'realtimeToken · RealtimeClient', + body: 'OpenAI, Grok, and ElevenLabs with VAD modes and tool calling inside a live session.', + icon: Waveform, + }, + { + label: 'Images + video', + detail: 'generateImage · generateVideo', + body: 'Per-model typed options across OpenAI, Gemini, Grok, OpenRouter, and fal.ai, with an async job lifecycle for video.', + icon: Radio, + }, +] + +function FeatureRail({ items }: { items: Array }) { return (
- {modalities.map((modality, index) => { - const Icon = modality.icon + {items.map((item, index) => { + const Icon = item.icon return (
-

- {modality.label} -

+

{item.label}

- {modality.detail} + {item.detail} +

+

+ {item.body}

diff --git a/src/libraries/ai.tsx b/src/libraries/ai.tsx index 64f98babe..c84d1bd44 100644 --- a/src/libraries/ai.tsx +++ b/src/libraries/ai.tsx @@ -7,39 +7,46 @@ const textStyles = `text-category-data` export const aiProject = { ...ai, - description: `A powerful, open-source AI SDK with a unified interface across multiple providers. No vendor lock-in, no proprietary formats, just clean TypeScript and honest open source.`, + description: `The headless agent framework for TypeScript. TanStack AI runs the agent loop as typed primitives you compose yourself: tool calls, reasoning, human-in-the-loop interrupts, memory, and streaming state. Bring your own UI framework, model provider, server, and transport. Native AG-UI over the wire, MIT licensed, no hosted gateway and no platform to buy into.`, latestBranch: 'main', defaultDocs: 'getting-started/overview', featureHighlights: [ { - title: 'Provider Agnostic', - icon: , + title: 'A Real Agent Loop', + icon: , description: (
- Official adapters for OpenRouter, OpenAI, Anthropic, Gemini, Ollama, - Groq, Grok/xAI, ElevenLabs, and fal.ai. Import only the adapters your - app needs. + chat() drives the loop and you control every part of it: + isomorphic tools you place on the client or the server, composable{' '} + {`(state) => boolean`} stop strategies, and interrupts + that pause a run for human approval and resume exactly where it + stopped, with no database required.
), }, { - title: 'AG-UI Native Clients', - icon: , + title: 'Bring Your Own Everything', + icon: , description: (
- A headless client plus React, Vue, Solid, Svelte, and Preact bindings - all speak the same AG-UI request and event protocol. + Your provider, server, transport, auth, and deploy target. Adapters + for OpenRouter, OpenAI, Anthropic, Gemini, Bedrock, Mistral, Groq, + Grok/xAI, Ollama, ElevenLabs, and fal.ai, plus{' '} + openaiCompatible for anything else. Import only what you + use: every activity is a separate, tree-shakeable module.
), }, { - title: 'Typed Tools & Media', - icon: , + title: 'Headless, Not Opinionated', + icon: , description: (
- Type-safe client/server tools, provider-native tools, structured - output, reasoning streams, image, speech, transcription, realtime - voice, and video generation. + A framework-free core with React, Vue, Solid, Svelte, Preact, Angular, + and React Native bindings on top, plus official Octane bindings from + the Octane team. All of them speak native AG-UI over SSE, HTTP + streams, XHR, RPC, or your own transport. No components to fight, no + styles to override.
), }, diff --git a/src/libraries/libraries.ts b/src/libraries/libraries.ts index 0b27366b3..09bb239b2 100644 --- a/src/libraries/libraries.ts +++ b/src/libraries/libraries.ts @@ -650,13 +650,20 @@ export const ai: LibrarySlim = { ...categoryStyles.data, name: 'TanStack AI', to: '/ai', - tagline: - 'A powerful, open-source AI SDK with a unified interface across multiple providers', + tagline: 'The headless agent framework for TypeScript. Bring your own stack', description: - 'A powerful, open-source AI SDK with a unified interface across multiple providers. No vendor lock-in, no proprietary formats, just clean TypeScript and honest open source.', + 'The headless agent framework for TypeScript. TanStack AI runs the agent loop as typed primitives you compose yourself: tool calls, reasoning, human-in-the-loop interrupts, memory, and streaming state. Eleven provider adapters, seven UI framework bindings, sandboxed code execution, MCP, and coding-agent harnesses behind one interface. Native AG-UI over the wire, MIT licensed, no hosted gateway and no platform to buy into.', badge: 'beta', repo: 'tanstack/ai', - frameworks: ['react', 'vue', 'solid', 'svelte', 'preact', 'vanilla'], + frameworks: [ + 'react', + 'vue', + 'solid', + 'svelte', + 'preact', + 'angular', + 'vanilla', + ], corePackageName: '@tanstack/ai-client', npmPackageNames: ['@tanstack/ai-client'], latestVersion: 'v0', @@ -669,6 +676,7 @@ export const ai: LibrarySlim = { solid: '@tanstack/ai-solid', svelte: '@tanstack/ai-svelte', preact: '@tanstack/ai-preact', + angular: '@tanstack/ai-angular', vanilla: '@tanstack/ai-client', }, frameworkDocs: { @@ -677,6 +685,7 @@ export const ai: LibrarySlim = { solid: 'api/ai-solid', svelte: 'getting-started/quick-start-svelte', preact: 'api/ai-preact', + angular: 'getting-started/quick-start-angular', vanilla: 'api/ai-client', }, sitemap: { diff --git a/tests/ai-framework-doc-links.test.ts b/tests/ai-framework-doc-links.test.ts index 846ea331f..bab7cc194 100644 --- a/tests/ai-framework-doc-links.test.ts +++ b/tests/ai-framework-doc-links.test.ts @@ -12,6 +12,7 @@ const expectedFrameworks: Framework[] = [ 'solid', 'svelte', 'preact', + 'angular', 'vanilla', ] @@ -21,6 +22,7 @@ const expectedPackages: Partial> = { solid: '@tanstack/ai-solid', svelte: '@tanstack/ai-svelte', preact: '@tanstack/ai-preact', + angular: '@tanstack/ai-angular', vanilla: '@tanstack/ai-client', } @@ -30,6 +32,7 @@ const expectedDocsPaths: Partial> = { solid: 'api/ai-solid', svelte: 'getting-started/quick-start-svelte', preact: 'api/ai-preact', + angular: 'getting-started/quick-start-angular', vanilla: 'api/ai-client', } From 12f8e979e74fd21c8ed9267111b154ec440999d2 Mon Sep 17 00:00:00 2001 From: Alem Tuzlak Date: Thu, 30 Jul 2026 19:56:28 +0200 Subject: [PATCH 2/4] feat(ai): add quick start, devtools, and a three-way comparison Three sections the landing page was missing, each grounded in APIs and facts checked against source rather than written from memory. Quick start: the page never showed any code. A server route and a client hook now sit side by side, using the real signatures from the docs, including useChat({ connection: fetchServerSentEvents(...) }) rather than an invented options shape. A caption notes the server route is identical across every binding. Devtools: previously four words inside a paragraph. Now a mock panel in the page's own idiom, showing the hook dashboard and a run timeline that ends on an interrupt with a resumable run, which ties back to the interrupts claim earlier on the page. Comparison: a three-way table against the Vercel AI SDK and Mastra, framed as tradeoffs so each column has a real answer. Rows cover the wire protocol, client bindings, where the agent runs, whether a hosted service sits in the loop, and licensing. Verified from source: @mastra/core 1.56 is Apache 2.0 with ee/ carved out and ships React plus a framework-free client against its own Hono server; ai 7.0 is Apache 2.0 with React, Vue, Svelte, and Angular bindings. Deliberately excluded from the table: code mode, sandboxed harnesses, MCP, and memory. The in-repo comparison doc treats some of those as ours alone, but vercel/ai now ships code-mode, harness-claude-code, harness-codex, harness-opencode, sandbox-vercel, and devtools packages, so claiming them would be wrong. The footnote says as much and gives the versions and date it was checked. Code identifiers in the always-dark code surface use fixed token colors; --landing-accent-bright resolves to a dark terracotta in light mode and was unreadable there. Both quick-start windows flex so their code panes match height. --- src/components/landing/AiLanding.tsx | 368 ++++++++++++++++++++++++++- 1 file changed, 367 insertions(+), 1 deletion(-) diff --git a/src/components/landing/AiLanding.tsx b/src/components/landing/AiLanding.tsx index 92dc76dd3..c35d04ab8 100644 --- a/src/components/landing/AiLanding.tsx +++ b/src/components/landing/AiLanding.tsx @@ -1,6 +1,7 @@ import * as React from 'react' import { BracketsCurly, + Bug, Code, Cube, Database, @@ -8,6 +9,7 @@ import { Plug, Radio, Robot, + Scales, Terminal, Waveform, type Icon, @@ -165,6 +167,17 @@ export default function AiLanding() { promptLabel="Copy AI prompt" > + + +
- +
+ + +
+
+
+ + + ) } +function CodeLine({ + children, + indent = 0, +}: { + children?: React.ReactNode + indent?: number +}) { + return

{children || ' '}

+} + +function Kw({ children }: { children: React.ReactNode }) { + return {children} +} + +// ponytail: the code surface is always dark, so these use fixed token colors. +// --landing-accent-bright resolves to a dark terracotta in light mode and is +// unreadable here. +function Fn({ children }: { children: React.ReactNode }) { + return {children} +} + +function Str({ children }: { children: React.ReactNode }) { + return {children} +} + +function Cmt({ children }: { children: React.ReactNode }) { + return {children} +} + +function CodeSurface({ children }: { children: React.ReactNode }) { + return ( +
+ {children} +
+ ) +} + +function QuickStart() { + return ( +
+ + + + import {'{ chat, toServerSentEventsResponse }'}{' '} + from '@tanstack/ai' + + + import {'{ openrouterText }'} from{' '} + '@tanstack/ai-openrouter' + + + + export async function POST(request: Request) {'{'} + + + const {'{ messages }'} = await request. + json() + + + + const stream = chat({'{'} + + + adapter: openrouterText( + 'anthropic/claude-sonnet-4.5'), + + messages, + tools: [lookupInvoice], + {'})'} + + + // your route, your auth, your deploy target + + + return toServerSentEventsResponse(stream) + + {'}'} + + + + + + + import {'{ useChat, fetchServerSentEvents }'} from{' '} + '@tanstack/ai-react' + + + + export function Chat() {'{'} + + + const {'{ messages, sendMessage, interrupts }'} ={' '} + useChat({'{'} + + + connection: fetchServerSentEvents('/api/chat'), + + {'})'} + + + // typed state and events. no components, no styles. + + + return messages.map((message) => ( + + + <Bubble key={'{'}message.id{'}'} {'{'}...message{'}'}{' '} + /> + + )) + {'}'} + + + +

+ Swap ai-react for ai-vue, ai-solid, ai-svelte, ai-preact, ai-angular, or + the framework-free ai-client. The server route never changes. +

+
+ ) +} + function AiGraphChatHero() { const [activeClient, setActiveClient] = React.useState(0) const [activeServer, setActiveServer] = React.useState(0) @@ -905,6 +1063,214 @@ const modalities: Array = [ }, ] +const devtoolsHooks = [ + { detail: 'useChat · 12 msgs', name: 'Support Chat', selected: true }, + { detail: 'useGenerateImage', name: 'Image Studio' }, + { detail: 'useObject', name: 'Invoice Extract' }, + { detail: 'useTranscription', name: 'Call Notes' }, +] + +const devtoolsTimeline: Array<{ + detail: string + label: string + tone: 'accent' | 'muted' | 'warn' +}> = [ + { + label: 'user turn', + detail: '"refund the duplicate charge"', + tone: 'muted', + }, + { + label: 'memory recall', + detail: '3 facts injected · 214 tokens', + tone: 'accent', + }, + { + label: 'tool call', + detail: 'lookupInvoice { id: "inv_8841" }', + tone: 'accent', + }, + { + label: 'tool result', + detail: '{ total: 4200, status: "paid" }', + tone: 'accent', + }, + { + label: 'interrupt', + detail: 'chargeCard · awaiting approval', + tone: 'warn', + }, + { + label: 'finish reason', + detail: 'interrupt · run resumable', + tone: 'muted', + }, +] + +function DevtoolsPanel() { + return ( + +
+
+

+ hooks +

+ {devtoolsHooks.map((hook) => ( +
+

+ {hook.name} +

+

+ {hook.detail} +

+
+ ))} +
+ +
+
+

+ run timeline +

+

+ thread_7f2 · run_3 +

+
+
+ {devtoolsTimeline.map((event) => ( +
+ + {event.label} + + + {event.detail} + +
+ ))} +
+
+
+
+ ) +} + +const comparisonColumns = ['TanStack AI', 'Vercel AI SDK', 'Mastra'] +const comparisonRows: Array<{ label: string; values: Array }> = [ + { + label: 'Protocol to your UI', + values: [ + 'AG-UI, both directions', + 'Its own UI message stream', + 'Its own HTTP API, AG-UI via CopilotKit', + ], + }, + { + label: 'Client bindings', + values: [ + 'React, Vue, Solid, Svelte, Preact, Angular, plus a framework-free core', + 'React, Vue, Svelte, Angular', + 'React, plus a framework-free client', + ], + }, + { + label: 'Where the agent runs', + values: [ + 'Any server you already have', + 'Any server you already have', + 'Its own Hono server, or embedded in your app', + ], + }, + { + label: 'Hosted service in the loop', + values: ['None', 'Optional Vercel platform', 'Optional Mastra platform'], + }, + { + label: 'License', + values: ['MIT', 'Apache 2.0', 'Apache 2.0, with ee/ under separate terms'], + }, +] + +function ComparisonTable() { + return ( +
+
+ + + + + {comparisonColumns.map((column, index) => ( + + ))} + + + + {comparisonRows.map((row) => ( + + + {row.values.map((value, index) => ( + + ))} + + ))} + +
+ Dimension + + {column} +
+ {row.label} + + {value} +
+
+

+ Checked against @tanstack/ai 0.42, ai 7.0, and @mastra/core 1.56 in July + 2026. All three ship typed tools, MCP, memory, sandboxed code execution, + and coding-agent harnesses, so those are not differentiators. If we have + a detail wrong, tell us and we will fix it. +

+
+ ) +} + function FeatureRail({ items }: { items: Array }) { return (
From a14926dc933b7b2c2fec60ee7f493e5826f601b8 Mon Sep 17 00:00:00 2001 From: Alem Tuzlak Date: Thu, 30 Jul 2026 20:09:21 +0200 Subject: [PATCH 3/4] fix(ai): drop comparison section, correct the code snippets Remove the three-way comparison section, its table component, and its data for now, along with the Scales icon import. Fix two wrong identifiers in the quick start. The OpenRouter adapter exports openRouterText, not openrouterText. Realtime tokens are per-provider (openaiRealtimeToken, elevenlabsRealtimeToken, grokRealtimeToken, geminiRealtimeToken); there is no bare realtimeToken export, so the modality rail now names the OpenAI one. Rewrite the server snippet as a TanStack Start route using createFileRoute with server.handlers.POST, matching the shape in the quick-start docs, rather than a bare framework-agnostic POST function. Extend the client snippet to resolve an interrupt with interrupt.resolveInterrupt(true), keyed on interrupt.id and labelled from interrupt.toolName, all verified against docs/interrupts/tool-approval. This also balances the two panes, which the taller server route had left lopsided. Every other identifier on the page was audited against exports in tanstack/ai origin/main: chat, toolDefinition, openaiText, openaiCompatible, summarize, generateSpeech, generateTranscription, generateAudio, generateImage, generateVideo, RealtimeClient, memoryMiddleware, mcpTool, maxIterations, and untilFinishReason all resolve. --- src/components/landing/AiLanding.tsx | 184 +++++++++------------------ 1 file changed, 62 insertions(+), 122 deletions(-) diff --git a/src/components/landing/AiLanding.tsx b/src/components/landing/AiLanding.tsx index c35d04ab8..1c59db51b 100644 --- a/src/components/landing/AiLanding.tsx +++ b/src/components/landing/AiLanding.tsx @@ -9,7 +9,6 @@ import { Plug, Radio, Robot, - Scales, Terminal, Waveform, type Icon, @@ -247,17 +246,6 @@ export default function AiLanding() {
- - - ) } @@ -302,43 +290,59 @@ function CodeSurface({ children }: { children: React.ReactNode }) { function QuickStart() { return (
- + import {'{ chat, toServerSentEventsResponse }'}{' '} from '@tanstack/ai' - import {'{ openrouterText }'} from{' '} + import {'{ openRouterText }'} from{' '} '@tanstack/ai-openrouter' + + import {'{ createFileRoute }'} from{' '} + '@tanstack/react-router' + - export async function POST(request: Request) {'{'} + export const Route = createFileRoute( + '/api/chat')({'{'} - + server: {'{'} + handlers: {'{'} + + POST: async ({'{ request }'}) => {'{'} + + const {'{ messages }'} = await request. json() - + const stream = chat({'{'} - - adapter: openrouterText( + + adapter: openRouterText( 'anthropic/claude-sonnet-4.5'), - messages, - tools: [lookupInvoice], - {'})'} + messages, + tools: [lookupInvoice], + {'})'} - + // your route, your auth, your deploy target - + return toServerSentEventsResponse(stream) - {'}'} + {'},'} + {'},'} + {'},'} + {'})'} @@ -365,13 +369,42 @@ function QuickStart() { // typed state and events. no components, no styles. - return messages.map((message) => ( + return ( - + <> + + {'{'}messages.map((message) => ( + + <Bubble key={'{'}message.id{'}'} {'{'}...message{'}'}{' '} /> - )) + )){'}'} + + + + {'{/* the loop paused. you decide when it continues. */}'} + + + + {'{'}interrupts.map((interrupt) => ( + + + <button key={'{'}interrupt.id{'}'} + + + onClick={'{'}() => interrupt.resolveInterrupt( + true){'}'}> + + + Approve {'{'}interrupt.toolName{'}'} + + + </button> + + )){'}'} + </> + ) {'}'} @@ -1051,7 +1084,7 @@ const modalities: Array = [ }, { label: 'Realtime voice', - detail: 'realtimeToken · RealtimeClient', + detail: 'openaiRealtimeToken · RealtimeClient', body: 'OpenAI, Grok, and ElevenLabs with VAD modes and tool calling inside a live session.', icon: Waveform, }, @@ -1178,99 +1211,6 @@ function DevtoolsPanel() { ) } -const comparisonColumns = ['TanStack AI', 'Vercel AI SDK', 'Mastra'] -const comparisonRows: Array<{ label: string; values: Array }> = [ - { - label: 'Protocol to your UI', - values: [ - 'AG-UI, both directions', - 'Its own UI message stream', - 'Its own HTTP API, AG-UI via CopilotKit', - ], - }, - { - label: 'Client bindings', - values: [ - 'React, Vue, Solid, Svelte, Preact, Angular, plus a framework-free core', - 'React, Vue, Svelte, Angular', - 'React, plus a framework-free client', - ], - }, - { - label: 'Where the agent runs', - values: [ - 'Any server you already have', - 'Any server you already have', - 'Its own Hono server, or embedded in your app', - ], - }, - { - label: 'Hosted service in the loop', - values: ['None', 'Optional Vercel platform', 'Optional Mastra platform'], - }, - { - label: 'License', - values: ['MIT', 'Apache 2.0', 'Apache 2.0, with ee/ under separate terms'], - }, -] - -function ComparisonTable() { - return ( -
-
- - - - - {comparisonColumns.map((column, index) => ( - - ))} - - - - {comparisonRows.map((row) => ( - - - {row.values.map((value, index) => ( - - ))} - - ))} - -
- Dimension - - {column} -
- {row.label} - - {value} -
-
-

- Checked against @tanstack/ai 0.42, ai 7.0, and @mastra/core 1.56 in July - 2026. All three ship typed tools, MCP, memory, sandboxed code execution, - and coding-agent harnesses, so those are not differentiators. If we have - a detail wrong, tell us and we will fix it. -

-
- ) -} - function FeatureRail({ items }: { items: Array }) { return (
From 1a6b6e49d9cb353f317a495fa74b38283d694ca4 Mon Sep 17 00:00:00 2001 From: Alem Tuzlak Date: Thu, 30 Jul 2026 20:12:50 +0200 Subject: [PATCH 4/4] copy(ai): lead the protocol section with AG-UI compliance The old title, "An open protocol is the seam. Not our servers.", buried the concrete claim behind a metaphor. State the compliance up front instead, and move the eyebrow to "Open protocol" so it is not just a restatement of the heading. The body now earns the claim in order: compliant both ways, therefore the runtime is replaceable, therefore the transport is yours. --- src/components/landing/AiLanding.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/landing/AiLanding.tsx b/src/components/landing/AiLanding.tsx index 1c59db51b..b8e5e3ca9 100644 --- a/src/components/landing/AiLanding.tsx +++ b/src/components/landing/AiLanding.tsx @@ -203,10 +203,10 @@ export default function AiLanding() {