From 5262fbc02195e281677e13fa86859a50f9f137cc Mon Sep 17 00:00:00 2001 From: Nikas Belogolov Date: Sun, 25 Jan 2026 12:08:00 +0200 Subject: [PATCH 01/11] feat: add status property to useChat across all frameworks --- .../typescript/ai-client/src/chat-client.ts | 4 +- packages/typescript/ai-client/src/index.ts | 1 + packages/typescript/ai-client/src/types.ts | 14 ++++ packages/typescript/ai-preact/src/types.ts | 8 ++- packages/typescript/ai-preact/src/use-chat.ts | 24 +++++-- .../ai-preact/tests/use-chat.test.ts | 62 ++++++++++++++++- packages/typescript/ai-react/src/types.ts | 8 ++- packages/typescript/ai-react/src/use-chat.ts | 25 +++++-- .../ai-react/tests/use-chat.test.ts | 67 +++++++++++++++++-- packages/typescript/ai-solid/src/types.ts | 12 +++- packages/typescript/ai-solid/src/use-chat.ts | 23 ++++++- .../typescript/ai-solid/tests/test-utils.ts | 5 +- .../ai-solid/tests/use-chat.test.ts | 66 ++++++++++++++++-- .../ai-svelte/src/create-chat.svelte.ts | 24 ++++++- packages/typescript/ai-svelte/src/types.ts | 10 ++- .../ai-svelte/tests/use-chat.test.ts | 14 +++- packages/typescript/ai-vue/src/types.ts | 12 +++- packages/typescript/ai-vue/src/use-chat.ts | 24 +++++-- .../typescript/ai-vue/tests/test-utils.ts | 9 +-- .../typescript/ai-vue/tests/use-chat.test.ts | 51 +++++++++++++- 20 files changed, 408 insertions(+), 55 deletions(-) diff --git a/packages/typescript/ai-client/src/chat-client.ts b/packages/typescript/ai-client/src/chat-client.ts index 1d1ba091..47d5684a 100644 --- a/packages/typescript/ai-client/src/chat-client.ts +++ b/packages/typescript/ai-client/src/chat-client.ts @@ -36,6 +36,7 @@ export class ChatClient { onError: (error: Error) => void onMessagesChange: (messages: Array) => void onLoadingChange: (isLoading: boolean) => void + onStreamStart: () => void onErrorChange: (error: Error | undefined) => void } } @@ -62,6 +63,7 @@ export class ChatClient { onError: options.onError || (() => {}), onMessagesChange: options.onMessagesChange || (() => {}), onLoadingChange: options.onLoadingChange || (() => {}), + onStreamStart: options.onStreamStart || (() => {}), onErrorChange: options.onErrorChange || (() => {}), }, } @@ -75,7 +77,7 @@ export class ChatClient { this.callbacksRef.current.onMessagesChange(messages) }, onStreamStart: () => { - // Stream started + this.callbacksRef.current.onStreamStart() }, onStreamEnd: (message: UIMessage) => { this.callbacksRef.current.onFinish(message) diff --git a/packages/typescript/ai-client/src/index.ts b/packages/typescript/ai-client/src/index.ts index 5bc664c0..e1da2705 100644 --- a/packages/typescript/ai-client/src/index.ts +++ b/packages/typescript/ai-client/src/index.ts @@ -11,6 +11,7 @@ export type { ChatClientOptions, ChatRequestBody, InferChatMessages, + ChatClientState, } from './types' export { clientTools, createChatClientOptions } from './types' export type { diff --git a/packages/typescript/ai-client/src/types.ts b/packages/typescript/ai-client/src/types.ts index 4f83debb..1d0ffc60 100644 --- a/packages/typescript/ai-client/src/types.ts +++ b/packages/typescript/ai-client/src/types.ts @@ -26,6 +26,15 @@ export type ToolResultState = | 'complete' // Result is complete | 'error' // Error occurred +/** + * ChatClient state - track the lifecycle of a chat + */ +export type ChatClientState = + | 'ready' + | 'submitted' + | 'streaming' + | 'error' + /** * Message parts - building blocks of UIMessage */ @@ -191,6 +200,11 @@ export interface ChatClientOptions< */ onErrorChange?: (error: Error | undefined) => void + /** + * Callback when stream starts + */ + onStreamStart?: () => void + /** * Client-side tools with execution logic * When provided, tools with execute functions will be called automatically diff --git a/packages/typescript/ai-preact/src/types.ts b/packages/typescript/ai-preact/src/types.ts index 21333907..37fad0a8 100644 --- a/packages/typescript/ai-preact/src/types.ts +++ b/packages/typescript/ai-preact/src/types.ts @@ -1,12 +1,13 @@ import type { AnyClientTool, ModelMessage } from '@tanstack/ai' import type { ChatClientOptions, + ChatClientState, ChatRequestBody, UIMessage, } from '@tanstack/ai-client' // Re-export types from ai-client -export type { UIMessage, ChatRequestBody } +export type { ChatRequestBody, UIMessage } /** * Options for the useChat hook. @@ -95,4 +96,9 @@ export interface UseChatReturn< * Clear all messages */ clear: () => void + + /** + * Current generation status + */ + status: ChatClientState } diff --git a/packages/typescript/ai-preact/src/use-chat.ts b/packages/typescript/ai-preact/src/use-chat.ts index c3f0f208..0fffea40 100644 --- a/packages/typescript/ai-preact/src/use-chat.ts +++ b/packages/typescript/ai-preact/src/use-chat.ts @@ -1,3 +1,5 @@ +import type { AnyClientTool, ModelMessage } from '@tanstack/ai' +import { ChatClient, ChatClientState } from '@tanstack/ai-client' import { useCallback, useEffect, @@ -6,8 +8,6 @@ import { useRef, useState, } from 'preact/hooks' -import { ChatClient } from '@tanstack/ai-client' -import type { AnyClientTool, ModelMessage } from '@tanstack/ai' import type { UIMessage, UseChatOptions, UseChatReturn } from './types' @@ -22,6 +22,7 @@ export function useChat = any>( ) const [isLoading, setIsLoading] = useState(false) const [error, setError] = useState(undefined) + const [status, setStatus] = useState('ready') // Track current messages in a ref to preserve them when client is recreated const messagesRef = useRef>>( @@ -51,8 +52,14 @@ export function useChat = any>( body: optionsRef.current.body, onResponse: optionsRef.current.onResponse, onChunk: optionsRef.current.onChunk, - onFinish: optionsRef.current.onFinish, - onError: optionsRef.current.onError, + onFinish: (message) => { + setStatus('ready') + optionsRef.current.onFinish?.(message) + }, + onError: (err) => { + setStatus('error') + optionsRef.current.onError?.(err) + }, tools: optionsRef.current.tools, streamProcessor: options.streamProcessor, onMessagesChange: (newMessages: Array>) => { @@ -60,6 +67,14 @@ export function useChat = any>( }, onLoadingChange: (newIsLoading: boolean) => { setIsLoading(newIsLoading) + if (newIsLoading) { + setStatus('submitted') + } else { + setStatus((prev) => (prev === 'error' ? 'error' : 'ready')) + } + }, + onStreamStart: () => { + setStatus('streaming') }, onErrorChange: (newError: Error | undefined) => { setError(newError) @@ -154,6 +169,7 @@ export function useChat = any>( stop, isLoading, error, + status, setMessages: setMessagesManually, clear, addToolResult, diff --git a/packages/typescript/ai-preact/tests/use-chat.test.ts b/packages/typescript/ai-preact/tests/use-chat.test.ts index b7bbedb6..3b35dbca 100644 --- a/packages/typescript/ai-preact/tests/use-chat.test.ts +++ b/packages/typescript/ai-preact/tests/use-chat.test.ts @@ -1,13 +1,13 @@ -import { describe, expect, it, vi } from 'vitest' +import type { ModelMessage } from '@tanstack/ai' import { act, waitFor } from '@testing-library/preact' +import { describe, expect, it, vi } from 'vitest' +import type { UIMessage } from '../src/types' import { createMockConnectionAdapter, createTextChunks, createToolCallChunks, renderUseChat, } from './test-utils' -import type { UIMessage } from '../src/types' -import type { ModelMessage } from '@tanstack/ai' describe('useChat', () => { describe('initialization', () => { @@ -559,6 +559,62 @@ describe('useChat', () => { }) }) + describe('status', () => { + it('should have initial status of ready', () => { + const adapter = createMockConnectionAdapter() + const { result } = renderUseChat({ connection: adapter }) + expect(result.current.status).toBe('ready') + }) + + it('should transition through states during generation', async () => { + const chunks = createTextChunks('Response') + const adapter = createMockConnectionAdapter({ + chunks, + chunkDelay: 50, + }) + const { result } = renderUseChat({ connection: adapter }) + + let sendPromise: Promise + act(() => { + sendPromise = result.current.sendMessage('Test') + }) + + // Should leave ready state + await waitFor(() => { + expect(result.current.status).not.toBe('ready') + }) + + // Should be submitted or streaming + expect(['submitted', 'streaming']).toContain(result.current.status) + + // Should return to ready eventually + await act(async () => { + await sendPromise! + }) + + await waitFor(() => { + expect(result.current.status).toBe('ready') + }) + }) + + it('should transition to error on error', async () => { + const error = new Error('Network error') + const adapter = createMockConnectionAdapter({ + shouldError: true, + error, + }) + const { result } = renderUseChat({ connection: adapter }) + + await act(async () => { + await result.current.sendMessage('Test') + }) + + await waitFor(() => { + expect(result.current.status).toBe('error') + }) + }) + }) + describe('clear', () => { it('should clear all messages', async () => { const chunks = createTextChunks('Response') diff --git a/packages/typescript/ai-react/src/types.ts b/packages/typescript/ai-react/src/types.ts index 80eaccea..33257bb3 100644 --- a/packages/typescript/ai-react/src/types.ts +++ b/packages/typescript/ai-react/src/types.ts @@ -1,6 +1,7 @@ import type { AnyClientTool, ModelMessage } from '@tanstack/ai' import type { ChatClientOptions, + ChatClientState, ChatRequestBody, UIMessage, } from '@tanstack/ai-client' @@ -26,7 +27,7 @@ export type { UIMessage, ChatRequestBody } export type UseChatOptions = any> = Omit< ChatClientOptions, - 'onMessagesChange' | 'onLoadingChange' | 'onErrorChange' + 'onMessagesChange' | 'onLoadingChange' | 'onErrorChange' | 'onStreamStart' > export interface UseChatReturn< @@ -86,6 +87,11 @@ export interface UseChatReturn< */ error: Error | undefined + /** + * Current status of the chat client + */ + status: ChatClientState + /** * Set messages manually */ diff --git a/packages/typescript/ai-react/src/use-chat.ts b/packages/typescript/ai-react/src/use-chat.ts index f9511e41..4cb8f863 100644 --- a/packages/typescript/ai-react/src/use-chat.ts +++ b/packages/typescript/ai-react/src/use-chat.ts @@ -1,6 +1,6 @@ -import { useCallback, useEffect, useId, useMemo, useRef, useState } from 'react' -import { ChatClient } from '@tanstack/ai-client' import type { AnyClientTool, ModelMessage } from '@tanstack/ai' +import { ChatClient, ChatClientState } from '@tanstack/ai-client' +import { useCallback, useEffect, useId, useMemo, useRef, useState } from 'react' import type { UIMessage, UseChatOptions, UseChatReturn } from './types' @@ -15,6 +15,7 @@ export function useChat = any>( ) const [isLoading, setIsLoading] = useState(false) const [error, setError] = useState(undefined) + const [status, setStatus] = useState('ready') // Track current messages in a ref to preserve them when client is recreated const messagesRef = useRef>>( @@ -50,8 +51,17 @@ export function useChat = any>( body: optionsRef.current.body, onResponse: optionsRef.current.onResponse, onChunk: optionsRef.current.onChunk, - onFinish: optionsRef.current.onFinish, - onError: optionsRef.current.onError, + onStreamStart: () => { + setStatus("streaming") + }, + onFinish: (message: UIMessage) => { + setStatus('ready') + optionsRef.current.onFinish?.(message) + }, + onError: (error: Error) => { + setStatus('error') + optionsRef.current.onError?.(error) + }, tools: optionsRef.current.tools, streamProcessor: options.streamProcessor, onMessagesChange: (newMessages: Array>) => { @@ -59,6 +69,11 @@ export function useChat = any>( }, onLoadingChange: (newIsLoading: boolean) => { setIsLoading(newIsLoading) + if (newIsLoading) { + setStatus('submitted') + } else { + setStatus((prev) => (prev === 'error' ? 'error' : 'ready')) + } }, onErrorChange: (newError: Error | undefined) => { setError(newError) @@ -66,6 +81,7 @@ export function useChat = any>( }) }, [clientId]) + // Sync initial messages on mount only // Note: initialMessages are passed to ChatClient constructor, but we also // set them here to ensure React state is in sync @@ -154,6 +170,7 @@ export function useChat = any>( stop, isLoading, error, + status, setMessages: setMessagesManually, clear, addToolResult, diff --git a/packages/typescript/ai-react/tests/use-chat.test.ts b/packages/typescript/ai-react/tests/use-chat.test.ts index c0a9e8d2..2cea5075 100644 --- a/packages/typescript/ai-react/tests/use-chat.test.ts +++ b/packages/typescript/ai-react/tests/use-chat.test.ts @@ -1,14 +1,13 @@ -import { describe, it, expect, vi, beforeEach } from 'vitest' +import type { ModelMessage } from '@tanstack/ai' import { waitFor } from '@testing-library/react' -import { useChat } from '../src/use-chat' +import { describe, expect, it, vi } from 'vitest' +import type { UIMessage } from '../src/types' import { - renderUseChat, createMockConnectionAdapter, createTextChunks, createToolCallChunks, + renderUseChat, } from './test-utils' -import type { UIMessage } from '../src/types' -import type { ModelMessage } from '@tanstack/ai' describe('useChat', () => { describe('initialization', () => { @@ -379,9 +378,9 @@ describe('useChat', () => { ) const firstContent = firstAssistantMessage?.parts.find((p) => p.type === 'text')?.type === - 'text' + 'text' ? (firstAssistantMessage.parts.find((p) => p.type === 'text') as any) - .content + .content : '' // Reload with new adapter @@ -519,6 +518,60 @@ describe('useChat', () => { }) }) + describe('status', () => { + it('should have initial status of ready', () => { + const adapter = createMockConnectionAdapter() + const { result } = renderUseChat({ connection: adapter }) + expect(result.current.status).toBe('ready') + }) + + it('should transition through states during generation', async () => { + const chunks = createTextChunks('Response') + const adapter = createMockConnectionAdapter({ + chunks, + chunkDelay: 50, + }) + const { result } = renderUseChat({ connection: adapter }) + + const sendPromise = result.current.sendMessage('Test') + + // Should leave ready state + await waitFor(() => { + expect(result.current.status).not.toBe('ready') + }) + + // Should be submitted or streaming + expect(['submitted', 'streaming']).toContain(result.current.status) + + // Should eventually match streaming + await waitFor(() => { + expect(result.current.status).toBe('streaming') + }) + + await sendPromise + + // Should return to ready + await waitFor(() => { + expect(result.current.status).toBe('ready') + }) + }) + + it('should transition to error on error', async () => { + const error = new Error('Network error') + const adapter = createMockConnectionAdapter({ + shouldError: true, + error, + }) + const { result } = renderUseChat({ connection: adapter }) + + await result.current.sendMessage('Test') + + await waitFor(() => { + expect(result.current.status).toBe('error') + }) + }) + }) + describe('clear', () => { it('should clear all messages', async () => { const chunks = createTextChunks('Response') diff --git a/packages/typescript/ai-solid/src/types.ts b/packages/typescript/ai-solid/src/types.ts index 45cbc9c7..0ded94ea 100644 --- a/packages/typescript/ai-solid/src/types.ts +++ b/packages/typescript/ai-solid/src/types.ts @@ -1,13 +1,14 @@ -import type { Accessor } from 'solid-js' import type { AnyClientTool, ModelMessage } from '@tanstack/ai' import type { ChatClientOptions, + ChatClientState, ChatRequestBody, - UIMessage, + UIMessage } from '@tanstack/ai-client' +import type { Accessor } from 'solid-js' // Re-export types from ai-client -export type { UIMessage, ChatRequestBody } +export type { ChatRequestBody, UIMessage } /** * Options for the useChat hook. @@ -96,6 +97,11 @@ export interface UseChatReturn< * Clear all messages */ clear: () => void + + /** + * Current generation status + */ + status: Accessor } // Note: createChatClientOptions and InferChatMessages are now in @tanstack/ai-client diff --git a/packages/typescript/ai-solid/src/use-chat.ts b/packages/typescript/ai-solid/src/use-chat.ts index 2a15fb37..f79cccd8 100644 --- a/packages/typescript/ai-solid/src/use-chat.ts +++ b/packages/typescript/ai-solid/src/use-chat.ts @@ -4,8 +4,9 @@ import { createSignal, createUniqueId, } from 'solid-js' -import { ChatClient } from '@tanstack/ai-client' + import type { AnyClientTool, ModelMessage } from '@tanstack/ai' +import { ChatClient, ChatClientState } from '@tanstack/ai-client' import type { UIMessage, UseChatOptions, UseChatReturn } from './types' export function useChat = any>( @@ -19,6 +20,7 @@ export function useChat = any>( ) const [isLoading, setIsLoading] = createSignal(false) const [error, setError] = createSignal(undefined) + const [status, setStatus] = createSignal('ready') // Create ChatClient instance with callbacks to sync state // Note: Options are captured at client creation time. @@ -32,8 +34,14 @@ export function useChat = any>( body: options.body, onResponse: options.onResponse, onChunk: options.onChunk, - onFinish: options.onFinish, - onError: options.onError, + onFinish: (message) => { + setStatus('ready') + options.onFinish?.(message) + }, + onError: (err) => { + setStatus('error') + options.onError?.(err) + }, tools: options.tools, streamProcessor: options.streamProcessor, onMessagesChange: (newMessages: Array>) => { @@ -41,6 +49,14 @@ export function useChat = any>( }, onLoadingChange: (newIsLoading: boolean) => { setIsLoading(newIsLoading) + if (newIsLoading) { + setStatus('submitted') + } else { + setStatus((prev) => (prev === 'error' ? 'error' : 'ready')) + } + }, + onStreamStart: () => { + setStatus('streaming') }, onErrorChange: (newError: Error | undefined) => { setError(newError) @@ -125,6 +141,7 @@ export function useChat = any>( stop, isLoading, error, + status, setMessages: setMessagesManually, clear, addToolResult, diff --git a/packages/typescript/ai-solid/tests/test-utils.ts b/packages/typescript/ai-solid/tests/test-utils.ts index 47680ffe..791dcd22 100644 --- a/packages/typescript/ai-solid/tests/test-utils.ts +++ b/packages/typescript/ai-solid/tests/test-utils.ts @@ -3,11 +3,11 @@ export { createMockConnectionAdapter, createTextChunks, createToolCallChunks, - type MockConnectionAdapterOptions, + type MockConnectionAdapterOptions } from '../../ai-client/tests/test-utils' import { renderHook } from '@solidjs/testing-library' -import type { UseChatOptions, UseChatReturn } from '../src/types' +import type { UseChatOptions } from '../src/types' import { useChat } from '../src/use-chat' /** @@ -34,6 +34,7 @@ export function renderUseChat(options?: UseChatOptions) { messages: hook.messages(), isLoading: hook.isLoading(), error: hook.error(), + status: hook.status(), sendMessage: hook.sendMessage, append: hook.append, reload: hook.reload, diff --git a/packages/typescript/ai-solid/tests/use-chat.test.ts b/packages/typescript/ai-solid/tests/use-chat.test.ts index a392d96f..4d1180e8 100644 --- a/packages/typescript/ai-solid/tests/use-chat.test.ts +++ b/packages/typescript/ai-solid/tests/use-chat.test.ts @@ -1,13 +1,13 @@ -import { describe, it, expect, vi } from 'vitest' import { waitFor } from '@solidjs/testing-library' +import type { ModelMessage } from '@tanstack/ai' +import { describe, expect, it, vi } from 'vitest' +import type { UIMessage } from '../src/types' import { - renderUseChat, createMockConnectionAdapter, createTextChunks, createToolCallChunks, + renderUseChat, } from './test-utils' -import type { UIMessage } from '../src/types' -import type { ModelMessage } from '@tanstack/ai' describe('useChat', () => { describe('initialization', () => { @@ -378,9 +378,9 @@ describe('useChat', () => { ) const firstContent = firstAssistantMessage?.parts.find((p) => p.type === 'text')?.type === - 'text' + 'text' ? (firstAssistantMessage.parts.find((p) => p.type === 'text') as any) - .content + .content : '' // Reload with new adapter @@ -518,6 +518,60 @@ describe('useChat', () => { }) }) + describe('status', () => { + it('should have initial status of ready', () => { + const adapter = createMockConnectionAdapter() + const { result } = renderUseChat({ connection: adapter }) + expect(result.current.status).toBe('ready') + }) + + it('should transition through states during generation', async () => { + const chunks = createTextChunks('Response') + const adapter = createMockConnectionAdapter({ + chunks, + chunkDelay: 50, + }) + const { result } = renderUseChat({ connection: adapter }) + + const sendPromise = result.current.sendMessage('Test') + + // Should leave ready state + await waitFor(() => { + expect(result.current.status).not.toBe('ready') + }) + + // Should be submitted or streaming + expect(['submitted', 'streaming']).toContain(result.current.status) + + // Should eventually match streaming + await waitFor(() => { + expect(result.current.status).toBe('streaming') + }) + + await sendPromise + + // Should return to ready + await waitFor(() => { + expect(result.current.status).toBe('ready') + }) + }) + + it('should transition to error on error', async () => { + const error = new Error('Network error') + const adapter = createMockConnectionAdapter({ + shouldError: true, + error, + }) + const { result } = renderUseChat({ connection: adapter }) + + await result.current.sendMessage('Test') + + await waitFor(() => { + expect(result.current.status).toBe('error') + }) + }) + }) + describe('clear', () => { it('should clear all messages', async () => { const chunks = createTextChunks('Response') diff --git a/packages/typescript/ai-svelte/src/create-chat.svelte.ts b/packages/typescript/ai-svelte/src/create-chat.svelte.ts index c4081d27..888273c0 100644 --- a/packages/typescript/ai-svelte/src/create-chat.svelte.ts +++ b/packages/typescript/ai-svelte/src/create-chat.svelte.ts @@ -1,5 +1,5 @@ -import { ChatClient } from '@tanstack/ai-client' import type { AnyClientTool, ModelMessage } from '@tanstack/ai' +import { ChatClient, ChatClientState } from '@tanstack/ai-client' import type { CreateChatOptions, CreateChatReturn, UIMessage } from './types' /** @@ -44,6 +44,7 @@ export function createChat = any>( let messages = $state>>(options.initialMessages || []) let isLoading = $state(false) let error = $state(undefined) + let status = $state('ready') // Create ChatClient instance const client = new ChatClient({ @@ -53,8 +54,14 @@ export function createChat = any>( body: options.body, onResponse: options.onResponse, onChunk: options.onChunk, - onFinish: options.onFinish, - onError: options.onError, + onFinish: (message) => { + status = 'ready' + options.onFinish?.(message) + }, + onError: (err) => { + status = 'error' + options.onError?.(err) + }, tools: options.tools, streamProcessor: options.streamProcessor, onMessagesChange: (newMessages: Array>) => { @@ -62,6 +69,14 @@ export function createChat = any>( }, onLoadingChange: (newIsLoading: boolean) => { isLoading = newIsLoading + if (newIsLoading) { + status = 'submitted' + } else { + status = error ? 'error' : 'ready' + } + }, + onStreamStart: () => { + status = 'streaming' }, onErrorChange: (newError: Error | undefined) => { error = newError @@ -127,6 +142,9 @@ export function createChat = any>( get error() { return error }, + get status() { + return status + }, sendMessage, append, reload, diff --git a/packages/typescript/ai-svelte/src/types.ts b/packages/typescript/ai-svelte/src/types.ts index 5d07e34f..44efe88d 100644 --- a/packages/typescript/ai-svelte/src/types.ts +++ b/packages/typescript/ai-svelte/src/types.ts @@ -1,12 +1,13 @@ import type { AnyClientTool, ModelMessage } from '@tanstack/ai' import type { ChatClientOptions, + ChatClientState, ChatRequestBody, - UIMessage, + UIMessage } from '@tanstack/ai-client' // Re-export types from ai-client -export type { UIMessage, ChatRequestBody } +export type { ChatRequestBody, UIMessage } /** * Options for the createChat function. @@ -96,6 +97,11 @@ export interface CreateChatReturn< * Clear all messages */ clear: () => void + + /** + * Current generation status (reactive getter) + */ + readonly status: ChatClientState } // Note: createChatClientOptions and InferChatMessages are now in @tanstack/ai-client diff --git a/packages/typescript/ai-svelte/tests/use-chat.test.ts b/packages/typescript/ai-svelte/tests/use-chat.test.ts index 8431e2de..ff78075d 100644 --- a/packages/typescript/ai-svelte/tests/use-chat.test.ts +++ b/packages/typescript/ai-svelte/tests/use-chat.test.ts @@ -1,4 +1,4 @@ -import { describe, it, expect, beforeEach, vi } from 'vitest' +import { beforeEach, describe, expect, it, vi } from 'vitest' import { createChat } from '../src/create-chat.svelte' import { createMockConnectionAdapter } from './test-utils' @@ -146,4 +146,16 @@ describe('createChat', () => { expect(chat.error).toBeUndefined() expect(chat.error).toBeUndefined() }) + + it('should expose reactive status property', () => { + const mockConnection = createMockConnectionAdapter({ chunks: [] }) + + const chat = createChat({ + connection: mockConnection, + }) + + // Access status multiple times + expect(chat.status).toBe('ready') + expect(chat.status).toBe('ready') + }) }) diff --git a/packages/typescript/ai-vue/src/types.ts b/packages/typescript/ai-vue/src/types.ts index d1b7c5cf..f6467c88 100644 --- a/packages/typescript/ai-vue/src/types.ts +++ b/packages/typescript/ai-vue/src/types.ts @@ -1,13 +1,14 @@ -import type { DeepReadonly, ShallowRef } from 'vue' import type { AnyClientTool, ModelMessage } from '@tanstack/ai' import type { ChatClientOptions, + ChatClientState, ChatRequestBody, - UIMessage, + UIMessage } from '@tanstack/ai-client' +import type { DeepReadonly, ShallowRef } from 'vue' // Re-export types from ai-client -export type { UIMessage, ChatRequestBody } +export type { ChatRequestBody, UIMessage } /** * Options for the useChat composable. @@ -96,6 +97,11 @@ export interface UseChatReturn< * Clear all messages */ clear: () => void + + /** + * Current generation status + */ + status: DeepReadonly> } // Note: createChatClientOptions and InferChatMessages are now in @tanstack/ai-client diff --git a/packages/typescript/ai-vue/src/use-chat.ts b/packages/typescript/ai-vue/src/use-chat.ts index f190d0ee..c0690d59 100644 --- a/packages/typescript/ai-vue/src/use-chat.ts +++ b/packages/typescript/ai-vue/src/use-chat.ts @@ -1,6 +1,6 @@ -import { ChatClient } from '@tanstack/ai-client' -import { onScopeDispose, readonly, shallowRef, useId } from 'vue' import type { AnyClientTool, ModelMessage } from '@tanstack/ai' +import { ChatClient, ChatClientState } from '@tanstack/ai-client' +import { onScopeDispose, readonly, shallowRef, useId } from 'vue' import type { UIMessage, UseChatOptions, UseChatReturn } from './types' export function useChat = any>( @@ -14,6 +14,7 @@ export function useChat = any>( ) const isLoading = shallowRef(false) const error = shallowRef(undefined) + const status = shallowRef('ready') // Create ChatClient instance with callbacks to sync state const client = new ChatClient({ @@ -23,8 +24,14 @@ export function useChat = any>( body: options.body, onResponse: options.onResponse, onChunk: options.onChunk, - onFinish: options.onFinish, - onError: options.onError, + onFinish: (message) => { + status.value = 'ready' + options.onFinish?.(message) + }, + onError: (err) => { + status.value = 'error' + options.onError?.(err) + }, tools: options.tools, streamProcessor: options.streamProcessor, onMessagesChange: (newMessages: Array>) => { @@ -32,6 +39,14 @@ export function useChat = any>( }, onLoadingChange: (newIsLoading: boolean) => { isLoading.value = newIsLoading + if (newIsLoading) { + status.value = 'submitted' + } else { + status.value = error.value ? 'error' : 'ready' + } + }, + onStreamStart: () => { + status.value = 'streaming' }, onErrorChange: (newError: Error | undefined) => { error.value = newError @@ -97,6 +112,7 @@ export function useChat = any>( stop, isLoading: readonly(isLoading), error: readonly(error), + status: readonly(status), setMessages: setMessagesManually, clear, addToolResult, diff --git a/packages/typescript/ai-vue/tests/test-utils.ts b/packages/typescript/ai-vue/tests/test-utils.ts index d2d3ef26..f99aaba3 100644 --- a/packages/typescript/ai-vue/tests/test-utils.ts +++ b/packages/typescript/ai-vue/tests/test-utils.ts @@ -1,14 +1,14 @@ -import { defineComponent } from 'vue' +import type { UIMessage } from '@tanstack/ai-client' import { mount } from '@vue/test-utils' -import { useChat } from '../src/use-chat' +import { defineComponent } from 'vue' import type { UseChatOptions } from '../src/types' -import type { UIMessage } from '@tanstack/ai-client' +import { useChat } from '../src/use-chat' // Re-export test utilities from ai-client export { createMockConnectionAdapter, createTextChunks, - createToolCallChunks, + createToolCallChunks } from '../../ai-client/tests/test-utils' /** @@ -42,6 +42,7 @@ export function renderUseChat(options?: UseChatOptions) { messages: hook.messages as Array, isLoading: hook.isLoading, error: hook.error, + status: hook.status, sendMessage: hook.sendMessage, append: hook.append, reload: hook.reload, diff --git a/packages/typescript/ai-vue/tests/use-chat.test.ts b/packages/typescript/ai-vue/tests/use-chat.test.ts index 52a77555..2c950ae7 100644 --- a/packages/typescript/ai-vue/tests/use-chat.test.ts +++ b/packages/typescript/ai-vue/tests/use-chat.test.ts @@ -1,13 +1,13 @@ -import { describe, expect, it, vi } from 'vitest' +import type { ModelMessage } from '@tanstack/ai' import { flushPromises } from '@vue/test-utils' +import { describe, expect, it, vi } from 'vitest' +import type { UIMessage } from '../src/types' import { createMockConnectionAdapter, createTextChunks, createToolCallChunks, renderUseChat, } from './test-utils' -import type { UIMessage } from '../src/types' -import type { ModelMessage } from '@tanstack/ai' describe('useChat', () => { describe('initialization', () => { @@ -459,6 +459,51 @@ describe('useChat', () => { }) }) + describe('status', () => { + it('should have initial status of ready', () => { + const adapter = createMockConnectionAdapter() + const { result } = renderUseChat({ connection: adapter }) + expect(result.current.status).toBe('ready') + }) + + it('should transition through states during generation', async () => { + const chunks = createTextChunks('Response') + const adapter = createMockConnectionAdapter({ + chunks, + chunkDelay: 50, + }) + const { result } = renderUseChat({ connection: adapter }) + + const sendPromise = result.current.sendMessage('Test') + + // Should leave ready state + await flushPromises() + expect(result.current.status).not.toBe('ready') + + // Should be submitted or streaming + expect(['submitted', 'streaming']).toContain(result.current.status) + + // Should return to ready eventually + await sendPromise + await flushPromises() + expect(result.current.status).toBe('ready') + }) + + it('should transition to error on error', async () => { + const error = new Error('Network error') + const adapter = createMockConnectionAdapter({ + shouldError: true, + error, + }) + const { result } = renderUseChat({ connection: adapter }) + + await result.current.sendMessage('Test') + await flushPromises() + + expect(result.current.status).toBe('error') + }) + }) + describe('clear', () => { it('should clear all messages', async () => { const chunks = createTextChunks('Response') From 9e51f02b18ee09e813c9c5f5e9764568417d2152 Mon Sep 17 00:00:00 2001 From: Nikas Belogolov Date: Sun, 25 Jan 2026 12:14:32 +0200 Subject: [PATCH 02/11] chore: add changeset for status feature --- .changeset/shy-ravens-sink.md | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 .changeset/shy-ravens-sink.md diff --git a/.changeset/shy-ravens-sink.md b/.changeset/shy-ravens-sink.md new file mode 100644 index 00000000..701eef90 --- /dev/null +++ b/.changeset/shy-ravens-sink.md @@ -0,0 +1,10 @@ +--- +'@tanstack/ai-client': minor +'@tanstack/ai-preact': minor +'@tanstack/ai-svelte': minor +'@tanstack/ai-react': minor +'@tanstack/ai-solid': minor +'@tanstack/ai-vue': minor +--- + +Added status property to useChat to track the generation lifecycle (ready, submitted, streaming, error) From b1091e3d3a996ab7e658bd919cb9666213eaa959 Mon Sep 17 00:00:00 2001 From: Nikas Belogolov <30692665+nikas-belogolov@users.noreply.github.com> Date: Sun, 25 Jan 2026 16:23:29 +0200 Subject: [PATCH 03/11] Minor: JSDoc comment formatting inconsistency Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- packages/typescript/ai-client/src/types.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/typescript/ai-client/src/types.ts b/packages/typescript/ai-client/src/types.ts index 1d0ffc60..072fcdf9 100644 --- a/packages/typescript/ai-client/src/types.ts +++ b/packages/typescript/ai-client/src/types.ts @@ -202,7 +202,7 @@ export interface ChatClientOptions< /** * Callback when stream starts - */ + */ onStreamStart?: () => void /** From f5db6542d986d29919dfdcea47921296e5747f66 Mon Sep 17 00:00:00 2001 From: Nikas Belogolov Date: Sun, 25 Jan 2026 16:39:33 +0200 Subject: [PATCH 04/11] Fixed discrepancies --- packages/typescript/ai-preact/src/types.ts | 2 +- packages/typescript/ai-preact/src/use-chat.ts | 2 +- packages/typescript/ai-react/src/use-chat.ts | 2 +- packages/typescript/ai-solid/src/types.ts | 2 +- packages/typescript/ai-solid/src/use-chat.ts | 2 +- packages/typescript/ai-svelte/src/create-chat.svelte.ts | 2 +- packages/typescript/ai-svelte/src/types.ts | 2 +- packages/typescript/ai-vue/src/types.ts | 2 +- packages/typescript/ai-vue/src/use-chat.ts | 2 +- 9 files changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/typescript/ai-preact/src/types.ts b/packages/typescript/ai-preact/src/types.ts index 37fad0a8..d5eca7fb 100644 --- a/packages/typescript/ai-preact/src/types.ts +++ b/packages/typescript/ai-preact/src/types.ts @@ -27,7 +27,7 @@ export type { ChatRequestBody, UIMessage } export type UseChatOptions = any> = Omit< ChatClientOptions, - 'onMessagesChange' | 'onLoadingChange' | 'onErrorChange' + 'onMessagesChange' | 'onLoadingChange' | 'onErrorChange' | 'onStreamStart' > export interface UseChatReturn< diff --git a/packages/typescript/ai-preact/src/use-chat.ts b/packages/typescript/ai-preact/src/use-chat.ts index 0fffea40..23b08952 100644 --- a/packages/typescript/ai-preact/src/use-chat.ts +++ b/packages/typescript/ai-preact/src/use-chat.ts @@ -1,5 +1,5 @@ import type { AnyClientTool, ModelMessage } from '@tanstack/ai' -import { ChatClient, ChatClientState } from '@tanstack/ai-client' +import { ChatClient, type ChatClientState } from '@tanstack/ai-client' import { useCallback, useEffect, diff --git a/packages/typescript/ai-react/src/use-chat.ts b/packages/typescript/ai-react/src/use-chat.ts index 4cb8f863..b7d54a94 100644 --- a/packages/typescript/ai-react/src/use-chat.ts +++ b/packages/typescript/ai-react/src/use-chat.ts @@ -1,5 +1,5 @@ import type { AnyClientTool, ModelMessage } from '@tanstack/ai' -import { ChatClient, ChatClientState } from '@tanstack/ai-client' +import { ChatClient, type ChatClientState } from '@tanstack/ai-client' import { useCallback, useEffect, useId, useMemo, useRef, useState } from 'react' import type { UIMessage, UseChatOptions, UseChatReturn } from './types' diff --git a/packages/typescript/ai-solid/src/types.ts b/packages/typescript/ai-solid/src/types.ts index 0ded94ea..e4bd32c5 100644 --- a/packages/typescript/ai-solid/src/types.ts +++ b/packages/typescript/ai-solid/src/types.ts @@ -28,7 +28,7 @@ export type { ChatRequestBody, UIMessage } export type UseChatOptions = any> = Omit< ChatClientOptions, - 'onMessagesChange' | 'onLoadingChange' | 'onErrorChange' + 'onMessagesChange' | 'onLoadingChange' | 'onErrorChange' | 'onStreamStart' > export interface UseChatReturn< diff --git a/packages/typescript/ai-solid/src/use-chat.ts b/packages/typescript/ai-solid/src/use-chat.ts index f79cccd8..d83aa8d9 100644 --- a/packages/typescript/ai-solid/src/use-chat.ts +++ b/packages/typescript/ai-solid/src/use-chat.ts @@ -6,7 +6,7 @@ import { } from 'solid-js' import type { AnyClientTool, ModelMessage } from '@tanstack/ai' -import { ChatClient, ChatClientState } from '@tanstack/ai-client' +import { ChatClient, type ChatClientState } from '@tanstack/ai-client' import type { UIMessage, UseChatOptions, UseChatReturn } from './types' export function useChat = any>( diff --git a/packages/typescript/ai-svelte/src/create-chat.svelte.ts b/packages/typescript/ai-svelte/src/create-chat.svelte.ts index 888273c0..cd9f5db9 100644 --- a/packages/typescript/ai-svelte/src/create-chat.svelte.ts +++ b/packages/typescript/ai-svelte/src/create-chat.svelte.ts @@ -1,5 +1,5 @@ import type { AnyClientTool, ModelMessage } from '@tanstack/ai' -import { ChatClient, ChatClientState } from '@tanstack/ai-client' +import { ChatClient, type ChatClientState } from '@tanstack/ai-client' import type { CreateChatOptions, CreateChatReturn, UIMessage } from './types' /** diff --git a/packages/typescript/ai-svelte/src/types.ts b/packages/typescript/ai-svelte/src/types.ts index 44efe88d..4cc593ba 100644 --- a/packages/typescript/ai-svelte/src/types.ts +++ b/packages/typescript/ai-svelte/src/types.ts @@ -28,7 +28,7 @@ export type CreateChatOptions< TTools extends ReadonlyArray = any, > = Omit< ChatClientOptions, - 'onMessagesChange' | 'onLoadingChange' | 'onErrorChange' + 'onMessagesChange' | 'onLoadingChange' | 'onErrorChange' | 'onStreamStart' > export interface CreateChatReturn< diff --git a/packages/typescript/ai-vue/src/types.ts b/packages/typescript/ai-vue/src/types.ts index f6467c88..5e9dc590 100644 --- a/packages/typescript/ai-vue/src/types.ts +++ b/packages/typescript/ai-vue/src/types.ts @@ -28,7 +28,7 @@ export type { ChatRequestBody, UIMessage } export type UseChatOptions = any> = Omit< ChatClientOptions, - 'onMessagesChange' | 'onLoadingChange' | 'onErrorChange' + 'onMessagesChange' | 'onLoadingChange' | 'onErrorChange' | 'onStreamStart' > export interface UseChatReturn< diff --git a/packages/typescript/ai-vue/src/use-chat.ts b/packages/typescript/ai-vue/src/use-chat.ts index c0690d59..d0ce0261 100644 --- a/packages/typescript/ai-vue/src/use-chat.ts +++ b/packages/typescript/ai-vue/src/use-chat.ts @@ -1,5 +1,5 @@ import type { AnyClientTool, ModelMessage } from '@tanstack/ai' -import { ChatClient, ChatClientState } from '@tanstack/ai-client' +import { ChatClient, type ChatClientState } from '@tanstack/ai-client' import { onScopeDispose, readonly, shallowRef, useId } from 'vue' import type { UIMessage, UseChatOptions, UseChatReturn } from './types' From 3ca6ca77541bb0c99ec5b5def618f1d823957e5a Mon Sep 17 00:00:00 2001 From: Nikas Belogolov <30692665+nikas-belogolov@users.noreply.github.com> Date: Sun, 25 Jan 2026 19:31:02 +0200 Subject: [PATCH 05/11] Use top-level type-only import for ChatClientState Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- packages/typescript/ai-vue/src/use-chat.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/typescript/ai-vue/src/use-chat.ts b/packages/typescript/ai-vue/src/use-chat.ts index d0ce0261..8e998bdb 100644 --- a/packages/typescript/ai-vue/src/use-chat.ts +++ b/packages/typescript/ai-vue/src/use-chat.ts @@ -1,5 +1,6 @@ import type { AnyClientTool, ModelMessage } from '@tanstack/ai' -import { ChatClient, type ChatClientState } from '@tanstack/ai-client' +import { ChatClient } from '@tanstack/ai-client' +import type { ChatClientState } from '@tanstack/ai-client' import { onScopeDispose, readonly, shallowRef, useId } from 'vue' import type { UIMessage, UseChatOptions, UseChatReturn } from './types' From 0b7e438611cc0394d7eee127d2ebc74ba38c258f Mon Sep 17 00:00:00 2001 From: Nikas Belogolov <30692665+nikas-belogolov@users.noreply.github.com> Date: Sun, 25 Jan 2026 19:31:24 +0200 Subject: [PATCH 06/11] Use top-level type-only import for ChatClientState Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- packages/typescript/ai-react/src/use-chat.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/typescript/ai-react/src/use-chat.ts b/packages/typescript/ai-react/src/use-chat.ts index b7d54a94..fbb626f9 100644 --- a/packages/typescript/ai-react/src/use-chat.ts +++ b/packages/typescript/ai-react/src/use-chat.ts @@ -1,5 +1,6 @@ import type { AnyClientTool, ModelMessage } from '@tanstack/ai' -import { ChatClient, type ChatClientState } from '@tanstack/ai-client' +import { ChatClient } from '@tanstack/ai-client' +import type { ChatClientState } from '@tanstack/ai-client' import { useCallback, useEffect, useId, useMemo, useRef, useState } from 'react' import type { UIMessage, UseChatOptions, UseChatReturn } from './types' From 2a94d48beab852851f276545e845173d1eb93c9f Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Mon, 26 Jan 2026 10:36:04 +0000 Subject: [PATCH 07/11] ci: apply automated fixes --- packages/typescript/ai-client/src/types.ts | 6 +----- packages/typescript/ai-react/src/use-chat.ts | 3 +-- packages/typescript/ai-react/tests/use-chat.test.ts | 4 ++-- packages/typescript/ai-solid/src/types.ts | 2 +- packages/typescript/ai-solid/tests/test-utils.ts | 2 +- packages/typescript/ai-solid/tests/use-chat.test.ts | 4 ++-- packages/typescript/ai-svelte/src/types.ts | 2 +- packages/typescript/ai-vue/src/types.ts | 2 +- packages/typescript/ai-vue/tests/test-utils.ts | 2 +- 9 files changed, 11 insertions(+), 16 deletions(-) diff --git a/packages/typescript/ai-client/src/types.ts b/packages/typescript/ai-client/src/types.ts index 072fcdf9..c1d764af 100644 --- a/packages/typescript/ai-client/src/types.ts +++ b/packages/typescript/ai-client/src/types.ts @@ -29,11 +29,7 @@ export type ToolResultState = /** * ChatClient state - track the lifecycle of a chat */ -export type ChatClientState = - | 'ready' - | 'submitted' - | 'streaming' - | 'error' +export type ChatClientState = 'ready' | 'submitted' | 'streaming' | 'error' /** * Message parts - building blocks of UIMessage diff --git a/packages/typescript/ai-react/src/use-chat.ts b/packages/typescript/ai-react/src/use-chat.ts index fbb626f9..52edfd13 100644 --- a/packages/typescript/ai-react/src/use-chat.ts +++ b/packages/typescript/ai-react/src/use-chat.ts @@ -53,7 +53,7 @@ export function useChat = any>( onResponse: optionsRef.current.onResponse, onChunk: optionsRef.current.onChunk, onStreamStart: () => { - setStatus("streaming") + setStatus('streaming') }, onFinish: (message: UIMessage) => { setStatus('ready') @@ -82,7 +82,6 @@ export function useChat = any>( }) }, [clientId]) - // Sync initial messages on mount only // Note: initialMessages are passed to ChatClient constructor, but we also // set them here to ensure React state is in sync diff --git a/packages/typescript/ai-react/tests/use-chat.test.ts b/packages/typescript/ai-react/tests/use-chat.test.ts index 2cea5075..e571e186 100644 --- a/packages/typescript/ai-react/tests/use-chat.test.ts +++ b/packages/typescript/ai-react/tests/use-chat.test.ts @@ -378,9 +378,9 @@ describe('useChat', () => { ) const firstContent = firstAssistantMessage?.parts.find((p) => p.type === 'text')?.type === - 'text' + 'text' ? (firstAssistantMessage.parts.find((p) => p.type === 'text') as any) - .content + .content : '' // Reload with new adapter diff --git a/packages/typescript/ai-solid/src/types.ts b/packages/typescript/ai-solid/src/types.ts index e4bd32c5..fc6925eb 100644 --- a/packages/typescript/ai-solid/src/types.ts +++ b/packages/typescript/ai-solid/src/types.ts @@ -3,7 +3,7 @@ import type { ChatClientOptions, ChatClientState, ChatRequestBody, - UIMessage + UIMessage, } from '@tanstack/ai-client' import type { Accessor } from 'solid-js' diff --git a/packages/typescript/ai-solid/tests/test-utils.ts b/packages/typescript/ai-solid/tests/test-utils.ts index 791dcd22..c6083516 100644 --- a/packages/typescript/ai-solid/tests/test-utils.ts +++ b/packages/typescript/ai-solid/tests/test-utils.ts @@ -3,7 +3,7 @@ export { createMockConnectionAdapter, createTextChunks, createToolCallChunks, - type MockConnectionAdapterOptions + type MockConnectionAdapterOptions, } from '../../ai-client/tests/test-utils' import { renderHook } from '@solidjs/testing-library' diff --git a/packages/typescript/ai-solid/tests/use-chat.test.ts b/packages/typescript/ai-solid/tests/use-chat.test.ts index 4d1180e8..0d299219 100644 --- a/packages/typescript/ai-solid/tests/use-chat.test.ts +++ b/packages/typescript/ai-solid/tests/use-chat.test.ts @@ -378,9 +378,9 @@ describe('useChat', () => { ) const firstContent = firstAssistantMessage?.parts.find((p) => p.type === 'text')?.type === - 'text' + 'text' ? (firstAssistantMessage.parts.find((p) => p.type === 'text') as any) - .content + .content : '' // Reload with new adapter diff --git a/packages/typescript/ai-svelte/src/types.ts b/packages/typescript/ai-svelte/src/types.ts index 4cc593ba..341860d3 100644 --- a/packages/typescript/ai-svelte/src/types.ts +++ b/packages/typescript/ai-svelte/src/types.ts @@ -3,7 +3,7 @@ import type { ChatClientOptions, ChatClientState, ChatRequestBody, - UIMessage + UIMessage, } from '@tanstack/ai-client' // Re-export types from ai-client diff --git a/packages/typescript/ai-vue/src/types.ts b/packages/typescript/ai-vue/src/types.ts index 5e9dc590..399e942d 100644 --- a/packages/typescript/ai-vue/src/types.ts +++ b/packages/typescript/ai-vue/src/types.ts @@ -3,7 +3,7 @@ import type { ChatClientOptions, ChatClientState, ChatRequestBody, - UIMessage + UIMessage, } from '@tanstack/ai-client' import type { DeepReadonly, ShallowRef } from 'vue' diff --git a/packages/typescript/ai-vue/tests/test-utils.ts b/packages/typescript/ai-vue/tests/test-utils.ts index f99aaba3..c6994c51 100644 --- a/packages/typescript/ai-vue/tests/test-utils.ts +++ b/packages/typescript/ai-vue/tests/test-utils.ts @@ -8,7 +8,7 @@ import { useChat } from '../src/use-chat' export { createMockConnectionAdapter, createTextChunks, - createToolCallChunks + createToolCallChunks, } from '../../ai-client/tests/test-utils' /** From c8ceb5f6e89f59472dc85c517e8ffb3f337994a5 Mon Sep 17 00:00:00 2001 From: Nikas Belogolov Date: Tue, 27 Jan 2026 00:11:01 +0200 Subject: [PATCH 08/11] collapsed status logic to ChatClient and update all framework hooks --- .../typescript/ai-client/src/chat-client.ts | 47 +++++++++---- packages/typescript/ai-client/src/types.ts | 4 +- .../ai-client/tests/chat-client.test.ts | 69 ++++++++++++++++++- packages/typescript/ai-preact/src/types.ts | 6 +- packages/typescript/ai-preact/src/use-chat.ts | 11 +-- .../ai-preact/tests/use-chat.test.ts | 30 ++++++++ packages/typescript/ai-react/src/types.ts | 8 ++- packages/typescript/ai-react/src/use-chat.ts | 14 ++-- .../ai-react/tests/use-chat.test.ts | 23 +++++++ packages/typescript/ai-solid/src/types.ts | 6 +- packages/typescript/ai-solid/src/use-chat.ts | 11 +-- .../ai-solid/tests/use-chat.test.ts | 27 +++++++- .../ai-svelte/src/create-chat.svelte.ts | 11 +-- packages/typescript/ai-svelte/src/types.ts | 6 +- .../ai-svelte/tests/use-chat.test.ts | 60 +++++++++++++++- packages/typescript/ai-vue/src/types.ts | 3 +- packages/typescript/ai-vue/src/use-chat.ts | 13 +--- .../typescript/ai-vue/tests/use-chat.test.ts | 22 ++++++ 18 files changed, 299 insertions(+), 72 deletions(-) diff --git a/packages/typescript/ai-client/src/chat-client.ts b/packages/typescript/ai-client/src/chat-client.ts index b35a282a..1985def5 100644 --- a/packages/typescript/ai-client/src/chat-client.ts +++ b/packages/typescript/ai-client/src/chat-client.ts @@ -1,18 +1,19 @@ +import type { AnyClientTool, ModelMessage, StreamChunk } from '@tanstack/ai' import { StreamProcessor, generateMessageId, normalizeToUIMessage, } from '@tanstack/ai' +import type { ConnectionAdapter } from './connection-adapters' +import type { ChatClientEventEmitter } from './events' import { DefaultChatClientEventEmitter } from './events' import type { ChatClientOptions, + ChatClientState, MessagePart, ToolCallPart, UIMessage, } from './types' -import type { AnyClientTool, ModelMessage, StreamChunk } from '@tanstack/ai' -import type { ConnectionAdapter } from './connection-adapters' -import type { ChatClientEventEmitter } from './events' export class ChatClient { private processor: StreamProcessor @@ -21,6 +22,7 @@ export class ChatClient { private body: Record = {} private isLoading = false private error: Error | undefined = undefined + private status: ChatClientState = 'ready' private abortController: AbortController | null = null private events: ChatClientEventEmitter private clientToolsRef: { current: Map } @@ -36,8 +38,8 @@ export class ChatClient { onError: (error: Error) => void onMessagesChange: (messages: Array) => void onLoadingChange: (isLoading: boolean) => void - onStreamStart: () => void onErrorChange: (error: Error | undefined) => void + onStatusChange: (status: ChatClientState) => void } } @@ -57,14 +59,14 @@ export class ChatClient { this.callbacksRef = { current: { - onResponse: options.onResponse || (() => {}), - onChunk: options.onChunk || (() => {}), - onFinish: options.onFinish || (() => {}), - onError: options.onError || (() => {}), - onMessagesChange: options.onMessagesChange || (() => {}), - onLoadingChange: options.onLoadingChange || (() => {}), - onStreamStart: options.onStreamStart || (() => {}), - onErrorChange: options.onErrorChange || (() => {}), + onResponse: options.onResponse || (() => { }), + onChunk: options.onChunk || (() => { }), + onFinish: options.onFinish || (() => { }), + onError: options.onError || (() => { }), + onMessagesChange: options.onMessagesChange || (() => { }), + onLoadingChange: options.onLoadingChange || (() => { }), + onErrorChange: options.onErrorChange || (() => { }), + onStatusChange: options.onStatusChange || (() => { }), }, } @@ -77,13 +79,15 @@ export class ChatClient { this.callbacksRef.current.onMessagesChange(messages) }, onStreamStart: () => { - this.callbacksRef.current.onStreamStart() + this.setStatus('streaming') }, onStreamEnd: (message: UIMessage) => { this.callbacksRef.current.onFinish(message) + this.setStatus('ready') }, onError: (error: Error) => { this.setError(error) + this.setStatus('error') this.callbacksRef.current.onError(error) }, onTextUpdate: (messageId: string, content: string) => { @@ -189,10 +193,18 @@ export class ChatClient { this.events.loadingChanged(isLoading) } + private setStatus(status: ChatClientState): void { + this.status = status + this.callbacksRef.current.onStatusChange(status) + } + private setError(error: Error | undefined): void { this.error = error this.callbacksRef.current.onErrorChange(error) this.events.errorChanged(error?.message || null) + if (error) { + this.setStatus('error') + } } /** @@ -297,6 +309,7 @@ export class ChatClient { */ private async streamResponse(): Promise { this.setIsLoading(true) + this.setStatus('submitted') this.setError(undefined) this.abortController = new AbortController() @@ -370,6 +383,7 @@ export class ChatClient { this.abortController = null } this.setIsLoading(false) + this.setStatus('ready') this.events.stopped() } @@ -504,6 +518,13 @@ export class ChatClient { return this.isLoading } + /** + * Get current status + */ + getStatus(): ChatClientState { + return this.status + } + /** * Get current error */ diff --git a/packages/typescript/ai-client/src/types.ts b/packages/typescript/ai-client/src/types.ts index c1d764af..8aff9e4d 100644 --- a/packages/typescript/ai-client/src/types.ts +++ b/packages/typescript/ai-client/src/types.ts @@ -197,9 +197,9 @@ export interface ChatClientOptions< onErrorChange?: (error: Error | undefined) => void /** - * Callback when stream starts + * Callback when chat status changes */ - onStreamStart?: () => void + onStatusChange?: (status: ChatClientState) => void /** * Client-side tools with execution logic diff --git a/packages/typescript/ai-client/tests/chat-client.test.ts b/packages/typescript/ai-client/tests/chat-client.test.ts index 34476519..55f55643 100644 --- a/packages/typescript/ai-client/tests/chat-client.test.ts +++ b/packages/typescript/ai-client/tests/chat-client.test.ts @@ -1,12 +1,12 @@ import { describe, expect, it, vi } from 'vitest' import { ChatClient } from '../src/chat-client' +import type { UIMessage } from '../src/types' import { createMockConnectionAdapter, createTextChunks, createThinkingChunks, createToolCallChunks, } from './test-utils' -import type { UIMessage } from '../src/types' describe('ChatClient', () => { describe('constructor', () => { @@ -391,6 +391,73 @@ describe('ChatClient', () => { }) }) + describe('status', () => { + it('should have initial status of ready', () => { + const adapter = createMockConnectionAdapter() + const client = new ChatClient({ connection: adapter }) + expect(client.getStatus()).toBe('ready') + }) + + it('should transition through states during generation', async () => { + const chunks = createTextChunks('Response') + const adapter = createMockConnectionAdapter({ + chunks, + chunkDelay: 20, + }) + const statuses: Array = [] + const client = new ChatClient({ + connection: adapter, + onStatusChange: (s) => statuses.push(s), + }) + + const promise = client.sendMessage('Test') + + // Should leave ready state + expect(client.getStatus()).not.toBe('ready') + + // Should be submitted or streaming + expect(['submitted', 'streaming']).toContain(client.getStatus()) + + await promise + + expect(statuses).toContain('submitted') + expect(statuses).toContain('streaming') + expect(statuses[statuses.length - 1]).toBe('ready') + }) + + it('should transition to error on error', async () => { + const adapter = createMockConnectionAdapter({ + shouldError: true, + error: new Error('AI Error'), + }) + const client = new ChatClient({ connection: adapter }) + + await client.sendMessage('Test') + + expect(client.getStatus()).toBe('error') + }) + + it('should transition to ready after stop', async () => { + const chunks = createTextChunks('Response') + const adapter = createMockConnectionAdapter({ + chunks, + chunkDelay: 50, + }) + const client = new ChatClient({ connection: adapter }) + + const promise = client.sendMessage('Test') + + // Wait for it to start + await new Promise((resolve) => setTimeout(resolve, 10)) + + client.stop() + + expect(client.getStatus()).toBe('ready') + + await promise + }) + }) + describe('tool calls', () => { it('should handle tool calls from stream', async () => { const chunks = createToolCallChunks([ diff --git a/packages/typescript/ai-preact/src/types.ts b/packages/typescript/ai-preact/src/types.ts index d5eca7fb..dcd7bbac 100644 --- a/packages/typescript/ai-preact/src/types.ts +++ b/packages/typescript/ai-preact/src/types.ts @@ -17,6 +17,7 @@ export type { ChatRequestBody, UIMessage } * - `onMessagesChange` - Managed by Preact state (exposed as `messages`) * - `onLoadingChange` - Managed by Preact state (exposed as `isLoading`) * - `onErrorChange` - Managed by Preact state (exposed as `error`) + * - `onStatusChange` - Managed by Preact state (exposed as `status`) * * All other callbacks (onResponse, onChunk, onFinish, onError) are * passed through to the underlying ChatClient and can be used for side effects. @@ -27,7 +28,10 @@ export type { ChatRequestBody, UIMessage } export type UseChatOptions = any> = Omit< ChatClientOptions, - 'onMessagesChange' | 'onLoadingChange' | 'onErrorChange' | 'onStreamStart' + | 'onMessagesChange' + | 'onLoadingChange' + | 'onErrorChange' + | 'onStatusChange' > export interface UseChatReturn< diff --git a/packages/typescript/ai-preact/src/use-chat.ts b/packages/typescript/ai-preact/src/use-chat.ts index 23b08952..28b6a6ed 100644 --- a/packages/typescript/ai-preact/src/use-chat.ts +++ b/packages/typescript/ai-preact/src/use-chat.ts @@ -53,11 +53,9 @@ export function useChat = any>( onResponse: optionsRef.current.onResponse, onChunk: optionsRef.current.onChunk, onFinish: (message) => { - setStatus('ready') optionsRef.current.onFinish?.(message) }, onError: (err) => { - setStatus('error') optionsRef.current.onError?.(err) }, tools: optionsRef.current.tools, @@ -67,14 +65,9 @@ export function useChat = any>( }, onLoadingChange: (newIsLoading: boolean) => { setIsLoading(newIsLoading) - if (newIsLoading) { - setStatus('submitted') - } else { - setStatus((prev) => (prev === 'error' ? 'error' : 'ready')) - } }, - onStreamStart: () => { - setStatus('streaming') + onStatusChange: (newStatus: ChatClientState) => { + setStatus(newStatus) }, onErrorChange: (newError: Error | undefined) => { setError(newError) diff --git a/packages/typescript/ai-preact/tests/use-chat.test.ts b/packages/typescript/ai-preact/tests/use-chat.test.ts index 3b35dbca..7dfc815d 100644 --- a/packages/typescript/ai-preact/tests/use-chat.test.ts +++ b/packages/typescript/ai-preact/tests/use-chat.test.ts @@ -613,6 +613,36 @@ describe('useChat', () => { expect(result.current.status).toBe('error') }) }) + + it('should transition to ready after stop', async () => { + const chunks = createTextChunks('Response') + const adapter = createMockConnectionAdapter({ + chunks, + chunkDelay: 50, + }) + const { result } = renderUseChat({ connection: adapter }) + + let sendPromise: Promise + act(() => { + sendPromise = result.current.sendMessage('Test') + }) + + await waitFor(() => { + expect(result.current.status).not.toBe('ready') + }) + + act(() => { + result.current.stop() + }) + + await waitFor(() => { + expect(result.current.status).toBe('ready') + }) + + await act(async () => { + await sendPromise!.catch(() => { }) + }) + }) }) describe('clear', () => { diff --git a/packages/typescript/ai-react/src/types.ts b/packages/typescript/ai-react/src/types.ts index 33257bb3..c8088b92 100644 --- a/packages/typescript/ai-react/src/types.ts +++ b/packages/typescript/ai-react/src/types.ts @@ -7,7 +7,7 @@ import type { } from '@tanstack/ai-client' // Re-export types from ai-client -export type { UIMessage, ChatRequestBody } +export type { ChatRequestBody, UIMessage } /** * Options for the useChat hook. @@ -17,6 +17,7 @@ export type { UIMessage, ChatRequestBody } * - `onMessagesChange` - Managed by React state (exposed as `messages`) * - `onLoadingChange` - Managed by React state (exposed as `isLoading`) * - `onErrorChange` - Managed by React state (exposed as `error`) + * - `onStatusChange` - Managed by React state (exposed as `status`) * * All other callbacks (onResponse, onChunk, onFinish, onError) are * passed through to the underlying ChatClient and can be used for side effects. @@ -27,7 +28,10 @@ export type { UIMessage, ChatRequestBody } export type UseChatOptions = any> = Omit< ChatClientOptions, - 'onMessagesChange' | 'onLoadingChange' | 'onErrorChange' | 'onStreamStart' + | 'onMessagesChange' + | 'onLoadingChange' + | 'onErrorChange' + | 'onStatusChange' > export interface UseChatReturn< diff --git a/packages/typescript/ai-react/src/use-chat.ts b/packages/typescript/ai-react/src/use-chat.ts index 52edfd13..123c4eb6 100644 --- a/packages/typescript/ai-react/src/use-chat.ts +++ b/packages/typescript/ai-react/src/use-chat.ts @@ -52,15 +52,10 @@ export function useChat = any>( body: optionsRef.current.body, onResponse: optionsRef.current.onResponse, onChunk: optionsRef.current.onChunk, - onStreamStart: () => { - setStatus('streaming') - }, onFinish: (message: UIMessage) => { - setStatus('ready') optionsRef.current.onFinish?.(message) }, onError: (error: Error) => { - setStatus('error') optionsRef.current.onError?.(error) }, tools: optionsRef.current.tools, @@ -70,15 +65,13 @@ export function useChat = any>( }, onLoadingChange: (newIsLoading: boolean) => { setIsLoading(newIsLoading) - if (newIsLoading) { - setStatus('submitted') - } else { - setStatus((prev) => (prev === 'error' ? 'error' : 'ready')) - } }, onErrorChange: (newError: Error | undefined) => { setError(newError) }, + onStatusChange: (status: ChatClientState) => { + setStatus(status) + } }) }, [clientId]) @@ -109,6 +102,7 @@ export function useChat = any>( // are captured at client creation time. Changes to these callbacks require // remounting the component or changing the connection to recreate the client. + const sendMessage = useCallback( async (content: string) => { await client.sendMessage(content) diff --git a/packages/typescript/ai-react/tests/use-chat.test.ts b/packages/typescript/ai-react/tests/use-chat.test.ts index e571e186..7a2ca676 100644 --- a/packages/typescript/ai-react/tests/use-chat.test.ts +++ b/packages/typescript/ai-react/tests/use-chat.test.ts @@ -570,6 +570,29 @@ describe('useChat', () => { expect(result.current.status).toBe('error') }) }) + + it('should transition to ready after stop', async () => { + const chunks = createTextChunks('Response') + const adapter = createMockConnectionAdapter({ + chunks, + chunkDelay: 50, + }) + const { result } = renderUseChat({ connection: adapter }) + + const sendPromise = result.current.sendMessage('Test') + + await waitFor(() => { + expect(result.current.status).not.toBe('ready') + }) + + result.current.stop() + + await sendPromise + + await waitFor(() => { + expect(result.current.status).toBe('ready') + }) + }) }) describe('clear', () => { diff --git a/packages/typescript/ai-solid/src/types.ts b/packages/typescript/ai-solid/src/types.ts index fc6925eb..5908ff1b 100644 --- a/packages/typescript/ai-solid/src/types.ts +++ b/packages/typescript/ai-solid/src/types.ts @@ -18,6 +18,7 @@ export type { ChatRequestBody, UIMessage } * - `onMessagesChange` - Managed by Solid signal (exposed as `messages`) * - `onLoadingChange` - Managed by Solid signal (exposed as `isLoading`) * - `onErrorChange` - Managed by Solid signal (exposed as `error`) + * - `onStatusChange` - Managed by Solid signal (exposed as `status`) * * All other callbacks (onResponse, onChunk, onFinish, onError) are * passed through to the underlying ChatClient and can be used for side effects. @@ -28,7 +29,10 @@ export type { ChatRequestBody, UIMessage } export type UseChatOptions = any> = Omit< ChatClientOptions, - 'onMessagesChange' | 'onLoadingChange' | 'onErrorChange' | 'onStreamStart' + | 'onMessagesChange' + | 'onLoadingChange' + | 'onErrorChange' + | 'onStatusChange' > export interface UseChatReturn< diff --git a/packages/typescript/ai-solid/src/use-chat.ts b/packages/typescript/ai-solid/src/use-chat.ts index d83aa8d9..10201fad 100644 --- a/packages/typescript/ai-solid/src/use-chat.ts +++ b/packages/typescript/ai-solid/src/use-chat.ts @@ -35,11 +35,9 @@ export function useChat = any>( onResponse: options.onResponse, onChunk: options.onChunk, onFinish: (message) => { - setStatus('ready') options.onFinish?.(message) }, onError: (err) => { - setStatus('error') options.onError?.(err) }, tools: options.tools, @@ -49,14 +47,9 @@ export function useChat = any>( }, onLoadingChange: (newIsLoading: boolean) => { setIsLoading(newIsLoading) - if (newIsLoading) { - setStatus('submitted') - } else { - setStatus((prev) => (prev === 'error' ? 'error' : 'ready')) - } }, - onStreamStart: () => { - setStatus('streaming') + onStatusChange: (newStatus: ChatClientState) => { + setStatus(newStatus) }, onErrorChange: (newError: Error | undefined) => { setError(newError) diff --git a/packages/typescript/ai-solid/tests/use-chat.test.ts b/packages/typescript/ai-solid/tests/use-chat.test.ts index 0d299219..e2a10364 100644 --- a/packages/typescript/ai-solid/tests/use-chat.test.ts +++ b/packages/typescript/ai-solid/tests/use-chat.test.ts @@ -378,9 +378,9 @@ describe('useChat', () => { ) const firstContent = firstAssistantMessage?.parts.find((p) => p.type === 'text')?.type === - 'text' + 'text' ? (firstAssistantMessage.parts.find((p) => p.type === 'text') as any) - .content + .content : '' // Reload with new adapter @@ -570,6 +570,29 @@ describe('useChat', () => { expect(result.current.status).toBe('error') }) }) + + it('should transition to ready after stop', async () => { + const chunks = createTextChunks('Response') + const adapter = createMockConnectionAdapter({ + chunks, + chunkDelay: 50, + }) + const { result } = renderUseChat({ connection: adapter }) + + const sendPromise = result.current.sendMessage('Test') + + await waitFor(() => { + expect(result.current.status).not.toBe('ready') + }) + + result.current.stop() + + await waitFor(() => { + expect(result.current.status).toBe('ready') + }) + + await sendPromise.catch(() => { }) + }) }) describe('clear', () => { diff --git a/packages/typescript/ai-svelte/src/create-chat.svelte.ts b/packages/typescript/ai-svelte/src/create-chat.svelte.ts index cd9f5db9..e501758d 100644 --- a/packages/typescript/ai-svelte/src/create-chat.svelte.ts +++ b/packages/typescript/ai-svelte/src/create-chat.svelte.ts @@ -55,11 +55,9 @@ export function createChat = any>( onResponse: options.onResponse, onChunk: options.onChunk, onFinish: (message) => { - status = 'ready' options.onFinish?.(message) }, onError: (err) => { - status = 'error' options.onError?.(err) }, tools: options.tools, @@ -69,14 +67,9 @@ export function createChat = any>( }, onLoadingChange: (newIsLoading: boolean) => { isLoading = newIsLoading - if (newIsLoading) { - status = 'submitted' - } else { - status = error ? 'error' : 'ready' - } }, - onStreamStart: () => { - status = 'streaming' + onStatusChange: (newStatus: ChatClientState) => { + status = newStatus }, onErrorChange: (newError: Error | undefined) => { error = newError diff --git a/packages/typescript/ai-svelte/src/types.ts b/packages/typescript/ai-svelte/src/types.ts index 341860d3..4d80ae37 100644 --- a/packages/typescript/ai-svelte/src/types.ts +++ b/packages/typescript/ai-svelte/src/types.ts @@ -17,6 +17,7 @@ export type { ChatRequestBody, UIMessage } * - `onMessagesChange` - Managed by Svelte state (exposed as `messages`) * - `onLoadingChange` - Managed by Svelte state (exposed as `isLoading`) * - `onErrorChange` - Managed by Svelte state (exposed as `error`) + * - `onStatusChange` - Managed by Svelte state (exposed as `status`) * * All other callbacks (onResponse, onChunk, onFinish, onError) are * passed through to the underlying ChatClient and can be used for side effects. @@ -28,7 +29,10 @@ export type CreateChatOptions< TTools extends ReadonlyArray = any, > = Omit< ChatClientOptions, - 'onMessagesChange' | 'onLoadingChange' | 'onErrorChange' | 'onStreamStart' + | 'onMessagesChange' + | 'onLoadingChange' + | 'onErrorChange' + | 'onStatusChange' > export interface CreateChatReturn< diff --git a/packages/typescript/ai-svelte/tests/use-chat.test.ts b/packages/typescript/ai-svelte/tests/use-chat.test.ts index ff78075d..12fd6333 100644 --- a/packages/typescript/ai-svelte/tests/use-chat.test.ts +++ b/packages/typescript/ai-svelte/tests/use-chat.test.ts @@ -1,6 +1,6 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' import { createChat } from '../src/create-chat.svelte' -import { createMockConnectionAdapter } from './test-utils' +import { createMockConnectionAdapter, createTextChunks } from './test-utils' describe('createChat', () => { beforeEach(() => { @@ -158,4 +158,62 @@ describe('createChat', () => { expect(chat.status).toBe('ready') expect(chat.status).toBe('ready') }) + + describe('status transitions', () => { + it('should transition through states during generation', async () => { + const chunks = createTextChunks('Response') + const mockConnection = createMockConnectionAdapter({ + chunks, + chunkDelay: 20, + }) + + const chat = createChat({ + connection: mockConnection, + }) + + const promise = chat.sendMessage('Test') + expect(chat.status).not.toBe('ready') + expect(['submitted', 'streaming']).toContain(chat.status) + + await promise + expect(chat.status).toBe('ready') + }) + + it('should transition to error on error', async () => { + const mockConnection = createMockConnectionAdapter({ + shouldError: true, + error: new Error('AI Error'), + }) + + const chat = createChat({ + connection: mockConnection, + }) + + await chat.sendMessage('Test') + expect(chat.status).toBe('error') + }) + + it('should transition to ready after stop', async () => { + const chunks = createTextChunks('Response') + const mockConnection = createMockConnectionAdapter({ + chunks, + chunkDelay: 50, + }) + + const chat = createChat({ + connection: mockConnection, + }) + + const promise = chat.sendMessage('Test') + + // Wait a bit for it to start + await new Promise((resolve) => setTimeout(resolve, 10)) + expect(chat.status).not.toBe('ready') + + chat.stop() + expect(chat.status).toBe('ready') + + await promise.catch(() => { }) + }) + }) }) diff --git a/packages/typescript/ai-vue/src/types.ts b/packages/typescript/ai-vue/src/types.ts index 399e942d..7c09f103 100644 --- a/packages/typescript/ai-vue/src/types.ts +++ b/packages/typescript/ai-vue/src/types.ts @@ -18,6 +18,7 @@ export type { ChatRequestBody, UIMessage } * - `onMessagesChange` - Managed by Vue ref (exposed as `messages`) * - `onLoadingChange` - Managed by Vue ref (exposed as `isLoading`) * - `onErrorChange` - Managed by Vue ref (exposed as `error`) + * - `onStatusChange` - Managed by Vue ref (exposed as `status`) * * All other callbacks (onResponse, onChunk, onFinish, onError) are * passed through to the underlying ChatClient and can be used for side effects. @@ -28,7 +29,7 @@ export type { ChatRequestBody, UIMessage } export type UseChatOptions = any> = Omit< ChatClientOptions, - 'onMessagesChange' | 'onLoadingChange' | 'onErrorChange' | 'onStreamStart' + 'onMessagesChange' | 'onLoadingChange' | 'onErrorChange' | 'onStatusChange' > export interface UseChatReturn< diff --git a/packages/typescript/ai-vue/src/use-chat.ts b/packages/typescript/ai-vue/src/use-chat.ts index 8e998bdb..41bc637e 100644 --- a/packages/typescript/ai-vue/src/use-chat.ts +++ b/packages/typescript/ai-vue/src/use-chat.ts @@ -1,6 +1,6 @@ import type { AnyClientTool, ModelMessage } from '@tanstack/ai' -import { ChatClient } from '@tanstack/ai-client' import type { ChatClientState } from '@tanstack/ai-client' +import { ChatClient } from '@tanstack/ai-client' import { onScopeDispose, readonly, shallowRef, useId } from 'vue' import type { UIMessage, UseChatOptions, UseChatReturn } from './types' @@ -26,11 +26,9 @@ export function useChat = any>( onResponse: options.onResponse, onChunk: options.onChunk, onFinish: (message) => { - status.value = 'ready' options.onFinish?.(message) }, onError: (err) => { - status.value = 'error' options.onError?.(err) }, tools: options.tools, @@ -40,14 +38,9 @@ export function useChat = any>( }, onLoadingChange: (newIsLoading: boolean) => { isLoading.value = newIsLoading - if (newIsLoading) { - status.value = 'submitted' - } else { - status.value = error.value ? 'error' : 'ready' - } }, - onStreamStart: () => { - status.value = 'streaming' + onStatusChange: (newStatus: ChatClientState) => { + status.value = newStatus }, onErrorChange: (newError: Error | undefined) => { error.value = newError diff --git a/packages/typescript/ai-vue/tests/use-chat.test.ts b/packages/typescript/ai-vue/tests/use-chat.test.ts index 2c950ae7..dcd66384 100644 --- a/packages/typescript/ai-vue/tests/use-chat.test.ts +++ b/packages/typescript/ai-vue/tests/use-chat.test.ts @@ -502,6 +502,28 @@ describe('useChat', () => { expect(result.current.status).toBe('error') }) + + it('should transition to ready after stop', async () => { + const chunks = createTextChunks('Response') + const adapter = createMockConnectionAdapter({ + chunks, + chunkDelay: 50, + }) + const { result } = renderUseChat({ connection: adapter }) + + const sendPromise = result.current.sendMessage('Test') + + // Wait for it to start + await flushPromises() + expect(result.current.status).not.toBe('ready') + + result.current.stop() + await flushPromises() + + expect(result.current.status).toBe('ready') + + await sendPromise.catch(() => { }) + }) }) describe('clear', () => { From a4c1371fb70d3aba417cd7bb25ba9284ff8781a1 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Tue, 27 Jan 2026 12:31:57 +0000 Subject: [PATCH 09/11] ci: apply automated fixes --- packages/typescript/ai-client/src/chat-client.ts | 16 ++++++++-------- packages/typescript/ai-preact/src/types.ts | 5 +---- .../typescript/ai-preact/tests/use-chat.test.ts | 2 +- packages/typescript/ai-react/src/types.ts | 5 +---- packages/typescript/ai-react/src/use-chat.ts | 3 +-- packages/typescript/ai-solid/src/types.ts | 5 +---- .../typescript/ai-solid/tests/use-chat.test.ts | 6 +++--- packages/typescript/ai-svelte/src/types.ts | 5 +---- .../typescript/ai-svelte/tests/use-chat.test.ts | 2 +- .../typescript/ai-vue/tests/use-chat.test.ts | 2 +- 10 files changed, 19 insertions(+), 32 deletions(-) diff --git a/packages/typescript/ai-client/src/chat-client.ts b/packages/typescript/ai-client/src/chat-client.ts index 1985def5..5009ff7b 100644 --- a/packages/typescript/ai-client/src/chat-client.ts +++ b/packages/typescript/ai-client/src/chat-client.ts @@ -59,14 +59,14 @@ export class ChatClient { this.callbacksRef = { current: { - onResponse: options.onResponse || (() => { }), - onChunk: options.onChunk || (() => { }), - onFinish: options.onFinish || (() => { }), - onError: options.onError || (() => { }), - onMessagesChange: options.onMessagesChange || (() => { }), - onLoadingChange: options.onLoadingChange || (() => { }), - onErrorChange: options.onErrorChange || (() => { }), - onStatusChange: options.onStatusChange || (() => { }), + onResponse: options.onResponse || (() => {}), + onChunk: options.onChunk || (() => {}), + onFinish: options.onFinish || (() => {}), + onError: options.onError || (() => {}), + onMessagesChange: options.onMessagesChange || (() => {}), + onLoadingChange: options.onLoadingChange || (() => {}), + onErrorChange: options.onErrorChange || (() => {}), + onStatusChange: options.onStatusChange || (() => {}), }, } diff --git a/packages/typescript/ai-preact/src/types.ts b/packages/typescript/ai-preact/src/types.ts index dcd7bbac..7679e08a 100644 --- a/packages/typescript/ai-preact/src/types.ts +++ b/packages/typescript/ai-preact/src/types.ts @@ -28,10 +28,7 @@ export type { ChatRequestBody, UIMessage } export type UseChatOptions = any> = Omit< ChatClientOptions, - | 'onMessagesChange' - | 'onLoadingChange' - | 'onErrorChange' - | 'onStatusChange' + 'onMessagesChange' | 'onLoadingChange' | 'onErrorChange' | 'onStatusChange' > export interface UseChatReturn< diff --git a/packages/typescript/ai-preact/tests/use-chat.test.ts b/packages/typescript/ai-preact/tests/use-chat.test.ts index 7dfc815d..6a6c3098 100644 --- a/packages/typescript/ai-preact/tests/use-chat.test.ts +++ b/packages/typescript/ai-preact/tests/use-chat.test.ts @@ -640,7 +640,7 @@ describe('useChat', () => { }) await act(async () => { - await sendPromise!.catch(() => { }) + await sendPromise!.catch(() => {}) }) }) }) diff --git a/packages/typescript/ai-react/src/types.ts b/packages/typescript/ai-react/src/types.ts index c8088b92..0bca9883 100644 --- a/packages/typescript/ai-react/src/types.ts +++ b/packages/typescript/ai-react/src/types.ts @@ -28,10 +28,7 @@ export type { ChatRequestBody, UIMessage } export type UseChatOptions = any> = Omit< ChatClientOptions, - | 'onMessagesChange' - | 'onLoadingChange' - | 'onErrorChange' - | 'onStatusChange' + 'onMessagesChange' | 'onLoadingChange' | 'onErrorChange' | 'onStatusChange' > export interface UseChatReturn< diff --git a/packages/typescript/ai-react/src/use-chat.ts b/packages/typescript/ai-react/src/use-chat.ts index 123c4eb6..65f6d59a 100644 --- a/packages/typescript/ai-react/src/use-chat.ts +++ b/packages/typescript/ai-react/src/use-chat.ts @@ -71,7 +71,7 @@ export function useChat = any>( }, onStatusChange: (status: ChatClientState) => { setStatus(status) - } + }, }) }, [clientId]) @@ -102,7 +102,6 @@ export function useChat = any>( // are captured at client creation time. Changes to these callbacks require // remounting the component or changing the connection to recreate the client. - const sendMessage = useCallback( async (content: string) => { await client.sendMessage(content) diff --git a/packages/typescript/ai-solid/src/types.ts b/packages/typescript/ai-solid/src/types.ts index 5908ff1b..050fb801 100644 --- a/packages/typescript/ai-solid/src/types.ts +++ b/packages/typescript/ai-solid/src/types.ts @@ -29,10 +29,7 @@ export type { ChatRequestBody, UIMessage } export type UseChatOptions = any> = Omit< ChatClientOptions, - | 'onMessagesChange' - | 'onLoadingChange' - | 'onErrorChange' - | 'onStatusChange' + 'onMessagesChange' | 'onLoadingChange' | 'onErrorChange' | 'onStatusChange' > export interface UseChatReturn< diff --git a/packages/typescript/ai-solid/tests/use-chat.test.ts b/packages/typescript/ai-solid/tests/use-chat.test.ts index e2a10364..27667cb1 100644 --- a/packages/typescript/ai-solid/tests/use-chat.test.ts +++ b/packages/typescript/ai-solid/tests/use-chat.test.ts @@ -378,9 +378,9 @@ describe('useChat', () => { ) const firstContent = firstAssistantMessage?.parts.find((p) => p.type === 'text')?.type === - 'text' + 'text' ? (firstAssistantMessage.parts.find((p) => p.type === 'text') as any) - .content + .content : '' // Reload with new adapter @@ -591,7 +591,7 @@ describe('useChat', () => { expect(result.current.status).toBe('ready') }) - await sendPromise.catch(() => { }) + await sendPromise.catch(() => {}) }) }) diff --git a/packages/typescript/ai-svelte/src/types.ts b/packages/typescript/ai-svelte/src/types.ts index 4d80ae37..7c869ece 100644 --- a/packages/typescript/ai-svelte/src/types.ts +++ b/packages/typescript/ai-svelte/src/types.ts @@ -29,10 +29,7 @@ export type CreateChatOptions< TTools extends ReadonlyArray = any, > = Omit< ChatClientOptions, - | 'onMessagesChange' - | 'onLoadingChange' - | 'onErrorChange' - | 'onStatusChange' + 'onMessagesChange' | 'onLoadingChange' | 'onErrorChange' | 'onStatusChange' > export interface CreateChatReturn< diff --git a/packages/typescript/ai-svelte/tests/use-chat.test.ts b/packages/typescript/ai-svelte/tests/use-chat.test.ts index 12fd6333..73dea2b2 100644 --- a/packages/typescript/ai-svelte/tests/use-chat.test.ts +++ b/packages/typescript/ai-svelte/tests/use-chat.test.ts @@ -213,7 +213,7 @@ describe('createChat', () => { chat.stop() expect(chat.status).toBe('ready') - await promise.catch(() => { }) + await promise.catch(() => {}) }) }) }) diff --git a/packages/typescript/ai-vue/tests/use-chat.test.ts b/packages/typescript/ai-vue/tests/use-chat.test.ts index dcd66384..49287c8f 100644 --- a/packages/typescript/ai-vue/tests/use-chat.test.ts +++ b/packages/typescript/ai-vue/tests/use-chat.test.ts @@ -522,7 +522,7 @@ describe('useChat', () => { expect(result.current.status).toBe('ready') - await sendPromise.catch(() => { }) + await sendPromise.catch(() => {}) }) }) From 0d2d7b066685ad995cc84674c43fbfe289852e7a Mon Sep 17 00:00:00 2001 From: Nikas Belogolov Date: Tue, 27 Jan 2026 17:47:37 +0200 Subject: [PATCH 10/11] removed setStatus from setError, fixed edge-cases with status, refactored test cases for status --- .../typescript/ai-client/src/chat-client.ts | 4 +- .../ai-client/tests/chat-client.test.ts | 41 +------ .../ai-preact/tests/use-chat.test.ts | 109 ++++++++++-------- .../ai-react/tests/use-chat.test.ts | 48 +------- .../ai-solid/tests/use-chat.test.ts | 50 ++------ .../ai-svelte/tests/use-chat.test.ts | 1 + .../typescript/ai-vue/tests/use-chat.test.ts | 46 +------- 7 files changed, 79 insertions(+), 220 deletions(-) diff --git a/packages/typescript/ai-client/src/chat-client.ts b/packages/typescript/ai-client/src/chat-client.ts index 5009ff7b..e89761f3 100644 --- a/packages/typescript/ai-client/src/chat-client.ts +++ b/packages/typescript/ai-client/src/chat-client.ts @@ -202,9 +202,6 @@ export class ChatClient { this.error = error this.callbacksRef.current.onErrorChange(error) this.events.errorChanged(error?.message || null) - if (error) { - this.setStatus('error') - } } /** @@ -340,6 +337,7 @@ export class ChatClient { return } this.setError(err) + this.setStatus('error') this.callbacksRef.current.onError(err) } } finally { diff --git a/packages/typescript/ai-client/tests/chat-client.test.ts b/packages/typescript/ai-client/tests/chat-client.test.ts index 55f55643..651d58b4 100644 --- a/packages/typescript/ai-client/tests/chat-client.test.ts +++ b/packages/typescript/ai-client/tests/chat-client.test.ts @@ -280,6 +280,7 @@ describe('ChatClient', () => { await appendPromise expect(client.getIsLoading()).toBe(false) + expect(client.getStatus()).toBe('ready') }) }) @@ -392,11 +393,6 @@ describe('ChatClient', () => { }) describe('status', () => { - it('should have initial status of ready', () => { - const adapter = createMockConnectionAdapter() - const client = new ChatClient({ connection: adapter }) - expect(client.getStatus()).toBe('ready') - }) it('should transition through states during generation', async () => { const chunks = createTextChunks('Response') @@ -424,38 +420,6 @@ describe('ChatClient', () => { expect(statuses).toContain('streaming') expect(statuses[statuses.length - 1]).toBe('ready') }) - - it('should transition to error on error', async () => { - const adapter = createMockConnectionAdapter({ - shouldError: true, - error: new Error('AI Error'), - }) - const client = new ChatClient({ connection: adapter }) - - await client.sendMessage('Test') - - expect(client.getStatus()).toBe('error') - }) - - it('should transition to ready after stop', async () => { - const chunks = createTextChunks('Response') - const adapter = createMockConnectionAdapter({ - chunks, - chunkDelay: 50, - }) - const client = new ChatClient({ connection: adapter }) - - const promise = client.sendMessage('Test') - - // Wait for it to start - await new Promise((resolve) => setTimeout(resolve, 10)) - - client.stop() - - expect(client.getStatus()).toBe('ready') - - await promise - }) }) describe('tool calls', () => { @@ -538,6 +502,7 @@ describe('ChatClient', () => { await client.sendMessage('Hello') expect(client.getError()).toBe(error) + expect(client.getStatus()).toBe('error') }) it('should clear error on successful request', async () => { @@ -553,12 +518,14 @@ describe('ChatClient', () => { await client.sendMessage('Fail') expect(client.getError()).toBeDefined() + expect(client.getStatus()).toBe('error') // Update connection via updateOptions client.updateOptions({ connection: successAdapter }) await client.sendMessage('Success') expect(client.getError()).toBeUndefined() + expect(client.getStatus()).not.toBe('error') }) }) diff --git a/packages/typescript/ai-preact/tests/use-chat.test.ts b/packages/typescript/ai-preact/tests/use-chat.test.ts index 6a6c3098..4ab16aab 100644 --- a/packages/typescript/ai-preact/tests/use-chat.test.ts +++ b/packages/typescript/ai-preact/tests/use-chat.test.ts @@ -18,6 +18,7 @@ describe('useChat', () => { expect(result.current.messages).toEqual([]) expect(result.current.isLoading).toBe(false) expect(result.current.error).toBeUndefined() + expect(result.current.status).toBe('ready') }) it('should initialize with provided messages', () => { @@ -506,6 +507,7 @@ describe('useChat', () => { await waitFor( () => { expect(result.current.isLoading).toBe(false) + expect(result.current.status).toBe('ready') }, { timeout: 1000 }, ) @@ -521,6 +523,7 @@ describe('useChat', () => { result.current.stop() expect(result.current.isLoading).toBe(false) + expect(result.current.status).toBe('ready') }) it('should clear loading state when stopped', async () => { @@ -547,6 +550,7 @@ describe('useChat', () => { await waitFor( () => { expect(result.current.isLoading).toBe(false) + expect(result.current.status).toBe('ready') }, { timeout: 1000 }, ) @@ -560,11 +564,11 @@ describe('useChat', () => { }) describe('status', () => { - it('should have initial status of ready', () => { - const adapter = createMockConnectionAdapter() - const { result } = renderUseChat({ connection: adapter }) - expect(result.current.status).toBe('ready') - }) + // it('should have initial status of ready', () => { + // const adapter = createMockConnectionAdapter() + // const { result } = renderUseChat({ connection: adapter }) + // expect(result.current.status).toBe('ready') + // }) it('should transition through states during generation', async () => { const chunks = createTextChunks('Response') @@ -597,52 +601,52 @@ describe('useChat', () => { }) }) - it('should transition to error on error', async () => { - const error = new Error('Network error') - const adapter = createMockConnectionAdapter({ - shouldError: true, - error, - }) - const { result } = renderUseChat({ connection: adapter }) - - await act(async () => { - await result.current.sendMessage('Test') - }) - - await waitFor(() => { - expect(result.current.status).toBe('error') - }) - }) - - it('should transition to ready after stop', async () => { - const chunks = createTextChunks('Response') - const adapter = createMockConnectionAdapter({ - chunks, - chunkDelay: 50, - }) - const { result } = renderUseChat({ connection: adapter }) - - let sendPromise: Promise - act(() => { - sendPromise = result.current.sendMessage('Test') - }) - - await waitFor(() => { - expect(result.current.status).not.toBe('ready') - }) - - act(() => { - result.current.stop() - }) - - await waitFor(() => { - expect(result.current.status).toBe('ready') - }) - - await act(async () => { - await sendPromise!.catch(() => {}) - }) - }) + // it('should transition to error on error', async () => { + // const error = new Error('Network error') + // const adapter = createMockConnectionAdapter({ + // shouldError: true, + // error, + // }) + // const { result } = renderUseChat({ connection: adapter }) + + // await act(async () => { + // await result.current.sendMessage('Test') + // }) + + // await waitFor(() => { + // expect(result.current.status).toBe('error') + // }) + // }) + + // it('should transition to ready after stop', async () => { + // const chunks = createTextChunks('Response') + // const adapter = createMockConnectionAdapter({ + // chunks, + // chunkDelay: 50, + // }) + // const { result } = renderUseChat({ connection: adapter }) + + // let sendPromise: Promise + // act(() => { + // sendPromise = result.current.sendMessage('Test') + // }) + + // await waitFor(() => { + // expect(result.current.status).not.toBe('ready') + // }) + + // act(() => { + // result.current.stop() + // }) + + // await waitFor(() => { + // expect(result.current.status).toBe('ready') + // }) + + // await act(async () => { + // await sendPromise!.catch(() => {}) + // }) + // }) }) describe('clear', () => { @@ -1115,6 +1119,7 @@ describe('useChat', () => { expect(result.current.error?.message).toBe('Network request failed') expect(result.current.isLoading).toBe(false) + expect(result.current.status).toBe('error') }) it('should handle stream errors', async () => { @@ -1134,6 +1139,7 @@ describe('useChat', () => { }) expect(result.current.error?.message).toBe('Stream error') + expect(result.current.status).toBe('error') }) it('should clear error on successful operation', async () => { @@ -1151,6 +1157,7 @@ describe('useChat', () => { await waitFor(() => { expect(result.current.error).toBeDefined() + expect(result.current.status).toBe('error') }) // Switch to working adapter diff --git a/packages/typescript/ai-react/tests/use-chat.test.ts b/packages/typescript/ai-react/tests/use-chat.test.ts index 7a2ca676..1eba78a7 100644 --- a/packages/typescript/ai-react/tests/use-chat.test.ts +++ b/packages/typescript/ai-react/tests/use-chat.test.ts @@ -18,6 +18,7 @@ describe('useChat', () => { expect(result.current.messages).toEqual([]) expect(result.current.isLoading).toBe(false) expect(result.current.error).toBeUndefined() + expect(result.current.status).toBe('ready') }) it('should initialize with provided messages', () => { @@ -472,6 +473,7 @@ describe('useChat', () => { await waitFor( () => { expect(result.current.isLoading).toBe(false) + expect(result.current.status).toBe('ready') }, { timeout: 1000 }, ) @@ -487,6 +489,7 @@ describe('useChat', () => { result.current.stop() expect(result.current.isLoading).toBe(false) + expect(result.current.status).toBe('ready') }) it('should clear loading state when stopped', async () => { @@ -508,6 +511,7 @@ describe('useChat', () => { await waitFor( () => { expect(result.current.isLoading).toBe(false) + expect(result.current.status).toBe('ready') }, { timeout: 1000 }, ) @@ -519,12 +523,6 @@ describe('useChat', () => { }) describe('status', () => { - it('should have initial status of ready', () => { - const adapter = createMockConnectionAdapter() - const { result } = renderUseChat({ connection: adapter }) - expect(result.current.status).toBe('ready') - }) - it('should transition through states during generation', async () => { const chunks = createTextChunks('Response') const adapter = createMockConnectionAdapter({ @@ -555,44 +553,6 @@ describe('useChat', () => { expect(result.current.status).toBe('ready') }) }) - - it('should transition to error on error', async () => { - const error = new Error('Network error') - const adapter = createMockConnectionAdapter({ - shouldError: true, - error, - }) - const { result } = renderUseChat({ connection: adapter }) - - await result.current.sendMessage('Test') - - await waitFor(() => { - expect(result.current.status).toBe('error') - }) - }) - - it('should transition to ready after stop', async () => { - const chunks = createTextChunks('Response') - const adapter = createMockConnectionAdapter({ - chunks, - chunkDelay: 50, - }) - const { result } = renderUseChat({ connection: adapter }) - - const sendPromise = result.current.sendMessage('Test') - - await waitFor(() => { - expect(result.current.status).not.toBe('ready') - }) - - result.current.stop() - - await sendPromise - - await waitFor(() => { - expect(result.current.status).toBe('ready') - }) - }) }) describe('clear', () => { diff --git a/packages/typescript/ai-solid/tests/use-chat.test.ts b/packages/typescript/ai-solid/tests/use-chat.test.ts index 27667cb1..188a5ead 100644 --- a/packages/typescript/ai-solid/tests/use-chat.test.ts +++ b/packages/typescript/ai-solid/tests/use-chat.test.ts @@ -18,6 +18,7 @@ describe('useChat', () => { expect(result.current.messages).toEqual([]) expect(result.current.isLoading).toBe(false) expect(result.current.error).toBeUndefined() + expect(result.current.status).toBe('ready') }) it('should initialize with provided messages', () => { @@ -472,6 +473,7 @@ describe('useChat', () => { await waitFor( () => { expect(result.current.isLoading).toBe(false) + expect(result.current.status).toBe('ready') }, { timeout: 1000 }, ) @@ -487,6 +489,7 @@ describe('useChat', () => { result.current.stop() expect(result.current.isLoading).toBe(false) + expect(result.current.status).toBe('ready') }) it('should clear loading state when stopped', async () => { @@ -508,6 +511,7 @@ describe('useChat', () => { await waitFor( () => { expect(result.current.isLoading).toBe(false) + expect(result.current.status).toBe('ready') }, { timeout: 1000 }, ) @@ -519,11 +523,6 @@ describe('useChat', () => { }) describe('status', () => { - it('should have initial status of ready', () => { - const adapter = createMockConnectionAdapter() - const { result } = renderUseChat({ connection: adapter }) - expect(result.current.status).toBe('ready') - }) it('should transition through states during generation', async () => { const chunks = createTextChunks('Response') @@ -555,44 +554,6 @@ describe('useChat', () => { expect(result.current.status).toBe('ready') }) }) - - it('should transition to error on error', async () => { - const error = new Error('Network error') - const adapter = createMockConnectionAdapter({ - shouldError: true, - error, - }) - const { result } = renderUseChat({ connection: adapter }) - - await result.current.sendMessage('Test') - - await waitFor(() => { - expect(result.current.status).toBe('error') - }) - }) - - it('should transition to ready after stop', async () => { - const chunks = createTextChunks('Response') - const adapter = createMockConnectionAdapter({ - chunks, - chunkDelay: 50, - }) - const { result } = renderUseChat({ connection: adapter }) - - const sendPromise = result.current.sendMessage('Test') - - await waitFor(() => { - expect(result.current.status).not.toBe('ready') - }) - - result.current.stop() - - await waitFor(() => { - expect(result.current.status).toBe('ready') - }) - - await sendPromise.catch(() => {}) - }) }) describe('clear', () => { @@ -1001,6 +962,7 @@ describe('useChat', () => { expect(result.current.error?.message).toBe('Network request failed') expect(result.current.isLoading).toBe(false) + expect(result.current.status).toBe('error') }) it('should handle stream errors', async () => { @@ -1015,6 +977,7 @@ describe('useChat', () => { await waitFor(() => { expect(result.current.error).toBeDefined() + expect(result.current.status).toBe('error') }) expect(result.current.error?.message).toBe('Stream error') @@ -1033,6 +996,7 @@ describe('useChat', () => { await waitFor(() => { expect(result.current.error).toBeDefined() + expect(result.current.status).toBe('error') }) // Switch to working adapter diff --git a/packages/typescript/ai-svelte/tests/use-chat.test.ts b/packages/typescript/ai-svelte/tests/use-chat.test.ts index 73dea2b2..292f5324 100644 --- a/packages/typescript/ai-svelte/tests/use-chat.test.ts +++ b/packages/typescript/ai-svelte/tests/use-chat.test.ts @@ -17,6 +17,7 @@ describe('createChat', () => { expect(chat.messages).toEqual([]) expect(chat.isLoading).toBe(false) expect(chat.error).toBeUndefined() + expect(chat.status).toBe('ready') }) it('should initialize with initial messages', () => { diff --git a/packages/typescript/ai-vue/tests/use-chat.test.ts b/packages/typescript/ai-vue/tests/use-chat.test.ts index 49287c8f..c654f593 100644 --- a/packages/typescript/ai-vue/tests/use-chat.test.ts +++ b/packages/typescript/ai-vue/tests/use-chat.test.ts @@ -18,6 +18,7 @@ describe('useChat', () => { expect(result.current.messages).toEqual([]) expect(result.current.isLoading).toBe(false) expect(result.current.error).toBeUndefined() + expect(result.current.status).toBe('ready') }) it('should initialize with provided messages', () => { @@ -421,6 +422,7 @@ describe('useChat', () => { // Should eventually stop loading expect(result.current.isLoading).toBe(false) + expect(result.current.status).toBe('ready') }) it('should be safe to call multiple times', () => { @@ -433,6 +435,7 @@ describe('useChat', () => { result.current.stop() expect(result.current.isLoading).toBe(false) + expect(result.current.status).toBe('ready') }) it('should clear loading state when stopped', async () => { @@ -456,16 +459,11 @@ describe('useChat', () => { await flushPromises() expect(result.current.isLoading).toBe(false) + expect(result.current.status).toBe('ready') }) }) describe('status', () => { - it('should have initial status of ready', () => { - const adapter = createMockConnectionAdapter() - const { result } = renderUseChat({ connection: adapter }) - expect(result.current.status).toBe('ready') - }) - it('should transition through states during generation', async () => { const chunks = createTextChunks('Response') const adapter = createMockConnectionAdapter({ @@ -488,42 +486,6 @@ describe('useChat', () => { await flushPromises() expect(result.current.status).toBe('ready') }) - - it('should transition to error on error', async () => { - const error = new Error('Network error') - const adapter = createMockConnectionAdapter({ - shouldError: true, - error, - }) - const { result } = renderUseChat({ connection: adapter }) - - await result.current.sendMessage('Test') - await flushPromises() - - expect(result.current.status).toBe('error') - }) - - it('should transition to ready after stop', async () => { - const chunks = createTextChunks('Response') - const adapter = createMockConnectionAdapter({ - chunks, - chunkDelay: 50, - }) - const { result } = renderUseChat({ connection: adapter }) - - const sendPromise = result.current.sendMessage('Test') - - // Wait for it to start - await flushPromises() - expect(result.current.status).not.toBe('ready') - - result.current.stop() - await flushPromises() - - expect(result.current.status).toBe('ready') - - await sendPromise.catch(() => {}) - }) }) describe('clear', () => { From 3990b7667d62064f47263f2c12dc417862b52f4a Mon Sep 17 00:00:00 2001 From: Nikas Belogolov Date: Tue, 27 Jan 2026 17:58:04 +0200 Subject: [PATCH 11/11] removed unused test cases --- .../ai-preact/tests/use-chat.test.ts | 53 ------------------- 1 file changed, 53 deletions(-) diff --git a/packages/typescript/ai-preact/tests/use-chat.test.ts b/packages/typescript/ai-preact/tests/use-chat.test.ts index 4ab16aab..aa21d1ca 100644 --- a/packages/typescript/ai-preact/tests/use-chat.test.ts +++ b/packages/typescript/ai-preact/tests/use-chat.test.ts @@ -564,12 +564,6 @@ describe('useChat', () => { }) describe('status', () => { - // it('should have initial status of ready', () => { - // const adapter = createMockConnectionAdapter() - // const { result } = renderUseChat({ connection: adapter }) - // expect(result.current.status).toBe('ready') - // }) - it('should transition through states during generation', async () => { const chunks = createTextChunks('Response') const adapter = createMockConnectionAdapter({ @@ -600,53 +594,6 @@ describe('useChat', () => { expect(result.current.status).toBe('ready') }) }) - - // it('should transition to error on error', async () => { - // const error = new Error('Network error') - // const adapter = createMockConnectionAdapter({ - // shouldError: true, - // error, - // }) - // const { result } = renderUseChat({ connection: adapter }) - - // await act(async () => { - // await result.current.sendMessage('Test') - // }) - - // await waitFor(() => { - // expect(result.current.status).toBe('error') - // }) - // }) - - // it('should transition to ready after stop', async () => { - // const chunks = createTextChunks('Response') - // const adapter = createMockConnectionAdapter({ - // chunks, - // chunkDelay: 50, - // }) - // const { result } = renderUseChat({ connection: adapter }) - - // let sendPromise: Promise - // act(() => { - // sendPromise = result.current.sendMessage('Test') - // }) - - // await waitFor(() => { - // expect(result.current.status).not.toBe('ready') - // }) - - // act(() => { - // result.current.stop() - // }) - - // await waitFor(() => { - // expect(result.current.status).toBe('ready') - // }) - - // await act(async () => { - // await sendPromise!.catch(() => {}) - // }) - // }) }) describe('clear', () => {