diff --git a/.changeset/fix-anthropic-multi-turn-tool-calls.md b/.changeset/fix-anthropic-multi-turn-tool-calls.md new file mode 100644 index 000000000..34553ecec --- /dev/null +++ b/.changeset/fix-anthropic-multi-turn-tool-calls.md @@ -0,0 +1,35 @@ +--- +'@tanstack/ai': patch +'@tanstack/ai-client': patch +'@tanstack/ai-anthropic': patch +'@tanstack/ai-gemini': patch +--- + +fix(ai, ai-client, ai-anthropic, ai-gemini): fix multi-turn conversations failing after tool calls + +**Core (@tanstack/ai):** + +- Lazy assistant message creation: `StreamProcessor` now defers creating the assistant message until the first content-bearing chunk arrives (text, tool call, thinking, or error), eliminating empty `parts: []` messages from appearing during auto-continuation when the model returns no content +- Add `prepareAssistantMessage()` (lazy) alongside deprecated `startAssistantMessage()` (eager, backwards-compatible) +- Add `getCurrentAssistantMessageId()` to check if a message was created +- **Rewrite `uiMessageToModelMessages()` to preserve part ordering**: the function now walks parts sequentially instead of separating by type, producing correctly interleaved assistant/tool messages (text1 + toolCall1 → toolResult1 → text2 + toolCall2 → toolResult2) instead of concatenating all text and batching all tool calls. This fixes multi-round tool flows where the model would see garbled conversation history and re-call tools unnecessarily. +- Deduplicate tool result messages: when a client tool has both a `tool-result` part and a `tool-call` part with `output`, only one `role: 'tool'` message is emitted per tool call ID + +**Client (@tanstack/ai-client):** + +- Update `ChatClient.processStream()` to use lazy assistant message creation, preventing UI flicker from empty messages being created then removed + +**Anthropic:** + +- Fix consecutive user-role messages violating Anthropic's alternating role requirement by merging them in `formatMessages` +- Deduplicate `tool_result` blocks with the same `tool_use_id` +- Filter out empty assistant messages from conversation history +- Suppress duplicate `RUN_FINISHED` event from `message_stop` when `message_delta` already emitted one +- Fix `TEXT_MESSAGE_END` incorrectly emitting for `tool_use` content blocks +- Add Claude Opus 4.6 model support with adaptive thinking and effort parameter + +**Gemini:** + +- Fix consecutive user-role messages violating Gemini's alternating role requirement by merging them in `formatMessages` +- Deduplicate `functionResponse` parts with the same name (tool call ID) +- Filter out empty model messages from conversation history diff --git a/examples/ts-group-chat/package.json b/examples/ts-group-chat/package.json index 90137ede0..3d91b522c 100644 --- a/examples/ts-group-chat/package.json +++ b/examples/ts-group-chat/package.json @@ -14,11 +14,11 @@ "@tanstack/ai-client": "workspace:*", "@tanstack/ai-react": "workspace:*", "@tanstack/react-devtools": "^0.8.2", - "@tanstack/react-router": "^1.141.1", - "@tanstack/react-router-devtools": "^1.139.7", - "@tanstack/react-router-ssr-query": "^1.139.7", - "@tanstack/react-start": "^1.141.1", - "@tanstack/router-plugin": "^1.139.7", + "@tanstack/react-router": "^1.158.4", + "@tanstack/react-router-devtools": "^1.158.4", + "@tanstack/react-router-ssr-query": "^1.158.4", + "@tanstack/react-start": "^1.159.0", + "@tanstack/router-plugin": "^1.158.4", "capnweb": "^0.1.0", "react": "^19.2.3", "react-dom": "^19.2.3", diff --git a/examples/ts-react-chat/package.json b/examples/ts-react-chat/package.json index f58f54bda..d74d35af3 100644 --- a/examples/ts-react-chat/package.json +++ b/examples/ts-react-chat/package.json @@ -20,14 +20,14 @@ "@tanstack/ai-openrouter": "workspace:*", "@tanstack/ai-react": "workspace:*", "@tanstack/ai-react-ui": "workspace:*", - "@tanstack/nitro-v2-vite-plugin": "^1.141.0", + "@tanstack/nitro-v2-vite-plugin": "^1.154.7", "@tanstack/react-devtools": "^0.8.2", - "@tanstack/react-router": "^1.141.1", - "@tanstack/react-router-devtools": "^1.139.7", - "@tanstack/react-router-ssr-query": "^1.139.7", - "@tanstack/react-start": "^1.141.1", + "@tanstack/react-router": "^1.158.4", + "@tanstack/react-router-devtools": "^1.158.4", + "@tanstack/react-router-ssr-query": "^1.158.4", + "@tanstack/react-start": "^1.159.0", "@tanstack/react-store": "^0.8.0", - "@tanstack/router-plugin": "^1.139.7", + "@tanstack/router-plugin": "^1.158.4", "@tanstack/store": "^0.8.0", "highlight.js": "^11.11.1", "lucide-react": "^0.561.0", diff --git a/examples/ts-react-chat/src/routeTree.gen.ts b/examples/ts-react-chat/src/routeTree.gen.ts index 627a240cf..f7099165f 100644 --- a/examples/ts-react-chat/src/routeTree.gen.ts +++ b/examples/ts-react-chat/src/routeTree.gen.ts @@ -39,7 +39,7 @@ export interface FileRoutesByFullPath { '/': typeof IndexRoute '/api/tanchat': typeof ApiTanchatRoute '/example/guitars/$guitarId': typeof ExampleGuitarsGuitarIdRoute - '/example/guitars': typeof ExampleGuitarsIndexRoute + '/example/guitars/': typeof ExampleGuitarsIndexRoute } export interface FileRoutesByTo { '/': typeof IndexRoute @@ -60,7 +60,7 @@ export interface FileRouteTypes { | '/' | '/api/tanchat' | '/example/guitars/$guitarId' - | '/example/guitars' + | '/example/guitars/' fileRoutesByTo: FileRoutesByTo to: '/' | '/api/tanchat' | '/example/guitars/$guitarId' | '/example/guitars' id: @@ -97,7 +97,7 @@ declare module '@tanstack/react-router' { '/example/guitars/': { id: '/example/guitars/' path: '/example/guitars' - fullPath: '/example/guitars' + fullPath: '/example/guitars/' preLoaderRoute: typeof ExampleGuitarsIndexRouteImport parentRoute: typeof rootRouteImport } diff --git a/examples/ts-solid-chat/package.json b/examples/ts-solid-chat/package.json index 3a9ea9e8d..bfe5ec050 100644 --- a/examples/ts-solid-chat/package.json +++ b/examples/ts-solid-chat/package.json @@ -19,8 +19,8 @@ "@tanstack/ai-openai": "workspace:*", "@tanstack/ai-solid": "workspace:*", "@tanstack/ai-solid-ui": "workspace:*", - "@tanstack/nitro-v2-vite-plugin": "^1.141.0", - "@tanstack/router-plugin": "^1.139.7", + "@tanstack/nitro-v2-vite-plugin": "^1.154.7", + "@tanstack/router-plugin": "^1.158.4", "@tanstack/solid-ai-devtools": "workspace:*", "@tanstack/solid-devtools": "^0.7.15", "@tanstack/solid-router": "^1.139.10", diff --git a/packages/typescript/ai-anthropic/src/adapters/text.ts b/packages/typescript/ai-anthropic/src/adapters/text.ts index a4f41bbbe..862717f71 100644 --- a/packages/typescript/ai-anthropic/src/adapters/text.ts +++ b/packages/typescript/ai-anthropic/src/adapters/text.ts @@ -247,6 +247,7 @@ export class AnthropicTextAdapter< const validKeys: Array = [ 'container', 'context_management', + 'effort', 'mcp_servers', 'service_tier', 'stop_sequences', @@ -450,7 +451,74 @@ export class AnthropicTextAdapter< }) } - return formattedMessages + // Post-process: Anthropic requires strictly alternating user/assistant roles. + // Tool results are sent as role:'user' messages, which can create consecutive + // user messages when followed by a new user message. Merge them. + return this.mergeConsecutiveSameRoleMessages(formattedMessages) + } + + /** + * Merge consecutive messages of the same role into a single message. + * Anthropic's API requires strictly alternating user/assistant roles. + * Tool results are wrapped as role:'user' messages, which can collide + * with actual user messages in multi-turn conversations. + * + * Also filters out empty assistant messages (e.g., from a previous failed request). + */ + private mergeConsecutiveSameRoleMessages( + messages: InternalTextProviderOptions['messages'], + ): InternalTextProviderOptions['messages'] { + const merged: InternalTextProviderOptions['messages'] = [] + + for (const msg of messages) { + // Skip empty assistant messages (no content or empty string) + if (msg.role === 'assistant') { + const hasContent = Array.isArray(msg.content) + ? msg.content.length > 0 + : typeof msg.content === 'string' && msg.content.length > 0 + if (!hasContent) { + continue + } + } + + const prev = merged[merged.length - 1] + if (prev && prev.role === msg.role) { + // Normalize both contents to arrays and concatenate + const prevBlocks = Array.isArray(prev.content) + ? prev.content + : typeof prev.content === 'string' && prev.content + ? [{ type: 'text' as const, text: prev.content }] + : [] + const msgBlocks = Array.isArray(msg.content) + ? msg.content + : typeof msg.content === 'string' && msg.content + ? [{ type: 'text' as const, text: msg.content }] + : [] + prev.content = [...prevBlocks, ...msgBlocks] + } else { + merged.push({ ...msg }) + } + } + + // De-duplicate tool_result blocks with the same tool_use_id. + // This can happen when the core layer generates tool results from both + // the tool-result part and the tool-call part's output field. + for (const msg of merged) { + if (Array.isArray(msg.content)) { + const seenToolResultIds = new Set() + msg.content = msg.content.filter((block: any) => { + if (block.type === 'tool_result' && block.tool_use_id) { + if (seenToolResultIds.has(block.tool_use_id)) { + return false // Remove duplicate + } + seenToolResultIds.add(block.tool_use_id) + } + return true + }) + } + } + + return merged } private async *processAnthropicStream( @@ -473,6 +541,9 @@ export class AnthropicTextAdapter< let stepId: string | null = null let hasEmittedRunStarted = false let hasEmittedTextMessageStart = false + let hasEmittedRunFinished = false + // Track current content block type for proper content_block_stop handling + let currentBlockType: string | null = null try { for await (const event of stream) { @@ -488,6 +559,7 @@ export class AnthropicTextAdapter< } if (event.type === 'content_block_start') { + currentBlockType = event.content_block.type if (event.content_block.type === 'tool_use') { currentToolIndex++ toolCallsMap.set(currentToolIndex, { @@ -572,59 +644,68 @@ export class AnthropicTextAdapter< } } } else if (event.type === 'content_block_stop') { - const existing = toolCallsMap.get(currentToolIndex) - if (existing) { - // If tool call wasn't started yet (no args), start it now - if (!existing.started) { - existing.started = true + if (currentBlockType === 'tool_use') { + const existing = toolCallsMap.get(currentToolIndex) + if (existing) { + // If tool call wasn't started yet (no args), start it now + if (!existing.started) { + existing.started = true + yield { + type: 'TOOL_CALL_START', + toolCallId: existing.id, + toolName: existing.name, + model, + timestamp, + index: currentToolIndex, + } + } + + // Emit TOOL_CALL_END + let parsedInput: unknown = {} + try { + const parsed = existing.input ? JSON.parse(existing.input) : {} + parsedInput = parsed && typeof parsed === 'object' ? parsed : {} + } catch { + parsedInput = {} + } + yield { - type: 'TOOL_CALL_START', + type: 'TOOL_CALL_END', toolCallId: existing.id, toolName: existing.name, model, timestamp, - index: currentToolIndex, + input: parsedInput, } } - - // Emit TOOL_CALL_END - let parsedInput: unknown = {} - try { - const parsed = existing.input ? JSON.parse(existing.input) : {} - parsedInput = parsed && typeof parsed === 'object' ? parsed : {} - } catch { - parsedInput = {} - } - - yield { - type: 'TOOL_CALL_END', - toolCallId: existing.id, - toolName: existing.name, - model, - timestamp, - input: parsedInput, + } else { + // Emit TEXT_MESSAGE_END only for text blocks (not tool_use blocks) + if (hasEmittedTextMessageStart && accumulatedContent) { + yield { + type: 'TEXT_MESSAGE_END', + messageId, + model, + timestamp, + } } } - - // Emit TEXT_MESSAGE_END if we had text content - if (hasEmittedTextMessageStart && accumulatedContent) { + currentBlockType = null + } else if (event.type === 'message_stop') { + // Only emit RUN_FINISHED from message_stop if message_delta didn't already emit one. + // message_delta carries the real stop_reason (tool_use, end_turn, etc.), + // while message_stop is just a completion signal. + if (!hasEmittedRunFinished) { yield { - type: 'TEXT_MESSAGE_END', - messageId, + type: 'RUN_FINISHED', + runId, model, timestamp, + finishReason: 'stop', } } - } else if (event.type === 'message_stop') { - yield { - type: 'RUN_FINISHED', - runId, - model, - timestamp, - finishReason: 'stop', - } } else if (event.type === 'message_delta') { if (event.delta.stop_reason) { + hasEmittedRunFinished = true switch (event.delta.stop_reason) { case 'tool_use': { yield { diff --git a/packages/typescript/ai-anthropic/src/model-meta.ts b/packages/typescript/ai-anthropic/src/model-meta.ts index fa48e5038..5c1487449 100644 --- a/packages/typescript/ai-anthropic/src/model-meta.ts +++ b/packages/typescript/ai-anthropic/src/model-meta.ts @@ -1,6 +1,8 @@ import type { + AnthropicAdaptiveThinkingOptions, AnthropicContainerOptions, AnthropicContextManagementOptions, + AnthropicEffortOptions, AnthropicMCPOptions, AnthropicSamplingOptions, AnthropicServiceTierOptions, @@ -19,6 +21,7 @@ interface ModelMeta< supports: { input: Array<'text' | 'image' | 'audio' | 'video' | 'document'> extended_thinking?: boolean + adaptive_thinking?: boolean priority_tier?: boolean } context_window?: number @@ -136,6 +139,38 @@ const CLAUDE_OPUS_4_1 = { AnthropicSamplingOptions > +const CLAUDE_OPUS_4_6 = { + name: 'claude-opus-4-6', + id: 'claude-opus-4-6', + context_window: 200_000, + max_output_tokens: 128_000, + knowledge_cutoff: '2025-05-01', + pricing: { + input: { + normal: 5, + }, + output: { + normal: 25, + }, + }, + supports: { + input: ['text', 'image', 'document'], + extended_thinking: true, + adaptive_thinking: true, + priority_tier: true, + }, +} as const satisfies ModelMeta< + AnthropicContainerOptions & + AnthropicContextManagementOptions & + AnthropicMCPOptions & + AnthropicServiceTierOptions & + AnthropicStopSequencesOptions & + AnthropicAdaptiveThinkingOptions & + AnthropicEffortOptions & + AnthropicToolChoiceOptions & + AnthropicSamplingOptions +> + const CLAUDE_OPUS_4_5 = { name: 'claude-opus-4-5', id: 'claude-opus-4-5', @@ -361,6 +396,7 @@ const CLAUDE_HAIKU_3 = { : unknown */ export const ANTHROPIC_MODELS = [ + CLAUDE_OPUS_4_6.id, CLAUDE_OPUS_4_5.id, CLAUDE_SONNET_4_5.id, CLAUDE_HAIKU_4_5.id, @@ -382,6 +418,17 @@ export const ANTHROPIC_MODELS = [ // Manual type map for per-model provider options // Models are differentiated by extended_thinking and priority_tier support export type AnthropicChatModelProviderOptionsByName = { + // Opus 4.6: adaptive thinking, effort parameter, 128K output + [CLAUDE_OPUS_4_6.id]: AnthropicContainerOptions & + AnthropicContextManagementOptions & + AnthropicMCPOptions & + AnthropicServiceTierOptions & + AnthropicStopSequencesOptions & + AnthropicAdaptiveThinkingOptions & + AnthropicEffortOptions & + AnthropicToolChoiceOptions & + AnthropicSamplingOptions + // Models with both extended_thinking and priority_tier [CLAUDE_OPUS_4_5.id]: AnthropicContainerOptions & AnthropicContextManagementOptions & @@ -470,6 +517,7 @@ export type AnthropicChatModelProviderOptionsByName = { * @see https://docs.anthropic.com/claude/docs/pdf-support */ export type AnthropicModelInputModalitiesByName = { + [CLAUDE_OPUS_4_6.id]: typeof CLAUDE_OPUS_4_6.supports.input [CLAUDE_OPUS_4_5.id]: typeof CLAUDE_OPUS_4_5.supports.input [CLAUDE_SONNET_4_5.id]: typeof CLAUDE_SONNET_4_5.supports.input [CLAUDE_HAIKU_4_5.id]: typeof CLAUDE_HAIKU_4_5.supports.input diff --git a/packages/typescript/ai-anthropic/src/text/text-provider-options.ts b/packages/typescript/ai-anthropic/src/text/text-provider-options.ts index 8c4dfeccf..b26da487d 100644 --- a/packages/typescript/ai-anthropic/src/text/text-provider-options.ts +++ b/packages/typescript/ai-anthropic/src/text/text-provider-options.ts @@ -92,6 +92,42 @@ Must be ≥1024 and less than max_tokens } } +export interface AnthropicAdaptiveThinkingOptions { + /** + * Configuration for Claude's adaptive thinking (Opus 4.6+). + * + * In adaptive mode, Claude dynamically decides when and how much to think. + * Use the effort parameter to control thinking depth. + * `thinking: {type: "enabled"}` with `budget_tokens` is deprecated on Opus 4.6. + */ + thinking?: + | { + type: 'adaptive' + } + | { + /** + * @deprecated Use `type: 'adaptive'` with the effort parameter on Opus 4.6+. + */ + budget_tokens: number + type: 'enabled' + } + | { + type: 'disabled' + } +} + +export interface AnthropicEffortOptions { + /** + * Controls the thinking depth for adaptive thinking mode (Opus 4.6+). + * + * - `max`: Absolute highest capability + * - `high`: Default - Claude will almost always think + * - `medium`: Balanced cost-quality + * - `low`: May skip thinking for simpler problems + */ + effort?: 'max' | 'high' | 'medium' | 'low' +} + export interface AnthropicToolChoiceOptions { tool_choice?: BetaToolChoiceAny | BetaToolChoiceTool | BetaToolChoiceAuto } @@ -115,7 +151,9 @@ export type ExternalTextProviderOptions = AnthropicContainerOptions & AnthropicStopSequencesOptions & AnthropicThinkingOptions & AnthropicToolChoiceOptions & - AnthropicSamplingOptions + AnthropicSamplingOptions & + Partial & + Partial export interface InternalTextProviderOptions extends ExternalTextProviderOptions { model: string diff --git a/packages/typescript/ai-anthropic/tests/anthropic-adapter.test.ts b/packages/typescript/ai-anthropic/tests/anthropic-adapter.test.ts index 5e3db4348..76c50ccf2 100644 --- a/packages/typescript/ai-anthropic/tests/anthropic-adapter.test.ts +++ b/packages/typescript/ai-anthropic/tests/anthropic-adapter.test.ts @@ -183,4 +183,508 @@ describe('Anthropic adapter option mapping', () => { type: 'custom', }) }) + + it('merges consecutive user messages when tool results precede a follow-up user message', async () => { + // This is the core multi-turn bug: after a tool call + result, the next user message + // creates consecutive role:'user' messages (tool_result as user + new user message). + // Anthropic's API requires strictly alternating user/assistant roles. + const mockStream = (async function* () { + yield { + type: 'content_block_start', + index: 0, + content_block: { type: 'text', text: '' }, + } + yield { + type: 'content_block_delta', + index: 0, + delta: { type: 'text_delta', text: 'Here is a recommendation' }, + } + yield { + type: 'content_block_stop', + index: 0, + } + yield { + type: 'message_delta', + delta: { stop_reason: 'end_turn' }, + usage: { output_tokens: 10 }, + } + yield { type: 'message_stop' } + })() + + mocks.betaMessagesCreate.mockResolvedValueOnce(mockStream) + + const adapter = createAdapter('claude-3-7-sonnet-20250219') + + // Multi-turn: user -> assistant(tool_calls) -> tool_result -> follow-up user + const chunks: StreamChunk[] = [] + for await (const chunk of chat({ + adapter, + messages: [ + { role: 'user', content: 'What is the weather in Berlin?' }, + { + role: 'assistant', + content: 'Let me check the weather.', + toolCalls: [ + { + id: 'call_1', + type: 'function', + function: { name: 'lookup_weather', arguments: toolArguments }, + }, + ], + }, + { role: 'tool', toolCallId: 'call_1', content: '{"temp":72}' }, + { role: 'user', content: 'What about Paris?' }, + ], + tools: [weatherTool], + })) { + chunks.push(chunk) + } + + expect(mocks.betaMessagesCreate).toHaveBeenCalledTimes(1) + const [payload] = mocks.betaMessagesCreate.mock.calls[0] + + // The tool_result (user) and follow-up user message should be merged into one user message + expect(payload.messages).toEqual([ + { + role: 'user', + content: 'What is the weather in Berlin?', + }, + { + role: 'assistant', + content: [ + { type: 'text', text: 'Let me check the weather.' }, + { + type: 'tool_use', + id: 'call_1', + name: 'lookup_weather', + input: { location: 'Berlin' }, + }, + ], + }, + { + role: 'user', + content: [ + { + type: 'tool_result', + tool_use_id: 'call_1', + content: '{"temp":72}', + }, + { type: 'text', text: 'What about Paris?' }, + ], + }, + ]) + + // Verify roles strictly alternate: user, assistant, user + const roles = payload.messages.map((m: any) => m.role) + for (let i = 1; i < roles.length; i++) { + expect(roles[i]).not.toBe(roles[i - 1]) + } + }) + + it('merges multiple consecutive tool result messages into one user message', async () => { + // When multiple tools are called, each tool result becomes a role:'user' message. + // These must be merged into a single user message. + const mockStream = (async function* () { + yield { + type: 'content_block_start', + index: 0, + content_block: { type: 'text', text: '' }, + } + yield { + type: 'content_block_delta', + index: 0, + delta: { type: 'text_delta', text: 'Here are the results' }, + } + yield { + type: 'content_block_stop', + index: 0, + } + yield { + type: 'message_delta', + delta: { stop_reason: 'end_turn' }, + usage: { output_tokens: 5 }, + } + yield { type: 'message_stop' } + })() + + mocks.betaMessagesCreate.mockResolvedValueOnce(mockStream) + + const adapter = createAdapter('claude-3-7-sonnet-20250219') + + const chunks: StreamChunk[] = [] + for await (const chunk of chat({ + adapter, + messages: [ + { role: 'user', content: 'Weather in Berlin and Paris?' }, + { + role: 'assistant', + content: null, + toolCalls: [ + { + id: 'call_berlin', + type: 'function', + function: { + name: 'lookup_weather', + arguments: JSON.stringify({ location: 'Berlin' }), + }, + }, + { + id: 'call_paris', + type: 'function', + function: { + name: 'lookup_weather', + arguments: JSON.stringify({ location: 'Paris' }), + }, + }, + ], + }, + { role: 'tool', toolCallId: 'call_berlin', content: '{"temp":72}' }, + { role: 'tool', toolCallId: 'call_paris', content: '{"temp":68}' }, + ], + tools: [weatherTool], + })) { + chunks.push(chunk) + } + + expect(mocks.betaMessagesCreate).toHaveBeenCalledTimes(1) + const [payload] = mocks.betaMessagesCreate.mock.calls[0] + + // Both tool results should be merged into a single user message + expect(payload.messages).toEqual([ + { + role: 'user', + content: 'Weather in Berlin and Paris?', + }, + { + role: 'assistant', + content: [ + { + type: 'tool_use', + id: 'call_berlin', + name: 'lookup_weather', + input: { location: 'Berlin' }, + }, + { + type: 'tool_use', + id: 'call_paris', + name: 'lookup_weather', + input: { location: 'Paris' }, + }, + ], + }, + { + role: 'user', + content: [ + { + type: 'tool_result', + tool_use_id: 'call_berlin', + content: '{"temp":72}', + }, + { + type: 'tool_result', + tool_use_id: 'call_paris', + content: '{"temp":68}', + }, + ], + }, + ]) + + // Verify roles strictly alternate + const roles = payload.messages.map((m: any) => m.role) + for (let i = 1; i < roles.length; i++) { + expect(roles[i]).not.toBe(roles[i - 1]) + } + }) + + it('handles full multi-turn flow with duplicate tool results, empty assistant, and follow-up', async () => { + // This reproduces the exact bug scenario from the testing panel: + // 1. Assistant calls getGuitars + recommendGuitar (with text) + // 2. Tool results include duplicates (from both tool-result and tool-call output) + // 3. An empty assistant message exists (from the client tool round-trip) + // 4. User sends a follow-up message + // All of: duplicates, empty assistant, consecutive user messages must be handled. + const mockStream = (async function* () { + yield { + type: 'content_block_start', + index: 0, + content_block: { type: 'text', text: '' }, + } + yield { + type: 'content_block_delta', + index: 0, + delta: { type: 'text_delta', text: 'Electric guitars available' }, + } + yield { type: 'content_block_stop', index: 0 } + yield { + type: 'message_delta', + delta: { stop_reason: 'end_turn' }, + usage: { output_tokens: 5 }, + } + yield { type: 'message_stop' } + })() + + mocks.betaMessagesCreate.mockResolvedValueOnce(mockStream) + + const adapter = createAdapter('claude-3-7-sonnet-20250219') + + const chunks: StreamChunk[] = [] + for await (const chunk of chat({ + adapter, + messages: [ + { role: 'user', content: "what's a good acoustic guitar?" }, + { + role: 'assistant', + content: "I'll help you find a good acoustic guitar!", + toolCalls: [ + { + id: 'toolu_getGuitars', + type: 'function', + function: { name: 'getGuitars', arguments: '' }, + }, + { + id: 'toolu_recommend', + type: 'function', + function: { + name: 'recommendGuitar', + arguments: '{"id": 7}', + }, + }, + ], + }, + // Tool result from tool-result part + { + role: 'tool', + toolCallId: 'toolu_getGuitars', + content: '[{"id":7,"name":"Guitar"}]', + }, + // Tool result from tool-result part + { + role: 'tool', + toolCallId: 'toolu_recommend', + content: '{"id":7}', + }, + // DUPLICATE tool result from tool-call output field + { + role: 'tool', + toolCallId: 'toolu_recommend', + content: '{"id":7}', + }, + // Empty assistant from client tool round-trip + { role: 'assistant', content: null }, + // User follow-up + { role: 'user', content: "what's a good electric guitar?" }, + ], + tools: [weatherTool], + })) { + chunks.push(chunk) + } + + expect(mocks.betaMessagesCreate).toHaveBeenCalledTimes(1) + const [payload] = mocks.betaMessagesCreate.mock.calls[0] + + // Verify: no consecutive same-role messages, no empty assistants, no duplicate tool_results + const roles = payload.messages.map((m: any) => m.role) + for (let i = 1; i < roles.length; i++) { + expect(roles[i]).not.toBe(roles[i - 1]) + } + + // Should have exactly 3 messages: user, assistant, user (merged tool results + follow-up) + expect(payload.messages).toHaveLength(3) + expect(payload.messages[0].role).toBe('user') + expect(payload.messages[1].role).toBe('assistant') + expect(payload.messages[2].role).toBe('user') + + // The merged user message should have tool results (de-duplicated) + follow-up text + const lastUserContent = payload.messages[2].content + expect(Array.isArray(lastUserContent)).toBe(true) + + // Count tool_result blocks - should have 2 (one per tool), not 3 (no duplicate) + const toolResultBlocks = lastUserContent.filter( + (b: any) => b.type === 'tool_result', + ) + expect(toolResultBlocks).toHaveLength(2) + + // Should have the follow-up text + const textBlocks = lastUserContent.filter((b: any) => b.type === 'text') + expect(textBlocks).toHaveLength(1) + expect(textBlocks[0].text).toBe("what's a good electric guitar?") + }) + + it('filters out empty assistant messages from conversation history', async () => { + // An empty assistant message (from a previous failed request) should be filtered out. + const mockStream = (async function* () { + yield { + type: 'content_block_start', + index: 0, + content_block: { type: 'text', text: '' }, + } + yield { + type: 'content_block_delta', + index: 0, + delta: { type: 'text_delta', text: 'Response' }, + } + yield { + type: 'content_block_stop', + index: 0, + } + yield { + type: 'message_delta', + delta: { stop_reason: 'end_turn' }, + usage: { output_tokens: 3 }, + } + yield { type: 'message_stop' } + })() + + mocks.betaMessagesCreate.mockResolvedValueOnce(mockStream) + + const adapter = createAdapter('claude-3-7-sonnet-20250219') + + const chunks: StreamChunk[] = [] + for await (const chunk of chat({ + adapter, + messages: [ + { role: 'user', content: 'Hello' }, + { role: 'assistant', content: '' }, // Empty assistant from failed request + { role: 'user', content: 'Try again' }, + ], + })) { + chunks.push(chunk) + } + + expect(mocks.betaMessagesCreate).toHaveBeenCalledTimes(1) + const [payload] = mocks.betaMessagesCreate.mock.calls[0] + + // The empty assistant message should be filtered out, and consecutive + // user messages should be merged + expect(payload.messages).toEqual([ + { + role: 'user', + content: [ + { type: 'text', text: 'Hello' }, + { type: 'text', text: 'Try again' }, + ], + }, + ]) + }) +}) + +describe('Anthropic stream processing', () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + it('does not emit duplicate RUN_FINISHED from message_stop after message_delta', async () => { + // message_delta with stop_reason already emits RUN_FINISHED. + // message_stop should NOT emit another one. + const mockStream = (async function* () { + yield { + type: 'content_block_start', + index: 0, + content_block: { type: 'text', text: '' }, + } + yield { + type: 'content_block_delta', + index: 0, + delta: { type: 'text_delta', text: 'Hello' }, + } + yield { + type: 'content_block_stop', + index: 0, + } + yield { + type: 'message_delta', + delta: { stop_reason: 'end_turn' }, + usage: { output_tokens: 3 }, + } + yield { type: 'message_stop' } + })() + + mocks.betaMessagesCreate.mockResolvedValueOnce(mockStream) + + const adapter = createAdapter('claude-3-7-sonnet-20250219') + + const chunks: StreamChunk[] = [] + for await (const chunk of chat({ + adapter, + messages: [{ role: 'user', content: 'Hi' }], + })) { + chunks.push(chunk) + } + + // Should have exactly ONE RUN_FINISHED event (from message_delta), not two + const runFinished = chunks.filter((c) => c.type === 'RUN_FINISHED') + expect(runFinished).toHaveLength(1) + expect(runFinished[0]).toMatchObject({ + type: 'RUN_FINISHED', + finishReason: 'stop', + }) + }) + + it('does not emit TEXT_MESSAGE_END for tool_use content blocks', async () => { + // When text is followed by a tool_use block, TEXT_MESSAGE_END should only + // fire once (for the text block), not again when the tool block stops. + const mockStream = (async function* () { + // Text block + yield { + type: 'content_block_start', + index: 0, + content_block: { type: 'text', text: '' }, + } + yield { + type: 'content_block_delta', + index: 0, + delta: { type: 'text_delta', text: 'Let me check' }, + } + yield { type: 'content_block_stop', index: 0 } + // Tool use block + yield { + type: 'content_block_start', + index: 1, + content_block: { + type: 'tool_use', + id: 'tool_1', + name: 'lookup_weather', + }, + } + yield { + type: 'content_block_delta', + index: 1, + delta: { + type: 'input_json_delta', + partial_json: '{"location":"Berlin"}', + }, + } + yield { type: 'content_block_stop', index: 1 } + yield { + type: 'message_delta', + delta: { stop_reason: 'tool_use' }, + usage: { output_tokens: 10 }, + } + yield { type: 'message_stop' } + })() + + mocks.betaMessagesCreate.mockResolvedValueOnce(mockStream) + + const adapter = createAdapter('claude-3-7-sonnet-20250219') + + const chunks: StreamChunk[] = [] + for await (const chunk of chat({ + adapter, + messages: [{ role: 'user', content: 'Weather in Berlin?' }], + tools: [weatherTool], + })) { + chunks.push(chunk) + } + + // TEXT_MESSAGE_END should appear exactly once (for the text block) + const textMessageEnds = chunks.filter((c) => c.type === 'TEXT_MESSAGE_END') + expect(textMessageEnds).toHaveLength(1) + + // RUN_FINISHED should appear exactly once (from message_delta with tool_use) + const runFinished = chunks.filter((c) => c.type === 'RUN_FINISHED') + expect(runFinished).toHaveLength(1) + expect(runFinished[0]).toMatchObject({ + finishReason: 'tool_calls', + }) + }) }) diff --git a/packages/typescript/ai-client/src/chat-client.ts b/packages/typescript/ai-client/src/chat-client.ts index e95389fba..65554a447 100644 --- a/packages/typescript/ai-client/src/chat-client.ts +++ b/packages/typescript/ai-client/src/chat-client.ts @@ -230,31 +230,37 @@ export class ChatClient { */ private async processStream( source: AsyncIterable, - ): Promise { + ): Promise { // Generate a stream ID for this streaming operation this.currentStreamId = this.generateUniqueId('stream') - // Start a new assistant message - const messageId = this.processor.startAssistantMessage() - this.currentMessageId = messageId - - // Emit message appended event for the new assistant message - const assistantMessage: UIMessage = { - id: messageId, - role: 'assistant', - parts: [], - createdAt: new Date(), - } - this.events.messageAppended( - assistantMessage, - this.currentStreamId || undefined, - ) + // Prepare for a new assistant message (created lazily on first content) + this.processor.prepareAssistantMessage() // Process each chunk for await (const chunk of source) { this.callbacksRef.current.onChunk(chunk) this.processor.processChunk(chunk) + // Track the message ID once the processor lazily creates it + if (!this.currentMessageId) { + const newMessageId = + this.processor.getCurrentAssistantMessageId() ?? null + if (newMessageId) { + this.currentMessageId = newMessageId + // Emit message appended event now that the assistant message exists + const assistantMessage = this.processor + .getMessages() + .find((m: UIMessage) => m.id === newMessageId) + if (assistantMessage) { + this.events.messageAppended( + assistantMessage, + this.currentStreamId || undefined, + ) + } + } + } + // Yield control back to event loop to allow UI updates await new Promise((resolve) => setTimeout(resolve, 0)) } @@ -268,24 +274,20 @@ export class ChatClient { // Finalize the stream this.processor.finalizeStream() + // Get the message ID (may be null if no content arrived) + const messageId = this.processor.getCurrentAssistantMessageId() + // Clear the current stream and message IDs this.currentStreamId = null this.currentMessageId = null - // Return the assistant message - const messages = this.processor.getMessages() - const finalAssistantMessage = messages.find( - (m: UIMessage) => m.id === messageId, - ) + // Return the assistant message if one was created + if (messageId) { + const messages = this.processor.getMessages() + return messages.find((m: UIMessage) => m.id === messageId) || null + } - return ( - finalAssistantMessage || { - id: messageId, - role: 'assistant', - parts: [], - createdAt: new Date(), - } - ) + return null } /** diff --git a/packages/typescript/ai-gemini/src/adapters/text.ts b/packages/typescript/ai-gemini/src/adapters/text.ts index 031298c18..3854ed4de 100644 --- a/packages/typescript/ai-gemini/src/adapters/text.ts +++ b/packages/typescript/ai-gemini/src/adapters/text.ts @@ -16,6 +16,7 @@ import type { StructuredOutputResult, } from '@tanstack/ai/adapters' import type { + Content, GenerateContentParameters, GenerateContentResponse, GoogleGenAI, @@ -520,7 +521,7 @@ export class GeminiTextAdapter< private formatMessages( messages: Array, ): GenerateContentParameters['contents'] { - return messages.map((msg) => { + const formatted = messages.map((msg) => { const role: 'user' | 'model' = msg.role === 'assistant' ? 'model' : 'user' const parts: Array = [] @@ -574,6 +575,67 @@ export class GeminiTextAdapter< parts: parts.length > 0 ? parts : [{ text: '' }], } }) + + // Post-process: Gemini requires strictly alternating user/model roles. + // Tool results are mapped to role:'user', which can create consecutive + // user messages when followed by a new user message. Merge them. + return this.mergeConsecutiveSameRoleMessages(formatted) + } + + /** + * Merge consecutive messages of the same role into a single message. + * Gemini's API requires strictly alternating user/model roles. + * Tool results are mapped to role:'user', which can collide with actual + * user messages in multi-turn conversations. + * + * Also filters out empty model messages (e.g., from a previous failed request) + * and deduplicates functionResponse parts with the same name (tool call ID). + */ + private mergeConsecutiveSameRoleMessages( + messages: Array, + ): Array { + const merged: Array = [] + + for (const msg of messages) { + const parts = msg.parts || [] + + // Skip empty model messages (no parts or only empty text) + if (msg.role === 'model') { + const hasContent = + parts.length > 0 && + !parts.every( + (p) => 'text' in p && (p as { text: string }).text === '', + ) + if (!hasContent) { + continue + } + } + + const prev = merged[merged.length - 1] + if (prev && prev.role === msg.role) { + // Merge parts arrays + prev.parts = [...(prev.parts || []), ...parts] + } else { + merged.push({ ...msg, parts: [...parts] }) + } + } + + // Deduplicate functionResponse parts with the same name (tool call ID) + for (const msg of merged) { + if (!msg.parts) continue + const seenFunctionResponseNames = new Set() + msg.parts = msg.parts.filter((part) => { + if ('functionResponse' in part && part.functionResponse?.name) { + if (seenFunctionResponseNames.has(part.functionResponse.name)) { + return false + } + seenFunctionResponseNames.add(part.functionResponse.name) + } + return true + }) + } + + return merged } private mapCommonOptionsToGemini( diff --git a/packages/typescript/ai-gemini/tests/gemini-adapter.test.ts b/packages/typescript/ai-gemini/tests/gemini-adapter.test.ts index 9add4bdc7..58c75ceee 100644 --- a/packages/typescript/ai-gemini/tests/gemini-adapter.test.ts +++ b/packages/typescript/ai-gemini/tests/gemini-adapter.test.ts @@ -330,6 +330,174 @@ describe('GeminiAdapter through AI', () => { }) }) + it('merges consecutive user messages when tool results precede a follow-up user message', async () => { + const streamChunks = [ + { + candidates: [ + { + content: { + parts: [{ text: 'Here is a recommendation' }], + }, + finishReason: 'STOP', + }, + ], + usageMetadata: { + promptTokenCount: 10, + candidatesTokenCount: 5, + totalTokenCount: 15, + }, + }, + ] + + mocks.generateContentStreamSpy.mockResolvedValue(createStream(streamChunks)) + + const adapter = createTextAdapter() + + for await (const _ of chat({ + adapter, + messages: [ + { role: 'user', content: 'What is the weather in Berlin?' }, + { + role: 'assistant', + content: 'Let me check.', + toolCalls: [ + { + id: 'call_1', + type: 'function', + function: { + name: 'lookup_weather', + arguments: '{"location":"Berlin"}', + }, + }, + ], + }, + { role: 'tool', toolCallId: 'call_1', content: '{"temp":72}' }, + { role: 'user', content: 'What about Paris?' }, + ], + tools: [weatherTool], + })) { + /* consume */ + } + + expect(mocks.generateContentStreamSpy).toHaveBeenCalledTimes(1) + const [payload] = mocks.generateContentStreamSpy.mock.calls[0] + + // Tool result (user) and follow-up user message should be merged + const roles = payload.contents.map((m: any) => m.role) + for (let i = 1; i < roles.length; i++) { + expect(roles[i]).not.toBe(roles[i - 1]) + } + + // Should have 3 messages: user, model, user (merged tool result + follow-up) + expect(payload.contents).toHaveLength(3) + expect(payload.contents[0].role).toBe('user') + expect(payload.contents[1].role).toBe('model') + expect(payload.contents[2].role).toBe('user') + + // Last user message should contain both functionResponse and text + const lastParts = payload.contents[2].parts + const hasFunctionResponse = lastParts.some((p: any) => p.functionResponse) + const hasText = lastParts.some((p: any) => p.text === 'What about Paris?') + expect(hasFunctionResponse).toBe(true) + expect(hasText).toBe(true) + }) + + it('handles full multi-turn with duplicate tool results and empty model message', async () => { + const streamChunks = [ + { + candidates: [ + { + content: { + parts: [{ text: 'Electric guitars available' }], + }, + finishReason: 'STOP', + }, + ], + usageMetadata: { + promptTokenCount: 20, + candidatesTokenCount: 5, + totalTokenCount: 25, + }, + }, + ] + + mocks.generateContentStreamSpy.mockResolvedValue(createStream(streamChunks)) + + const adapter = createTextAdapter() + + for await (const _ of chat({ + adapter, + messages: [ + { role: 'user', content: "what's a good acoustic guitar?" }, + { + role: 'assistant', + content: null, + toolCalls: [ + { + id: 'call_guitars', + type: 'function', + function: { name: 'getGuitars', arguments: '' }, + }, + { + id: 'call_recommend', + type: 'function', + function: { + name: 'recommendGuitar', + arguments: '{"id":7}', + }, + }, + ], + }, + { + role: 'tool', + toolCallId: 'call_guitars', + content: '[{"id":7,"name":"Guitar"}]', + }, + { + role: 'tool', + toolCallId: 'call_recommend', + content: '{"id":7}', + }, + // Duplicate tool result (from client tool output) + { + role: 'tool', + toolCallId: 'call_recommend', + content: '{"id":7}', + }, + // Empty assistant from client tool round-trip + { role: 'assistant', content: null }, + // Follow-up + { role: 'user', content: "what's a good electric guitar?" }, + ], + tools: [weatherTool], + })) { + /* consume */ + } + + expect(mocks.generateContentStreamSpy).toHaveBeenCalledTimes(1) + const [payload] = mocks.generateContentStreamSpy.mock.calls[0] + + // No consecutive same-role messages + const roles = payload.contents.map((m: any) => m.role) + for (let i = 1; i < roles.length; i++) { + expect(roles[i]).not.toBe(roles[i - 1]) + } + + // Should be 3 messages: user, model, user + expect(payload.contents).toHaveLength(3) + + // Last user should have deduplicated functionResponses + follow-up text + const lastParts = payload.contents[2].parts + const functionResponses = lastParts.filter((p: any) => p.functionResponse) + // 2 unique tool call IDs, not 3 (duplicate removed) + expect(functionResponses).toHaveLength(2) + + const textParts = lastParts.filter( + (p: any) => p.text === "what's a good electric guitar?", + ) + expect(textParts).toHaveLength(1) + }) + it('uses summarize function with models API', async () => { const summaryText = 'Short and sweet.' mocks.generateContentSpy.mockResolvedValueOnce({ diff --git a/packages/typescript/ai/src/activities/chat/messages.ts b/packages/typescript/ai/src/activities/chat/messages.ts index 43e4a614e..c8dc9228a 100644 --- a/packages/typescript/ai/src/activities/chat/messages.ts +++ b/packages/typescript/ai/src/activities/chat/messages.ts @@ -1,27 +1,22 @@ import type { - AudioPart, ContentPart, - DocumentPart, - ImagePart, MessagePart, ModelMessage, TextPart, ToolCallPart, - ToolResultPart, UIMessage, - VideoPart, } from '../../types' // =========================== // Message Converters // =========================== /** - * Helper to check if a part is a multimodal content part (image, audio, video, document) + * Check if a MessagePart is a content part (text, image, audio, video, document) + * that maps directly to a ModelMessage ContentPart. */ -function isMultimodalPart( - part: MessagePart, -): part is ImagePart | AudioPart | VideoPart | DocumentPart { +function isContentPart(part: MessagePart): part is ContentPart { return ( + part.type === 'text' || part.type === 'image' || part.type === 'audio' || part.type === 'video' || @@ -30,19 +25,34 @@ function isMultimodalPart( } /** - * Helper to extract text content from string or ContentPart array - * For multimodal content, this extracts only the text parts + * Collapse an array of ContentParts into the most compact ModelMessage content: + * - Empty array → null + * - All text parts → joined string (or null if empty) + * - Mixed content → ContentPart array as-is */ -function getTextContent(content: string | null | Array): string { - if (content === null) { - return '' - } - if (typeof content === 'string') { - return content +function collapseContentParts( + parts: Array, +): string | null | Array { + if (parts.length === 0) return null + + const allText = parts.every((p) => p.type === 'text') + if (allText) { + const joined = parts.map((p) => p.content).join('') + return joined || null } - // Extract text from ContentPart array + + return parts +} + +/** + * Extract text content from ModelMessage content (string, null, or ContentPart array). + * Used when only the text portion is needed (e.g., tool result content). + */ +function getTextContent(content: string | null | Array): string { + if (content === null) return '' + if (typeof content === 'string') return content return content - .filter((part) => part.type === 'text') + .filter((part): part is TextPart => part.type === 'text') .map((part) => part.content) .join('') } @@ -69,157 +79,214 @@ export function convertMessagesToModelMessages( /** * Convert a UIMessage to ModelMessage(s) * - * This conversion handles the parts-based structure: - * - Text parts → content field (string or as part of ContentPart array) - * - Multimodal parts (image, audio, video, document) → ContentPart array - * - ToolCall parts → toolCalls array - * - ToolResult parts → separate role="tool" messages + * Walks the parts array IN ORDER to preserve the interleaving of text, + * tool calls, and tool results. This is critical for multi-round tool + * flows where the model generates text, calls a tool, gets the result, + * then generates more text and calls another tool. + * + * The output preserves the sequential structure: + * text1 → toolCall1 → toolResult1 → text2 → toolCall2 → toolResult2 + * becomes: + * assistant: {content: "text1", toolCalls: [toolCall1]} + * tool: toolResult1 + * assistant: {content: "text2", toolCalls: [toolCall2]} + * tool: toolResult2 * * @param uiMessage - The UIMessage to convert - * @returns An array of ModelMessages (may be multiple if tool results are present) + * @returns An array of ModelMessages preserving part ordering */ export function uiMessageToModelMessages( uiMessage: UIMessage, ): Array { - const messageList: Array = [] - // Skip system messages - they're handled via systemPrompts, not ModelMessages if (uiMessage.role === 'system') { - return messageList + return [] } - // Separate parts by type - // Note: thinking parts are UI-only and not included in ModelMessages - const textParts: Array = [] - const multimodalParts: Array< - ImagePart | AudioPart | VideoPart | DocumentPart - > = [] - const toolCallParts: Array = [] - const toolResultParts: Array = [] + // For non-assistant messages (user), use the simpler path since they + // don't have tool calls or tool results to interleave + if (uiMessage.role !== 'assistant') { + return [buildUserOrToolMessage(uiMessage)] + } + // For assistant messages, walk parts in order to preserve interleaving + return buildAssistantMessages(uiMessage) +} + +/** + * Build a single ModelMessage for user messages (simple path). + * Preserves ordering of text and multimodal content parts. + */ +function buildUserOrToolMessage(uiMessage: UIMessage): ModelMessage { + const contentParts: Array = [] for (const part of uiMessage.parts) { - if (part.type === 'text') { - textParts.push(part) - } else if (isMultimodalPart(part)) { - multimodalParts.push(part) - } else if (part.type === 'tool-call') { - toolCallParts.push(part) - } else if (part.type === 'tool-result') { - toolResultParts.push(part) + if (isContentPart(part)) { + contentParts.push(part) } - // thinking parts are skipped - they're UI-only } - // Build the content field - // If we have multimodal parts, use ContentPart array format - // Otherwise, use simple string format for backward compatibility - let content: string | null | Array - if (multimodalParts.length > 0) { - // Build ContentPart array preserving the order of text and multimodal parts - const contentParts: Array = [] - for (const part of uiMessage.parts) { - if (part.type === 'text') { - contentParts.push(part) - } else if (isMultimodalPart(part)) { - contentParts.push(part) - } + return { + role: uiMessage.role as 'user' | 'assistant' | 'tool', + content: collapseContentParts(contentParts), + } +} + +// Accumulator for building an assistant segment (content + tool calls) +interface AssistantSegment { + contentParts: Array + toolCalls: Array<{ + id: string + type: 'function' + function: { name: string; arguments: string } + }> +} + +function createSegment(): AssistantSegment { + return { contentParts: [], toolCalls: [] } +} + +function isToolCallIncluded(part: ToolCallPart): boolean { + return ( + part.state === 'input-complete' || + part.state === 'approval-responded' || + part.output !== undefined + ) +} + +/** + * Build ModelMessages for an assistant UIMessage, preserving the + * sequential interleaving of text, tool calls, and tool results. + * + * Walks parts in order. Text and tool-call parts accumulate into the + * current "segment". When a tool-result part is encountered, the + * current segment is flushed as an assistant message, then the tool + * result is emitted as a tool message. + */ +function buildAssistantMessages(uiMessage: UIMessage): Array { + const messageList: Array = [] + let current = createSegment() + + // Track emitted tool result IDs to avoid duplicates. + // A tool call can have BOTH an explicit tool-result part AND an output + // field on the tool-call part. We only want one per tool call ID. + const emittedToolResultIds = new Set() + + function flushSegment(): void { + const content = collapseContentParts(current.contentParts) + const hasContent = content !== null + const hasToolCalls = current.toolCalls.length > 0 + + if (hasContent || hasToolCalls) { + messageList.push({ + role: 'assistant', + content, + ...(hasToolCalls && { toolCalls: current.toolCalls }), + }) } - content = contentParts - } else { - // Simple string content for text-only messages - content = textParts.map((p) => p.content).join('') || null + current = createSegment() } - const toolCalls = - toolCallParts.length > 0 - ? toolCallParts - .filter( - (p) => - p.state === 'input-complete' || - p.state === 'approval-responded' || - p.output !== undefined, // Include if has output (client tool result) - ) - .map((p) => ({ - id: p.id, + for (const part of uiMessage.parts) { + switch (part.type) { + case 'text': + case 'image': + case 'audio': + case 'video': + case 'document': + current.contentParts.push(part) + break + + case 'tool-call': + if (isToolCallIncluded(part)) { + current.toolCalls.push({ + id: part.id, type: 'function' as const, function: { - name: p.name, - arguments: p.arguments, + name: part.name, + arguments: part.arguments, }, - })) - : undefined + }) + } + break - // Create the main message - // For multimodal content, we always create a message even if content is an empty array - const hasContent = Array.isArray(content) ? true : content !== null - if (uiMessage.role !== 'assistant' || hasContent || !toolCalls) { - messageList.push({ - role: uiMessage.role, - content, - ...(toolCalls && toolCalls.length > 0 && { toolCalls }), - }) - } else if (toolCalls.length > 0) { - // Assistant message with only tool calls - messageList.push({ - role: 'assistant', - content, - toolCalls, - }) - } + case 'tool-result': + // Flush the current assistant segment before emitting the tool result + flushSegment() - // Add tool result messages for completed tool calls - // This includes: - // 1. Explicit tool-result parts (from server tools) - // 2. Client tool calls with output set - // 3. Approval-responded tool calls (approval result) - for (const toolResultPart of toolResultParts) { - if ( - toolResultPart.state === 'complete' || - toolResultPart.state === 'error' - ) { - messageList.push({ - role: 'tool', - content: toolResultPart.content, - toolCallId: toolResultPart.toolCallId, - }) + // Emit the tool result + if ( + (part.state === 'complete' || part.state === 'error') && + !emittedToolResultIds.has(part.toolCallId) + ) { + messageList.push({ + role: 'tool', + content: part.content, + toolCallId: part.toolCallId, + }) + emittedToolResultIds.add(part.toolCallId) + } + break + + // thinking parts are skipped - they're UI-only + default: + break } } - // Add tool result messages for client tool results (tools with output) - // and approval responses (so iteration tracking works correctly) - for (const toolCallPart of toolCallParts) { - // Client tool with output - add as tool result - if (toolCallPart.output !== undefined && !toolCallPart.approval) { + // Flush any remaining accumulated content + flushSegment() + + // Emit tool results from client tool-call parts with output or approval, + // but only if not already covered by an explicit tool-result part above. + // These are appended at the end since they don't have explicit tool-result + // parts in the parts array to trigger inline emission. + for (const part of uiMessage.parts) { + if (part.type !== 'tool-call') continue + + // Client tool with output - add as tool result (if not already emitted) + if ( + part.output !== undefined && + !part.approval && + !emittedToolResultIds.has(part.id) + ) { messageList.push({ role: 'tool', - content: JSON.stringify(toolCallPart.output), - toolCallId: toolCallPart.id, + content: JSON.stringify(part.output), + toolCallId: part.id, }) + emittedToolResultIds.add(part.id) } // Approval response - add as tool result for iteration tracking - // For APPROVED: includes pendingExecution marker so the tool still executes - // For DENIED: just marks the tool as complete (no execution needed) if ( - toolCallPart.state === 'approval-responded' && - toolCallPart.approval?.approved !== undefined + part.state === 'approval-responded' && + part.approval?.approved !== undefined && + !emittedToolResultIds.has(part.id) ) { - const approved = toolCallPart.approval.approved + const approved = part.approval.approved messageList.push({ role: 'tool', content: JSON.stringify({ approved, - // Mark approved tools as pending execution - they still need to run ...(approved && { pendingExecution: true }), message: approved ? 'User approved this action' : 'User denied this action', }), - toolCallId: toolCallPart.id, + toolCallId: part.id, }) + emittedToolResultIds.add(part.id) } } + // If no messages were produced (e.g., empty parts), emit a minimal assistant message + if (messageList.length === 0) { + messageList.push({ + role: 'assistant', + content: null, + }) + } + return messageList } @@ -241,13 +308,29 @@ export function modelMessageToUIMessage( ): UIMessage { const parts: Array = [] - // Handle content (convert multimodal content to text for UI) - const textContent = getTextContent(modelMessage.content) - if (textContent) { + // Handle tool results (when role is "tool") - only produce tool-result part, + // not a text part (the content IS the tool result, not display text) + if (modelMessage.role === 'tool' && modelMessage.toolCallId) { parts.push({ - type: 'text', - content: textContent, + type: 'tool-result', + toolCallId: modelMessage.toolCallId, + content: getTextContent(modelMessage.content), + state: 'complete', }) + } else if (Array.isArray(modelMessage.content)) { + // Multimodal content - preserve all content parts as MessageParts + for (const part of modelMessage.content) { + parts.push(part) + } + } else { + // String or null content + const textContent = getTextContent(modelMessage.content) + if (textContent) { + parts.push({ + type: 'text', + content: textContent, + }) + } } // Handle tool calls @@ -263,16 +346,6 @@ export function modelMessageToUIMessage( } } - // Handle tool results (when role is "tool") - if (modelMessage.role === 'tool' && modelMessage.toolCallId) { - parts.push({ - type: 'tool-result', - toolCallId: modelMessage.toolCallId, - content: getTextContent(modelMessage.content), - state: 'complete', - }) - } - return { id: id || generateMessageId(), role: modelMessage.role === 'tool' ? 'assistant' : modelMessage.role, diff --git a/packages/typescript/ai/src/activities/chat/stream/processor.ts b/packages/typescript/ai/src/activities/chat/stream/processor.ts index 4f40ca2d6..0df160790 100644 --- a/packages/typescript/ai/src/activities/chat/stream/processor.ts +++ b/packages/typescript/ai/src/activities/chat/stream/processor.ts @@ -135,6 +135,7 @@ export class StreamProcessor { private toolCallOrder: Array = [] private finishReason: string | null = null private isDone = false + private hasError = false // Track if we've had tool calls since the last text segment started private hasToolCallsSinceTextStart = false @@ -213,12 +214,47 @@ export class StreamProcessor { } /** - * Start streaming a new assistant message - * Returns the message ID + * Prepare for a new assistant message stream. + * Does NOT create the message immediately -- the message is created lazily + * when the first content-bearing chunk arrives via ensureAssistantMessage(). + * This prevents empty assistant messages from flickering in the UI when + * auto-continuation produces no content. */ - startAssistantMessage(): string { + prepareAssistantMessage(): void { // Reset stream state for new message this.resetStreamState() + // Clear the current assistant message ID so ensureAssistantMessage() + // will create a fresh message on the first content chunk + this.currentAssistantMessageId = null + } + + /** + * @deprecated Use prepareAssistantMessage() instead. This eagerly creates + * an assistant message which can cause empty message flicker. + */ + startAssistantMessage(): string { + this.prepareAssistantMessage() + return this.ensureAssistantMessage() + } + + /** + * Get the current assistant message ID (if one has been created). + * Returns null if prepareAssistantMessage() was called but no content + * has arrived yet. + */ + getCurrentAssistantMessageId(): string | null { + return this.currentAssistantMessageId + } + + /** + * Lazily create the assistant message if it hasn't been created yet. + * Called by content handlers on the first content-bearing chunk. + * Returns the message ID. + */ + private ensureAssistantMessage(): string { + if (this.currentAssistantMessageId) { + return this.currentAssistantMessageId + } const assistantMessage: UIMessage = { id: generateMessageId(), @@ -457,6 +493,8 @@ export class StreamProcessor { private handleTextMessageContentEvent( chunk: Extract, ): void { + this.ensureAssistantMessage() + // Content arriving means all current tool calls are complete this.completeAllToolCalls() @@ -519,6 +557,8 @@ export class StreamProcessor { private handleToolCallStartEvent( chunk: Extract, ): void { + this.ensureAssistantMessage() + // Mark that we've seen tool calls since the last text segment this.hasToolCallsSinceTextStart = true @@ -654,6 +694,8 @@ export class StreamProcessor { private handleRunErrorEvent( chunk: Extract, ): void { + this.ensureAssistantMessage() + this.hasError = true // Emit error event this.events.onError?.(new Error(chunk.error.message || 'An error occurred')) } @@ -664,6 +706,8 @@ export class StreamProcessor { private handleStepFinishedEvent( chunk: Extract, ): void { + this.ensureAssistantMessage() + const previous = this.thinkingContent let nextThinking = previous @@ -865,7 +909,25 @@ export class StreamProcessor { this.emitTextUpdate() } - // Emit stream end event + // Remove the assistant message if it only contains whitespace text + // (no tool calls, no meaningful content). This handles models like Gemini + // that sometimes return just "\n" during auto-continuation. + // Preserve the message on errors so the UI can show error state. + if (this.currentAssistantMessageId && !this.hasError) { + const assistantMessage = this.messages.find( + (m) => m.id === this.currentAssistantMessageId, + ) + if (assistantMessage && this.isWhitespaceOnlyMessage(assistantMessage)) { + this.messages = this.messages.filter( + (m) => m.id !== this.currentAssistantMessageId, + ) + this.emitMessagesChange() + this.currentAssistantMessageId = null + return + } + } + + // Emit stream end event (only if a message was actually created) if (this.currentAssistantMessageId) { const assistantMessage = this.messages.find( (m) => m.id === this.currentAssistantMessageId, @@ -876,6 +938,19 @@ export class StreamProcessor { } } + /** + * Check if a message contains only whitespace text and nothing else. + * Returns false if the message has tool calls, tool results, or + * any non-whitespace text content. + */ + private isWhitespaceOnlyMessage(message: UIMessage): boolean { + if (message.parts.length === 0) return true + + return message.parts.every( + (part) => part.type === 'text' && !part.content.trim(), + ) + } + /** * Get completed tool calls in API format */ @@ -951,6 +1026,7 @@ export class StreamProcessor { this.toolCallOrder = [] this.finishReason = null this.isDone = false + this.hasError = false this.hasToolCallsSinceTextStart = false this.chunkStrategy.reset?.() } diff --git a/packages/typescript/ai/tests/message-converters.test.ts b/packages/typescript/ai/tests/message-converters.test.ts index 60df58ec0..64a31acc1 100644 --- a/packages/typescript/ai/tests/message-converters.test.ts +++ b/packages/typescript/ai/tests/message-converters.test.ts @@ -1,6 +1,9 @@ import { describe, expect, it } from 'vitest' import { + convertMessagesToModelMessages, modelMessageToUIMessage, + modelMessagesToUIMessages, + normalizeToUIMessage, uiMessageToModelMessages, } from '../src/activities/chat/messages' import type { ContentPart, ModelMessage, UIMessage } from '../src/types' @@ -314,6 +317,13 @@ describe('Message Converters', () => { id: 'msg-1', role: 'assistant', parts: [ + { + type: 'tool-call', + id: 'tool-1', + name: 'getWeather', + arguments: '{"city": "NYC"}', + state: 'input-complete', + }, { type: 'tool-result', toolCallId: 'tool-1', @@ -325,12 +335,156 @@ describe('Message Converters', () => { const result = uiMessageToModelMessages(uiMessage) - // Should have assistant message + tool message + // Should have assistant message (with tool call) + tool result message expect(result.length).toBe(2) + expect(result[0]?.role).toBe('assistant') + expect(result[0]?.toolCalls?.[0]?.id).toBe('tool-1') expect(result[1]?.role).toBe('tool') expect(result[1]?.toolCallId).toBe('tool-1') expect(result[1]?.content).toBe('{"temp": 72}') }) + + it('should preserve interleaving of text, tool calls, and tool results', () => { + const uiMessage: UIMessage = { + id: 'msg-1', + role: 'assistant', + parts: [ + { type: 'text', content: 'Let me check the weather.' }, + { + type: 'tool-call', + id: 'tc-1', + name: 'getWeather', + arguments: '{"city": "NYC"}', + state: 'input-complete', + }, + { + type: 'tool-result', + toolCallId: 'tc-1', + content: '{"temp": 72}', + state: 'complete', + }, + { type: 'text', content: 'The temperature is 72F.' }, + ], + } + + const result = uiMessageToModelMessages(uiMessage) + + // Should produce: assistant(text1 + toolCall) → tool(result) → assistant(text2) + expect(result.length).toBe(3) + + expect(result[0]?.role).toBe('assistant') + expect(result[0]?.content).toBe('Let me check the weather.') + expect(result[0]?.toolCalls).toHaveLength(1) + expect(result[0]?.toolCalls?.[0]?.id).toBe('tc-1') + + expect(result[1]?.role).toBe('tool') + expect(result[1]?.toolCallId).toBe('tc-1') + expect(result[1]?.content).toBe('{"temp": 72}') + + expect(result[2]?.role).toBe('assistant') + expect(result[2]?.content).toBe('The temperature is 72F.') + expect(result[2]?.toolCalls).toBeUndefined() + }) + + it('should handle multi-round tool flow (text1 -> tool1 -> result1 -> text2 -> tool2 -> result2)', () => { + const uiMessage: UIMessage = { + id: 'msg-1', + role: 'assistant', + parts: [ + { type: 'text', content: 'Let me check our inventory.' }, + { + type: 'tool-call', + id: 'tc-get', + name: 'getGuitars', + arguments: '', + state: 'input-complete', + }, + { + type: 'tool-result', + toolCallId: 'tc-get', + content: '[{"id":7,"name":"Travelin Man"}]', + state: 'complete', + }, + { + type: 'text', + content: 'I found a great guitar! Let me recommend it.', + }, + { + type: 'tool-call', + id: 'tc-rec', + name: 'recommendGuitar', + arguments: '{"id": 7}', + state: 'input-complete', + output: { id: 7 }, + }, + { + type: 'tool-result', + toolCallId: 'tc-rec', + content: '{"id":7}', + state: 'complete', + }, + ], + } + + const result = uiMessageToModelMessages(uiMessage) + + // Should produce: + // 1. assistant(text1 + getGuitars) + // 2. tool(getGuitars result) + // 3. assistant(text2 + recommendGuitar) + // 4. tool(recommendGuitar result) -- only once, no duplicate + expect(result.length).toBe(4) + + expect(result[0]?.role).toBe('assistant') + expect(result[0]?.content).toBe('Let me check our inventory.') + expect(result[0]?.toolCalls?.[0]?.function.name).toBe('getGuitars') + + expect(result[1]?.role).toBe('tool') + expect(result[1]?.toolCallId).toBe('tc-get') + + expect(result[2]?.role).toBe('assistant') + expect(result[2]?.content).toBe( + 'I found a great guitar! Let me recommend it.', + ) + expect(result[2]?.toolCalls?.[0]?.function.name).toBe('recommendGuitar') + + expect(result[3]?.role).toBe('tool') + expect(result[3]?.toolCallId).toBe('tc-rec') + + // No duplicate tool result for recommendGuitar (has both output and tool-result) + const toolMessages = result.filter((m) => m.role === 'tool') + expect(toolMessages).toHaveLength(2) + }) + + it('should handle tool-call-only segment (no text before tool call)', () => { + const uiMessage: UIMessage = { + id: 'msg-1', + role: 'assistant', + parts: [ + { + type: 'tool-call', + id: 'tc-1', + name: 'getGuitars', + arguments: '{}', + state: 'input-complete', + }, + { + type: 'tool-result', + toolCallId: 'tc-1', + content: '[]', + state: 'complete', + }, + ], + } + + const result = uiMessageToModelMessages(uiMessage) + + expect(result.length).toBe(2) + expect(result[0]?.role).toBe('assistant') + expect(result[0]?.content).toBeNull() + expect(result[0]?.toolCalls).toHaveLength(1) + expect(result[1]?.role).toBe('tool') + }) }) describe('modelMessageToUIMessage', () => { @@ -358,7 +512,7 @@ describe('Message Converters', () => { expect(result.id).toBe('custom-id') }) - it('should convert multimodal content to text', () => { + it('should preserve multimodal content parts', () => { const modelMessage: ModelMessage = { role: 'user', content: [ @@ -372,8 +526,13 @@ describe('Message Converters', () => { const result = modelMessageToUIMessage(modelMessage) - // Currently, modelMessageToUIMessage only extracts text content - expect(result.parts).toEqual([{ type: 'text', content: 'What is this?' }]) + expect(result.parts).toEqual([ + { type: 'text', content: 'What is this?' }, + { + type: 'image', + source: { type: 'url', value: 'https://example.com/img.jpg' }, + }, + ]) }) it('should handle tool message', () => { @@ -393,5 +552,885 @@ describe('Message Converters', () => { state: 'complete', }) }) + + it('should convert assistant message with toolCalls and text', () => { + const modelMessage: ModelMessage = { + role: 'assistant', + content: 'Let me check the weather.', + toolCalls: [ + { + id: 'tc-1', + type: 'function', + function: { + name: 'getWeather', + arguments: '{"city": "NYC"}', + }, + }, + ], + } + + const result = modelMessageToUIMessage(modelMessage) + + expect(result.role).toBe('assistant') + expect(result.parts).toEqual([ + { type: 'text', content: 'Let me check the weather.' }, + { + type: 'tool-call', + id: 'tc-1', + name: 'getWeather', + arguments: '{"city": "NYC"}', + state: 'input-complete', + }, + ]) + }) + + it('should convert assistant message with toolCalls and null content', () => { + const modelMessage: ModelMessage = { + role: 'assistant', + content: null, + toolCalls: [ + { + id: 'tc-1', + type: 'function', + function: { + name: 'getWeather', + arguments: '{"city": "NYC"}', + }, + }, + ], + } + + const result = modelMessageToUIMessage(modelMessage) + + expect(result.role).toBe('assistant') + // Should have only tool-call part, no text part + expect(result.parts).toEqual([ + { + type: 'tool-call', + id: 'tc-1', + name: 'getWeather', + arguments: '{"city": "NYC"}', + state: 'input-complete', + }, + ]) + }) + + it('should preserve multimodal content parts (image, audio, video, document)', () => { + const modelMessage: ModelMessage = { + role: 'user', + content: [ + { type: 'text', content: 'What is this?' }, + { + type: 'image', + source: { type: 'url', value: 'https://example.com/img.jpg' }, + }, + { + type: 'audio', + source: { + type: 'data', + value: 'base64audio', + mimeType: 'audio/mp3', + }, + }, + { + type: 'video', + source: { type: 'url', value: 'https://example.com/video.mp4' }, + }, + { + type: 'document', + source: { + type: 'data', + value: 'base64pdf', + mimeType: 'application/pdf', + }, + }, + ], + } + + const result = modelMessageToUIMessage(modelMessage) + + expect(result.parts.length).toBe(5) + expect(result.parts[0]).toEqual({ + type: 'text', + content: 'What is this?', + }) + expect(result.parts[1]).toEqual({ + type: 'image', + source: { type: 'url', value: 'https://example.com/img.jpg' }, + }) + expect(result.parts[2]).toEqual({ + type: 'audio', + source: { + type: 'data', + value: 'base64audio', + mimeType: 'audio/mp3', + }, + }) + expect(result.parts[3]).toEqual({ + type: 'video', + source: { type: 'url', value: 'https://example.com/video.mp4' }, + }) + expect(result.parts[4]).toEqual({ + type: 'document', + source: { + type: 'data', + value: 'base64pdf', + mimeType: 'application/pdf', + }, + }) + }) + + it('should handle null content', () => { + const modelMessage: ModelMessage = { + role: 'assistant', + content: null, + } + + const result = modelMessageToUIMessage(modelMessage) + + expect(result.role).toBe('assistant') + expect(result.parts).toEqual([]) + }) + + it('should handle empty string content', () => { + const modelMessage: ModelMessage = { + role: 'assistant', + content: '', + } + + const result = modelMessageToUIMessage(modelMessage) + + expect(result.role).toBe('assistant') + // Empty string has no text content, so no text part + expect(result.parts).toEqual([]) + }) + + it('should not produce redundant text part for tool messages', () => { + const modelMessage: ModelMessage = { + role: 'tool', + content: '{"temp": 72}', + toolCallId: 'tool-1', + } + + const result = modelMessageToUIMessage(modelMessage) + + // Should have only the tool-result part, NOT a text part + tool-result + const textParts = result.parts.filter((p) => p.type === 'text') + const toolResultParts = result.parts.filter( + (p) => p.type === 'tool-result', + ) + expect(textParts).toHaveLength(0) + expect(toolResultParts).toHaveLength(1) + expect(toolResultParts[0]).toEqual({ + type: 'tool-result', + toolCallId: 'tool-1', + content: '{"temp": 72}', + state: 'complete', + }) + }) + + it('should preserve multimodal content with metadata', () => { + const modelMessage: ModelMessage = { + role: 'user', + content: [ + { type: 'text', content: 'Analyze' }, + { + type: 'image', + source: { type: 'url', value: 'https://example.com/cat.jpg' }, + metadata: { detail: 'high' }, + }, + ], + } + + const result = modelMessageToUIMessage(modelMessage) + + expect(result.parts.length).toBe(2) + expect(result.parts[1]).toEqual({ + type: 'image', + source: { type: 'url', value: 'https://example.com/cat.jpg' }, + metadata: { detail: 'high' }, + }) + }) + }) + + describe('uiMessageToModelMessages - duplicate tool result prevention', () => { + it('should not create duplicate tool results when tool-call has output AND tool-result exists', () => { + // This scenario happens when a client tool executes: the UIMessage has both + // a tool-call part with output AND a tool-result part for the same toolCallId + const uiMessage: UIMessage = { + id: 'msg-1', + role: 'assistant', + parts: [ + { + type: 'text', + content: 'Let me recommend a guitar.', + }, + { + type: 'tool-call', + id: 'tc-1', + name: 'recommendGuitar', + arguments: '{"id": 7}', + state: 'input-complete', + output: { id: 7 }, + }, + { + type: 'tool-result', + toolCallId: 'tc-1', + content: '{"id":7}', + state: 'complete', + }, + ], + } + + const result = uiMessageToModelMessages(uiMessage) + + // Should have: 1 assistant message + 1 tool result (NOT 2) + const toolMessages = result.filter((m) => m.role === 'tool') + expect(toolMessages).toHaveLength(1) + expect(toolMessages[0]?.toolCallId).toBe('tc-1') + }) + + it('should handle multi-round tool calls without duplicating results', () => { + // This scenario simulates the full multi-round message: + // text1 + getGuitars tool call + getGuitars result + text2 + recommendGuitar tool call + recommendGuitar result + const uiMessage: UIMessage = { + id: 'msg-1', + role: 'assistant', + parts: [ + { type: 'text', content: 'Let me check our inventory.' }, + { + type: 'tool-call', + id: 'tc-get', + name: 'getGuitars', + arguments: '', + state: 'input-complete', + }, + { + type: 'tool-result', + toolCallId: 'tc-get', + content: '[{"id":7,"name":"Travelin Man Guitar"}]', + state: 'complete', + }, + { type: 'text', content: 'I found a great guitar!' }, + { + type: 'tool-call', + id: 'tc-rec', + name: 'recommendGuitar', + arguments: '{"id": 7}', + state: 'input-complete', + output: { id: 7 }, + }, + { + type: 'tool-result', + toolCallId: 'tc-rec', + content: '{"id":7}', + state: 'complete', + }, + ], + } + + const result = uiMessageToModelMessages(uiMessage) + + // Should have exactly 2 tool result messages (one per tool call, no duplicates) + const toolMessages = result.filter((m) => m.role === 'tool') + expect(toolMessages).toHaveLength(2) + expect(toolMessages[0]?.toolCallId).toBe('tc-get') + expect(toolMessages[1]?.toolCallId).toBe('tc-rec') + }) + }) + + describe('modelMessagesToUIMessages', () => { + it('should convert simple user + assistant conversation', () => { + const modelMessages: Array = [ + { role: 'user', content: 'Hello' }, + { role: 'assistant', content: 'Hi there!' }, + ] + + const result = modelMessagesToUIMessages(modelMessages) + + expect(result.length).toBe(2) + expect(result[0]?.role).toBe('user') + expect(result[0]?.parts).toEqual([{ type: 'text', content: 'Hello' }]) + expect(result[1]?.role).toBe('assistant') + expect(result[1]?.parts).toEqual([{ type: 'text', content: 'Hi there!' }]) + }) + + it('should merge tool result into preceding assistant message', () => { + const modelMessages: Array = [ + { + role: 'assistant', + content: 'Let me check.', + toolCalls: [ + { + id: 'tc-1', + type: 'function', + function: { name: 'getWeather', arguments: '{"city":"NYC"}' }, + }, + ], + }, + { + role: 'tool', + content: '{"temp": 72}', + toolCallId: 'tc-1', + }, + ] + + const result = modelMessagesToUIMessages(modelMessages) + + // Tool result should be merged into the assistant message + expect(result.length).toBe(1) + expect(result[0]?.role).toBe('assistant') + expect(result[0]?.parts).toEqual([ + { type: 'text', content: 'Let me check.' }, + { + type: 'tool-call', + id: 'tc-1', + name: 'getWeather', + arguments: '{"city":"NYC"}', + state: 'input-complete', + }, + { + type: 'tool-result', + toolCallId: 'tc-1', + content: '{"temp": 72}', + state: 'complete', + }, + ]) + }) + + it('should handle multi-round tool flow with proper merging', () => { + const modelMessages: Array = [ + { + role: 'assistant', + content: 'Checking inventory.', + toolCalls: [ + { + id: 'tc-1', + type: 'function', + function: { name: 'getGuitars', arguments: '' }, + }, + ], + }, + { + role: 'tool', + content: '[{"id":7}]', + toolCallId: 'tc-1', + }, + { + role: 'assistant', + content: 'Found one! Recommending.', + toolCalls: [ + { + id: 'tc-2', + type: 'function', + function: { name: 'recommend', arguments: '{"id":7}' }, + }, + ], + }, + { + role: 'tool', + content: '{"recommended":true}', + toolCallId: 'tc-2', + }, + ] + + const result = modelMessagesToUIMessages(modelMessages) + + // Each assistant message should have its tool result merged in + expect(result.length).toBe(2) + + expect(result[0]?.parts).toEqual([ + { type: 'text', content: 'Checking inventory.' }, + { + type: 'tool-call', + id: 'tc-1', + name: 'getGuitars', + arguments: '', + state: 'input-complete', + }, + { + type: 'tool-result', + toolCallId: 'tc-1', + content: '[{"id":7}]', + state: 'complete', + }, + ]) + + expect(result[1]?.parts).toEqual([ + { type: 'text', content: 'Found one! Recommending.' }, + { + type: 'tool-call', + id: 'tc-2', + name: 'recommend', + arguments: '{"id":7}', + state: 'input-complete', + }, + { + type: 'tool-result', + toolCallId: 'tc-2', + content: '{"recommended":true}', + state: 'complete', + }, + ]) + }) + + it('should create standalone message for orphan tool result', () => { + const modelMessages: Array = [ + { + role: 'tool', + content: '{"result": "orphan"}', + toolCallId: 'tc-1', + }, + ] + + const result = modelMessagesToUIMessages(modelMessages) + + expect(result.length).toBe(1) + expect(result[0]?.role).toBe('assistant') + expect(result[0]?.parts).toContainEqual({ + type: 'tool-result', + toolCallId: 'tc-1', + content: '{"result": "orphan"}', + state: 'complete', + }) + }) + + it('should not merge tool result across user messages', () => { + const modelMessages: Array = [ + { role: 'user', content: 'Hi' }, + { role: 'assistant', content: 'Hello!' }, + { role: 'user', content: 'Another question' }, + { + role: 'tool', + content: '{"result": "orphan"}', + toolCallId: 'tc-1', + }, + ] + + const result = modelMessagesToUIMessages(modelMessages) + + // Tool result should NOT merge into the assistant message (user message in between) + expect(result.length).toBe(4) + expect(result[3]?.role).toBe('assistant') + expect(result[3]?.parts).toContainEqual({ + type: 'tool-result', + toolCallId: 'tc-1', + content: '{"result": "orphan"}', + state: 'complete', + }) + }) + + it('should handle complex interleaved conversation', () => { + const modelMessages: Array = [ + { role: 'user', content: 'Check the weather' }, + { + role: 'assistant', + content: null, + toolCalls: [ + { + id: 'tc-1', + type: 'function', + function: { name: 'getWeather', arguments: '{"city":"NYC"}' }, + }, + ], + }, + { role: 'tool', content: '{"temp":72}', toolCallId: 'tc-1' }, + { role: 'assistant', content: 'The temperature is 72F.' }, + ] + + const result = modelMessagesToUIMessages(modelMessages) + + expect(result.length).toBe(3) + expect(result[0]?.role).toBe('user') + + // Assistant with tool call + merged tool result + expect(result[1]?.role).toBe('assistant') + const assistantParts = result[1]?.parts || [] + expect(assistantParts).toContainEqual({ + type: 'tool-call', + id: 'tc-1', + name: 'getWeather', + arguments: '{"city":"NYC"}', + state: 'input-complete', + }) + expect(assistantParts).toContainEqual({ + type: 'tool-result', + toolCallId: 'tc-1', + content: '{"temp":72}', + state: 'complete', + }) + + // Final assistant text + expect(result[2]?.role).toBe('assistant') + expect(result[2]?.parts).toEqual([ + { type: 'text', content: 'The temperature is 72F.' }, + ]) + }) + }) + + describe('convertMessagesToModelMessages', () => { + it('should pass through ModelMessages unchanged', () => { + const messages: Array = [ + { role: 'user', content: 'Hello' }, + { role: 'assistant', content: 'Hi' }, + ] + + const result = convertMessagesToModelMessages(messages) + + expect(result).toEqual(messages) + }) + + it('should convert UIMessages to ModelMessages', () => { + const messages: Array = [ + { + id: 'msg-1', + role: 'user', + parts: [{ type: 'text', content: 'Hello' }], + }, + ] + + const result = convertMessagesToModelMessages(messages) + + expect(result).toEqual([{ role: 'user', content: 'Hello' }]) + }) + + it('should handle mixed UIMessage and ModelMessage array', () => { + const messages: Array = [ + { + id: 'msg-1', + role: 'user', + parts: [{ type: 'text', content: 'Hello' }], + }, + { role: 'assistant', content: 'Hi there!' }, + ] + + const result = convertMessagesToModelMessages(messages) + + expect(result).toEqual([ + { role: 'user', content: 'Hello' }, + { role: 'assistant', content: 'Hi there!' }, + ]) + }) + + it('should handle empty array', () => { + const result = convertMessagesToModelMessages([]) + expect(result).toEqual([]) + }) + }) + + describe('normalizeToUIMessage', () => { + it('should pass through UIMessage with existing id and createdAt', () => { + const date = new Date('2025-01-01') + const message: UIMessage = { + id: 'existing-id', + role: 'user', + parts: [{ type: 'text', content: 'Hello' }], + createdAt: date, + } + + const result = normalizeToUIMessage(message, () => 'generated-id') + + expect(result.id).toBe('existing-id') + expect(result.createdAt).toBe(date) + expect(result.parts).toEqual([{ type: 'text', content: 'Hello' }]) + }) + + it('should generate id for UIMessage without id', () => { + const message = { + id: '', + role: 'user' as const, + parts: [{ type: 'text' as const, content: 'Hello' }], + } + + const result = normalizeToUIMessage(message, () => 'generated-id') + + expect(result.id).toBe('generated-id') + expect(result.createdAt).toBeTruthy() + }) + + it('should convert ModelMessage to UIMessage', () => { + const message: ModelMessage = { + role: 'user', + content: 'Hello', + } + + const result = normalizeToUIMessage(message, () => 'generated-id') + + expect(result.id).toBe('generated-id') + expect(result.role).toBe('user') + expect(result.parts).toEqual([{ type: 'text', content: 'Hello' }]) + expect(result.createdAt).toBeTruthy() + }) + }) + + describe('Round-trip symmetry: UI -> Model -> UI', () => { + it('should round-trip simple text user message', () => { + const original: UIMessage = { + id: 'msg-1', + role: 'user', + parts: [{ type: 'text', content: 'Hello world' }], + } + + const modelMessages = uiMessageToModelMessages(original) + const uiMessages = modelMessagesToUIMessages(modelMessages) + + expect(uiMessages.length).toBe(1) + expect(uiMessages[0]?.role).toBe(original.role) + expect(uiMessages[0]?.parts).toEqual(original.parts) + }) + + it('should round-trip assistant with tool-call + tool-result', () => { + const original: UIMessage = { + id: 'msg-1', + role: 'assistant', + parts: [ + { type: 'text', content: 'Let me check.' }, + { + type: 'tool-call', + id: 'tc-1', + name: 'getWeather', + arguments: '{"city":"NYC"}', + state: 'input-complete', + }, + { + type: 'tool-result', + toolCallId: 'tc-1', + content: '{"temp": 72}', + state: 'complete', + }, + ], + } + + const modelMessages = uiMessageToModelMessages(original) + const uiMessages = modelMessagesToUIMessages(modelMessages) + + // Should produce a single assistant UIMessage with all parts merged back + expect(uiMessages.length).toBe(1) + expect(uiMessages[0]?.role).toBe('assistant') + expect(uiMessages[0]?.parts).toEqual(original.parts) + }) + + it('should round-trip multimodal user message with image', () => { + const original: UIMessage = { + id: 'msg-1', + role: 'user', + parts: [ + { type: 'text', content: 'What is this?' }, + { + type: 'image', + source: { type: 'url', value: 'https://example.com/img.jpg' }, + }, + ], + } + + const modelMessages = uiMessageToModelMessages(original) + const uiMessages = modelMessagesToUIMessages(modelMessages) + + expect(uiMessages.length).toBe(1) + expect(uiMessages[0]?.role).toBe('user') + expect(uiMessages[0]?.parts).toEqual(original.parts) + }) + + it('should round-trip multi-round tool flow', () => { + const original: UIMessage = { + id: 'msg-1', + role: 'assistant', + parts: [ + { type: 'text', content: 'Checking inventory.' }, + { + type: 'tool-call', + id: 'tc-1', + name: 'getGuitars', + arguments: '', + state: 'input-complete', + }, + { + type: 'tool-result', + toolCallId: 'tc-1', + content: '[{"id":7}]', + state: 'complete', + }, + { type: 'text', content: 'Found one!' }, + { + type: 'tool-call', + id: 'tc-2', + name: 'recommend', + arguments: '{"id":7}', + state: 'input-complete', + }, + { + type: 'tool-result', + toolCallId: 'tc-2', + content: '{"recommended":true}', + state: 'complete', + }, + ], + } + + const modelMessages = uiMessageToModelMessages(original) + const uiMessages = modelMessagesToUIMessages(modelMessages) + + // Multi-round should produce multiple UIMessages (one per segment) + // but when recombined, the structure should match segments + expect(uiMessages.length).toBe(2) + + // First segment: text + tool-call + tool-result + expect(uiMessages[0]?.parts).toEqual([ + { type: 'text', content: 'Checking inventory.' }, + { + type: 'tool-call', + id: 'tc-1', + name: 'getGuitars', + arguments: '', + state: 'input-complete', + }, + { + type: 'tool-result', + toolCallId: 'tc-1', + content: '[{"id":7}]', + state: 'complete', + }, + ]) + + // Second segment: text + tool-call + tool-result + expect(uiMessages[1]?.parts).toEqual([ + { type: 'text', content: 'Found one!' }, + { + type: 'tool-call', + id: 'tc-2', + name: 'recommend', + arguments: '{"id":7}', + state: 'input-complete', + }, + { + type: 'tool-result', + toolCallId: 'tc-2', + content: '{"recommended":true}', + state: 'complete', + }, + ]) + }) + }) + + describe('Round-trip symmetry: Model -> UI -> Model', () => { + it('should round-trip simple text messages', () => { + const original: Array = [ + { role: 'user', content: 'Hello' }, + { role: 'assistant', content: 'Hi there!' }, + ] + + const uiMessages = modelMessagesToUIMessages(original) + const modelMessages = convertMessagesToModelMessages(uiMessages) + + expect(modelMessages).toEqual(original) + }) + + it('should round-trip assistant with toolCalls + tool result', () => { + const original: Array = [ + { + role: 'assistant', + content: 'Let me check.', + toolCalls: [ + { + id: 'tc-1', + type: 'function', + function: { name: 'getWeather', arguments: '{"city":"NYC"}' }, + }, + ], + }, + { + role: 'tool', + content: '{"temp": 72}', + toolCallId: 'tc-1', + }, + ] + + const uiMessages = modelMessagesToUIMessages(original) + const modelMessages = convertMessagesToModelMessages(uiMessages) + + expect(modelMessages).toEqual(original) + }) + + it('should round-trip multimodal content array', () => { + const original: Array = [ + { + role: 'user', + content: [ + { type: 'text', content: 'What is this?' }, + { + type: 'image', + source: { type: 'url', value: 'https://example.com/img.jpg' }, + }, + ], + }, + ] + + const uiMessages = modelMessagesToUIMessages(original) + const modelMessages = convertMessagesToModelMessages(uiMessages) + + expect(modelMessages).toEqual(original) + }) + + it('should round-trip multi-round tool conversation', () => { + const original: Array = [ + { role: 'user', content: 'Check guitars' }, + { + role: 'assistant', + content: 'Checking.', + toolCalls: [ + { + id: 'tc-1', + type: 'function', + function: { name: 'getGuitars', arguments: '' }, + }, + ], + }, + { role: 'tool', content: '[{"id":7}]', toolCallId: 'tc-1' }, + { + role: 'assistant', + content: 'Found one!', + toolCalls: [ + { + id: 'tc-2', + type: 'function', + function: { name: 'recommend', arguments: '{"id":7}' }, + }, + ], + }, + { + role: 'tool', + content: '{"recommended":true}', + toolCallId: 'tc-2', + }, + { role: 'assistant', content: 'Here is my recommendation.' }, + ] + + const uiMessages = modelMessagesToUIMessages(original) + const modelMessages = convertMessagesToModelMessages(uiMessages) + + expect(modelMessages).toEqual(original) + }) + + it('should round-trip assistant with null content and toolCalls', () => { + const original: Array = [ + { + role: 'assistant', + content: null, + toolCalls: [ + { + id: 'tc-1', + type: 'function', + function: { name: 'getWeather', arguments: '{}' }, + }, + ], + }, + { role: 'tool', content: '{"temp":72}', toolCallId: 'tc-1' }, + ] + + const uiMessages = modelMessagesToUIMessages(original) + const modelMessages = convertMessagesToModelMessages(uiMessages) + + expect(modelMessages).toEqual(original) + }) }) }) diff --git a/packages/typescript/ai/tests/stream-processor.test.ts b/packages/typescript/ai/tests/stream-processor.test.ts index 3358e2977..858224141 100644 --- a/packages/typescript/ai/tests/stream-processor.test.ts +++ b/packages/typescript/ai/tests/stream-processor.test.ts @@ -6,7 +6,7 @@ describe('StreamProcessor', () => { describe('handleTextMessageContentEvent', () => { it('should handle TEXT_MESSAGE_CONTENT with delta', () => { const processor = new StreamProcessor() - processor.startAssistantMessage() + processor.prepareAssistantMessage() processor.processChunk({ type: 'TEXT_MESSAGE_CONTENT', @@ -42,7 +42,7 @@ describe('StreamProcessor', () => { it('should handle TEXT_MESSAGE_CONTENT with undefined delta (issue #257)', () => { const processor = new StreamProcessor() - processor.startAssistantMessage() + processor.prepareAssistantMessage() // Simulate a chunk where delta is undefined (which can happen in practice) processor.processChunk({ @@ -82,7 +82,7 @@ describe('StreamProcessor', () => { it('should handle TEXT_MESSAGE_CONTENT with empty delta', () => { const processor = new StreamProcessor() - processor.startAssistantMessage() + processor.prepareAssistantMessage() // Empty delta should fall back to content processor.processChunk({ @@ -112,7 +112,7 @@ describe('StreamProcessor', () => { it('should handle TEXT_MESSAGE_CONTENT with only content (no delta)', () => { const processor = new StreamProcessor() - processor.startAssistantMessage() + processor.prepareAssistantMessage() // Some servers may only send content without delta processor.processChunk({ @@ -146,12 +146,14 @@ describe('StreamProcessor', () => { content: 'Hello world', }) }) + }) - it('should have empty parts when no TEXT_MESSAGE_CONTENT is received', () => { + describe('lazy assistant message creation', () => { + it('should not create assistant message when no content arrives', () => { const processor = new StreamProcessor() - processor.startAssistantMessage() + processor.prepareAssistantMessage() - // Only RUN_FINISHED without any text content + // Only RUN_FINISHED without any content-bearing chunks processor.processChunk({ type: 'RUN_FINISHED', model: 'test', @@ -159,10 +161,198 @@ describe('StreamProcessor', () => { finishReason: 'stop', } as StreamChunk) + processor.finalizeStream() + + const messages = processor.getMessages() + // No message should have been created + expect(messages).toHaveLength(0) + expect(processor.getCurrentAssistantMessageId()).toBeNull() + }) + + it('should create assistant message lazily on first text content', () => { + const processor = new StreamProcessor() + processor.prepareAssistantMessage() + + // No message yet + expect(processor.getMessages()).toHaveLength(0) + expect(processor.getCurrentAssistantMessageId()).toBeNull() + + // First text chunk triggers creation + processor.processChunk({ + type: 'TEXT_MESSAGE_CONTENT', + messageId: 'msg-1', + delta: 'Hello!', + model: 'test', + timestamp: Date.now(), + } as StreamChunk) + + // Now the message exists + expect(processor.getMessages()).toHaveLength(1) + expect(processor.getCurrentAssistantMessageId()).not.toBeNull() + expect(processor.getMessages()[0]?.role).toBe('assistant') + }) + + it('should create assistant message lazily on first tool call', () => { + const processor = new StreamProcessor() + processor.prepareAssistantMessage() + + // No message yet + expect(processor.getMessages()).toHaveLength(0) + + processor.processChunk({ + type: 'TOOL_CALL_START', + toolCallId: 'tc-1', + toolName: 'getGuitars', + model: 'test', + timestamp: Date.now(), + } as StreamChunk) + + // Now the message exists with a tool call part + expect(processor.getMessages()).toHaveLength(1) + expect( + processor.getMessages()[0]?.parts.some((p) => p.type === 'tool-call'), + ).toBe(true) + }) + + it('should create assistant message lazily on error', () => { + const processor = new StreamProcessor() + processor.prepareAssistantMessage() + + // No message yet + expect(processor.getMessages()).toHaveLength(0) + + processor.processChunk({ + type: 'RUN_ERROR', + runId: 'run-1', + model: 'test', + timestamp: Date.now(), + error: { message: 'Something went wrong' }, + } as StreamChunk) + + processor.finalizeStream() + + // Error creates a message so UI can display error state const messages = processor.getMessages() expect(messages).toHaveLength(1) - // Parts should be empty when no content was received + expect(messages[0]?.role).toBe('assistant') expect(messages[0]?.parts).toHaveLength(0) }) + + it('should create assistant message lazily on thinking content', () => { + const processor = new StreamProcessor() + processor.prepareAssistantMessage() + + // No message yet + expect(processor.getMessages()).toHaveLength(0) + + processor.processChunk({ + type: 'STEP_FINISHED', + stepId: 'step-1', + model: 'test', + timestamp: Date.now(), + delta: 'thinking...', + content: 'thinking...', + } as StreamChunk) + + // Now the message exists with thinking content + expect(processor.getMessages()).toHaveLength(1) + expect( + processor.getMessages()[0]?.parts.some((p) => p.type === 'thinking'), + ).toBe(true) + }) + + it('should not create assistant message during empty multi-turn continuation', () => { + const processor = new StreamProcessor() + + // Simulate first turn: user message + assistant with tool calls + processor.addUserMessage('recommend a guitar') + processor.prepareAssistantMessage() + + processor.processChunk({ + type: 'TOOL_CALL_START', + toolCallId: 'tc-1', + toolName: 'getGuitars', + model: 'test', + timestamp: Date.now(), + } as StreamChunk) + + processor.processChunk({ + type: 'TOOL_CALL_END', + toolCallId: 'tc-1', + toolName: 'getGuitars', + model: 'test', + timestamp: Date.now(), + input: {}, + } as StreamChunk) + + processor.processChunk({ + type: 'RUN_FINISHED', + model: 'test', + timestamp: Date.now(), + finishReason: 'tool_calls', + } as StreamChunk) + + processor.finalizeStream() + + // Should have: user + assistant with tool call + expect(processor.getMessages()).toHaveLength(2) + + // Simulate auto-continuation: prepare but no content arrives + processor.prepareAssistantMessage() + + processor.processChunk({ + type: 'RUN_FINISHED', + model: 'test', + timestamp: Date.now(), + finishReason: 'stop', + } as StreamChunk) + + processor.finalizeStream() + + // No new message was created - still just user + assistant with tool call + const messages = processor.getMessages() + expect(messages).toHaveLength(2) + expect(messages[1]?.role).toBe('assistant') + expect(messages[1]?.parts.some((p) => p.type === 'tool-call')).toBe(true) + }) + + it('should keep assistant message with meaningful text content', () => { + const processor = new StreamProcessor() + processor.prepareAssistantMessage() + + processor.processChunk({ + type: 'TEXT_MESSAGE_CONTENT', + messageId: 'msg-1', + delta: 'Hello!', + model: 'test', + timestamp: Date.now(), + } as StreamChunk) + + processor.processChunk({ + type: 'RUN_FINISHED', + model: 'test', + timestamp: Date.now(), + finishReason: 'stop', + } as StreamChunk) + + processor.finalizeStream() + + const messages = processor.getMessages() + expect(messages).toHaveLength(1) + expect(messages[0]?.parts[0]).toEqual({ + type: 'text', + content: 'Hello!', + }) + }) + + it('should support deprecated startAssistantMessage for backwards compatibility', () => { + const processor = new StreamProcessor() + const messageId = processor.startAssistantMessage() + + // startAssistantMessage eagerly creates the message + expect(messageId).toBeTruthy() + expect(processor.getMessages()).toHaveLength(1) + expect(processor.getMessages()[0]?.id).toBe(messageId) + }) }) }) diff --git a/packages/typescript/smoke-tests/e2e/package.json b/packages/typescript/smoke-tests/e2e/package.json index af604c097..07e5e8594 100644 --- a/packages/typescript/smoke-tests/e2e/package.json +++ b/packages/typescript/smoke-tests/e2e/package.json @@ -18,10 +18,10 @@ "@tanstack/ai-client": "workspace:*", "@tanstack/ai-openai": "workspace:*", "@tanstack/ai-react": "workspace:*", - "@tanstack/nitro-v2-vite-plugin": "^1.141.0", - "@tanstack/react-router": "^1.141.1", - "@tanstack/react-start": "^1.141.1", - "@tanstack/router-plugin": "^1.139.7", + "@tanstack/nitro-v2-vite-plugin": "^1.154.7", + "@tanstack/react-router": "^1.158.4", + "@tanstack/react-start": "^1.159.0", + "@tanstack/router-plugin": "^1.158.4", "@tanstack/tests-adapters": "workspace:*", "react": "^19.2.3", "react-dom": "^19.2.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7b8afb554..eb07698c9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -114,20 +114,20 @@ importers: specifier: ^0.8.2 version: 0.8.4(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(csstype@3.2.3)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(solid-js@1.9.10) '@tanstack/react-router': - specifier: ^1.141.1 - version: 1.141.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + specifier: ^1.158.4 + version: 1.158.4(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@tanstack/react-router-devtools': - specifier: ^1.139.7 - version: 1.141.1(@tanstack/react-router@1.141.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@tanstack/router-core@1.157.16)(csstype@3.2.3)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(solid-js@1.9.10) + specifier: ^1.158.4 + version: 1.158.4(@tanstack/react-router@1.158.4(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@tanstack/router-core@1.158.4)(csstype@3.2.3)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@tanstack/react-router-ssr-query': - specifier: ^1.139.7 - version: 1.141.1(@tanstack/query-core@5.90.12)(@tanstack/react-query@5.90.12(react@19.2.3))(@tanstack/react-router@1.141.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@tanstack/router-core@1.157.16)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + specifier: ^1.158.4 + version: 1.158.4(@tanstack/query-core@5.90.12)(@tanstack/react-query@5.90.12(react@19.2.3))(@tanstack/react-router@1.158.4(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@tanstack/router-core@1.158.4)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@tanstack/react-start': - specifier: ^1.141.1 - version: 1.141.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(vite-plugin-solid@2.11.10(solid-js@1.9.10)(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) + specifier: ^1.159.0 + version: 1.159.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(vite-plugin-solid@2.11.10(solid-js@1.9.10)(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) '@tanstack/router-plugin': - specifier: ^1.139.7 - version: 1.141.1(@tanstack/react-router@1.141.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(vite-plugin-solid@2.11.10(solid-js@1.9.10)(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) + specifier: ^1.158.4 + version: 1.158.4(@tanstack/react-router@1.158.4(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(vite-plugin-solid@2.11.10(solid-js@1.9.10)(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) capnweb: specifier: ^0.1.0 version: 0.1.0 @@ -223,29 +223,29 @@ importers: specifier: workspace:* version: link:../../packages/typescript/ai-react-ui '@tanstack/nitro-v2-vite-plugin': - specifier: ^1.141.0 + specifier: ^1.154.7 version: 1.154.7(rolldown@1.0.0-beta.53)(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) '@tanstack/react-devtools': specifier: ^0.8.2 version: 0.8.4(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(csstype@3.2.3)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(solid-js@1.9.10) '@tanstack/react-router': - specifier: ^1.141.1 - version: 1.157.16(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + specifier: ^1.158.4 + version: 1.158.4(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@tanstack/react-router-devtools': - specifier: ^1.139.7 - version: 1.157.16(@tanstack/react-router@1.157.16(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@tanstack/router-core@1.157.16)(csstype@3.2.3)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + specifier: ^1.158.4 + version: 1.158.4(@tanstack/react-router@1.158.4(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@tanstack/router-core@1.158.4)(csstype@3.2.3)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@tanstack/react-router-ssr-query': - specifier: ^1.139.7 - version: 1.141.1(@tanstack/query-core@5.90.12)(@tanstack/react-query@5.90.12(react@19.2.3))(@tanstack/react-router@1.157.16(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@tanstack/router-core@1.157.16)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + specifier: ^1.158.4 + version: 1.158.4(@tanstack/query-core@5.90.12)(@tanstack/react-query@5.90.12(react@19.2.3))(@tanstack/react-router@1.158.4(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@tanstack/router-core@1.158.4)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@tanstack/react-start': - specifier: ^1.141.1 - version: 1.141.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(vite-plugin-solid@2.11.10(solid-js@1.9.10)(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) + specifier: ^1.159.0 + version: 1.159.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(vite-plugin-solid@2.11.10(solid-js@1.9.10)(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) '@tanstack/react-store': specifier: ^0.8.0 version: 0.8.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@tanstack/router-plugin': - specifier: ^1.139.7 - version: 1.141.1(@tanstack/react-router@1.157.16(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(vite-plugin-solid@2.11.10(solid-js@1.9.10)(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) + specifier: ^1.158.4 + version: 1.158.4(@tanstack/react-router@1.158.4(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(vite-plugin-solid@2.11.10(solid-js@1.9.10)(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) '@tanstack/store': specifier: ^0.8.0 version: 0.8.0 @@ -359,11 +359,11 @@ importers: specifier: workspace:* version: link:../../packages/typescript/ai-solid-ui '@tanstack/nitro-v2-vite-plugin': - specifier: ^1.141.0 - version: 1.141.0(rolldown@1.0.0-beta.53)(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) + specifier: ^1.154.7 + version: 1.154.7(rolldown@1.0.0-beta.53)(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) '@tanstack/router-plugin': - specifier: ^1.139.7 - version: 1.141.1(@tanstack/react-router@1.157.16(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(vite-plugin-solid@2.11.10(solid-js@1.9.10)(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) + specifier: ^1.158.4 + version: 1.158.4(@tanstack/react-router@1.158.4(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(vite-plugin-solid@2.11.10(solid-js@1.9.10)(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) '@tanstack/solid-ai-devtools': specifier: workspace:* version: link:../../packages/typescript/solid-ai-devtools @@ -375,13 +375,13 @@ importers: version: 1.141.1(solid-js@1.9.10) '@tanstack/solid-router-devtools': specifier: ^1.139.10 - version: 1.141.1(@tanstack/router-core@1.157.16)(@tanstack/solid-router@1.141.1(solid-js@1.9.10))(csstype@3.2.3)(solid-js@1.9.10) + version: 1.141.1(@tanstack/router-core@1.158.4)(@tanstack/solid-router@1.141.1(solid-js@1.9.10))(csstype@3.2.3)(solid-js@1.9.10) '@tanstack/solid-router-ssr-query': specifier: ^1.139.10 - version: 1.141.1(@tanstack/query-core@5.90.12)(@tanstack/router-core@1.157.16)(@tanstack/solid-query@5.90.15(solid-js@1.9.10))(@tanstack/solid-router@1.141.1(solid-js@1.9.10))(eslint@9.39.2(jiti@2.6.1))(solid-js@1.9.10)(typescript@5.9.3) + version: 1.141.1(@tanstack/query-core@5.90.12)(@tanstack/router-core@1.158.4)(@tanstack/solid-query@5.90.15(solid-js@1.9.10))(@tanstack/solid-router@1.141.1(solid-js@1.9.10))(eslint@9.39.2(jiti@2.6.1))(solid-js@1.9.10)(typescript@5.9.3) '@tanstack/solid-start': specifier: ^1.139.10 - version: 1.141.1(@tanstack/react-router@1.157.16(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(solid-js@1.9.10)(vite-plugin-solid@2.11.10(solid-js@1.9.10)(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) + version: 1.141.1(@tanstack/react-router@1.158.4(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(solid-js@1.9.10)(vite-plugin-solid@2.11.10(solid-js@1.9.10)(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) '@tanstack/solid-store': specifier: ^0.8.0 version: 0.8.0(solid-js@1.9.10) @@ -1150,17 +1150,17 @@ importers: specifier: workspace:* version: link:../../ai-react '@tanstack/nitro-v2-vite-plugin': - specifier: ^1.141.0 - version: 1.141.0(rolldown@1.0.0-beta.53)(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) + specifier: ^1.154.7 + version: 1.154.7(rolldown@1.0.0-beta.53)(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) '@tanstack/react-router': - specifier: ^1.141.1 - version: 1.141.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + specifier: ^1.158.4 + version: 1.158.4(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@tanstack/react-start': - specifier: ^1.141.1 - version: 1.141.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(vite-plugin-solid@2.11.10(solid-js@1.9.10)(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) + specifier: ^1.159.0 + version: 1.159.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(vite-plugin-solid@2.11.10(solid-js@1.9.10)(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) '@tanstack/router-plugin': - specifier: ^1.139.7 - version: 1.141.1(@tanstack/react-router@1.141.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(vite-plugin-solid@2.11.10(solid-js@1.9.10)(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) + specifier: ^1.158.4 + version: 1.158.4(@tanstack/react-router@1.158.4(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(vite-plugin-solid@2.11.10(solid-js@1.9.10)(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) '@tanstack/tests-adapters': specifier: workspace:* version: link:../adapters @@ -1260,17 +1260,17 @@ importers: specifier: workspace:* version: link:../../packages/typescript/ai-react-ui '@tanstack/nitro-v2-vite-plugin': - specifier: ^1.141.0 - version: 1.141.0(rolldown@1.0.0-beta.53)(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) + specifier: ^1.154.7 + version: 1.154.7(rolldown@1.0.0-beta.53)(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) '@tanstack/react-router': - specifier: ^1.141.1 - version: 1.141.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + specifier: ^1.158.4 + version: 1.158.4(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@tanstack/react-start': - specifier: ^1.141.1 - version: 1.141.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(vite-plugin-solid@2.11.10(solid-js@1.9.10)(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) + specifier: ^1.159.0 + version: 1.159.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(vite-plugin-solid@2.11.10(solid-js@1.9.10)(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) '@tanstack/start': specifier: ^1.120.20 - version: 1.120.20(@types/node@24.10.3)(db0@0.3.4)(ioredis@5.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(rolldown@1.0.0-beta.53)(terser@5.44.1)(tsx@4.21.0)(vite-plugin-solid@2.11.10(solid-js@1.9.10)(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))(yaml@2.8.2) + version: 1.120.20(@types/node@24.10.3)(db0@0.3.4)(ioredis@5.8.2)(jiti@2.6.1)(lightningcss@1.30.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(rolldown@1.0.0-beta.53)(terser@5.44.1)(tsx@4.21.0)(vite-plugin-solid@2.11.10(solid-js@1.9.10)(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))(yaml@2.8.2) highlight.js: specifier: ^11.11.1 version: 11.11.1 @@ -3525,12 +3525,6 @@ packages: resolution: {integrity: sha512-xyIfof8eHBuub1CkBnbKNKQXeRZC4dClhmzePHVOEel4G7lk/dW+TQ16da7CFdeNLv6u6Owf5VoBQxoo6DFTSA==} engines: {node: '>=12'} - '@tanstack/nitro-v2-vite-plugin@1.141.0': - resolution: {integrity: sha512-OW0U7ftm4unRKhL9AZoTtdYIT3UnKfY8UQ25QNzI2uUPeVOyE9/yINFEgPGzS6gn4jQmJ3N2QvFZNvEjrbD0iA==} - engines: {node: '>=22.12'} - peerDependencies: - vite: '>=7.0.0' - '@tanstack/nitro-v2-vite-plugin@1.154.7': resolution: {integrity: sha512-THhjYwW+cREhmQyW/iATonY46RwYV8tbMnxBzIu77ceQOIHxkA1kVhLecb/oG5VdTduQnHVe90BD9qohX0mDHg==} engines: {node: '>=22.12'} @@ -3554,32 +3548,20 @@ packages: peerDependencies: react: ^18 || ^19 - '@tanstack/react-router-devtools@1.141.1': - resolution: {integrity: sha512-+XCn9cXSe1fZAD9jRrezEYE0ojn9U+Y0lRTRFdR8n51wx0UzJ6xe/Pewtw0rp03h/zmBR0pX+HRNU9NJDneWGA==} - engines: {node: '>=12'} - peerDependencies: - '@tanstack/react-router': ^1.141.1 - '@tanstack/router-core': ^1.141.1 - react: '>=18.0.0 || >=19.0.0' - react-dom: '>=18.0.0 || >=19.0.0' - peerDependenciesMeta: - '@tanstack/router-core': - optional: true - - '@tanstack/react-router-devtools@1.157.16': - resolution: {integrity: sha512-g6ekyzumfLBX6T5e+Vu2r37Z2CFJKrWRFqIy3vZ6A3x7OcuPV8uXNjyrLSiT/IsGTiF8YzwI4nWJa4fyd7NlCw==} + '@tanstack/react-router-devtools@1.158.4': + resolution: {integrity: sha512-/EkrrJGTPC7MwLfcYYmZM71ANDMLbwcYvBtDA+48LqHUKal8mpWlaodiWdFFnVQ7ny/unbUxljgdrNV9YZiyFQ==} engines: {node: '>=12'} peerDependencies: - '@tanstack/react-router': ^1.157.16 - '@tanstack/router-core': ^1.157.16 + '@tanstack/react-router': ^1.158.4 + '@tanstack/router-core': ^1.158.4 react: '>=18.0.0 || >=19.0.0' react-dom: '>=18.0.0 || >=19.0.0' peerDependenciesMeta: '@tanstack/router-core': optional: true - '@tanstack/react-router-ssr-query@1.141.1': - resolution: {integrity: sha512-80KEKpHx9OZwXcdCOZDyftqxWMP8QHGU6LQgyIsuunPrf76Wf1riyBnZrPOAOJPcjM3PDhTHLNIfmRBnAtItUw==} + '@tanstack/react-router-ssr-query@1.158.4': + resolution: {integrity: sha512-f+XzxO06ILM2i5CGtWqcb3+yaAvp8XgT5hMykKmwwaBnf3Ctc6O8tN/05Ovj0ajXWuROk3HTjg67OcWD7JxI6Q==} engines: {node: '>=12'} peerDependencies: '@tanstack/query-core': '>=5.90.0' @@ -3595,8 +3577,8 @@ packages: react: '>=18.0.0 || >=19.0.0' react-dom: '>=18.0.0 || >=19.0.0' - '@tanstack/react-router@1.157.16': - resolution: {integrity: sha512-xwFQa7S7dhBhm3aJYwU79cITEYgAKSrcL6wokaROIvl2JyIeazn8jueWqUPJzFjv+QF6Q8euKRlKUEyb5q2ymg==} + '@tanstack/react-router@1.158.4': + resolution: {integrity: sha512-i15xXumgvpuM+4NSuIwgouGezuj9eHjZsgpTZSQ7E9pa8rYmhZbWnf8xU68qaLmaKIol/e75o/YzVH2QWHs3iQ==} engines: {node: '>=12'} peerDependencies: react: '>=18.0.0 || >=19.0.0' @@ -3609,6 +3591,13 @@ packages: react: '>=18.0.0 || >=19.0.0' react-dom: '>=18.0.0 || >=19.0.0' + '@tanstack/react-start-client@1.158.4': + resolution: {integrity: sha512-ctEBgpYAPZ3i4EPZlJ45XS/lXPO73MkELec+hXf8NfK0lDQDaUy7LfWu41NPaftdZFJPOncDCfutwpUXD98YlA==} + engines: {node: '>=22.12.0'} + peerDependencies: + react: '>=18.0.0 || >=19.0.0' + react-dom: '>=18.0.0 || >=19.0.0' + '@tanstack/react-start-plugin@1.131.50': resolution: {integrity: sha512-ys+sGvnnE8BUNjGsngg+MGn3F5lV4okL5CWEKFzjBSjQsrTN7apGfmqvBP3O6PkRPHpXZ8X3Z5QsFvSc0CaDRQ==} engines: {node: '>=12'} @@ -3627,8 +3616,15 @@ packages: react: '>=18.0.0 || >=19.0.0' react-dom: '>=18.0.0 || >=19.0.0' - '@tanstack/react-start@1.141.1': - resolution: {integrity: sha512-03iELlg9T9ZN9rKAM1BTCCIBptLbaoZYCZXe0xGf4ZLs3Md+EhmJZibtKluclVQcnjzeiE0T17j1A/YxvVwTZg==} + '@tanstack/react-start-server@1.159.0': + resolution: {integrity: sha512-1nPj7TEOpoIlTW0lftaHuU9Ol1ZDQwRCUWr6UvaPUbapq9nWR8kwYFjyCLbopBjyakFFNgz88/stdbZObt5h2A==} + engines: {node: '>=22.12.0'} + peerDependencies: + react: '>=18.0.0 || >=19.0.0' + react-dom: '>=18.0.0 || >=19.0.0' + + '@tanstack/react-start@1.159.0': + resolution: {integrity: sha512-/ky8Pbu0cmj5dAQfi8LXHpAd/eepyQqDo0eSI/OPYQ2wZ8u8UPwycFvou8t8mq5pkinu+l7JX45UD7mNvzvVNg==} engines: {node: '>=22.12.0'} peerDependencies: react: '>=18.0.0 || >=19.0.0' @@ -3649,8 +3645,8 @@ packages: resolution: {integrity: sha512-fR1GGpp6v3dVKu4KIAjEh+Sd0qGLQd/wvCOVHeopSY6aFidXKCzwrS5cBOBqoPPWTKmn6CdW1a0CzFr5Furdog==} engines: {node: '>=12'} - '@tanstack/router-core@1.157.16': - resolution: {integrity: sha512-eJuVgM7KZYTTr4uPorbUzUflmljMVcaX2g6VvhITLnHmg9SBx9RAgtQ1HmT+72mzyIbRSlQ1q0fY/m+of/fosA==} + '@tanstack/router-core@1.158.4': + resolution: {integrity: sha512-KikgYdyrEFqsjjgv9pMhDTMmASMAyFRvUiKFdQPQtXq3aD1qv/zck4CbA4bfzp9N9nYu/qvWwU1mlYU4u5JeXg==} engines: {node: '>=12'} '@tanstack/router-devtools-core@1.141.1': @@ -3664,11 +3660,11 @@ packages: csstype: optional: true - '@tanstack/router-devtools-core@1.157.16': - resolution: {integrity: sha512-XBJTs/kMZYK6J2zhbGucHNuypwDB1t2vi8K5To+V6dUnLGBEyfQTf01fegiF4rpL1yXgomdGnP6aTiOFgldbVg==} + '@tanstack/router-devtools-core@1.158.4': + resolution: {integrity: sha512-9MKzstYp/6sNRSwJY2b9ipVW8b8/x1iSFNfLhOJur2tnjB3RhwCDfy0u+to70BrRpBEWeq7jvJoVdP029gzUUg==} engines: {node: '>=12'} peerDependencies: - '@tanstack/router-core': ^1.157.16 + '@tanstack/router-core': ^1.158.4 csstype: ^3.0.10 peerDependenciesMeta: csstype: @@ -3682,6 +3678,10 @@ packages: resolution: {integrity: sha512-21RbVAoIDn7s/n/PKMN6U60d5hCeVADrBH/uN6B/poMT4MVYtJXqISVzkc2RAboVRw6eRdYFeF+YlwA3nF6y3Q==} engines: {node: '>=12'} + '@tanstack/router-generator@1.158.4': + resolution: {integrity: sha512-RQmqMTT0oV8dS/3Glcq9SPzDZqOPyKb/LVFUkNoTfMwW88WyGnQcYqZAkmVk/CGBWWDfwObOUZoGq5jTF7bG8w==} + engines: {node: '>=12'} + '@tanstack/router-plugin@1.131.50': resolution: {integrity: sha512-gdEBPGzx7llQNRnaqfPJ1iaPS3oqB8SlvKRG5l7Fxp4q4yINgkeowFYSKEhPOc9bjoNhGrIHOlvPTPXEzAQXzQ==} engines: {node: '>=12'} @@ -3724,6 +3724,27 @@ packages: webpack: optional: true + '@tanstack/router-plugin@1.158.4': + resolution: {integrity: sha512-g2sytAhljw6Jd6Klu37OZ75+o+vhiGdbWtnBy/4rYLC4NN6hSnjgJQRI3+h1CI1KQ4EUgsZYZr/hgE1KHoiWYQ==} + engines: {node: '>=12'} + peerDependencies: + '@rsbuild/core': '>=1.0.2' + '@tanstack/react-router': ^1.158.4 + vite: '>=5.0.0 || >=6.0.0 || >=7.0.0' + vite-plugin-solid: ^2.11.10 + webpack: '>=5.92.0' + peerDependenciesMeta: + '@rsbuild/core': + optional: true + '@tanstack/react-router': + optional: true + vite: + optional: true + vite-plugin-solid: + optional: true + webpack: + optional: true + '@tanstack/router-ssr-query-core@1.141.1': resolution: {integrity: sha512-bkRXUhktifxBewnnphH59E0sGcsUI1NmNqxzCAmXIb93xYgafhjUGGYwfK6FqFBOmCB5isr32exGO3+UMHJr/A==} engines: {node: '>=12'} @@ -3731,6 +3752,13 @@ packages: '@tanstack/query-core': '>=5.90.0' '@tanstack/router-core': '>=1.127.0' + '@tanstack/router-ssr-query-core@1.158.4': + resolution: {integrity: sha512-gZRx0pGaRc7NPrwQSAfnn/DVWEsd01cf5TaW5yTyf3R5ZP/I++KNEW3lBXyRo1RyKedPC45R+Id6HpDeEaidyg==} + engines: {node: '>=12'} + peerDependencies: + '@tanstack/query-core': '>=5.90.0' + '@tanstack/router-core': '>=1.127.0' + '@tanstack/router-utils@1.131.2': resolution: {integrity: sha512-sr3x0d2sx9YIJoVth0QnfEcAcl+39sQYaNQxThtHmRpyeFYNyM2TTH+Ud3TNEnI3bbzmLYEUD+7YqB987GzhDA==} engines: {node: '>=12'} @@ -3739,6 +3767,10 @@ packages: resolution: {integrity: sha512-/eFGKCiix1SvjxwgzrmH4pHjMiMxc+GA4nIbgEkG2RdAJqyxLcRhd7RPLG0/LZaJ7d0ad3jrtRqsHLv2152Vbw==} engines: {node: '>=12'} + '@tanstack/router-utils@1.158.0': + resolution: {integrity: sha512-qZ76eaLKU6Ae9iI/mc5zizBX149DXXZkBVVO3/QRIll79uKLJZHQlMKR++2ba7JsciBWz1pgpIBcCJPE9S0LVg==} + engines: {node: '>=12'} + '@tanstack/server-functions-plugin@1.131.2': resolution: {integrity: sha512-hWsaSgEZAVyzHg8+IcJWCEtfI9ZSlNELErfLiGHG9XCHEXMegFWsrESsKHlASzJqef9RsuOLDl+1IMPIskwdDw==} engines: {node: '>=12'} @@ -3820,6 +3852,10 @@ packages: resolution: {integrity: sha512-Rk/b0ekX7p0ZBKOg9WM5c632YPqu7GlvZSYnAjNi1GDp1/sET6g2Trp+GAjs1s8kakp2pMQ4sZUG/11grCMfJw==} engines: {node: '>=22.12.0'} + '@tanstack/start-client-core@1.158.4': + resolution: {integrity: sha512-qpUYwJMMCEKgJuMz2CJLt53XrObi1BSjV1gG5SgBWRRVOHL8zky55tu1fEqHEa26jTTA6mUcBnPzYE8vIjRpAw==} + engines: {node: '>=22.12.0'} + '@tanstack/start-config@1.120.20': resolution: {integrity: sha512-oH/mfTSHV8Qbil74tWicPLW6+kKmT3esXCnDzvrkhi3+N8ZuVUDr01Qpil0Wxf9lLPfM5L6VX03nF4hSU8vljg==} engines: {node: '>=12'} @@ -3828,6 +3864,10 @@ packages: react-dom: '>=18.0.0 || >=19.0.0' vite: ^6.0.0 + '@tanstack/start-fn-stubs@1.154.7': + resolution: {integrity: sha512-D69B78L6pcFN5X5PHaydv7CScQcKLzJeEYqs7jpuyyqGQHSUIZUjS955j+Sir8cHhuDIovCe2LmsYHeZfWf3dQ==} + engines: {node: '>=22.12.0'} + '@tanstack/start-plugin-core@1.131.50': resolution: {integrity: sha512-eFvMA0chqLtHbq+8ojp1fXN7AQjhmeoOpQaZaU1d51wb7ugetrn0k3OuHblxtE/O0L4HEC9s4X5zmFJt0vLh0w==} engines: {node: '>=12'} @@ -3840,6 +3880,12 @@ packages: peerDependencies: vite: '>=7.0.0' + '@tanstack/start-plugin-core@1.159.0': + resolution: {integrity: sha512-HGcji+Mhste9mDKUlKpRPfoIOaURr7UqQZ3AMb+6zpbXumc+apYW/CvlvWdF/hoZGBSVAniFpwXgV5L5IimnhA==} + engines: {node: '>=22.12.0'} + peerDependencies: + vite: '>=7.0.0' + '@tanstack/start-server-core@1.131.50': resolution: {integrity: sha512-3SWwwhW2GKMhPSaqWRal6Jj1Y9ObfdWEXKFQid1LBuk5xk/Es4bmW68o++MbVgs/GxUxyeZ3TRVqb0c7RG1sog==} engines: {node: '>=12'} @@ -3848,6 +3894,10 @@ packages: resolution: {integrity: sha512-Qk/lZ/+iGUyNYeAAuj89bLR6GXLD/9BIpAR2CUwlS+xXGL0kQmOFcb1UvccWZ2QwtW+csxJW4NeQOeMuqsfyhA==} engines: {node: '>=22.12.0'} + '@tanstack/start-server-core@1.159.0': + resolution: {integrity: sha512-oE9UkWc7uIDvjAOsmzZ65Vz+JLb4S+bhMLGjx84lWY0G+GelJJvdr0rQiUFTWPIsbIxO2pdyIY995H55VUcowg==} + engines: {node: '>=22.12.0'} + '@tanstack/start-server-functions-client@1.131.50': resolution: {integrity: sha512-4aM17fFdVAFH6uLPswKJxzrhhIjcCwKqzfTcgY3OnhUKnaZBTQwJA+nUHQCI6IWvEvrcrNVtFTtv13TkDk3YMw==} engines: {node: '>=12'} @@ -3876,6 +3926,10 @@ packages: resolution: {integrity: sha512-UPOQd4qsytgmc+pHeeS3oIZQazhyGAmEaCS/IrZI42TzpuVh2ZbLVssKEoDziheNP1dH5KT2lsL1bU9asAw7tA==} engines: {node: '>=22.12.0'} + '@tanstack/start-storage-context@1.158.4': + resolution: {integrity: sha512-tz70q/6LTytstBIMRYt5GDRjPJPOHjnPNay85RJdq9ZlQKryeDThnshEttlBTDAxZP7wtwOv00lcAgFLFGP1hA==} + engines: {node: '>=22.12.0'} + '@tanstack/start@1.120.20': resolution: {integrity: sha512-fQO+O/5xJpli5KlV6pwDz6DtpbqO/0atdVSyVnkemzk0Mej9azm4HXtw+cKkIPtsSplWs4B1EbMtgGMb9ADhSA==} engines: {node: '>=12'} @@ -3898,6 +3952,10 @@ packages: resolution: {integrity: sha512-CJrWtr6L9TVzEImm9S7dQINx+xJcYP/aDkIi6gnaWtIgbZs1pnzsE0yJc2noqXZ+yAOqLx3TBGpBEs9tS0P9/A==} engines: {node: '>=12'} + '@tanstack/virtual-file-routes@1.154.7': + resolution: {integrity: sha512-cHHDnewHozgjpI+MIVp9tcib6lYEQK5MyUr0ChHpHFGBl8Xei55rohFK0I0ve/GKoHeioaK42Smd8OixPp6CTg==} + engines: {node: '>=12'} + '@tanstack/vite-config@0.4.1': resolution: {integrity: sha512-FOl8EF6SAcljanKSm5aBeJaflFcxQAytTbxtNW8HC6D4x+UBW68IC4tBcrlrsI0wXHBmC/Gz4Ovvv8qCtiXSgQ==} engines: {node: '>=18'} @@ -4554,6 +4612,9 @@ packages: babel-dead-code-elimination@1.0.10: resolution: {integrity: sha512-DV5bdJZTzZ0zn0DC24v3jD7Mnidh6xhKa4GfKCbq3sfW8kaWhDdZjP3i81geA8T33tdYqWKw4D3fVv0CwEgKVA==} + babel-dead-code-elimination@1.0.12: + resolution: {integrity: sha512-GERT7L2TiYcYDtYk1IpD+ASAYXjKbLTDPhBtYj7X1NuRMDTMtAx9kyBenub1Ev41lo91OHCKdmP+egTDmfQ7Ig==} + babel-plugin-jsx-dom-expressions@0.40.3: resolution: {integrity: sha512-5HOwwt0BYiv/zxl7j8Pf2bGL6rDXfV6nUhLs8ygBX+EFJXzBPHM/euj9j/6deMZ6wa52Wb2PBaAV5U/jKwIY1w==} peerDependencies: @@ -5708,6 +5769,15 @@ packages: crossws: optional: true + h3@2.0.1-rc.11: + resolution: {integrity: sha512-2myzjCqy32c1As9TjZW9fNZXtLqNedjFSrdFy2AjFBQQ3LzrnGoDdFDYfC0tV2e4vcyfJ2Sfo/F6NQhO2Ly/Mw==} + engines: {node: '>=20.11.1'} + peerDependencies: + crossws: ^0.4.1 + peerDependenciesMeta: + crossws: + optional: true + happy-dom@20.0.11: resolution: {integrity: sha512-QsCdAUHAmiDeKeaNojb1OHOPF7NjcWPBR7obdu3NwH2a/oyQaLg5d0aaCy/9My6CdPChYF07dvz5chaXBGaD4g==} engines: {node: '>=20.0.0'} @@ -7625,6 +7695,11 @@ packages: sprintf-js@1.0.3: resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} + srvx@0.10.1: + resolution: {integrity: sha512-A//xtfak4eESMWWydSRFUVvCTQbSwivnGCEf8YGPe2eHU0+Z6znfUTCPF0a7oV3sObSOcrXHlL6Bs9vVctfXdg==} + engines: {node: '>=20.16.0'} + hasBin: true + srvx@0.8.16: resolution: {integrity: sha512-hmcGW4CgroeSmzgF1Ihwgl+Ths0JqAJ7HwjP2X7e3JzY7u4IydLMcdnlqGQiQGUswz+PO9oh/KtCpOISIvs9QQ==} engines: {node: '>=20.16.0'} @@ -10699,42 +10774,6 @@ snapshots: '@tanstack/history@1.154.14': {} - '@tanstack/nitro-v2-vite-plugin@1.141.0(rolldown@1.0.0-beta.53)(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))': - dependencies: - nitropack: 2.12.9(rolldown@1.0.0-beta.53) - pathe: 2.0.3 - vite: 7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) - transitivePeerDependencies: - - '@azure/app-configuration' - - '@azure/cosmos' - - '@azure/data-tables' - - '@azure/identity' - - '@azure/keyvault-secrets' - - '@azure/storage-blob' - - '@capacitor/preferences' - - '@deno/kv' - - '@electric-sql/pglite' - - '@libsql/client' - - '@netlify/blobs' - - '@planetscale/database' - - '@upstash/redis' - - '@vercel/blob' - - '@vercel/functions' - - '@vercel/kv' - - aws4fetch - - bare-abort-controller - - better-sqlite3 - - drizzle-orm - - encoding - - idb-keyval - - mysql2 - - react-native-b4a - - rolldown - - sqlite3 - - supports-color - - uploadthing - - xml2js - '@tanstack/nitro-v2-vite-plugin@1.154.7(rolldown@1.0.0-beta.53)(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))': dependencies: nitropack: 2.13.1(rolldown@1.0.0-beta.53) @@ -10791,46 +10830,23 @@ snapshots: '@tanstack/query-core': 5.90.12 react: 19.2.3 - '@tanstack/react-router-devtools@1.141.1(@tanstack/react-router@1.141.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@tanstack/router-core@1.157.16)(csstype@3.2.3)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(solid-js@1.9.10)': + '@tanstack/react-router-devtools@1.158.4(@tanstack/react-router@1.158.4(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@tanstack/router-core@1.158.4)(csstype@3.2.3)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: - '@tanstack/react-router': 1.141.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@tanstack/router-devtools-core': 1.141.1(@tanstack/router-core@1.157.16)(csstype@3.2.3)(solid-js@1.9.10) + '@tanstack/react-router': 1.158.4(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@tanstack/router-devtools-core': 1.158.4(@tanstack/router-core@1.158.4)(csstype@3.2.3) react: 19.2.3 react-dom: 19.2.3(react@19.2.3) optionalDependencies: - '@tanstack/router-core': 1.157.16 + '@tanstack/router-core': 1.158.4 transitivePeerDependencies: - csstype - - solid-js - '@tanstack/react-router-devtools@1.157.16(@tanstack/react-router@1.157.16(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@tanstack/router-core@1.157.16)(csstype@3.2.3)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': - dependencies: - '@tanstack/react-router': 1.157.16(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@tanstack/router-devtools-core': 1.157.16(@tanstack/router-core@1.157.16)(csstype@3.2.3) - react: 19.2.3 - react-dom: 19.2.3(react@19.2.3) - optionalDependencies: - '@tanstack/router-core': 1.157.16 - transitivePeerDependencies: - - csstype - - '@tanstack/react-router-ssr-query@1.141.1(@tanstack/query-core@5.90.12)(@tanstack/react-query@5.90.12(react@19.2.3))(@tanstack/react-router@1.141.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@tanstack/router-core@1.157.16)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + '@tanstack/react-router-ssr-query@1.158.4(@tanstack/query-core@5.90.12)(@tanstack/react-query@5.90.12(react@19.2.3))(@tanstack/react-router@1.158.4(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@tanstack/router-core@1.158.4)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: '@tanstack/query-core': 5.90.12 '@tanstack/react-query': 5.90.12(react@19.2.3) - '@tanstack/react-router': 1.141.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@tanstack/router-ssr-query-core': 1.141.1(@tanstack/query-core@5.90.12)(@tanstack/router-core@1.157.16) - react: 19.2.3 - react-dom: 19.2.3(react@19.2.3) - transitivePeerDependencies: - - '@tanstack/router-core' - - '@tanstack/react-router-ssr-query@1.141.1(@tanstack/query-core@5.90.12)(@tanstack/react-query@5.90.12(react@19.2.3))(@tanstack/react-router@1.157.16(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@tanstack/router-core@1.157.16)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': - dependencies: - '@tanstack/query-core': 5.90.12 - '@tanstack/react-query': 5.90.12(react@19.2.3) - '@tanstack/react-router': 1.157.16(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@tanstack/router-ssr-query-core': 1.141.1(@tanstack/query-core@5.90.12)(@tanstack/router-core@1.157.16) + '@tanstack/react-router': 1.158.4(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@tanstack/router-ssr-query-core': 1.158.4(@tanstack/query-core@5.90.12)(@tanstack/router-core@1.158.4) react: 19.2.3 react-dom: 19.2.3(react@19.2.3) transitivePeerDependencies: @@ -10847,11 +10863,11 @@ snapshots: tiny-invariant: 1.3.3 tiny-warning: 1.0.3 - '@tanstack/react-router@1.157.16(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + '@tanstack/react-router@1.158.4(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: '@tanstack/history': 1.154.14 '@tanstack/react-store': 0.8.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@tanstack/router-core': 1.157.16 + '@tanstack/router-core': 1.158.4 isbot: 5.1.32 react: 19.2.3 react-dom: 19.2.3(react@19.2.3) @@ -10868,9 +10884,19 @@ snapshots: tiny-invariant: 1.3.3 tiny-warning: 1.0.3 - '@tanstack/react-start-plugin@1.131.50(@tanstack/react-router@1.157.16(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@vitejs/plugin-react@4.7.0(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(rolldown@1.0.0-beta.53)(vite-plugin-solid@2.11.10(solid-js@1.9.10)(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))': + '@tanstack/react-start-client@1.158.4(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@tanstack/react-router': 1.158.4(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@tanstack/router-core': 1.158.4 + '@tanstack/start-client-core': 1.158.4 + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + tiny-invariant: 1.3.3 + tiny-warning: 1.0.3 + + '@tanstack/react-start-plugin@1.131.50(@tanstack/react-router@1.158.4(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@vitejs/plugin-react@4.7.0(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(rolldown@1.0.0-beta.53)(vite-plugin-solid@2.11.10(solid-js@1.9.10)(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))': dependencies: - '@tanstack/start-plugin-core': 1.131.50(@tanstack/react-router@1.157.16(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(rolldown@1.0.0-beta.53)(vite-plugin-solid@2.11.10(solid-js@1.9.10)(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) + '@tanstack/start-plugin-core': 1.131.50(@tanstack/react-router@1.158.4(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(rolldown@1.0.0-beta.53)(vite-plugin-solid@2.11.10(solid-js@1.9.10)(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) '@vitejs/plugin-react': 4.7.0(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) pathe: 2.0.3 vite: 7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) @@ -10910,11 +10936,11 @@ snapshots: - webpack - xml2js - '@tanstack/react-start-router-manifest@1.120.19(@types/node@24.10.3)(db0@0.3.4)(ioredis@5.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(rolldown@1.0.0-beta.53)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)': + '@tanstack/react-start-router-manifest@1.120.19(@types/node@24.10.3)(db0@0.3.4)(ioredis@5.8.2)(jiti@2.6.1)(lightningcss@1.30.2)(rolldown@1.0.0-beta.53)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)': dependencies: '@tanstack/router-core': 1.141.1 tiny-invariant: 1.3.3 - vinxi: 0.5.3(@types/node@24.10.3)(db0@0.3.4)(ioredis@5.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(rolldown@1.0.0-beta.53)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) + vinxi: 0.5.3(@types/node@24.10.3)(db0@0.3.4)(ioredis@5.8.2)(jiti@2.6.1)(lightningcss@1.30.2)(rolldown@1.0.0-beta.53)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) transitivePeerDependencies: - '@azure/app-configuration' - '@azure/cosmos' @@ -10972,15 +10998,27 @@ snapshots: transitivePeerDependencies: - crossws - '@tanstack/react-start@1.141.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(vite-plugin-solid@2.11.10(solid-js@1.9.10)(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))': + '@tanstack/react-start-server@1.159.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: - '@tanstack/react-router': 1.141.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@tanstack/react-start-client': 1.141.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@tanstack/react-start-server': 1.141.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@tanstack/router-utils': 1.141.0 - '@tanstack/start-client-core': 1.141.1 - '@tanstack/start-plugin-core': 1.141.1(@tanstack/react-router@1.141.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(vite-plugin-solid@2.11.10(solid-js@1.9.10)(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) - '@tanstack/start-server-core': 1.141.1 + '@tanstack/history': 1.154.14 + '@tanstack/react-router': 1.158.4(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@tanstack/router-core': 1.158.4 + '@tanstack/start-client-core': 1.158.4 + '@tanstack/start-server-core': 1.159.0 + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + transitivePeerDependencies: + - crossws + + '@tanstack/react-start@1.159.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(vite-plugin-solid@2.11.10(solid-js@1.9.10)(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))': + dependencies: + '@tanstack/react-router': 1.158.4(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@tanstack/react-start-client': 1.158.4(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@tanstack/react-start-server': 1.159.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@tanstack/router-utils': 1.158.0 + '@tanstack/start-client-core': 1.158.4 + '@tanstack/start-plugin-core': 1.159.0(@tanstack/react-router@1.158.4(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(vite-plugin-solid@2.11.10(solid-js@1.9.10)(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) + '@tanstack/start-server-core': 1.159.0 pathe: 2.0.3 react: 19.2.3 react-dom: 19.2.3(react@19.2.3) @@ -11004,8 +11042,8 @@ snapshots: '@tanstack/history': 1.131.2 '@tanstack/store': 0.7.7 cookie-es: 1.2.2 - seroval: 1.4.0 - seroval-plugins: 1.4.0(seroval@1.4.0) + seroval: 1.5.0 + seroval-plugins: 1.5.0(seroval@1.5.0) tiny-invariant: 1.3.3 tiny-warning: 1.0.3 @@ -11019,7 +11057,7 @@ snapshots: tiny-invariant: 1.3.3 tiny-warning: 1.0.3 - '@tanstack/router-core@1.157.16': + '@tanstack/router-core@1.158.4': dependencies: '@tanstack/history': 1.154.14 '@tanstack/store': 0.8.0 @@ -11029,9 +11067,9 @@ snapshots: tiny-invariant: 1.3.3 tiny-warning: 1.0.3 - '@tanstack/router-devtools-core@1.141.1(@tanstack/router-core@1.157.16)(csstype@3.2.3)(solid-js@1.9.10)': + '@tanstack/router-devtools-core@1.141.1(@tanstack/router-core@1.158.4)(csstype@3.2.3)(solid-js@1.9.10)': dependencies: - '@tanstack/router-core': 1.157.16 + '@tanstack/router-core': 1.158.4 clsx: 2.1.1 goober: 2.1.18(csstype@3.2.3) solid-js: 1.9.10 @@ -11039,9 +11077,9 @@ snapshots: optionalDependencies: csstype: 3.2.3 - '@tanstack/router-devtools-core@1.157.16(@tanstack/router-core@1.157.16)(csstype@3.2.3)': + '@tanstack/router-devtools-core@1.158.4(@tanstack/router-core@1.158.4)(csstype@3.2.3)': dependencies: - '@tanstack/router-core': 1.157.16 + '@tanstack/router-core': 1.158.4 clsx: 2.1.1 goober: 2.1.18(csstype@3.2.3) tiny-invariant: 1.3.3 @@ -11074,7 +11112,20 @@ snapshots: transitivePeerDependencies: - supports-color - '@tanstack/router-plugin@1.131.50(@tanstack/react-router@1.157.16(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(vite-plugin-solid@2.11.10(solid-js@1.9.10)(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))': + '@tanstack/router-generator@1.158.4': + dependencies: + '@tanstack/router-core': 1.158.4 + '@tanstack/router-utils': 1.158.0 + '@tanstack/virtual-file-routes': 1.154.7 + prettier: 3.7.4 + recast: 0.23.11 + source-map: 0.7.6 + tsx: 4.21.0 + zod: 3.25.76 + transitivePeerDependencies: + - supports-color + + '@tanstack/router-plugin@1.131.50(@tanstack/react-router@1.158.4(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(vite-plugin-solid@2.11.10(solid-js@1.9.10)(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))': dependencies: '@babel/core': 7.28.5 '@babel/plugin-syntax-jsx': 7.27.1(@babel/core@7.28.5) @@ -11091,13 +11142,13 @@ snapshots: unplugin: 2.3.11 zod: 3.25.76 optionalDependencies: - '@tanstack/react-router': 1.157.16(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@tanstack/react-router': 1.158.4(react-dom@19.2.3(react@19.2.3))(react@19.2.3) vite: 7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) vite-plugin-solid: 2.11.10(solid-js@1.9.10)(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) transitivePeerDependencies: - supports-color - '@tanstack/router-plugin@1.141.1(@tanstack/react-router@1.141.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(vite-plugin-solid@2.11.10(solid-js@1.9.10)(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))': + '@tanstack/router-plugin@1.141.1(@tanstack/react-router@1.158.4(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(vite-plugin-solid@2.11.10(solid-js@1.9.10)(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))': dependencies: '@babel/core': 7.28.5 '@babel/plugin-syntax-jsx': 7.27.1(@babel/core@7.28.5) @@ -11114,13 +11165,13 @@ snapshots: unplugin: 2.3.11 zod: 3.25.76 optionalDependencies: - '@tanstack/react-router': 1.141.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@tanstack/react-router': 1.158.4(react-dom@19.2.3(react@19.2.3))(react@19.2.3) vite: 7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) vite-plugin-solid: 2.11.10(solid-js@1.9.10)(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) transitivePeerDependencies: - supports-color - '@tanstack/router-plugin@1.141.1(@tanstack/react-router@1.157.16(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(vite-plugin-solid@2.11.10(solid-js@1.9.10)(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))': + '@tanstack/router-plugin@1.158.4(@tanstack/react-router@1.158.4(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(vite-plugin-solid@2.11.10(solid-js@1.9.10)(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))': dependencies: '@babel/core': 7.28.5 '@babel/plugin-syntax-jsx': 7.27.1(@babel/core@7.28.5) @@ -11128,25 +11179,29 @@ snapshots: '@babel/template': 7.27.2 '@babel/traverse': 7.28.5 '@babel/types': 7.28.5 - '@tanstack/router-core': 1.141.1 - '@tanstack/router-generator': 1.141.1 - '@tanstack/router-utils': 1.141.0 - '@tanstack/virtual-file-routes': 1.141.0 - babel-dead-code-elimination: 1.0.10 + '@tanstack/router-core': 1.158.4 + '@tanstack/router-generator': 1.158.4 + '@tanstack/router-utils': 1.158.0 + '@tanstack/virtual-file-routes': 1.154.7 chokidar: 3.6.0 unplugin: 2.3.11 zod: 3.25.76 optionalDependencies: - '@tanstack/react-router': 1.157.16(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@tanstack/react-router': 1.158.4(react-dom@19.2.3(react@19.2.3))(react@19.2.3) vite: 7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) vite-plugin-solid: 2.11.10(solid-js@1.9.10)(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) transitivePeerDependencies: - supports-color - '@tanstack/router-ssr-query-core@1.141.1(@tanstack/query-core@5.90.12)(@tanstack/router-core@1.157.16)': + '@tanstack/router-ssr-query-core@1.141.1(@tanstack/query-core@5.90.12)(@tanstack/router-core@1.158.4)': dependencies: '@tanstack/query-core': 5.90.12 - '@tanstack/router-core': 1.157.16 + '@tanstack/router-core': 1.158.4 + + '@tanstack/router-ssr-query-core@1.158.4(@tanstack/query-core@5.90.12)(@tanstack/router-core@1.158.4)': + dependencies: + '@tanstack/query-core': 5.90.12 + '@tanstack/router-core': 1.158.4 '@tanstack/router-utils@1.131.2': dependencies: @@ -11172,6 +11227,20 @@ snapshots: transitivePeerDependencies: - supports-color + '@tanstack/router-utils@1.158.0': + dependencies: + '@babel/core': 7.28.5 + '@babel/generator': 7.28.5 + '@babel/parser': 7.28.5 + '@babel/types': 7.28.5 + ansis: 4.2.0 + babel-dead-code-elimination: 1.0.12 + diff: 8.0.2 + pathe: 2.0.3 + tinyglobby: 0.2.15 + transitivePeerDependencies: + - supports-color + '@tanstack/server-functions-plugin@1.131.2(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))': dependencies: '@babel/code-frame': 7.27.1 @@ -11218,20 +11287,20 @@ snapshots: '@tanstack/query-core': 5.90.12 solid-js: 1.9.10 - '@tanstack/solid-router-devtools@1.141.1(@tanstack/router-core@1.157.16)(@tanstack/solid-router@1.141.1(solid-js@1.9.10))(csstype@3.2.3)(solid-js@1.9.10)': + '@tanstack/solid-router-devtools@1.141.1(@tanstack/router-core@1.158.4)(@tanstack/solid-router@1.141.1(solid-js@1.9.10))(csstype@3.2.3)(solid-js@1.9.10)': dependencies: - '@tanstack/router-devtools-core': 1.141.1(@tanstack/router-core@1.157.16)(csstype@3.2.3)(solid-js@1.9.10) + '@tanstack/router-devtools-core': 1.141.1(@tanstack/router-core@1.158.4)(csstype@3.2.3)(solid-js@1.9.10) '@tanstack/solid-router': 1.141.1(solid-js@1.9.10) solid-js: 1.9.10 optionalDependencies: - '@tanstack/router-core': 1.157.16 + '@tanstack/router-core': 1.158.4 transitivePeerDependencies: - csstype - '@tanstack/solid-router-ssr-query@1.141.1(@tanstack/query-core@5.90.12)(@tanstack/router-core@1.157.16)(@tanstack/solid-query@5.90.15(solid-js@1.9.10))(@tanstack/solid-router@1.141.1(solid-js@1.9.10))(eslint@9.39.2(jiti@2.6.1))(solid-js@1.9.10)(typescript@5.9.3)': + '@tanstack/solid-router-ssr-query@1.141.1(@tanstack/query-core@5.90.12)(@tanstack/router-core@1.158.4)(@tanstack/solid-query@5.90.15(solid-js@1.9.10))(@tanstack/solid-router@1.141.1(solid-js@1.9.10))(eslint@9.39.2(jiti@2.6.1))(solid-js@1.9.10)(typescript@5.9.3)': dependencies: '@tanstack/query-core': 5.90.12 - '@tanstack/router-ssr-query-core': 1.141.1(@tanstack/query-core@5.90.12)(@tanstack/router-core@1.157.16) + '@tanstack/router-ssr-query-core': 1.141.1(@tanstack/query-core@5.90.12)(@tanstack/router-core@1.158.4) '@tanstack/solid-query': 5.90.15(solid-js@1.9.10) '@tanstack/solid-router': 1.141.1(solid-js@1.9.10) eslint-plugin-solid: 0.14.5(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) @@ -11276,13 +11345,13 @@ snapshots: transitivePeerDependencies: - crossws - '@tanstack/solid-start@1.141.1(@tanstack/react-router@1.157.16(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(solid-js@1.9.10)(vite-plugin-solid@2.11.10(solid-js@1.9.10)(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))': + '@tanstack/solid-start@1.141.1(@tanstack/react-router@1.158.4(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(solid-js@1.9.10)(vite-plugin-solid@2.11.10(solid-js@1.9.10)(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))': dependencies: '@tanstack/solid-router': 1.141.1(solid-js@1.9.10) '@tanstack/solid-start-client': 1.141.1(solid-js@1.9.10) '@tanstack/solid-start-server': 1.141.1(solid-js@1.9.10) '@tanstack/start-client-core': 1.141.1 - '@tanstack/start-plugin-core': 1.141.1(@tanstack/react-router@1.157.16(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(vite-plugin-solid@2.11.10(solid-js@1.9.10)(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) + '@tanstack/start-plugin-core': 1.141.1(@tanstack/react-router@1.158.4(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(vite-plugin-solid@2.11.10(solid-js@1.9.10)(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) '@tanstack/start-server-core': 1.141.1 pathe: 2.0.3 solid-js: 1.9.10 @@ -11300,11 +11369,11 @@ snapshots: '@tanstack/store': 0.8.0 solid-js: 1.9.10 - '@tanstack/start-api-routes@1.120.19(@types/node@24.10.3)(db0@0.3.4)(ioredis@5.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(rolldown@1.0.0-beta.53)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)': + '@tanstack/start-api-routes@1.120.19(@types/node@24.10.3)(db0@0.3.4)(ioredis@5.8.2)(jiti@2.6.1)(lightningcss@1.30.2)(rolldown@1.0.0-beta.53)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)': dependencies: '@tanstack/router-core': 1.141.1 '@tanstack/start-server-core': 1.141.1 - vinxi: 0.5.3(@types/node@24.10.3)(db0@0.3.4)(ioredis@5.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(rolldown@1.0.0-beta.53)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) + vinxi: 0.5.3(@types/node@24.10.3)(db0@0.3.4)(ioredis@5.8.2)(jiti@2.6.1)(lightningcss@1.30.2)(rolldown@1.0.0-beta.53)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) transitivePeerDependencies: - '@azure/app-configuration' - '@azure/cosmos' @@ -11367,12 +11436,21 @@ snapshots: tiny-invariant: 1.3.3 tiny-warning: 1.0.3 - '@tanstack/start-config@1.120.20(@types/node@24.10.3)(db0@0.3.4)(ioredis@5.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(rolldown@1.0.0-beta.53)(terser@5.44.1)(tsx@4.21.0)(vite-plugin-solid@2.11.10(solid-js@1.9.10)(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))(yaml@2.8.2)': + '@tanstack/start-client-core@1.158.4': dependencies: - '@tanstack/react-router': 1.157.16(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@tanstack/react-start-plugin': 1.131.50(@tanstack/react-router@1.157.16(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@vitejs/plugin-react@4.7.0(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(rolldown@1.0.0-beta.53)(vite-plugin-solid@2.11.10(solid-js@1.9.10)(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) + '@tanstack/router-core': 1.158.4 + '@tanstack/start-fn-stubs': 1.154.7 + '@tanstack/start-storage-context': 1.158.4 + seroval: 1.5.0 + tiny-invariant: 1.3.3 + tiny-warning: 1.0.3 + + '@tanstack/start-config@1.120.20(@types/node@24.10.3)(db0@0.3.4)(ioredis@5.8.2)(jiti@2.6.1)(lightningcss@1.30.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(rolldown@1.0.0-beta.53)(terser@5.44.1)(tsx@4.21.0)(vite-plugin-solid@2.11.10(solid-js@1.9.10)(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))(yaml@2.8.2)': + dependencies: + '@tanstack/react-router': 1.158.4(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@tanstack/react-start-plugin': 1.131.50(@tanstack/react-router@1.158.4(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@vitejs/plugin-react@4.7.0(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(rolldown@1.0.0-beta.53)(vite-plugin-solid@2.11.10(solid-js@1.9.10)(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) '@tanstack/router-generator': 1.141.1 - '@tanstack/router-plugin': 1.141.1(@tanstack/react-router@1.157.16(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(vite-plugin-solid@2.11.10(solid-js@1.9.10)(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) + '@tanstack/router-plugin': 1.158.4(@tanstack/react-router@1.158.4(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(vite-plugin-solid@2.11.10(solid-js@1.9.10)(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) '@tanstack/server-functions-plugin': 1.141.0(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) '@tanstack/start-server-functions-handler': 1.120.19 '@vitejs/plugin-react': 4.7.0(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) @@ -11381,7 +11459,7 @@ snapshots: ofetch: 1.5.1 react: 19.2.3 react-dom: 19.2.3(react@19.2.3) - vinxi: 0.5.3(@types/node@24.10.3)(db0@0.3.4)(ioredis@5.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(rolldown@1.0.0-beta.53)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) + vinxi: 0.5.3(@types/node@24.10.3)(db0@0.3.4)(ioredis@5.8.2)(jiti@2.6.1)(lightningcss@1.30.2)(rolldown@1.0.0-beta.53)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) vite: 7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) zod: 3.25.76 transitivePeerDependencies: @@ -11433,14 +11511,16 @@ snapshots: - xml2js - yaml - '@tanstack/start-plugin-core@1.131.50(@tanstack/react-router@1.157.16(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(rolldown@1.0.0-beta.53)(vite-plugin-solid@2.11.10(solid-js@1.9.10)(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))': + '@tanstack/start-fn-stubs@1.154.7': {} + + '@tanstack/start-plugin-core@1.131.50(@tanstack/react-router@1.158.4(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(rolldown@1.0.0-beta.53)(vite-plugin-solid@2.11.10(solid-js@1.9.10)(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))': dependencies: '@babel/code-frame': 7.26.2 '@babel/core': 7.28.5 '@babel/types': 7.28.5 '@tanstack/router-core': 1.131.50 '@tanstack/router-generator': 1.131.50 - '@tanstack/router-plugin': 1.131.50(@tanstack/react-router@1.157.16(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(vite-plugin-solid@2.11.10(solid-js@1.9.10)(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) + '@tanstack/router-plugin': 1.131.50(@tanstack/react-router@1.158.4(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(vite-plugin-solid@2.11.10(solid-js@1.9.10)(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) '@tanstack/router-utils': 1.131.2 '@tanstack/server-functions-plugin': 1.131.2(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) '@tanstack/start-server-core': 1.131.50 @@ -11449,9 +11529,9 @@ snapshots: babel-dead-code-elimination: 1.0.10 cheerio: 1.1.2 h3: 1.13.0 - nitropack: 2.12.9(rolldown@1.0.0-beta.53) + nitropack: 2.13.1(rolldown@1.0.0-beta.53) pathe: 2.0.3 - ufo: 1.6.1 + ufo: 1.6.3 vite: 7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) vitefu: 1.1.1(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) xmlbuilder2: 3.1.1 @@ -11491,7 +11571,7 @@ snapshots: - webpack - xml2js - '@tanstack/start-plugin-core@1.141.1(@tanstack/react-router@1.141.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(vite-plugin-solid@2.11.10(solid-js@1.9.10)(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))': + '@tanstack/start-plugin-core@1.141.1(@tanstack/react-router@1.158.4(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(vite-plugin-solid@2.11.10(solid-js@1.9.10)(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))': dependencies: '@babel/code-frame': 7.26.2 '@babel/core': 7.28.5 @@ -11499,7 +11579,7 @@ snapshots: '@rolldown/pluginutils': 1.0.0-beta.40 '@tanstack/router-core': 1.141.1 '@tanstack/router-generator': 1.141.1 - '@tanstack/router-plugin': 1.141.1(@tanstack/react-router@1.141.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(vite-plugin-solid@2.11.10(solid-js@1.9.10)(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) + '@tanstack/router-plugin': 1.141.1(@tanstack/react-router@1.158.4(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(vite-plugin-solid@2.11.10(solid-js@1.9.10)(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) '@tanstack/router-utils': 1.141.0 '@tanstack/server-functions-plugin': 1.141.0(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) '@tanstack/start-client-core': 1.141.1 @@ -11523,26 +11603,24 @@ snapshots: - vite-plugin-solid - webpack - '@tanstack/start-plugin-core@1.141.1(@tanstack/react-router@1.157.16(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(vite-plugin-solid@2.11.10(solid-js@1.9.10)(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))': + '@tanstack/start-plugin-core@1.159.0(@tanstack/react-router@1.158.4(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(vite-plugin-solid@2.11.10(solid-js@1.9.10)(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))': dependencies: - '@babel/code-frame': 7.26.2 + '@babel/code-frame': 7.27.1 '@babel/core': 7.28.5 '@babel/types': 7.28.5 '@rolldown/pluginutils': 1.0.0-beta.40 - '@tanstack/router-core': 1.141.1 - '@tanstack/router-generator': 1.141.1 - '@tanstack/router-plugin': 1.141.1(@tanstack/react-router@1.157.16(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(vite-plugin-solid@2.11.10(solid-js@1.9.10)(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) - '@tanstack/router-utils': 1.141.0 - '@tanstack/server-functions-plugin': 1.141.0(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) - '@tanstack/start-client-core': 1.141.1 - '@tanstack/start-server-core': 1.141.1 - babel-dead-code-elimination: 1.0.10 + '@tanstack/router-core': 1.158.4 + '@tanstack/router-generator': 1.158.4 + '@tanstack/router-plugin': 1.158.4(@tanstack/react-router@1.158.4(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(vite-plugin-solid@2.11.10(solid-js@1.9.10)(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) + '@tanstack/router-utils': 1.158.0 + '@tanstack/start-client-core': 1.158.4 + '@tanstack/start-server-core': 1.159.0 cheerio: 1.1.2 exsolve: 1.0.8 pathe: 2.0.3 - srvx: 0.8.16 + srvx: 0.10.1 tinyglobby: 0.2.15 - ufo: 1.6.1 + ufo: 1.6.3 vite: 7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) vitefu: 1.1.1(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) xmlbuilder2: 4.0.3 @@ -11565,7 +11643,7 @@ snapshots: isbot: 5.1.32 tiny-invariant: 1.3.3 tiny-warning: 1.0.3 - unctx: 2.4.1 + unctx: 2.5.0 '@tanstack/start-server-core@1.141.1': dependencies: @@ -11579,6 +11657,18 @@ snapshots: transitivePeerDependencies: - crossws + '@tanstack/start-server-core@1.159.0': + dependencies: + '@tanstack/history': 1.154.14 + '@tanstack/router-core': 1.158.4 + '@tanstack/start-client-core': 1.158.4 + '@tanstack/start-storage-context': 1.158.4 + h3-v2: h3@2.0.1-rc.11 + seroval: 1.5.0 + tiny-invariant: 1.3.3 + transitivePeerDependencies: + - crossws + '@tanstack/start-server-functions-client@1.131.50(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))': dependencies: '@tanstack/server-functions-plugin': 1.131.2(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) @@ -11629,13 +11719,17 @@ snapshots: dependencies: '@tanstack/router-core': 1.141.1 - '@tanstack/start@1.120.20(@types/node@24.10.3)(db0@0.3.4)(ioredis@5.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(rolldown@1.0.0-beta.53)(terser@5.44.1)(tsx@4.21.0)(vite-plugin-solid@2.11.10(solid-js@1.9.10)(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))(yaml@2.8.2)': + '@tanstack/start-storage-context@1.158.4': + dependencies: + '@tanstack/router-core': 1.158.4 + + '@tanstack/start@1.120.20(@types/node@24.10.3)(db0@0.3.4)(ioredis@5.8.2)(jiti@2.6.1)(lightningcss@1.30.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(rolldown@1.0.0-beta.53)(terser@5.44.1)(tsx@4.21.0)(vite-plugin-solid@2.11.10(solid-js@1.9.10)(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))(yaml@2.8.2)': dependencies: '@tanstack/react-start-client': 1.141.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@tanstack/react-start-router-manifest': 1.120.19(@types/node@24.10.3)(db0@0.3.4)(ioredis@5.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(rolldown@1.0.0-beta.53)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) + '@tanstack/react-start-router-manifest': 1.120.19(@types/node@24.10.3)(db0@0.3.4)(ioredis@5.8.2)(jiti@2.6.1)(lightningcss@1.30.2)(rolldown@1.0.0-beta.53)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) '@tanstack/react-start-server': 1.141.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@tanstack/start-api-routes': 1.120.19(@types/node@24.10.3)(db0@0.3.4)(ioredis@5.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(rolldown@1.0.0-beta.53)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) - '@tanstack/start-config': 1.120.20(@types/node@24.10.3)(db0@0.3.4)(ioredis@5.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(rolldown@1.0.0-beta.53)(terser@5.44.1)(tsx@4.21.0)(vite-plugin-solid@2.11.10(solid-js@1.9.10)(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))(yaml@2.8.2) + '@tanstack/start-api-routes': 1.120.19(@types/node@24.10.3)(db0@0.3.4)(ioredis@5.8.2)(jiti@2.6.1)(lightningcss@1.30.2)(rolldown@1.0.0-beta.53)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) + '@tanstack/start-config': 1.120.20(@types/node@24.10.3)(db0@0.3.4)(ioredis@5.8.2)(jiti@2.6.1)(lightningcss@1.30.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(rolldown@1.0.0-beta.53)(terser@5.44.1)(tsx@4.21.0)(vite-plugin-solid@2.11.10(solid-js@1.9.10)(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)))(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))(yaml@2.8.2) '@tanstack/start-server-functions-client': 1.131.50(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) '@tanstack/start-server-functions-handler': 1.120.19 '@tanstack/start-server-functions-server': 1.131.2(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) @@ -11708,6 +11802,8 @@ snapshots: '@tanstack/virtual-file-routes@1.141.0': {} + '@tanstack/virtual-file-routes@1.154.7': {} + '@tanstack/vite-config@0.4.1(@types/node@24.10.3)(rollup@4.55.1)(typescript@5.9.3)(vite@7.2.7(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))': dependencies: rollup-plugin-preserve-directives: 0.4.0(rollup@4.55.1) @@ -12083,7 +12179,7 @@ snapshots: node-forge: 1.3.3 pathe: 1.1.2 std-env: 3.10.0 - ufo: 1.6.1 + ufo: 1.6.3 untun: 0.1.3 uqr: 0.1.2 @@ -12579,6 +12675,15 @@ snapshots: transitivePeerDependencies: - supports-color + babel-dead-code-elimination@1.0.12: + dependencies: + '@babel/core': 7.28.5 + '@babel/parser': 7.28.5 + '@babel/traverse': 7.28.5 + '@babel/types': 7.28.5 + transitivePeerDependencies: + - supports-color + babel-plugin-jsx-dom-expressions@0.40.3(@babel/core@7.28.5): dependencies: '@babel/core': 7.28.5 @@ -13903,7 +14008,7 @@ snapshots: iron-webcrypto: 1.2.1 ohash: 1.1.6 radix3: 1.1.2 - ufo: 1.6.1 + ufo: 1.6.3 uncrypto: 0.1.3 unenv: 1.10.0 @@ -13938,6 +14043,11 @@ snapshots: rou3: 0.7.12 srvx: 0.8.16 + h3@2.0.1-rc.11: + dependencies: + rou3: 0.7.12 + srvx: 0.10.1 + happy-dom@20.0.11: dependencies: '@types/node': 20.19.26 @@ -16432,6 +16542,8 @@ snapshots: sprintf-js@1.0.3: {} + srvx@0.10.1: {} + srvx@0.8.16: {} stable-hash-x@0.2.0: {} @@ -16969,20 +17081,6 @@ snapshots: db0: 0.3.4 ioredis: 5.8.2 - unstorage@1.17.3(db0@0.3.4)(ioredis@5.9.2): - dependencies: - anymatch: 3.1.3 - chokidar: 4.0.3 - destr: 2.0.5 - h3: 1.15.4 - lru-cache: 10.4.3 - node-fetch-native: 1.6.7 - ofetch: 1.5.1 - ufo: 1.6.1 - optionalDependencies: - db0: 0.3.4 - ioredis: 5.9.2 - unstorage@1.17.4(db0@0.3.4)(ioredis@5.9.2): dependencies: anymatch: 3.1.3 @@ -17064,7 +17162,7 @@ snapshots: '@types/unist': 3.0.3 vfile-message: 4.0.3 - vinxi@0.5.3(@types/node@24.10.3)(db0@0.3.4)(ioredis@5.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(rolldown@1.0.0-beta.53)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2): + vinxi@0.5.3(@types/node@24.10.3)(db0@0.3.4)(ioredis@5.8.2)(jiti@2.6.1)(lightningcss@1.30.2)(rolldown@1.0.0-beta.53)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2): dependencies: '@babel/core': 7.28.5 '@babel/plugin-syntax-jsx': 7.27.1(@babel/core@7.28.5) @@ -17086,7 +17184,7 @@ snapshots: hookable: 5.5.3 http-proxy: 1.18.1 micromatch: 4.0.8 - nitropack: 2.12.9(rolldown@1.0.0-beta.53) + nitropack: 2.13.1(rolldown@1.0.0-beta.53) node-fetch-native: 1.6.7 path-to-regexp: 6.3.0 pathe: 1.1.2 @@ -17097,7 +17195,7 @@ snapshots: ufo: 1.6.1 unctx: 2.4.1 unenv: 1.10.0 - unstorage: 1.17.3(db0@0.3.4)(ioredis@5.9.2) + unstorage: 1.17.3(db0@0.3.4)(ioredis@5.8.2) vite: 6.4.1(@types/node@24.10.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) zod: 3.25.76 transitivePeerDependencies: diff --git a/scripts/fix-version-bump.ts b/scripts/fix-version-bump.ts new file mode 100644 index 000000000..8a5c31032 --- /dev/null +++ b/scripts/fix-version-bump.ts @@ -0,0 +1,156 @@ +import { readFileSync, writeFileSync } from 'node:fs' +import { resolve } from 'node:path' +import { glob } from 'tinyglobby' + +const WRONG_VERSION = '1.0.0' + +interface PackageToFix { + name: string + packageJsonPath: string + changelogPath: string + detectedVersion: string | null +} + +function parseArgs(): { version: string | null } { + const args = process.argv.slice(2) + const versionIndex = args.findIndex( + (arg) => arg === '--version' || arg === '-v', + ) + + if (versionIndex !== -1 && args[versionIndex + 1]) { + return { version: args[versionIndex + 1] } + } + + return { version: null } +} + +function detectVersionFromChangelog(changelogPath: string): string | null { + try { + const content = readFileSync(changelogPath, 'utf-8') + + // Look for "Updated dependencies" section and extract version numbers + // Pattern: - @tanstack/package-name@X.Y.Z + const dependencyPattern = /-\s+@tanstack\/[\w-]+@(\d+\.\d+\.\d+)/g + const matches = [...content.matchAll(dependencyPattern)] + + if (matches.length > 0) { + // Get the first dependency version (they should all be the same in a changeset bump) + const version = matches[0][1] + // Make sure it's not also 1.0.0 + if (version !== WRONG_VERSION) { + return version + } + } + + return null + } catch { + return null + } +} + +function fixPackageJson(path: string, newVersion: string): void { + const content = readFileSync(path, 'utf-8') + const updated = content.replace( + /"version":\s*"1\.0\.0"/, + `"version": "${newVersion}"`, + ) + writeFileSync(path, updated) +} + +function fixChangelog(path: string, newVersion: string): void { + const content = readFileSync(path, 'utf-8') + // Replace the first occurrence of "## 1.0.0" with the correct version + const updated = content.replace(/^## 1\.0\.0$/m, `## ${newVersion}`) + writeFileSync(path, updated) +} + +async function main() { + const { version: cliVersion } = parseArgs() + + console.log('🔍 Scanning for packages with version 1.0.0...\n') + + // Find all package.json files in packages/typescript + const packageJsonFiles = await glob('packages/typescript/*/package.json', { + ignore: ['**/node_modules/**'], + }) + + const packagesToFix: PackageToFix[] = [] + + for (const packageJsonPath of packageJsonFiles) { + try { + const content = readFileSync(packageJsonPath, 'utf-8') + const pkg = JSON.parse(content) + + if (pkg.version === WRONG_VERSION) { + const changelogPath = packageJsonPath.replace( + 'package.json', + 'CHANGELOG.md', + ) + const detectedVersion = detectVersionFromChangelog(changelogPath) + + packagesToFix.push({ + name: pkg.name, + packageJsonPath, + changelogPath, + detectedVersion, + }) + } + } catch { + // Skip files that can't be parsed + } + } + + if (packagesToFix.length === 0) { + console.log('✅ No packages found with version 1.0.0. Nothing to fix!') + process.exit(0) + } + + console.log(`Found ${packagesToFix.length} package(s) with version 1.0.0:\n`) + + const errors: string[] = [] + + for (const pkg of packagesToFix) { + const targetVersion = cliVersion || pkg.detectedVersion + + if (!targetVersion) { + errors.push(pkg.name) + console.log(` ❌ ${pkg.name} - could not auto-detect version`) + } else { + console.log(` 📦 ${pkg.name} → ${targetVersion}`) + } + } + + if (errors.length > 0) { + console.log('\n❌ Could not auto-detect version for some packages.') + console.log(' Please run with an explicit version:\n') + console.log(' node scripts/fix-version-bump.ts --version X.Y.Z\n') + process.exit(1) + } + + console.log('\n🔧 Fixing versions...\n') + + for (const pkg of packagesToFix) { + const targetVersion = cliVersion || pkg.detectedVersion! + + // Fix package.json + fixPackageJson(pkg.packageJsonPath, targetVersion) + console.log(` ✓ ${pkg.packageJsonPath}`) + + // Fix CHANGELOG.md + try { + fixChangelog(pkg.changelogPath, targetVersion) + console.log(` ✓ ${pkg.changelogPath}`) + } catch { + console.log(` ⚠ ${pkg.changelogPath} (not found or could not update)`) + } + } + + console.log('\n✅ Done! Version bump fixed.') + console.log('\nNext steps:') + console.log(' 1. Review the changes: git diff') + console.log( + ' 2. Commit: git add -A && git commit -m "fix: correct version bump to X.Y.Z"', + ) +} + +main().catch(console.error) diff --git a/testing/panel/package.json b/testing/panel/package.json index 688207804..53628b981 100644 --- a/testing/panel/package.json +++ b/testing/panel/package.json @@ -21,9 +21,9 @@ "@tanstack/ai-openrouter": "workspace:*", "@tanstack/ai-react": "workspace:*", "@tanstack/ai-react-ui": "workspace:*", - "@tanstack/nitro-v2-vite-plugin": "^1.141.0", - "@tanstack/react-router": "^1.141.1", - "@tanstack/react-start": "^1.141.1", + "@tanstack/nitro-v2-vite-plugin": "^1.154.7", + "@tanstack/react-router": "^1.158.4", + "@tanstack/react-start": "^1.159.0", "@tanstack/start": "^1.120.20", "highlight.js": "^11.11.1", "lucide-react": "^0.561.0", diff --git a/testing/panel/src/lib/model-selection.ts b/testing/panel/src/lib/model-selection.ts index 1efc2ab38..470c6a52d 100644 --- a/testing/panel/src/lib/model-selection.ts +++ b/testing/panel/src/lib/model-selection.ts @@ -83,7 +83,7 @@ export const MODEL_OPTIONS: Array = [ { provider: 'grok', model: 'grok-4', - label: 'Grok - Grok 4', + label: 'Grok - Grok 4 - slow thinking', }, { provider: 'grok', diff --git a/testing/panel/tests/tool-flow.spec.ts b/testing/panel/tests/tool-flow.spec.ts index 3c940d7a5..0879d92d6 100644 --- a/testing/panel/tests/tool-flow.spec.ts +++ b/testing/panel/tests/tool-flow.spec.ts @@ -154,6 +154,85 @@ for (const provider of toolProviders) { }) } +// =========================== +// Multi-turn follow-up tests +// =========================== +// These test that providers can handle follow-up messages AFTER tool calls. +// This specifically catches the Anthropic bug where consecutive user-role messages +// (tool_result + new user message) violate the alternating role constraint. + +for (const provider of toolProviders) { + test.describe(`${provider.name} - Multi-turn Tool Follow-up`, () => { + // Extended timeout for multi-turn conversations (two full LLM round-trips) + test.describe.configure({ retries: 2, timeout: 180_000 }) + + // Skip if provider is not available + test.skip( + () => !isProviderAvailable(provider), + `${provider.name} API key not configured (requires ${provider.envKey || 'no key'})`, + ) + + test('should handle follow-up message after tool call completes', async ({ + page, + }) => { + // Navigate to the chat page + await goToChatPage(page) + + // Select the provider and model + await selectProvider(page, provider.id, provider.defaultModel) + + // First message: trigger a tool call + await sendMessage( + page, + 'Use the getGuitars tool to show me what acoustic guitars you have.', + ) + + // Wait for the first response to complete (tool call + model response) + await waitForResponse(page, 120_000) + + // Verify the first turn worked - should have tool calls + const firstMessages = await getMessages(page) + const firstAssistant = firstMessages.filter( + (m: any) => m.role === 'assistant', + ) + expect(firstAssistant.length).toBeGreaterThan(0) + + // Send a follow-up message - this is where the bug manifested + // With the Anthropic bug, this would fail with consecutive user-role messages + await sendMessage(page, 'Now tell me about electric guitars instead.') + + // Wait for the follow-up response + await waitForResponse(page, 120_000) + + // Get all messages after the follow-up + const allMessages = await getMessages(page) + + // Should have at least 2 user messages and 2 assistant messages + const userMessages = allMessages.filter((m: any) => m.role === 'user') + const assistantMessages = allMessages.filter( + (m: any) => m.role === 'assistant', + ) + + expect(userMessages.length).toBeGreaterThanOrEqual(2) + expect(assistantMessages.length).toBeGreaterThanOrEqual(2) + + // The LAST assistant message should have non-empty text content + // (not just tool calls, and not an error) + const lastAssistant = assistantMessages[assistantMessages.length - 1] + const textParts = lastAssistant.parts?.filter( + (p: any) => p.type === 'text' && p.content && p.content.length > 0, + ) + + // The follow-up should have produced some text OR tool calls + // (both are valid responses - the key is it didn't error out) + const hasText = textParts?.length > 0 + const hasTools = + lastAssistant.parts?.some((p: any) => p.type === 'tool-call') || false + expect(hasText || hasTools).toBe(true) + }) + }) +} + // Verify we have tool-capable providers to test test('at least one tool-capable provider should be available', async () => { const available = getToolCapableProviders()