Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -230,15 +230,16 @@ The single canonical command is:
pnpm test:pr
```

This runs the exact target set the `PR` workflow runs in CI (`nx affected --targets=test:sherif,test:knip,test:docs,test:eslint,test:lib,test:types,test:build,build --exclude=examples/**,testing/**`).
This runs the exact target set the `PR` workflow runs in CI: first `nx affected --targets=test:sherif,test:knip,test:docs,test:kiira,test:eslint,test:lib,test:types,test:build,build --exclude=examples/**,testing/**` (the heavy targets, packages only), then a second `nx affected --targets=test:types --projects=examples/**,testing/**` pass that **does** type-check the example apps and `testing/` packages. The example/testing surfaces are deliberately excluded from the heavy targets (build/lib/etc.) but included for `test:types`, because call-site type regressions often only manifest where the library is actually consumed (see issue #820). Use `pnpm test:types:examples` to run just that second pass locally.

If you can't run `test:pr` (e.g. it's too slow on your machine), at minimum run each of these and confirm they're green before pushing:

- `pnpm test:sherif` — workspace consistency
- `pnpm test:knip` — unused dependencies
- `pnpm test:docs` — doc link verification
- `pnpm test:eslint` — lint
- `pnpm test:types` — typecheck
- `pnpm test:types` — typecheck (packages)
- `pnpm test:types:examples` — typecheck the example apps + `testing/` packages
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
- `pnpm test:lib` — unit tests
- `pnpm test:build` — build artifact verification
- `pnpm build` — build all affected packages
Expand Down
3 changes: 2 additions & 1 deletion examples/ts-angular-chat/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
"preview": "vite preview",
"test:types": "node scripts/typecheck.mjs"
},
"dependencies": {
"@angular/common": "^21.2.0",
Expand Down
28 changes: 28 additions & 0 deletions examples/ts-angular-chat/scripts/typecheck.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// Type-check this Angular example, including template type-checking.
//
// Angular template type-checking (`strictTemplates`) requires the Angular
// compiler (`ngc`), not plain `tsc`. `ngc` ships in `@angular/compiler-cli`,
// which is a *peer* dependency of `@angular/build` (a direct devDependency of
// this example). We resolve it from there instead of declaring it directly so
// CI's frozen lockfile install stays unchanged.
import { createRequire } from 'node:module'
import { spawnSync } from 'node:child_process'
import { dirname, join } from 'node:path'

const require = createRequire(import.meta.url)

// `@angular/compiler-cli` is resolvable from `@angular/build`'s location.
const buildPkg = require.resolve('@angular/build/package.json')
const cliPkgPath = require.resolve('@angular/compiler-cli/package.json', {
paths: [buildPkg],
})
const cliPkg = require(cliPkgPath)
const ngc = join(dirname(cliPkgPath), cliPkg.bin.ngc)

const { status } = spawnSync(
process.execPath,
[ngc, '-p', 'tsconfig.app.json', '--noEmit'],
{ stdio: 'inherit' },
)

process.exit(status ?? 1)
2 changes: 1 addition & 1 deletion examples/ts-angular-chat/src/app.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,7 @@ export class AppComponent {

/** A message is worth rendering if it has visible text or a tool call. */
isRenderable(message: {
parts: ReadonlyArray<{ type: string; content?: string }>
parts: ReadonlyArray<{ type: string; content?: unknown }>
}): boolean {
return message.parts.some(
(part) =>
Expand Down
31 changes: 31 additions & 0 deletions examples/ts-code-mode-web/src/lib/tool-result-content.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import type { UIMessage } from '@tanstack/ai-react'

type ToolResultPart = Extract<
UIMessage['parts'][number],
{ type: 'tool-result' }
>

/** `string | Array<ContentPart>` — a tool result's raw content. */
type ToolResultContent = ToolResultPart['content']

type ContentPartItem = Exclude<ToolResultContent, string>[number]

/**
* Reduce a tool-result part's `content` to a plain string for rendering.
*
* Tool results carry `string | Array<ContentPart>` (multimodal results are
* normalized to an array of content parts upstream). These demos render tool
* results as plain strings, so array content is flattened to the concatenation
* of its text parts; non-text parts (image, audio, video, document) have no
* string form here and are skipped.
*/
export function toolResultContentToString(content: ToolResultContent): string {
if (typeof content === 'string') return content
return content
.filter(
(part): part is Extract<ContentPartItem, { type: 'text' }> =>
part.type === 'text',
)
.map((part) => part.content)
.join('')
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
} from '@/components/reports/useReportSSE'
import ChatInput from '@/components/ChatInput'
import { Header } from '@/components'
import { toolResultContentToString } from '@/lib/tool-result-content'
import { applyUIEvent, applyUIUpdates } from '@/lib/reports/apply-event'
import type {
RefreshResult,
Expand Down Expand Up @@ -159,7 +160,7 @@ function Messages({ messages }: { messages: Array<UIMessage> }) {
for (const p of message.parts) {
if (p.type === 'tool-result') {
toolResults.set(p.toolCallId, {
content: p.content,
content: toolResultContentToString(p.content),
state: p.state,
error: p.error,
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import type { AnyTextAdapter, ServerTool, StreamChunk } from '@tanstack/ai'
import type { IsolateDriver } from '@tanstack/ai-code-mode'

import { databaseTools, getSchemaInfoTool } from '@/lib/tools/database-tools'
import { maxTokensModelOptions } from '@/lib/max-tokens-model-options'

type Provider = 'anthropic' | 'openai' | 'gemini'

Expand Down Expand Up @@ -284,7 +285,7 @@ export const Route = createFileRoute(
systemPrompts,
agentLoopStrategy: maxIterations(15),
abortController,
maxTokens: 8192,
modelOptions: maxTokensModelOptions(rawAdapter, 8192),
})

const instrumentedStream = wrapWithTimingEvents(stream, rawAdapter)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import type { VMEvent } from '@/components'
import { CodeBlock, ExecutionResult, JavaScriptVM, Header } from '@/components'
import ChatInput from '@/components/ChatInput'
import { formatDuration } from '@/lib/efficiency'
import { toolResultContentToString } from '@/lib/tool-result-content'

export const Route = createFileRoute('/_database-demo/database-demo' as any)({
component: DatabaseDemoPage,
Expand Down Expand Up @@ -378,7 +379,7 @@ function Messages({
for (const p of message.parts) {
if (p.type === 'tool-result') {
toolResults.set(p.toolCallId, {
content: p.content,
content: toolResultContentToString(p.content),
state: p.state,
error: p.error,
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { openaiText } from '@tanstack/ai-openai'
import { geminiText } from '@tanstack/ai-gemini'
import type { AnyTextAdapter, StreamChunk } from '@tanstack/ai'
import { productTools } from '@/lib/tools/product-tools'
import { maxTokensModelOptions } from '@/lib/max-tokens-model-options'

type Provider = 'anthropic' | 'openai' | 'gemini'

Expand Down Expand Up @@ -90,7 +91,7 @@ export const Route = createFileRoute('/_home/api/product-regular')({
],
agentLoopStrategy: maxIterations(30),
abortController,
maxTokens: 8192,
modelOptions: maxTokensModelOptions(adapter, 8192),
})

const requestStartTimeMs = Date.now()
Expand Down
5 changes: 3 additions & 2 deletions examples/ts-code-mode-web/src/routes/_home/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { parsePartialJSON } from '@tanstack/ai'
import { fetchServerSentEvents, useChat } from '@tanstack/ai-react'
import type { VMEvent } from '@/components'
import { CodeBlock, ExecutionResult, JavaScriptVM, Header } from '@/components'
import { toolResultContentToString } from '@/lib/tool-result-content'

export const Route = createFileRoute('/_home/')({
component: ProductDemoPage,
Expand Down Expand Up @@ -792,7 +793,7 @@ function CodeModePanel({
for (const p of message.parts) {
if (p.type === 'tool-result') {
toolResults.set(p.toolCallId, {
content: p.content,
content: toolResultContentToString(p.content),
state: p.state,
error: p.error,
})
Expand Down Expand Up @@ -1088,7 +1089,7 @@ function RegularToolsPanel({
for (const part of message.parts) {
if (part.type === 'tool-result') {
toolResults.set(part.toolCallId, {
content: part.content,
content: toolResultContentToString(part.content),
state: part.state,
error: part.error,
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import type { AnyTextAdapter, StreamChunk } from '@tanstack/ai'
import { allTools } from '@/lib/tools'
import { CODE_MODE_SYSTEM_PROMPT } from '@/lib/prompts'
import { exportConversationToPdfTool } from '@/lib/tools/export-pdf-tool'
import { maxTokensModelOptions } from '@/lib/max-tokens-model-options'

type Provider = 'anthropic' | 'openai' | 'gemini'

Expand Down Expand Up @@ -121,7 +122,7 @@ export const Route = createFileRoute('/_npm-github-chat/api/codemode')({
agentLoopStrategy: maxIterations(15),
abortController,
// Increase max tokens to allow for complex code generation
maxTokens: 8192,
modelOptions: maxTokensModelOptions(adapter, 8192),
})

const requestStartTimeMs = Date.now()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import {
} from '@/components'
import { NpmDataSidebar } from '@/components/NpmDataSidebar'
import { exportConversationToPdfTool } from '@/lib/tools/export-pdf-tool'
import { toolResultContentToString } from '@/lib/tool-result-content'

export const Route = createFileRoute('/_npm-github-chat/npm-github-chat')({
component: CodeModePage,
Expand Down Expand Up @@ -252,7 +253,7 @@ function Messages({
for (const p of message.parts) {
if (p.type === 'tool-result') {
toolResults.set(p.toolCallId, {
content: p.content,
content: toolResultContentToString(p.content),
state: p.state,
error: p.error,
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { allTools } from '@/lib/tools'
import { CODE_MODE_SYSTEM_PROMPT, REPORTS_SYSTEM_PROMPT } from '@/lib/prompts'
import { reportTools } from '@/lib/reports/tools'
import { createReportBindings } from '@/lib/reports/create-report-bindings'
import { maxTokensModelOptions } from '@/lib/max-tokens-model-options'

type Provider = 'anthropic' | 'openai' | 'gemini'

Expand Down Expand Up @@ -79,7 +80,7 @@ export const Route = createFileRoute('/_reporting/api/reports' as any)({
],
agentLoopStrategy: maxIterations(20),
abortController,
maxTokens: 8192,
modelOptions: maxTokensModelOptions(adapter, 8192),
})

const sseStream = toServerSentEventsStream(stream, abortController)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import {
usePersistedReports,
} from '@/components/reports'
import type { Report, UIEvent } from '@/lib/reports/types'
import { toolResultContentToString } from '@/lib/tool-result-content'

export const Route = createFileRoute('/_reporting/reporting-agent')({
component: ReportingAgentPage,
Expand Down Expand Up @@ -251,7 +252,7 @@ function Messages({
for (const p of message.parts) {
if (p.type === 'tool-result') {
toolResults.set(p.toolCallId, {
content: p.content,
content: toolResultContentToString(p.content),
state: p.state,
error: p.error,
})
Expand Down
11 changes: 9 additions & 2 deletions examples/ts-react-chat/src/routes/generations.image.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { createFileRoute } from '@tanstack/react-router'
import { useGenerateImage } from '@tanstack/ai-react'
import type { UseGenerateImageReturn } from '@tanstack/ai-react'
import { fetchServerSentEvents } from '@tanstack/ai-client'
import { resolveMediaPrompt } from '@tanstack/ai'
import { generateImageFn, generateImageStreamFn } from '../lib/server-fns'

function StreamingImageGeneration() {
Expand All @@ -29,7 +30,10 @@ function DirectImageGeneration() {
const [numberOfImages, setNumberOfImages] = useState(1)

const hookReturn = useGenerateImage({
fetcher: (input) => generateImageFn({ data: input }),
fetcher: (input) =>
generateImageFn({
data: { ...input, prompt: resolveMediaPrompt(input.prompt).text },
}),
})

return (
Expand All @@ -48,7 +52,10 @@ function ServerFnImageGeneration() {
const [numberOfImages, setNumberOfImages] = useState(1)

const hookReturn = useGenerateImage({
fetcher: (input) => generateImageStreamFn({ data: input }),
fetcher: (input) =>
generateImageStreamFn({
data: { ...input, prompt: resolveMediaPrompt(input.prompt).text },
}),
})

return (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -254,9 +254,6 @@ function StructuredOutputPage() {
outputSchema: GuitarRecommendationSchema,
connection: fetchServerSentEvents('/api/structured-output'),
forwardedProps: { provider, model, stream },
devtools: {
outputKind: 'structured',
},
onChunk: handleChunk,
onError: (err) => {
setError(err.message)
Expand Down
11 changes: 9 additions & 2 deletions examples/ts-react-chat/src/routes/generations.video.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { createFileRoute } from '@tanstack/react-router'
import { useGenerateVideo } from '@tanstack/ai-react'
import type { UseGenerateVideoReturn } from '@tanstack/ai-react'
import { fetchServerSentEvents } from '@tanstack/ai-client'
import { resolveMediaPrompt } from '@tanstack/ai'
import { generateVideoFn, generateVideoStreamFn } from '../lib/server-fns'

function StreamingVideoGeneration() {
Expand All @@ -21,7 +22,10 @@ function DirectVideoGeneration() {
const [prompt, setPrompt] = useState('')

const hookReturn = useGenerateVideo({
fetcher: (input) => generateVideoFn({ data: input }),
fetcher: (input) =>
generateVideoFn({
data: { ...input, prompt: resolveMediaPrompt(input.prompt).text },
}),
})

return (
Expand All @@ -33,7 +37,10 @@ function ServerFnVideoGeneration() {
const [prompt, setPrompt] = useState('')

const hookReturn = useGenerateVideo({
fetcher: (input) => generateVideoStreamFn({ data: input }),
fetcher: (input) =>
generateVideoStreamFn({
data: { ...input, prompt: resolveMediaPrompt(input.prompt).text },
}),
})

return (
Expand Down
12 changes: 4 additions & 8 deletions examples/ts-react-chat/src/routes/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ import {
import { clientTools } from '@tanstack/ai-client'
import { ThinkingPart } from '@tanstack/ai-react-ui'
import type { UIMessage } from '@tanstack/ai-react'
import type { ContentPart, TranscriptionResult } from '@tanstack/ai'
import type { ContentPart } from '@tanstack/ai'
import type { GeminiInteractionsCustomEventValue } from '@tanstack/ai-gemini/experimental'
import type { ModelOption } from '@/lib/model-selection'
import GuitarRecommendation from '@/components/example-GuitarRecommendation'
Expand Down Expand Up @@ -436,18 +436,14 @@ function ChatPage() {
// Voice input: record from the mic, transcribe via /api/transcribe, then drop
// the text into the composer for the user to review/edit/send. (Text chat
// models don't accept raw audio; transcription is the path that works.)
// NOTE: the explicit type arg works around an inference bug in the generation
// hooks' `onResult` (same root cause as the recorder's `onComplete`, now
// fixed there) — without it, `r` is implicitly `any` and the call won't
// typecheck under strict mode.
// `onResult`'s `r` infers as `TranscriptionResult` from the hook (no explicit
// type arg needed — the generation hooks' result-type inference handles it).
// Surface voice-input failures (permission denied, recorder error,
// transcription error) to the user rather than only logging them — a silent
// mic button is the worst outcome.
const [recordError, setRecordError] = useState<string | null>(null)

const { generate: transcribe, isLoading: isTranscribing } = useTranscription<
(r: TranscriptionResult) => void
>({
const { generate: transcribe, isLoading: isTranscribing } = useTranscription({
connection: fetchServerSentEvents('/api/transcribe'),
onResult: (r) => setInput((prev) => (prev ? `${prev} ${r.text}` : r.text)),
// A failed transcription (network/provider) is just as silent as a mic
Expand Down
1 change: 1 addition & 0 deletions examples/ts-react-native-chat/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
"dev:app": "node -e \"const { spawnSync } = require('node:child_process'); const env = { ...process.env, EXPO_NO_DOTENV: '1' }; delete env.OPENAI_API_KEY; delete env.OPENAI_MODEL; const args = ['exec', 'expo', 'start', '--lan', '--clear']; const result = process.platform === 'win32' ? spawnSync(env.ComSpec ?? 'cmd.exe', ['/d', '/s', '/c', 'pnpm.cmd', ...args], { stdio: 'inherit', env }) : spawnSync('pnpm', args, { stdio: 'inherit', env }); process.exit(result.status ?? 1)\"",
"test:dev-script": "node --test scripts/dev.test.mjs",
"typecheck": "tsc --noEmit",
"test:types": "tsc --noEmit",
"smoke:server": "tsx scripts/smoke-server.ts",
"smoke:expo": "node -e \"const { spawnSync } = require('node:child_process'); const env = { ...process.env, EXPO_NO_DOTENV: '1' }; delete env.OPENAI_API_KEY; delete env.OPENAI_MODEL; const args = ['exec', 'expo', 'export', '--platform', 'ios', '--no-bytecode', '--output-dir', '.expo-example-dist', '--max-workers', '0']; const result = process.platform === 'win32' ? spawnSync(env.ComSpec ?? 'cmd.exe', ['/d', '/s', '/c', 'pnpm.cmd', ...args], { stdio: 'inherit', env }) : spawnSync('pnpm', args, { stdio: 'inherit', env }); process.exit(result.status ?? 1)\"",
"verify:react-resolution": "node scripts/verify-react-resolution.mjs",
Expand Down
15 changes: 1 addition & 14 deletions examples/ts-react-native-chat/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,7 @@
"moduleResolution": "Bundler",
"allowImportingTsExtensions": true,
"noEmit": true,
"types": ["node", "react"],
"paths": {
"@tanstack/ai": ["../../packages/ai/src/index.ts"],
"@tanstack/ai/adapters": ["../../packages/ai/src/activities/index.ts"],
"@tanstack/ai/adapter-internals": [
"../../packages/ai/src/adapter-internals.ts"
],
"@tanstack/ai/client": ["../../packages/ai/src/client.ts"],
"@tanstack/ai-client": ["../../packages/ai-client/src/index.ts"],
"@tanstack/ai-utils": ["../../packages/ai-utils/src/index.ts"],
"@tanstack/ai-openai": ["../../packages/ai-openai/src/index.ts"],
"@tanstack/ai-react": ["../../packages/ai-react/src/index.ts"],
"@tanstack/openai-base": ["../../packages/openai-base/src/index.ts"]
}
"types": ["node", "react"]
},
"include": [
"index.ts",
Expand Down
Loading
Loading