Skip to content

feat: overhaul AI devtools hook dashboard#632

Open
AlemTuzlak wants to merge 1 commit into
mainfrom
feat/ai-devtools-overhaul
Open

feat: overhaul AI devtools hook dashboard#632
AlemTuzlak wants to merge 1 commit into
mainfrom
feat/ai-devtools-overhaul

Conversation

@AlemTuzlak
Copy link
Copy Markdown
Contributor

@AlemTuzlak AlemTuzlak commented May 24, 2026

Summary

  • add a hook-first AI devtools dashboard with active hook lifecycle tracking, categories, unread activity, run linking, fixture-aware tool replay, and generation previews
  • expand chat inspection with grouped runs/events, structured-output raw/partial rendering, tool approvals, token/usage surfaces, and synced conversation/user-view highlighting
  • wire devtools metadata through chat and generation hooks across framework adapters, plus React example coverage for generation hooks and structured-output useChat

Test Plan

  • pnpm format
  • pnpm test
  • Manual: verified /generations/structured-output registers useChat as a Structured hook in TanStack AI devtools

Summary by CodeRabbit

Release Notes

  • New Features

    • Hook-focused DevTools dashboard with real-time activity monitoring and categorized hook management
    • Tool fixture capture and replay system for testing AI tool interactions
    • Structured output streaming support with live previews
    • Generation hooks visualization including image, audio, video, and speech outputs
    • Enhanced tool inspection with approval tracking and state snapshots
  • Improvements

    • Redesigned DevTools UI emphasizing hook insights and conversation details
    • Added run timeline with visual progress tracking and phase counting
    • Improved fixture management with localStorage persistence and form generation from schemas

Review Change Stack

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 24, 2026

📝 Walkthrough

Walkthrough

Adds 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.

Changes

Hook-centric Devtools, Event Envelopes, and Client Integrations

