feat: overhaul AI devtools hook dashboard#632
Conversation
📝 WalkthroughWalkthroughAdds a hook-centric devtools UI and store, standardizes event envelopes with run/thread context, integrates Chat/Generation/Video clients with a devtools bridge and tool fixtures, updates example routes (including a Generation Hooks demo), refreshes docs, and expands tests. ChangesHook-centric Devtools, Event Envelopes, and Client Integrations
Sequence Diagram(s)sequenceDiagram
participant Hook as UI Hook (useChat/useGeneration)
participant Client as Chat/Gen/Video Client
participant Bridge as ClientDevtoolsBridge
participant EC as aiEventClient
Hook->>Client: start run / stream
Client->>Bridge: snapshot + run:started
Bridge->>EC: emit(enveloped user-visible/client-state)
EC-->>Hook: devtools:request-state (subscribe)
Client-->>Bridge: updates (text/tools/structured-output/progress)
Bridge->>EC: emit(run updates / hook:state-snapshot)
Client-->>Bridge: run finished/errored/cancelled
Bridge->>EC: emit(run:completed/errored/cancelled)
Estimated code review effort🎯 5 (Critical) | ⏱️ ~120 minutes Possibly related PRs
Suggested reviewers
Poem
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
|
🚀 Changeset Version Preview8 package(s) bumped directly, 15 bumped as dependents. 🟥 Major bumps
🟨 Minor bumps
🟩 Patch bumps
|
|
View your CI Pipeline Execution ↗ for commit 4b0f6ce
☁️ Nx Cloud last updated this comment at |
@tanstack/ai
@tanstack/ai-anthropic
@tanstack/ai-client
@tanstack/ai-code-mode
@tanstack/ai-code-mode-skills
@tanstack/ai-devtools-core
@tanstack/ai-elevenlabs
@tanstack/ai-event-client
@tanstack/ai-fal
@tanstack/ai-gemini
@tanstack/ai-grok
@tanstack/ai-groq
@tanstack/ai-isolate-cloudflare
@tanstack/ai-isolate-node
@tanstack/ai-isolate-quickjs
@tanstack/ai-ollama
@tanstack/ai-openai
@tanstack/ai-openrouter
@tanstack/ai-preact
@tanstack/ai-react
@tanstack/ai-react-ui
@tanstack/ai-solid
@tanstack/ai-solid-ui
@tanstack/ai-svelte
@tanstack/ai-utils
@tanstack/ai-vue
@tanstack/ai-vue-ui
@tanstack/openai-base
@tanstack/preact-ai-devtools
@tanstack/react-ai-devtools
@tanstack/solid-ai-devtools
commit: |
There was a problem hiding this comment.
Actionable comments posted: 7
🧹 Nitpick comments (3)
packages/typescript/ai-event-client/src/envelope.ts (1)
72-102: 💤 Low valueVerify intentional use of module-level
runtimeIdat line 93.Line 82 uses
resolvedRuntimeId(which respectsinput.runtimeId), but line 93 uses the module-levelruntimeIddirectly. If this is intentional (to ensure local uniqueness via the module's own runtime ID + counter), consider adding a brief comment explaining the distinction. Otherwise, line 93 should useresolvedRuntimeIdfor consistency.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/typescript/ai-event-client/src/envelope.ts` around lines 72 - 102, The eventId construction in createAIDevtoolsEventEnvelope mixes resolvedRuntimeId (which respects input.runtimeId) and the module-level runtimeId directly; decide and make it consistent: either replace the literal runtimeId in the eventId parts with resolvedRuntimeId so the generated eventId uses the resolved value throughout, or, if the intent is to combine the resolved runtimeId with the module-level runtimeId for local uniqueness, add a short comment above the array explaining why runtimeId (module-level) is used intentionally; update the array used to build eventId accordingly (references: createAIDevtoolsEventEnvelope, resolvedRuntimeId, runtimeId, eventId).packages/typescript/ai-client/src/chat-client.ts (1)
508-516: 💤 Low valueRemove underscore prefix from used parameter.
The
_fixtureparameter naming convention suggests an unused variable, but it's immediately assigned tofixtureand used throughout the method. Consider renaming to justfixturedirectly in the parameter.Suggested fix
private async applyToolFixture( - _fixture: AIDevtoolsToolFixture, + fixture: AIDevtoolsToolFixture, ): Promise<void> { - const fixture = _fixture const messages = this.processor.getMessages() as Array<UIMessage>🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/typescript/ai-client/src/chat-client.ts` around lines 508 - 516, In applyToolFixture, the parameter is named _fixture (suggesting unused) but immediately assigned to fixture and used; rename the parameter from _fixture to fixture and remove the redundant const fixture = _fixture assignment, then update usages in the function (e.g., threadId = fixture.threadId ?? this.threadId and the call to this.executeToolFixture(fixture, messages, threadId)) so the method uses the parameter directly.packages/typescript/ai-devtools/tests/preview-model.test.ts (1)
54-96: ⚡ Quick winConsider testing with actual DOM elements using happy-dom.
The test currently uses plain objects to simulate
element.dataset, which effectively tests the matching logic. However, ifhoverTargetMatchesElementis designed to work with real DOM elements in production, consider adding integration tests with actual elements using happy-dom to verify the complete flow.As per coding guidelines, Vitest unit tests should use happy-dom for DOM testing.
🧪 Example integration test with happy-dom
+import { beforeEach } from 'vitest' + +describe('preview model with DOM', () => { + beforeEach(() => { + document.body.innerHTML = '' + }) + + it('matches hover targets against real DOM elements', () => { + const target = createHoverTarget({ + messageIds: ['message-1'], + partIds: [toolCallPartId('call-1')], + origin: 'preview', + }) + + const element = document.createElement('div') + element.dataset.aiDevtoolsHoverMessageIds = 'message-1' + element.dataset.aiDevtoolsHoverPartIds = 'tool-call:call-1' + + expect(hoverTargetMatchesElement(target, element.dataset)).toBe(true) + }) +})🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/typescript/ai-devtools/tests/preview-model.test.ts` around lines 54 - 96, Test currently passes plain objects as element.dataset; replace/add an integration variant that uses a real DOM element created via happy-dom and exercise the same flows: create an element via the happy-dom document (e.g., document.createElement('div')), set dataset attributes or setAttribute for the same keys produced by getHoverDataAttributes, then assert hoverTargetMatchesElement(element, ...) and createHoverTargetFromDataAttributes(element.dataset) behave identically to the plain-object expectations; reference the existing helpers createHoverTarget, getHoverDataAttributes, hoverTargetMatchesElement, and createHoverTargetFromDataAttributes to locate where to plug the DOM-backed assertions and ensure the test file uses happy-dom as the Vitest DOM environment.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@examples/ts-react-chat/src/routes/generations.structured-output.tsx`:
- Around line 223-233: In handleGenerate the guard that reports a missing
structured-output.complete is inverted; change the final condition from checking
chat.status !== 'ready' to chat.status === 'ready' so that when stream is true
and sawCompleteRef.current is false and chat.status === 'ready' you call
setError('Stream ended before structured-output.complete'); update the check
that references sawCompleteRef, chat.status and stream in the handleGenerate
function accordingly.
In `@packages/typescript/ai-devtools/src/components/hooks/HookDetails.tsx`:
- Around line 785-831: The part rows rendered in HookDetails (inside the For
over visiblePreviewPartsForMessage(message)) are missing
data-ai-devtools-hover-* attributes, so setHoverTargetFromEvent cannot resolve
part-level hover targets; update the outer div for each part (the element with
class styles().hookDetails.previewPart) to include data attributes such as
data-ai-devtools-hover-type="part",
data-ai-devtools-hover-message-id={message.id} (or message._id), and a unique
part identifier like data-ai-devtools-hover-part={part.key || part.label ||
index} so setHoverTargetFromEvent can detect and distinguish part-level targets
and enable part-level sync/scroll to preview.
In `@packages/typescript/ai-devtools/src/components/hooks/ToolFixtureForm.tsx`:
- Around line 275-283: In parseFieldValue, stop silently truncating integer
inputs: when field.type === 'integer' validate that the parsed Number(rawValue)
is an integer (use Number.isInteger) and throw a descriptive Error like "<name>
must be an integer" if it's not; otherwise return the integer value — replace
the current Math.trunc-based behavior in the parseFieldValue function.
In `@packages/typescript/ai-devtools/src/components/Shell.tsx`:
- Around line 89-99: onCleanup currently removes document event listeners and
emits the devtools:closed event but can leave document.body in drag mode if
handleMouseUp didn't run; update the onCleanup block to also reset global
drag-related styles by setting document.body.style.cursor = '' and
document.body.style.userSelect = '' (or revert to their prior values) so any
drag state set by handleMouseMove/handleMouseUp is cleared; locate the onCleanup
call in Shell.tsx and add these two style resets alongside the existing
document.removeEventListener and
aiEventClient.emit/createAIDevtoolsEventEnvelope logic.
In `@packages/typescript/ai-devtools/src/store/ai-context.tsx`:
- Around line 1341-1348: The normalization for message parts currently discards
the tool-result's non-text output by not including part.output in the returned
object; update the branch that handles part.type === 'tool-result' (the object
constructed with type: 'tool-result', toolCallId, content, state, error) to also
preserve output by adding output: part.output so downstream previews and store
state retain non-text tool results.
In `@packages/typescript/ai-preact/src/use-chat.ts`:
- Around line 160-170: The cleanup effect currently depends on options.live
causing client.dispose() (and client.stop()/client.unsubscribe()) to run when
live toggles; change the effect so it only runs on unmount or when the client
instance changes by removing options.live from the dependency array (i.e. depend
only on client) so that client.mountDevtools() is mounted once per client and
client.dispose() is invoked only on unmount/client replacement; leave the
dedicated subscribe/unsubscribe effect to handle options.live toggling.
In `@packages/typescript/ai-react/src/use-chat.ts`:
- Around line 157-168: The devtools mounting effect currently depends on
options.live and its cleanup always calls client.dispose(), causing the client
to be torn down whenever live toggles; change the effect to only depend on
client (remove options.live from the dependency array) and reference
optionsRef.current.live inside the cleanup to decide between
client.unsubscribe() and client.stop() so the separate live-toggle effect
continues to manage subscribe/unsubscribe; specifically update the useEffect
around client.mountDevtools to use optionsRef.current.live instead of
options.live and remove options.live from the dependency list so
client.dispose() is not invoked on live changes.
---
Nitpick comments:
In `@packages/typescript/ai-client/src/chat-client.ts`:
- Around line 508-516: In applyToolFixture, the parameter is named _fixture
(suggesting unused) but immediately assigned to fixture and used; rename the
parameter from _fixture to fixture and remove the redundant const fixture =
_fixture assignment, then update usages in the function (e.g., threadId =
fixture.threadId ?? this.threadId and the call to
this.executeToolFixture(fixture, messages, threadId)) so the method uses the
parameter directly.
In `@packages/typescript/ai-devtools/tests/preview-model.test.ts`:
- Around line 54-96: Test currently passes plain objects as element.dataset;
replace/add an integration variant that uses a real DOM element created via
happy-dom and exercise the same flows: create an element via the happy-dom
document (e.g., document.createElement('div')), set dataset attributes or
setAttribute for the same keys produced by getHoverDataAttributes, then assert
hoverTargetMatchesElement(element, ...) and
createHoverTargetFromDataAttributes(element.dataset) behave identically to the
plain-object expectations; reference the existing helpers createHoverTarget,
getHoverDataAttributes, hoverTargetMatchesElement, and
createHoverTargetFromDataAttributes to locate where to plug the DOM-backed
assertions and ensure the test file uses happy-dom as the Vitest DOM
environment.
In `@packages/typescript/ai-event-client/src/envelope.ts`:
- Around line 72-102: The eventId construction in createAIDevtoolsEventEnvelope
mixes resolvedRuntimeId (which respects input.runtimeId) and the module-level
runtimeId directly; decide and make it consistent: either replace the literal
runtimeId in the eventId parts with resolvedRuntimeId so the generated eventId
uses the resolved value throughout, or, if the intent is to combine the resolved
runtimeId with the module-level runtimeId for local uniqueness, add a short
comment above the array explaining why runtimeId (module-level) is used
intentionally; update the array used to build eventId accordingly (references:
createAIDevtoolsEventEnvelope, resolvedRuntimeId, runtimeId, eventId).
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: fd30edd5-da12-4872-a9c7-da8b3aa8d16e
📒 Files selected for processing (101)
.changeset/ai-devtools-hook-dashboard.mddocs/getting-started/devtools.mdexamples/ts-react-chat/src/components/Header.tsxexamples/ts-react-chat/src/routeTree.gen.tsexamples/ts-react-chat/src/routes/api.structured-output.tsexamples/ts-react-chat/src/routes/generation-hooks.tsxexamples/ts-react-chat/src/routes/generations.structured-output.tsxpackages/typescript/ai-client/src/chat-client.tspackages/typescript/ai-client/src/devtools.tspackages/typescript/ai-client/src/events.tspackages/typescript/ai-client/src/generation-client.tspackages/typescript/ai-client/src/generation-types.tspackages/typescript/ai-client/src/index.tspackages/typescript/ai-client/src/types.tspackages/typescript/ai-client/src/video-generation-client.tspackages/typescript/ai-client/tests/devtools.test.tspackages/typescript/ai-client/tests/events.test.tspackages/typescript/ai-client/tests/generation-client.test.tspackages/typescript/ai-client/tests/generation-devtools.test.tspackages/typescript/ai-client/tests/video-generation-client.test.tspackages/typescript/ai-devtools/src/components/ConversationDetails.tsxpackages/typescript/ai-devtools/src/components/ConversationsList.tsxpackages/typescript/ai-devtools/src/components/Shell.tsxpackages/typescript/ai-devtools/src/components/conversation/ActivityEventsTab.tsxpackages/typescript/ai-devtools/src/components/conversation/ChunkBadges.tsxpackages/typescript/ai-devtools/src/components/conversation/ChunkItem.tsxpackages/typescript/ai-devtools/src/components/conversation/ChunksCollapsible.tsxpackages/typescript/ai-devtools/src/components/conversation/ChunksTab.tsxpackages/typescript/ai-devtools/src/components/conversation/ConversationHeader.tsxpackages/typescript/ai-devtools/src/components/conversation/ConversationTabs.tsxpackages/typescript/ai-devtools/src/components/conversation/IterationCard.tsxpackages/typescript/ai-devtools/src/components/conversation/IterationTimeline.tsxpackages/typescript/ai-devtools/src/components/conversation/MessageCard.tsxpackages/typescript/ai-devtools/src/components/conversation/MessageGroup.tsxpackages/typescript/ai-devtools/src/components/conversation/MessagesTab.tsxpackages/typescript/ai-devtools/src/components/conversation/SummariesTab.tsxpackages/typescript/ai-devtools/src/components/conversation/ToolCallDisplay.tsxpackages/typescript/ai-devtools/src/components/conversation/index.tspackages/typescript/ai-devtools/src/components/hooks/GenerationPanel.tsxpackages/typescript/ai-devtools/src/components/hooks/HookDashboard.tsxpackages/typescript/ai-devtools/src/components/hooks/HookDetails.tsxpackages/typescript/ai-devtools/src/components/hooks/ToolFixtureForm.tsxpackages/typescript/ai-devtools/src/components/hooks/hook-dashboard-model.tspackages/typescript/ai-devtools/src/components/hooks/index.tspackages/typescript/ai-devtools/src/components/hooks/preview-messages.tspackages/typescript/ai-devtools/src/components/hooks/preview-model.tspackages/typescript/ai-devtools/src/components/list/ConversationRow.tsxpackages/typescript/ai-devtools/src/components/list/index.tspackages/typescript/ai-devtools/src/components/utils/format.tspackages/typescript/ai-devtools/src/store/ai-context.tsxpackages/typescript/ai-devtools/src/store/hook-registry.tspackages/typescript/ai-devtools/src/store/message-event-utils.tspackages/typescript/ai-devtools/src/styles/use-styles.tspackages/typescript/ai-devtools/tests/hook-dashboard-model.test.tspackages/typescript/ai-devtools/tests/hook-registry.test.tspackages/typescript/ai-devtools/tests/message-event-utils.test.tspackages/typescript/ai-devtools/tests/preview-messages.test.tspackages/typescript/ai-devtools/tests/preview-model.test.tspackages/typescript/ai-event-client/src/devtools-middleware.tspackages/typescript/ai-event-client/src/envelope.tspackages/typescript/ai-event-client/src/index.tspackages/typescript/ai-event-client/tests/emit.test.tspackages/typescript/ai-event-client/tests/envelope.test.tspackages/typescript/ai-preact/src/types.tspackages/typescript/ai-preact/src/use-chat.tspackages/typescript/ai-react/src/use-chat.tspackages/typescript/ai-react/src/use-generate-audio.tspackages/typescript/ai-react/src/use-generate-image.tspackages/typescript/ai-react/src/use-generate-speech.tspackages/typescript/ai-react/src/use-generate-video.tspackages/typescript/ai-react/src/use-generation.tspackages/typescript/ai-react/src/use-summarize.tspackages/typescript/ai-react/src/use-transcription.tspackages/typescript/ai-solid/src/use-chat.tspackages/typescript/ai-solid/src/use-generate-audio.tspackages/typescript/ai-solid/src/use-generate-image.tspackages/typescript/ai-solid/src/use-generate-speech.tspackages/typescript/ai-solid/src/use-generate-video.tspackages/typescript/ai-solid/src/use-generation.tspackages/typescript/ai-solid/src/use-summarize.tspackages/typescript/ai-solid/src/use-transcription.tspackages/typescript/ai-svelte/src/create-chat.svelte.tspackages/typescript/ai-svelte/src/create-generate-audio.svelte.tspackages/typescript/ai-svelte/src/create-generate-image.svelte.tspackages/typescript/ai-svelte/src/create-generate-speech.svelte.tspackages/typescript/ai-svelte/src/create-generate-video.svelte.tspackages/typescript/ai-svelte/src/create-generation.svelte.tspackages/typescript/ai-svelte/src/create-summarize.svelte.tspackages/typescript/ai-svelte/src/create-transcription.svelte.tspackages/typescript/ai-svelte/src/types.tspackages/typescript/ai-vue/src/use-chat.tspackages/typescript/ai-vue/src/use-generate-audio.tspackages/typescript/ai-vue/src/use-generate-image.tspackages/typescript/ai-vue/src/use-generate-speech.tspackages/typescript/ai-vue/src/use-generate-video.tspackages/typescript/ai-vue/src/use-generation.tspackages/typescript/ai-vue/src/use-summarize.tspackages/typescript/ai-vue/src/use-transcription.tspackages/typescript/ai/src/activities/chat/index.tspackages/typescript/ai/src/activities/chat/middleware/types.tspackages/typescript/ai/src/activities/chat/stream/processor.ts
💤 Files with no reviewable changes (18)
- packages/typescript/ai-devtools/src/components/conversation/ToolCallDisplay.tsx
- packages/typescript/ai-devtools/src/components/conversation/ChunksTab.tsx
- packages/typescript/ai-devtools/src/components/list/ConversationRow.tsx
- packages/typescript/ai-devtools/src/components/ConversationsList.tsx
- packages/typescript/ai-devtools/src/components/conversation/SummariesTab.tsx
- packages/typescript/ai-devtools/src/components/conversation/ChunkBadges.tsx
- packages/typescript/ai-devtools/src/components/list/index.ts
- packages/typescript/ai-devtools/src/components/conversation/ConversationTabs.tsx
- packages/typescript/ai-devtools/src/components/conversation/ActivityEventsTab.tsx
- packages/typescript/ai-devtools/src/components/conversation/ConversationHeader.tsx
- packages/typescript/ai-devtools/src/components/conversation/ChunkItem.tsx
- packages/typescript/ai-devtools/src/components/ConversationDetails.tsx
- packages/typescript/ai-devtools/src/components/conversation/index.ts
- packages/typescript/ai-devtools/src/components/conversation/MessageCard.tsx
- packages/typescript/ai-devtools/src/components/utils/format.ts
- packages/typescript/ai-devtools/src/components/conversation/MessagesTab.tsx
- packages/typescript/ai-devtools/src/components/conversation/MessageGroup.tsx
- packages/typescript/ai-devtools/src/components/conversation/ChunksCollapsible.tsx
| const handleGenerate = async () => { | ||
| if (!prompt.trim()) return | ||
| sawCompleteRef.current = false | ||
| resetLocal() | ||
| chat.clear() | ||
| setIsStreaming(stream) | ||
| await chat.sendMessage(prompt.trim()) | ||
| setIsStreaming(false) | ||
| if (stream && !sawCompleteRef.current && chat.status !== 'ready') { | ||
| setError('Stream ended before structured-output.complete') | ||
| } |
There was a problem hiding this comment.
Fix the streamed completion guard condition.
On Line 231, the condition is inverted. A stream that ends in ready state without receiving structured-output.complete will currently pass silently.
Suggested fix
const handleGenerate = async () => {
if (!prompt.trim()) return
sawCompleteRef.current = false
resetLocal()
chat.clear()
setIsStreaming(stream)
- await chat.sendMessage(prompt.trim())
- setIsStreaming(false)
- if (stream && !sawCompleteRef.current && chat.status !== 'ready') {
- setError('Stream ended before structured-output.complete')
- }
+ await chat.sendMessage(prompt.trim())
+ setIsStreaming(false)
+ if (stream && !sawCompleteRef.current && chat.status === 'ready') {
+ setError('Stream ended before structured-output.complete')
+ }
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const handleGenerate = async () => { | |
| if (!prompt.trim()) return | |
| sawCompleteRef.current = false | |
| resetLocal() | |
| chat.clear() | |
| setIsStreaming(stream) | |
| await chat.sendMessage(prompt.trim()) | |
| setIsStreaming(false) | |
| if (stream && !sawCompleteRef.current && chat.status !== 'ready') { | |
| setError('Stream ended before structured-output.complete') | |
| } | |
| const handleGenerate = async () => { | |
| if (!prompt.trim()) return | |
| sawCompleteRef.current = false | |
| resetLocal() | |
| chat.clear() | |
| setIsStreaming(stream) | |
| await chat.sendMessage(prompt.trim()) | |
| setIsStreaming(false) | |
| if (stream && !sawCompleteRef.current && chat.status === 'ready') { | |
| setError('Stream ended before structured-output.complete') | |
| } | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@examples/ts-react-chat/src/routes/generations.structured-output.tsx` around
lines 223 - 233, In handleGenerate the guard that reports a missing
structured-output.complete is inverted; change the final condition from checking
chat.status !== 'ready' to chat.status === 'ready' so that when stream is true
and sawCompleteRef.current is false and chat.status === 'ready' you call
setError('Stream ended before structured-output.complete'); update the check
that references sawCompleteRef, chat.status and stream in the handleGenerate
function accordingly.
| <For each={visiblePreviewPartsForMessage(message)}> | ||
| {(part) => ( | ||
| <div class={styles().hookDetails.previewPart}> | ||
| <span class={styles().hookDetails.previewPartLabel}> | ||
| {part.label} | ||
| </span> | ||
| <Show | ||
| when={part.jsonItems?.length} | ||
| fallback={ | ||
| <span class={styles().hookDetails.previewPartContent}> | ||
| {part.content} | ||
| </span> | ||
| } | ||
| > | ||
| <div | ||
| class={`${styles().hookDetails.previewJsonItems} ${ | ||
| part.kind === 'structured-output' | ||
| ? styles().hookDetails.previewJsonItemsCompare | ||
| : '' | ||
| }`} | ||
| > | ||
| <For each={part.jsonItems}> | ||
| {(item) => ( | ||
| <div class={styles().hookDetails.previewJsonItem}> | ||
| <span | ||
| class={ | ||
| styles().hookDetails.previewJsonItemLabel | ||
| } | ||
| > | ||
| {item.label} | ||
| </span> | ||
| <div | ||
| class={styles().hookDetails.previewJsonPanel} | ||
| > | ||
| <JsonTree | ||
| value={item.value} | ||
| defaultExpansionDepth={1} | ||
| copyable | ||
| /> | ||
| </div> | ||
| </div> | ||
| )} | ||
| </For> | ||
| </div> | ||
| </Show> | ||
| </div> | ||
| )} |
There was a problem hiding this comment.
Add part-level hover data in the timeline pane.
Line 787 renders part rows without data-ai-devtools-hover-*, so setHoverTargetFromEvent can only resolve message-level targets from this pane. Part-level sync/scroll to preview won’t trigger here.
Suggested patch
- <div class={styles().hookDetails.previewPart}>
+ <div
+ {...getHoverDataAttributes({
+ messageIds: [message.id],
+ partIds: [part.id],
+ })}
+ class={styles().hookDetails.previewPart}
+ >🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/typescript/ai-devtools/src/components/hooks/HookDetails.tsx` around
lines 785 - 831, The part rows rendered in HookDetails (inside the For over
visiblePreviewPartsForMessage(message)) are missing data-ai-devtools-hover-*
attributes, so setHoverTargetFromEvent cannot resolve part-level hover targets;
update the outer div for each part (the element with class
styles().hookDetails.previewPart) to include data attributes such as
data-ai-devtools-hover-type="part",
data-ai-devtools-hover-message-id={message.id} (or message._id), and a unique
part identifier like data-ai-devtools-hover-part={part.key || part.label ||
index} so setHoverTargetFromEvent can detect and distinguish part-level targets
and enable part-level sync/scroll to preview.
| function parseFieldValue(field: SchemaField, rawValue: string): unknown { | ||
| if (field.type === 'boolean') return rawValue === 'true' | ||
| if (field.type === 'number' || field.type === 'integer') { | ||
| const value = Number(rawValue) | ||
| if (Number.isNaN(value)) { | ||
| throw new Error(`${field.name} must be a number.`) | ||
| } | ||
| return field.type === 'integer' ? Math.trunc(value) : value | ||
| } |
There was a problem hiding this comment.
Reject non-integer values instead of truncating them.
Line 282 silently mutates integer input with Math.trunc. That can produce incorrect fixture payloads without surfacing an error.
Suggested patch
function parseFieldValue(field: SchemaField, rawValue: string): unknown {
if (field.type === 'boolean') return rawValue === 'true'
- if (field.type === 'number' || field.type === 'integer') {
- const value = Number(rawValue)
- if (Number.isNaN(value)) {
- throw new Error(`${field.name} must be a number.`)
- }
- return field.type === 'integer' ? Math.trunc(value) : value
- }
+ if (field.type === 'number') {
+ const value = Number(rawValue)
+ if (!Number.isFinite(value)) {
+ throw new Error(`${field.name} must be a number.`)
+ }
+ return value
+ }
+ if (field.type === 'integer') {
+ const value = Number(rawValue)
+ if (!Number.isInteger(value)) {
+ throw new Error(`${field.name} must be an integer.`)
+ }
+ return value
+ }📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| function parseFieldValue(field: SchemaField, rawValue: string): unknown { | |
| if (field.type === 'boolean') return rawValue === 'true' | |
| if (field.type === 'number' || field.type === 'integer') { | |
| const value = Number(rawValue) | |
| if (Number.isNaN(value)) { | |
| throw new Error(`${field.name} must be a number.`) | |
| } | |
| return field.type === 'integer' ? Math.trunc(value) : value | |
| } | |
| function parseFieldValue(field: SchemaField, rawValue: string): unknown { | |
| if (field.type === 'boolean') return rawValue === 'true' | |
| if (field.type === 'number') { | |
| const value = Number(rawValue) | |
| if (!Number.isFinite(value)) { | |
| throw new Error(`${field.name} must be a number.`) | |
| } | |
| return value | |
| } | |
| if (field.type === 'integer') { | |
| const value = Number(rawValue) | |
| if (!Number.isInteger(value)) { | |
| throw new Error(`${field.name} must be an integer.`) | |
| } | |
| return value | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/typescript/ai-devtools/src/components/hooks/ToolFixtureForm.tsx`
around lines 275 - 283, In parseFieldValue, stop silently truncating integer
inputs: when field.type === 'integer' validate that the parsed Number(rawValue)
is an integer (use Number.isInteger) and throw a descriptive Error like "<name>
must be an integer" if it's not; otherwise return the integer value — replace
the current Math.trunc-based behavior in the parseFieldValue function.
| onCleanup(() => { | ||
| document.removeEventListener('mousemove', handleMouseMove) | ||
| document.removeEventListener('mouseup', handleMouseUp) | ||
| aiEventClient.emit('devtools:closed', { | ||
| ...createAIDevtoolsEventEnvelope({ | ||
| eventType: 'devtools:closed', | ||
| source: 'devtools', | ||
| visibility: 'devtools-action', | ||
| timestamp: Date.now(), | ||
| }), | ||
| }) |
There was a problem hiding this comment.
Reset global drag styles during cleanup.
Line 89-Line 99 can leave document.body in drag mode if unmount happens before handleMouseUp fires. Reset cursor and userSelect in onCleanup as well.
Suggested fix
onCleanup(() => {
document.removeEventListener('mousemove', handleMouseMove)
document.removeEventListener('mouseup', handleMouseUp)
+ setIsDragging(false)
+ document.body.style.cursor = ''
+ document.body.style.userSelect = ''
aiEventClient.emit('devtools:closed', {
...createAIDevtoolsEventEnvelope({
eventType: 'devtools:closed',🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/typescript/ai-devtools/src/components/Shell.tsx` around lines 89 -
99, onCleanup currently removes document event listeners and emits the
devtools:closed event but can leave document.body in drag mode if handleMouseUp
didn't run; update the onCleanup block to also reset global drag-related styles
by setting document.body.style.cursor = '' and document.body.style.userSelect =
'' (or revert to their prior values) so any drag state set by
handleMouseMove/handleMouseUp is cleared; locate the onCleanup call in Shell.tsx
and add these two style resets alongside the existing
document.removeEventListener and
aiEventClient.emit/createAIDevtoolsEventEnvelope logic.
| if (part.type === 'tool-result') { | ||
| return { | ||
| type: 'tool-result', | ||
| toolCallId: part.toolCallId, | ||
| content: part.content, | ||
| state: part.state, | ||
| error: part.error, | ||
| } |
There was a problem hiding this comment.
Preserve tool-result output when normalizing message parts.
Line 1341-1348 drops part.output, which loses non-text tool results in store state and downstream previews.
Suggested patch
if (part.type === 'tool-result') {
return {
type: 'tool-result',
toolCallId: part.toolCallId,
+ output: part.output,
content: part.content,
state: part.state,
error: part.error,
}
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| if (part.type === 'tool-result') { | |
| return { | |
| type: 'tool-result', | |
| toolCallId: part.toolCallId, | |
| content: part.content, | |
| state: part.state, | |
| error: part.error, | |
| } | |
| if (part.type === 'tool-result') { | |
| return { | |
| type: 'tool-result', | |
| toolCallId: part.toolCallId, | |
| output: part.output, | |
| content: part.content, | |
| state: part.state, | |
| error: part.error, | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/typescript/ai-devtools/src/store/ai-context.tsx` around lines 1341 -
1348, The normalization for message parts currently discards the tool-result's
non-text output by not including part.output in the returned object; update the
branch that handles part.type === 'tool-result' (the object constructed with
type: 'tool-result', toolCallId, content, state, error) to also preserve output
by adding output: part.output so downstream previews and store state retain
non-text tool results.
| client.mountDevtools() | ||
|
|
||
| return () => { | ||
| if (options.live) { | ||
| client.unsubscribe() | ||
| } else { | ||
| client.stop() | ||
| } | ||
| client.dispose() | ||
| } | ||
| }, [client, options.live]) |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Locate the file and print the relevant section with line numbers
FILE="packages/typescript/ai-preact/src/use-chat.ts"
if [ ! -f "$FILE" ]; then
echo "Missing file: $FILE"
exit 1
fi
echo "== File: $FILE =="
# Print a window around the referenced lines
sed -n '130,220p' "$FILE" | nl -ba -w2 -s': ' | sed -n '1,120p'
echo
echo "== Find all occurrences of options.live / optionsRef / dispose / stop / unsubscribe in this file =="
rg -n "optionsRef\.current\.live|options\.live|dispose\(|unsubscribe\(|\.stop\(" "$FILE" || true
echo
echo "== Check dependencies of the relevant useEffect by printing its full header/closure if nearby =="
# Print from the start of the effect definition region (best-effort: lines above 150)
sed -n '140,200p' "$FILE" | nl -ba -w2 -s': ' || trueRepository: TanStack/ai
Length of output: 175
🏁 Script executed:
#!/bin/bash
set -euo pipefail
FILE="packages/typescript/ai-preact/src/use-chat.ts"
if [ ! -f "$FILE" ]; then
echo "Missing file: $FILE"
exit 1
fi
echo "== File: $FILE =="
# Print a window around the referenced lines with line numbers (no nl)
sed -n '130,220p' "$FILE" | cat -n -ba -w1
echo
echo "== Find occurrences in this file =="
rg -n "optionsRef\.current\.live|options\.live|client\.dispose\(|client\.unsubscribe\(|client\.stop\(" "$FILE" || true
echo
echo "== Show the surrounding useEffect header/body for the first dispose() near the region =="
# Grab a slightly larger region for context
sed -n '120,210p' "$FILE" | cat -n -ba -w1Repository: TanStack/ai
Length of output: 197
🏁 Script executed:
#!/bin/bash
set -euo pipefail
FILE="packages/typescript/ai-preact/src/use-chat.ts"
if [ ! -f "$FILE" ]; then
echo "Missing file: $FILE"
exit 1
fi
print_with_lines () {
# usage: print_with_lines start end
local start="$1" end="$2"
sed -n "${start},${end}p" "$FILE" | awk '{printf "%4d:%s\n", (NR+('"${start}"'-1)), $0}'
}
echo "== File: $FILE =="
echo
echo "== Window lines 120-210 (with line numbers) =="
print_with_lines 120 210
echo
echo "== Search for live/optionsRef usage =="
rg -n "optionsRef\.current\.live|options\.live" "$FILE" || true
echo
echo "== Search for dispose/stop/unsubscribe usage =="
rg -n "client\.dispose\(|client\.unsubscribe\(|client\.stop\(" "$FILE" || true
echo
echo "== Search for useEffect dependency arrays involving options.live =="
# show any effect lines that mention options.live in dependencies
rg -n "useEffect\\(.*\\[.*options\\.live.*\\]" "$FILE" || true
# also show nearby lines if effect header is split; best-effort heuristic
rg -n "useEffect\\(" "$FILE" | head -n 30 || trueRepository: TanStack/ai
Length of output: 3662
🏁 Script executed:
#!/bin/bash
set -euo pipefail
FILE="packages/typescript/ai-preact/src/use-chat.ts"
echo "== Search for optionsRef definition/update in this file =="
rg -n "optionsRef" "$FILE"
echo
echo "== Window around optionsRef definition (best-effort) =="
# Try to locate first occurrence line number and print some context
first=$(rg -n "optionsRef" "$FILE" | head -n1 | cut -d: -f1)
start=$((first-30))
end=$((first+50))
if [ "$start" -lt 1 ]; then start=1; fi
sed -n "${start},${end}p" "$FILE" | awk '{print NR+('"$start"'-1)":"$0}'
echo
echo "== Search for assignment to optionsRef.current =="
rg -n "optionsRef\\.current\\s*=" "$FILE" || true
echo
echo "== Search for mount/stop/dispose client creation site (best-effort) =="
# Show from top to around where client is created
sed -n '1,120p' "$FILE" | awk '{print NR":"$0}' | head -n 120Repository: TanStack/ai
Length of output: 8833
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== Search for ChatClient dispose implementation in repo =="
rg -n "dispose\\(\\)" packages -S || true
echo
echo "== Search for class ChatClient definition =="
rg -n "class ChatClient" -S . || true
echo
echo "== Search for 'dispose:' or '.dispose =' patterns =="
rg -n "dispose\s*[:=]" -S packages || true
echo
echo "== Search for 'stop()' and 'unsubscribe()' implementations nearby =="
rg -n "unsubscribe\\(\\)|stop\\(\\)" packages -S || trueRepository: TanStack/ai
Length of output: 26330
🏁 Script executed:
#!/bin/bash
set -euo pipefail
CHAT_CLIENT="packages/typescript/ai-client/src/chat-client.ts"
echo "== ChatClient dispose implementation =="
# Show dispose() method region
rg -n "dispose\\(\\): void" "$CHAT_CLIENT"
rg -n "dispose\\(\\)" "$CHAT_CLIENT" | head -n 20 || true
# Print around dispose
DISPOSE_LINE=$(rg -n "dispose\\(\\): void" "$CHAT_CLIENT" | head -n1 | cut -d: -f1)
if [ -n "${DISPOSE_LINE:-}" ]; then
START=$((DISPOSE_LINE-80))
END=$((DISPOSE_LINE+80))
sed -n "${START},${END}p" "$CHAT_CLIENT" | awk '{printf "%4d:%s\n", NR+('"$START"'-1), $0}'
fi
echo
echo "== ChatClient unsubscribe() / stop() implementations =="
for FN in "unsubscribe" "stop" "subscribe"; do
LINE=$(rg -n " ${FN}\\(\\): void" "$CHAT_CLIENT" | head -n1 | cut -d: -f1 || true)
if [ -z "${LINE:-}" ]; then
LINE=$(rg -n "${FN}\\(\\): void" "$CHAT_CLIENT" | head -n1 | cut -d: -f1 || true)
fi
if [ -n "${LINE:-}" ]; then
START=$((LINE-40))
END=$((LINE+60))
echo "-- $FN at line $LINE --"
sed -n "${START},${END}p" "$CHAT_CLIENT" | awk '{printf "%4d:%s\n", NR+('"$START"'-1), $0}'
fi
done
echo
echo "== Compare other frameworks' use-chat cleanup for options.live dependency =="
for HOOK in "packages/typescript/ai-react/src/use-chat.ts" "packages/typescript/ai-solid/src/use-chat.ts"; do
if [ -f "$HOOK" ]; then
echo "-- $HOOK --"
rg -n "options\\.live|dispose\\(|useEffect\\(" "$HOOK" || true
# show window around cleanup effect that depends on options.live (best effort)
MATCH=$(rg -n "}, \\[client, options\\.live\\]" "$HOOK" | head -n1 || true)
if [ -n "${MATCH:-}" ]; then
L=$(echo "$MATCH" | cut -d: -f1)
START=$((L-60)); END=$((L+40))
sed -n "${START},${END}p" "$HOOK" | awk '{printf "%4d:%s\n", NR+('"$START"'-1), $0}'
fi
fi
doneRepository: TanStack/ai
Length of output: 16229
🏁 Script executed:
#!/bin/bash
set -euo pipefail
CHAT_CLIENT="packages/typescript/ai-client/src/chat-client.ts"
echo "== Search for mountDevtools implementation =="
rg -n "mountDevtools\\(" "$CHAT_CLIENT"
echo
echo "== Show mountDevtools region =="
LINE=$(rg -n "mountDevtools\\(" "$CHAT_CLIENT" | head -n1 | cut -d: -f1)
START=$((LINE-80)); END=$((LINE+120))
sed -n "${START},${END}p" "$CHAT_CLIENT" | awk '{printf "%4d:%s\n", NR+('"$START"'-1), $0}'
echo
echo "== Search for devtoolsBridge setup/dispose fields =="
rg -n "devtoolsBridge" "$CHAT_CLIENT" | head -n 50
echo
echo "== Show ChatClient constructor/devtoolsBridge init (best-effort) =="
# find constructor region
CTOR=$(rg -n "constructor\\(" "$CHAT_CLIENT" | head -n1 | cut -d: -f1 || true)
if [ -n "${CTOR:-}" ]; then
START=$((CTOR-60)); END=$((CTOR+140))
sed -n "${START},${END}p" "$CHAT_CLIENT" | awk '{printf "%4d:%s\n", NR+('"$START"'-1), $0}'
fi
echo
echo "== Search for devtoolsMounted usage =="
rg -n "devtoolsMounted" "$CHAT_CLIENT"Repository: TanStack/ai
Length of output: 17948
🏁 Script executed:
#!/bin/bash
set -euo pipefail
DEVTOOLS="packages/typescript/ai-client/src/devtools.ts"
echo "== Search for ClientDevtoolsBridge dispose() =="
rg -n "class ClientDevtoolsBridge|dispose\\(\\)" "$DEVTOOLS"
echo
echo "== Show ClientDevtoolsBridge class (around its dispose) =="
# locate dispose method definition
DISPOSE_LINE=$(rg -n "dispose\\(\\):" "$DEVTOOLS" | head -n 1 | cut -d: -f1 || true)
if [ -n "${DISPOSE_LINE:-}" ]; then
START=$((DISPOSE_LINE-80))
END=$((DISPOSE_LINE+80))
sed -n "${START},${END}p" "$DEVTOOLS" | awk '{printf "%4d:%s\n", NR+('"$START"'-1), $0}'
else
# fallback: print small region around first dispose occurrence
sed -n '1,260p' "$DEVTOOLS" | head -n 260
fi
echo
echo "== Search for 'disposed' flag usage =="
rg -n "disposed|isDisposed|destroy" "$DEVTOOLS" || trueRepository: TanStack/ai
Length of output: 5221
🏁 Script executed:
#!/bin/bash
set -euo pipefail
CHAT_CLIENT="packages/typescript/ai-client/src/chat-client.ts"
echo "== Search for 'disposed' in chat-client.ts =="
rg -n "disposed" "$CHAT_CLIENT"
echo
echo "== Search for devtoolsBridge disposed/prepare gating references in chat-client.ts =="
rg -n "prepareForEmit|emitRegistered|emitToolsRegistered|emitSnapshot" "$CHAT_CLIENT"
echo
echo "== Search for guards in public methods: subscribe/unsubscribe/sendMessage/append/reload/stop =="
rg -n "subscribe\\(|unsubscribe\\(|sendMessage\\(|append\\(|reload\\(|stop\\(" "$CHAT_CLIENT" | head -n 50Repository: TanStack/ai
Length of output: 101
🏁 Script executed:
#!/bin/bash
set -euo pipefail
DEVTOOLS="packages/typescript/ai-client/src/devtools.ts"
echo "== Find emitRegistered/emitSnapshot implementations =="
rg -n "emitRegistered\\(|emitSnapshot\\(|emitToolsRegistered\\(|prepareForMountEmit\\(|prepareForEmit\\(" "$DEVTOOLS"
echo
echo "== Show regions around emitRegistered / emitSnapshot / emitToolsRegistered =="
for FN in "emitRegistered" "emitSnapshot" "emitToolsRegistered"; do
LINE=$(rg -n " ${FN}\\(" "$DEVTOOLS" | head -n1 | cut -d: -f1 || true)
if [ -z "${LINE:-}" ]; then
continue
fi
START=$((LINE-40)); END=$((LINE+60))
echo "-- $FN around line $LINE --"
sed -n "${START},${END}p" "$DEVTOOLS" | awk '{printf "%4d:%s\n", (NR+('"$START"'-1)), $0}'
done
echo
echo "== Show prepareForEmit / prepareForMountEmit =="
for FN in "prepareForEmit" "prepareForMountEmit"; do
LINE=$(rg -n " ${FN}\\(" "$DEVTOOLS" | head -n1 | cut -d: -f1 || true)
if [ -z "${LINE:-}" ]; then
continue
fi
START=$((LINE-20)); END=$((LINE+60))
echo "-- $FN around line $LINE --"
sed -n "${START},${END}p" "$DEVTOOLS" | awk '{printf "%4d:%s\n", (NR+('"$START"'-1)), $0}'
doneRepository: TanStack/ai
Length of output: 14880
🏁 Script executed:
#!/bin/bash
set -euo pipefail
SOLID_FILE="packages/typescript/ai-solid/src/use-chat.ts"
if [ ! -f "$SOLID_FILE" ]; then
echo "Missing: $SOLID_FILE"
exit 1
fi
echo "== Solid use-chat live/devtools/cleanup around dispose =="
# Print around line ~160-220 with line numbers using awk
sed -n '130,230p' "$SOLID_FILE" | awk '{printf "%4d:%s\n", NR+129, $0}'
echo
echo "== Search for options.live in solid use-chat =="
rg -n "options\.live|dispose\\(" "$SOLID_FILE"Repository: TanStack/ai
Length of output: 3567
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Search for live toggling in preact/react tests
rg -n "live\\s*:\\s*(true|false)|options\\.live|setProps\\(\\{[^}]*live|toggle.*live|live\\)" packages/typescript/ai-preact/tests/use-chat.test.ts packages/typescript/ai-react/tests/use-chat.test.ts packages/typescript/ai-solid/tests/use-chat.test.ts || true
echo "----"
# Also search for dispose() expectations in these tests
rg -n "dispose\\(" packages/typescript/ai-preact/tests/use-chat.test.ts packages/typescript/ai-react/tests/use-chat.test.ts || true
echo "----"
# Search for "mountDevtools" assertions
rg -n "mountDevtools|devtools" packages/typescript/ai-preact/tests/use-chat.test.ts packages/typescript/ai-react/tests/use-chat.test.ts || trueRepository: TanStack/ai
Length of output: 725
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Helper to print a window with line numbers
print_window () {
local file="$1" start="$2" end="$3"
sed -n "${start},${end}p" "$file" | awk '{printf "%4d:%s\n", (NR+('"$start"'-1)), $0}'
}
FILES=(
packages/typescript/ai-preact/tests/use-chat.test.ts
packages/typescript/ai-react/tests/use-chat.test.ts
packages/typescript/ai-solid/tests/use-chat.test.ts
)
for f in "${FILES[@]}"; do
echo "================ $f ================"
# show renderUseChat setup near top of test file
print_window "$f" 1 120
echo
echo "-- occurrences of live / dispose / unsubscribe in $f --"
rg -n "live\\s*:|options\\.live|toggle.*live|rerender|setProps|dispose\\(|unsubscribe\\(|subscribe\\(|mountDevtools\\(" "$f" || true
echo
echo "-- inspect around first live:true occurrence --"
line=$(rg -n "live\\s*:\\s*true" "$f" | head -n1 | cut -d: -f1 || true)
if [ -n "${line:-}" ]; then
start=$((line-80)); end=$((line+120))
[ $start -lt 1 ] && start=1
print_window "$f" "$start" "$end"
fi
doneRepository: TanStack/ai
Length of output: 34326
Avoid disposing the chat client when options.live toggles in use-chat.
The devtools/cleanup effect depends on options.live, so flipping live runs the cleanup for the existing client instance and calls client.dispose() (and client.stop()/client.unsubscribe() based on the previous options.live value). Since options.live is already handled by the dedicated subscribe/unsubscribe effect, cleanup should only run on unmount/client replacement.
Suggested fix
useEffect(() => {
client.mountDevtools()
return () => {
- if (options.live) {
+ if (optionsRef.current.live) {
client.unsubscribe()
} else {
client.stop()
}
client.dispose()
}
- }, [client, options.live])
+ }, [client])🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/typescript/ai-preact/src/use-chat.ts` around lines 160 - 170, The
cleanup effect currently depends on options.live causing client.dispose() (and
client.stop()/client.unsubscribe()) to run when live toggles; change the effect
so it only runs on unmount or when the client instance changes by removing
options.live from the dependency array (i.e. depend only on client) so that
client.mountDevtools() is mounted once per client and client.dispose() is
invoked only on unmount/client replacement; leave the dedicated
subscribe/unsubscribe effect to handle options.live toggling.
| useEffect(() => { | ||
| client.mountDevtools() | ||
|
|
||
| return () => { | ||
| if (options.live) { | ||
| client.unsubscribe() | ||
| } else { | ||
| client.stop() | ||
| } | ||
| client.dispose() | ||
| } | ||
| }, [client, options.live]) |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Locate the target file and print the relevant section around the reported lines
FILE="packages/typescript/ai-react/src/use-chat.ts"
echo "== File check =="
ls -l "$FILE"
echo "== Snippet around lines 140-200 =="
nl -ba "$FILE" | sed -n '140,210p'
echo "== Search for optionsRef usage =="
rg -n "optionsRef" "$FILE" || true
echo "== Search for live toggling / unsubscribe / stop / dispose patterns =="
rg -n "live|unsubscribe\(|client\.stop\(|client\.dispose\(|mountDevtools\(" "$FILE"Repository: TanStack/ai
Length of output: 263
🏁 Script executed:
#!/bin/bash
set -euo pipefail
FILE="packages/typescript/ai-react/src/use-chat.ts"
echo "== File check =="
ls -l "$FILE"
echo "== Snippet around lines 140-210 (with line numbers) =="
awk 'NR>=140 && NR<=210 {printf "%d\t%s\n", NR, $0}' "$FILE" | sed -n '1,120p'
echo "== Search for optionsRef usage =="
rg -n "optionsRef" "$FILE" || true
echo "== Search for relevant client teardown calls =="
rg -n "dispose\(|unsubscribe\(|client\.stop\(|mountDevtools\(" "$FILE" || true
echo "== Search for the specific useEffect =="
rg -n "useEffect\\(\\(\\)\\s*=>|useEffect\\(" "$FILE" || true
echo "== Dump the useEffect block containing dispose (if found) =="
# Grab some context around dispose call(s)
rg -n "dispose\\(\\)" "$FILE" | while read -r line; do
ln="${line%%:*}"
start=$((ln-25))
end=$((ln+25))
echo "--- context around dispose at line $ln (showing $start-$end) ---"
awk -v s="$start" -v e="$end" 'NR>=s && NR<=e {printf "%d\t%s\n", NR, $0}' "$FILE"
doneRepository: TanStack/ai
Length of output: 4159
Prevent chat client teardown when live changes.
The devtools/mount useEffect reruns on every options.live toggle ([client, options.live]) and its cleanup always calls client.dispose(), even though subscribe/unsubscribe is already handled by the separate [client, options.live] effect. Use optionsRef.current.live and remove options.live from the dependency array.
Suggested fix
useEffect(() => {
client.mountDevtools()
return () => {
- if (options.live) {
+ if (optionsRef.current.live) {
client.unsubscribe()
} else {
client.stop()
}
client.dispose()
}
- }, [client, options.live])
+ }, [client])🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/typescript/ai-react/src/use-chat.ts` around lines 157 - 168, The
devtools mounting effect currently depends on options.live and its cleanup
always calls client.dispose(), causing the client to be torn down whenever live
toggles; change the effect to only depend on client (remove options.live from
the dependency array) and reference optionsRef.current.live inside the cleanup
to decide between client.unsubscribe() and client.stop() so the separate
live-toggle effect continues to manage subscribe/unsubscribe; specifically
update the useEffect around client.mountDevtools to use optionsRef.current.live
instead of options.live and remove options.live from the dependency list so
client.dispose() is not invoked on live changes.
Summary
useChatTest Plan
pnpm formatpnpm test/generations/structured-outputregistersuseChatas a Structured hook in TanStack AI devtoolsSummary by CodeRabbit
Release Notes
New Features
Improvements