Skip to content

Commit 2441148

Browse files
committed
docs: multi-tab coordination, error stack truncation changelog
1 parent 68a075f commit 2441148

File tree

3 files changed

+121
-0
lines changed

3 files changed

+121
-0
lines changed

docs/ai-chat/changelog.mdx

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,28 @@ sidebarTitle: "Changelog"
44
description: "Pre-release updates for AI chat agents."
55
---
66

7+
<Update label="April 17, 2026" description="0.0.0-chat-prerelease-20260417152143" tags={["SDK"]}>
8+
9+
## Multi-tab coordination
10+
11+
Prevent duplicate messages when the same chat is open in multiple browser tabs. Enable with `multiTab: true` on the transport.
12+
13+
```tsx
14+
const transport = useTriggerChatTransport({ task: "my-chat", multiTab: true, accessToken });
15+
const { messages, setMessages } = useChat({ id: chatId, transport });
16+
const { isReadOnly } = useMultiTabChat(transport, chatId, messages, setMessages);
17+
```
18+
19+
Only one tab can send at a time. Other tabs enter read-only mode with real-time message updates via `BroadcastChannel`. When the active tab's turn completes, any tab can send next. Crashed tabs are detected via heartbeat timeout (10s).
20+
21+
See [Multi-tab coordination](/ai-chat/frontend#multi-tab-coordination) and [`useMultiTabChat`](/ai-chat/reference#usemultitabchat).
22+
23+
## Error stack truncation
24+
25+
Large error stacks no longer OOM the worker process. Stacks are capped at 50 frames (top 5 + bottom 45), individual lines at 1024 chars, messages at 1000 chars. Applied in `parseError`, `sanitizeError`, and OTel span recording.
26+
27+
</Update>
28+
729
<Update label="April 15, 2026" description="0.0.0-chat-prerelease-20260415164455" tags={["SDK"]}>
830

931
## Fix: `resume: true` hangs on completed turns

docs/ai-chat/frontend.mdx

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -406,6 +406,65 @@ for await (const chunk of stream) {
406406
}
407407
```
408408

409+
## Multi-tab coordination
410+
411+
When the same chat is open in multiple browser tabs, `multiTab: true` prevents duplicate messages and syncs conversation state across tabs. Only one tab can send at a time. Other tabs enter read-only mode with real-time message updates.
412+
413+
```tsx
414+
import { useTriggerChatTransport } from "@trigger.dev/sdk/chat/react";
415+
import { useMultiTabChat } from "@trigger.dev/sdk/chat/react";
416+
import { useChat } from "@ai-sdk/react";
417+
418+
function Chat({ chatId }: { chatId: string }) {
419+
const transport = useTriggerChatTransport({
420+
task: "my-chat",
421+
accessToken,
422+
multiTab: true,
423+
});
424+
425+
const { messages, setMessages, sendMessage } = useChat({
426+
id: chatId,
427+
transport,
428+
});
429+
430+
const { isReadOnly } = useMultiTabChat(transport, chatId, messages, setMessages);
431+
432+
return (
433+
<div>
434+
{isReadOnly && (
435+
<div className="bg-amber-50 text-amber-700 p-2 text-sm">
436+
This chat is active in another tab. Messages are read-only.
437+
</div>
438+
)}
439+
{/* message list */}
440+
<input
441+
disabled={isReadOnly}
442+
placeholder={isReadOnly ? "Active in another tab" : "Type a message..."}
443+
/>
444+
</div>
445+
);
446+
}
447+
```
448+
449+
### How it works
450+
451+
1. When a tab sends a message, the transport "claims" the chatId via `BroadcastChannel`
452+
2. Other tabs detect the claim and enter read-only mode (`isReadOnly: true`)
453+
3. The active tab broadcasts its messages so read-only tabs see updates in real-time
454+
4. When the turn completes, the claim is released. Any tab can send next.
455+
5. Heartbeats detect crashed tabs (10s timeout clears stale claims)
456+
457+
### What `useMultiTabChat` does
458+
459+
- Returns `{ isReadOnly }` for disabling the input UI
460+
- Broadcasts `messages` from the active tab to other tabs
461+
- Calls `setMessages` on read-only tabs when messages arrive from the active tab
462+
- Tracks read-only state via the transport's `BroadcastChannel` coordinator
463+
464+
<Note>
465+
Multi-tab coordination is same-browser only (`BroadcastChannel` is a browser API). It gracefully degrades to a no-op in Node.js, SSR, or browsers without `BroadcastChannel` support. Cross-device coordination requires server-side involvement.
466+
</Note>
467+
409468
## Self-hosting
410469

411470
If you're self-hosting Trigger.dev, pass the `baseURL` option:

docs/ai-chat/reference.mdx

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -534,6 +534,20 @@ const transport = useTriggerChatTransport({
534534
});
535535
```
536536

537+
### multiTab
538+
539+
Enable multi-tab coordination. When `true`, only one browser tab can send messages to a given chatId at a time. Other tabs enter read-only mode with real-time message updates via `BroadcastChannel`.
540+
541+
```ts
542+
const transport = useTriggerChatTransport({
543+
task: "my-chat",
544+
accessToken,
545+
multiTab: true,
546+
});
547+
```
548+
549+
No-op when `BroadcastChannel` is unavailable (SSR, Node.js). See [Multi-tab coordination](/ai-chat/frontend#multi-tab-coordination).
550+
537551
### triggerOptions
538552

539553
Options forwarded to the Trigger.dev API when starting a new run. Only applies to the first message — subsequent messages reuse the same run.
@@ -629,6 +643,32 @@ const transport = useTriggerChatTransport<typeof myChat>({
629643

630644
The transport is created once on first render and reused across re-renders. Pass a type parameter for compile-time validation of the task ID.
631645

646+
## useMultiTabChat
647+
648+
React hook for multi-tab message coordination. Import from `@trigger.dev/sdk/chat/react`.
649+
650+
```tsx
651+
import { useMultiTabChat } from "@trigger.dev/sdk/chat/react";
652+
653+
const { isReadOnly } = useMultiTabChat(transport, chatId, messages, setMessages);
654+
```
655+
656+
| Parameter | Type | Description |
657+
|-----------|------|-------------|
658+
| `transport` | `TriggerChatTransport` | Transport instance with `multiTab: true` |
659+
| `chatId` | `string` | The chat session ID |
660+
| `messages` | `UIMessage[]` | Current messages from `useChat` |
661+
| `setMessages` | `(messages) => void` | Message setter from `useChat` |
662+
663+
**Returns:** `{ isReadOnly: boolean }``true` when another tab is actively sending to this chatId.
664+
665+
The hook handles:
666+
- Tracking read-only state from the transport's `BroadcastChannel` coordinator
667+
- Broadcasting messages when this tab is the active sender
668+
- Receiving messages from other tabs and updating via `setMessages`
669+
670+
See [Multi-tab coordination](/ai-chat/frontend#multi-tab-coordination).
671+
632672
## Related
633673

634674
- [Realtime Streams](/tasks/streams) — How streams work under the hood

0 commit comments

Comments
 (0)