Layer / File(s) Summary
Envelope and runtime dispatch
packages/typescript/ai-event-client/src/*
Adds event envelopes (source/visibility/ids), runtime id/dedupe, structured-output events, middleware context (runId/threadId), and emit/DOM dispatch with tests.
Client bridge and integrations
packages/typescript/ai-client/src/*, packages/typescript/ai-react/*, packages/typescript/ai-preact/*
Implements ClientDevtoolsBridge, run-context tracking, snapshots, structured-output emissions, tool fixture replay; integrates in chat/generation/video clients and React/Preact hooks; exposes devtools types/APIs.
Devtools store and UI overhaul
packages/typescript/ai-devtools/src/*
Replaces conversation UI with HookDashboard/HookDetails and GenerationPanel; adds preview/hover models and ToolFixtureForm; expands store for hooks/runs/fixtures/structured-output/audio; major style updates.
Examples: Generation Hooks and structured-output
examples/ts-react-chat/src/*
Adds /generation-hooks route and UI demonstrating generation hooks via fixture streams; updates structured-output API to SSE with phase counts; adjusts header and route tree.
Docs and changeset
.changeset/*, docs/getting-started/devtools.md
Documents hook dashboard, tool fixtures, event sources; records release note.
Tests
packages/**/tests/*
Adds/updates tests for envelopes/dispatch, clients with devtools, hook registry/dashboard, preview utilities, and video/generation behavior.

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)
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

  • TanStack/ai#511: Also changes chat-client run lifecycle and id propagation, overlapping with this PR’s run/context instrumentation.

Suggested reviewers

  • crutchcorn
  • tombeckenham

Poem

A rabbit taps the dashboard lights,
Hooks hop in rows, with streaming bites.
Runs sprout tails of phase-count bloom,
Tools replay fixtures—whoosh, vroom vroom!
Envelopes seal each event in flight—
I nibble tests; all greens tonight.
Carrots for commits! 🥕✨

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/ai-devtools-overhaul

@github-actions
Copy link
Copy Markdown
Contributor

🚀 Changeset Version Preview

8 package(s) bumped directly, 15 bumped as dependents.

🟥 Major bumps

Package Version Reason
@tanstack/ai-elevenlabs 0.2.10 → 1.0.0 Dependent
@tanstack/ai-openai 0.10.0 → 1.0.0 Dependent
@tanstack/ai-react-ui 0.8.1 → 1.0.0 Dependent
@tanstack/ai-solid-ui 0.7.1 → 1.0.0 Dependent

🟨 Minor bumps

Package Version Reason
@tanstack/ai-client 0.11.7 → 0.12.0 Changeset
@tanstack/ai-devtools-core 0.3.37 → 0.4.0 Changeset
@tanstack/ai-event-client 0.3.10 → 0.4.0 Changeset

🟩 Patch bumps

Package Version Reason
@tanstack/ai-preact 0.6.32 → 0.6.33 Changeset
@tanstack/ai-react 0.11.7 → 0.11.8 Changeset
@tanstack/ai-solid 0.10.7 → 0.10.8 Changeset
@tanstack/ai-svelte 0.10.7 → 0.10.8 Changeset
@tanstack/ai-vue 0.10.8 → 0.10.9 Changeset
@tanstack/ai 0.21.3 → 0.21.4 Dependent
@tanstack/ai-code-mode 0.1.20 → 0.1.21 Dependent
@tanstack/ai-code-mode-skills 0.1.20 → 0.1.21 Dependent
@tanstack/ai-fal 0.7.13 → 0.7.14 Dependent
@tanstack/ai-isolate-cloudflare 0.2.11 → 0.2.12 Dependent
@tanstack/ai-isolate-node 0.1.20 → 0.1.21 Dependent
@tanstack/ai-isolate-quickjs 0.1.20 → 0.1.21 Dependent
@tanstack/ai-vue-ui 0.2.3 → 0.2.4 Dependent
@tanstack/preact-ai-devtools 0.1.41 → 0.1.42 Dependent
@tanstack/react-ai-devtools 0.2.41 → 0.2.42 Dependent
@tanstack/solid-ai-devtools 0.2.41 → 0.2.42 Dependent

@nx-cloud
Copy link
Copy Markdown

nx-cloud Bot commented May 24, 2026

View your CI Pipeline Execution ↗ for commit 4b0f6ce

Command Status Duration Result
nx run-many --targets=build --exclude=examples/... ✅ Succeeded 1s View ↗

☁️ Nx Cloud last updated this comment at 2026-05-24 20:33:12 UTC

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented May 24, 2026

Open in StackBlitz

@tanstack/ai

npm i https://pkg.pr.new/@tanstack/ai@632

@tanstack/ai-anthropic

npm i https://pkg.pr.new/@tanstack/ai-anthropic@632

@tanstack/ai-client

npm i https://pkg.pr.new/@tanstack/ai-client@632

@tanstack/ai-code-mode

npm i https://pkg.pr.new/@tanstack/ai-code-mode@632

@tanstack/ai-code-mode-skills

npm i https://pkg.pr.new/@tanstack/ai-code-mode-skills@632

@tanstack/ai-devtools-core

npm i https://pkg.pr.new/@tanstack/ai-devtools-core@632

@tanstack/ai-elevenlabs

npm i https://pkg.pr.new/@tanstack/ai-elevenlabs@632

@tanstack/ai-event-client

npm i https://pkg.pr.new/@tanstack/ai-event-client@632

@tanstack/ai-fal

npm i https://pkg.pr.new/@tanstack/ai-fal@632

@tanstack/ai-gemini

npm i https://pkg.pr.new/@tanstack/ai-gemini@632

@tanstack/ai-grok

npm i https://pkg.pr.new/@tanstack/ai-grok@632

@tanstack/ai-groq

npm i https://pkg.pr.new/@tanstack/ai-groq@632

@tanstack/ai-isolate-cloudflare

npm i https://pkg.pr.new/@tanstack/ai-isolate-cloudflare@632

@tanstack/ai-isolate-node

npm i https://pkg.pr.new/@tanstack/ai-isolate-node@632

@tanstack/ai-isolate-quickjs

npm i https://pkg.pr.new/@tanstack/ai-isolate-quickjs@632

@tanstack/ai-ollama

npm i https://pkg.pr.new/@tanstack/ai-ollama@632

@tanstack/ai-openai

npm i https://pkg.pr.new/@tanstack/ai-openai@632

@tanstack/ai-openrouter

npm i https://pkg.pr.new/@tanstack/ai-openrouter@632

@tanstack/ai-preact

npm i https://pkg.pr.new/@tanstack/ai-preact@632

@tanstack/ai-react

npm i https://pkg.pr.new/@tanstack/ai-react@632

@tanstack/ai-react-ui

npm i https://pkg.pr.new/@tanstack/ai-react-ui@632

@tanstack/ai-solid

npm i https://pkg.pr.new/@tanstack/ai-solid@632

@tanstack/ai-solid-ui

npm i https://pkg.pr.new/@tanstack/ai-solid-ui@632

@tanstack/ai-svelte

npm i https://pkg.pr.new/@tanstack/ai-svelte@632

@tanstack/ai-utils

npm i https://pkg.pr.new/@tanstack/ai-utils@632

@tanstack/ai-vue

npm i https://pkg.pr.new/@tanstack/ai-vue@632

@tanstack/ai-vue-ui

npm i https://pkg.pr.new/@tanstack/ai-vue-ui@632

@tanstack/openai-base

npm i https://pkg.pr.new/@tanstack/openai-base@632

@tanstack/preact-ai-devtools

npm i https://pkg.pr.new/@tanstack/preact-ai-devtools@632

@tanstack/react-ai-devtools

npm i https://pkg.pr.new/@tanstack/react-ai-devtools@632

@tanstack/solid-ai-devtools

npm i https://pkg.pr.new/@tanstack/solid-ai-devtools@632

commit: 4b0f6ce

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

🧹 Nitpick comments (3)
packages/typescript/ai-event-client/src/envelope.ts (1)

72-102: 💤 Low value

Verify intentional use of module-level runtimeId at line 93.

Line 82 uses resolvedRuntimeId (which respects input.runtimeId), but line 93 uses the module-level runtimeId directly. 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 use resolvedRuntimeId for 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 value

Remove underscore prefix from used parameter.

The _fixture parameter naming convention suggests an unused variable, but it's immediately assigned to fixture and used throughout the method. Consider renaming to just fixture directly 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 win

Consider 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, if hoverTargetMatchesElement is 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

📥 Commits

Reviewing files that changed from the base of the PR and between 6176067 and 4b0f6ce.

📒 Files selected for processing (101)
  • .changeset/ai-devtools-hook-dashboard.md
  • docs/getting-started/devtools.md
  • examples/ts-react-chat/src/components/Header.tsx
  • examples/ts-react-chat/src/routeTree.gen.ts
  • examples/ts-react-chat/src/routes/api.structured-output.ts
  • examples/ts-react-chat/src/routes/generation-hooks.tsx
  • examples/ts-react-chat/src/routes/generations.structured-output.tsx
  • packages/typescript/ai-client/src/chat-client.ts
  • packages/typescript/ai-client/src/devtools.ts
  • packages/typescript/ai-client/src/events.ts
  • packages/typescript/ai-client/src/generation-client.ts
  • packages/typescript/ai-client/src/generation-types.ts
  • packages/typescript/ai-client/src/index.ts
  • packages/typescript/ai-client/src/types.ts
  • packages/typescript/ai-client/src/video-generation-client.ts
  • packages/typescript/ai-client/tests/devtools.test.ts
  • packages/typescript/ai-client/tests/events.test.ts
  • packages/typescript/ai-client/tests/generation-client.test.ts
  • packages/typescript/ai-client/tests/generation-devtools.test.ts
  • packages/typescript/ai-client/tests/video-generation-client.test.ts
  • packages/typescript/ai-devtools/src/components/ConversationDetails.tsx
  • packages/typescript/ai-devtools/src/components/ConversationsList.tsx
  • packages/typescript/ai-devtools/src/components/Shell.tsx
  • packages/typescript/ai-devtools/src/components/conversation/ActivityEventsTab.tsx
  • packages/typescript/ai-devtools/src/components/conversation/ChunkBadges.tsx
  • packages/typescript/ai-devtools/src/components/conversation/ChunkItem.tsx
  • packages/typescript/ai-devtools/src/components/conversation/ChunksCollapsible.tsx
  • packages/typescript/ai-devtools/src/components/conversation/ChunksTab.tsx
  • packages/typescript/ai-devtools/src/components/conversation/ConversationHeader.tsx
  • packages/typescript/ai-devtools/src/components/conversation/ConversationTabs.tsx
  • packages/typescript/ai-devtools/src/components/conversation/IterationCard.tsx
  • packages/typescript/ai-devtools/src/components/conversation/IterationTimeline.tsx
  • packages/typescript/ai-devtools/src/components/conversation/MessageCard.tsx
  • packages/typescript/ai-devtools/src/components/conversation/MessageGroup.tsx
  • packages/typescript/ai-devtools/src/components/conversation/MessagesTab.tsx
  • packages/typescript/ai-devtools/src/components/conversation/SummariesTab.tsx
  • packages/typescript/ai-devtools/src/components/conversation/ToolCallDisplay.tsx
  • packages/typescript/ai-devtools/src/components/conversation/index.ts
  • packages/typescript/ai-devtools/src/components/hooks/GenerationPanel.tsx
  • packages/typescript/ai-devtools/src/components/hooks/HookDashboard.tsx
  • packages/typescript/ai-devtools/src/components/hooks/HookDetails.tsx
  • packages/typescript/ai-devtools/src/components/hooks/ToolFixtureForm.tsx
  • packages/typescript/ai-devtools/src/components/hooks/hook-dashboard-model.ts
  • packages/typescript/ai-devtools/src/components/hooks/index.ts
  • packages/typescript/ai-devtools/src/components/hooks/preview-messages.ts
  • packages/typescript/ai-devtools/src/components/hooks/preview-model.ts
  • packages/typescript/ai-devtools/src/components/list/ConversationRow.tsx
  • packages/typescript/ai-devtools/src/components/list/index.ts
  • packages/typescript/ai-devtools/src/components/utils/format.ts
  • packages/typescript/ai-devtools/src/store/ai-context.tsx
  • packages/typescript/ai-devtools/src/store/hook-registry.ts
  • packages/typescript/ai-devtools/src/store/message-event-utils.ts
  • packages/typescript/ai-devtools/src/styles/use-styles.ts
  • packages/typescript/ai-devtools/tests/hook-dashboard-model.test.ts
  • packages/typescript/ai-devtools/tests/hook-registry.test.ts
  • packages/typescript/ai-devtools/tests/message-event-utils.test.ts
  • packages/typescript/ai-devtools/tests/preview-messages.test.ts
  • packages/typescript/ai-devtools/tests/preview-model.test.ts
  • packages/typescript/ai-event-client/src/devtools-middleware.ts
  • packages/typescript/ai-event-client/src/envelope.ts
  • packages/typescript/ai-event-client/src/index.ts
  • packages/typescript/ai-event-client/tests/emit.test.ts
  • packages/typescript/ai-event-client/tests/envelope.test.ts
  • packages/typescript/ai-preact/src/types.ts
  • packages/typescript/ai-preact/src/use-chat.ts
  • packages/typescript/ai-react/src/use-chat.ts
  • packages/typescript/ai-react/src/use-generate-audio.ts
  • packages/typescript/ai-react/src/use-generate-image.ts
  • packages/typescript/ai-react/src/use-generate-speech.ts
  • packages/typescript/ai-react/src/use-generate-video.ts
  • packages/typescript/ai-react/src/use-generation.ts
  • packages/typescript/ai-react/src/use-summarize.ts
  • packages/typescript/ai-react/src/use-transcription.ts
  • packages/typescript/ai-solid/src/use-chat.ts
  • packages/typescript/ai-solid/src/use-generate-audio.ts
  • packages/typescript/ai-solid/src/use-generate-image.ts
  • packages/typescript/ai-solid/src/use-generate-speech.ts
  • packages/typescript/ai-solid/src/use-generate-video.ts
  • packages/typescript/ai-solid/src/use-generation.ts
  • packages/typescript/ai-solid/src/use-summarize.ts
  • packages/typescript/ai-solid/src/use-transcription.ts
  • packages/typescript/ai-svelte/src/create-chat.svelte.ts
  • packages/typescript/ai-svelte/src/create-generate-audio.svelte.ts
  • packages/typescript/ai-svelte/src/create-generate-image.svelte.ts
  • packages/typescript/ai-svelte/src/create-generate-speech.svelte.ts
  • packages/typescript/ai-svelte/src/create-generate-video.svelte.ts
  • packages/typescript/ai-svelte/src/create-generation.svelte.ts
  • packages/typescript/ai-svelte/src/create-summarize.svelte.ts
  • packages/typescript/ai-svelte/src/create-transcription.svelte.ts
  • packages/typescript/ai-svelte/src/types.ts
  • packages/typescript/ai-vue/src/use-chat.ts
  • packages/typescript/ai-vue/src/use-generate-audio.ts
  • packages/typescript/ai-vue/src/use-generate-image.ts
  • packages/typescript/ai-vue/src/use-generate-speech.ts
  • packages/typescript/ai-vue/src/use-generate-video.ts
  • packages/typescript/ai-vue/src/use-generation.ts
  • packages/typescript/ai-vue/src/use-summarize.ts
  • packages/typescript/ai-vue/src/use-transcription.ts
  • packages/typescript/ai/src/activities/chat/index.ts
  • packages/typescript/ai/src/activities/chat/middleware/types.ts
  • packages/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

Comment on lines +223 to 233
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')
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

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.

Suggested change
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.

Comment on lines +785 to +831
<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>
)}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

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.

Comment on lines +275 to +283
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
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

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.

Suggested change
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.

Comment on lines 89 to +99
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(),
}),
})
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

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.

Comment on lines +1341 to +1348
if (part.type === 'tool-result') {
return {
type: 'tool-result',
toolCallId: part.toolCallId,
content: part.content,
state: part.state,
error: part.error,
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

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.

Suggested change
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.

Comment on lines +160 to 170
client.mountDevtools()

return () => {
if (options.live) {
client.unsubscribe()
} else {
client.stop()
}
client.dispose()
}
}, [client, options.live])
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 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': ' || true

Repository: 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 -w1

Repository: 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 || true

Repository: 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 120

Repository: 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 || true

Repository: 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
done

Repository: 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" || true

Repository: 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 50

Repository: 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}'
done

Repository: 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 || true

Repository: 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
done

Repository: 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.

Comment on lines 157 to 168
useEffect(() => {
client.mountDevtools()

return () => {
if (options.live) {
client.unsubscribe()
} else {
client.stop()
}
client.dispose()
}
}, [client, options.live])
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 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"
done

Repository: 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant