Describe the bug
aiEventClient.on() does not work as expected due to two issues in EventClient from @tanstack/devtools-event-client:
1. Double-prefixed event names
The AIDevtoolsEventMap type uses fully-prefixed keys (e.g. "tanstack-ai-devtools:text:message:created"), but both emit() and on() already prepend the pluginId at runtime:
// EventClient.on()
const eventName = `${this.#pluginId}:${eventSuffix}`;
// → "tanstack-ai-devtools:tanstack-ai-devtools:text:message:created" ❌
The emit() calls in @tanstack/ai source correctly use the short suffix:
// src/activities/chat/index.ts
aiEventClient.emit('text:message:created', { ... }) // ✅ correct
So on() also needs the short suffix to match, but TypeScript demands the full key from AIDevtoolsEventMap:
// TypeScript expects this (from the type map), but it double-prefixes at runtime:
aiEventClient.on("tanstack-ai-devtools:text:message:created", cb) // ❌ won't fire
// This works at runtime, but TypeScript errors:
aiEventClient.on("text:message:created", cb) // ✅ works, but TS error
Fix: The keys in AIDevtoolsEventMap should use the short suffix (without the tanstack-ai-devtools: prefix), since the EventClient already prepends the pluginId.
2. on() doesn't work on the server without { withEventTarget: true }
On server environments (Cloudflare Workers, Node, Bun) where there is no window and globalThis.__TANSTACK_EVENT_TARGET__ is not set, getGlobalTarget() falls through to:
const eventTarget = typeof EventTarget !== "undefined" ? new EventTarget() : void 0;
return eventTarget;
Since getGlobalTarget() is not cached (called via this.#eventTarget() each time), every call creates a new EventTarget instance. This means emit() and on() dispatch/listen on different targets — events never reach the listener.
Workaround: Passing { withEventTarget: true } to on() forces use of this.#internalEventTarget, which is a single shared instance on the EventClient. Then emit() also dispatches to it.
aiEventClient.on(
// @ts-expect-error -- see issue #1 above
"text:message:created",
(e) => console.log(e.payload),
{ withEventTarget: true } // required on server
)
Fix: getGlobalTarget() should cache the EventTarget it creates, so the same instance is used across all on()/emit() calls.
Your minimal, reproducible example
// In a TanStack Start server handler (e.g. API route on Cloudflare Workers)
import { aiEventClient, chat } from "@tanstack/ai"
// ❌ Does NOT fire — double prefix + separate EventTargets on server
aiEventClient.on("tanstack-ai-devtools:text:message:created", (e) => {
console.log(e.payload.content)
})
// ✅ Works with both fixes applied
aiEventClient.on(
// @ts-expect-error
"text:message:created",
(e) => console.log(e.payload.content),
{ withEventTarget: true }
)
Steps to reproduce
- Set up a TanStack Start app with
@tanstack/ai on a server environment (Cloudflare Workers, Node, or Bun)
- Register a listener with
aiEventClient.on("tanstack-ai-devtools:text:message:created", cb)
- Trigger a
chat() call
- Observe: the callback never fires
Expected behavior
aiEventClient.on("text:message:created", cb) should work without @ts-expect-error and without needing { withEventTarget: true } on the server.
How often does this bug happen?
Every time
Package version
@tanstack/ai 0.6.1, @tanstack/devtools-event-client 0.4.0
TypeScript version
5.x
Additional context
Found while building a chat API route in a TanStack Start + Cloudflare Workers app. The { withEventTarget: true } option works as a workaround for both issues.
Describe the bug
aiEventClient.on()does not work as expected due to two issues inEventClientfrom@tanstack/devtools-event-client:1. Double-prefixed event names
The
AIDevtoolsEventMaptype uses fully-prefixed keys (e.g."tanstack-ai-devtools:text:message:created"), but bothemit()andon()already prepend thepluginIdat runtime:The
emit()calls in@tanstack/aisource correctly use the short suffix:So
on()also needs the short suffix to match, but TypeScript demands the full key fromAIDevtoolsEventMap:Fix: The keys in
AIDevtoolsEventMapshould use the short suffix (without thetanstack-ai-devtools:prefix), since theEventClientalready prepends thepluginId.2.
on()doesn't work on the server without{ withEventTarget: true }On server environments (Cloudflare Workers, Node, Bun) where there is no
windowandglobalThis.__TANSTACK_EVENT_TARGET__is not set,getGlobalTarget()falls through to:Since
getGlobalTarget()is not cached (called viathis.#eventTarget()each time), every call creates a newEventTargetinstance. This meansemit()andon()dispatch/listen on different targets — events never reach the listener.Workaround: Passing
{ withEventTarget: true }toon()forces use ofthis.#internalEventTarget, which is a single shared instance on theEventClient. Thenemit()also dispatches to it.Fix:
getGlobalTarget()should cache theEventTargetit creates, so the same instance is used across allon()/emit()calls.Your minimal, reproducible example
Steps to reproduce
@tanstack/aion a server environment (Cloudflare Workers, Node, or Bun)aiEventClient.on("tanstack-ai-devtools:text:message:created", cb)chat()callExpected behavior
aiEventClient.on("text:message:created", cb)should work without@ts-expect-errorand without needing{ withEventTarget: true }on the server.How often does this bug happen?
Every time
Package version
@tanstack/ai0.6.1,@tanstack/devtools-event-client0.4.0TypeScript version
5.x
Additional context
Found while building a chat API route in a TanStack Start + Cloudflare Workers app. The
{ withEventTarget: true }option works as a workaround for both issues.