@assistant-ui/react 0.14.13 → 0.14.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (28) hide show
  1. package/dist/client/InMemoryThreadList.d.ts.map +1 -1
  2. package/dist/client/InMemoryThreadList.js +13 -3
  3. package/dist/client/InMemoryThreadList.js.map +1 -1
  4. package/dist/client/SingleThreadList.d.ts.map +1 -1
  5. package/dist/client/SingleThreadList.js +5 -2
  6. package/dist/client/SingleThreadList.js.map +1 -1
  7. package/dist/index.d.ts +2 -2
  8. package/dist/index.js +2 -2
  9. package/dist/legacy-runtime/runtime-cores/assistant-transport/replayBoundaryStream.d.ts +14 -0
  10. package/dist/legacy-runtime/runtime-cores/assistant-transport/replayBoundaryStream.d.ts.map +1 -0
  11. package/dist/legacy-runtime/runtime-cores/assistant-transport/replayBoundaryStream.js +101 -0
  12. package/dist/legacy-runtime/runtime-cores/assistant-transport/replayBoundaryStream.js.map +1 -0
  13. package/dist/legacy-runtime/runtime-cores/assistant-transport/useAssistantTransportRuntime.d.ts.map +1 -1
  14. package/dist/legacy-runtime/runtime-cores/assistant-transport/useAssistantTransportRuntime.js +12 -1
  15. package/dist/legacy-runtime/runtime-cores/assistant-transport/useAssistantTransportRuntime.js.map +1 -1
  16. package/dist/unstable/useMentionAdapter.d.ts +2 -2
  17. package/dist/unstable/useMentionAdapter.js +1 -1
  18. package/dist/unstable/useMentionAdapter.js.map +1 -1
  19. package/dist/utils/useToolArgsFieldStatus.d.ts +2 -2
  20. package/dist/utils/useToolArgsFieldStatus.d.ts.map +1 -1
  21. package/package.json +5 -5
  22. package/src/client/InMemoryThreadList.ts +23 -2
  23. package/src/client/SingleThreadList.ts +5 -1
  24. package/src/index.ts +10 -2
  25. package/src/legacy-runtime/runtime-cores/assistant-transport/replayBoundaryStream.test.ts +426 -0
  26. package/src/legacy-runtime/runtime-cores/assistant-transport/replayBoundaryStream.ts +146 -0
  27. package/src/legacy-runtime/runtime-cores/assistant-transport/useAssistantTransportRuntime.ts +16 -1
  28. package/src/unstable/useMentionAdapter.ts +2 -2
@@ -0,0 +1,146 @@
1
+ "use client";
2
+
3
+ import { useCallback, useEffect, useRef, useState } from "react";
4
+
5
+ export const REPLAY_CONTENT_LENGTH_HEADER = "Aui-Replay-Content-Length";
6
+
7
+ type ReplayBoundaryStreamOptions = {
8
+ setReplaying: (value: boolean) => void;
9
+ waitForRender: () => Promise<void>;
10
+ };
11
+
12
+ export const useReplayRenderWait = () => {
13
+ const [renderTicket, setRenderTicket] = useState(0);
14
+ const mountedRef = useRef(true);
15
+ const nextTicketRef = useRef(0);
16
+ const waitersRef = useRef<Array<{ ticket: number; resolve: () => void }>>([]);
17
+
18
+ const resolveWaiters = useCallback((committedTicket?: number) => {
19
+ const pendingWaiters = [];
20
+
21
+ for (const waiter of waitersRef.current) {
22
+ if (committedTicket === undefined || waiter.ticket <= committedTicket) {
23
+ waiter.resolve();
24
+ } else {
25
+ pendingWaiters.push(waiter);
26
+ }
27
+ }
28
+
29
+ waitersRef.current = pendingWaiters;
30
+ }, []);
31
+
32
+ useEffect(() => {
33
+ mountedRef.current = true;
34
+ resolveWaiters(renderTicket);
35
+ }, [renderTicket, resolveWaiters]);
36
+
37
+ useEffect(
38
+ () => () => {
39
+ mountedRef.current = false;
40
+ resolveWaiters();
41
+ },
42
+ [resolveWaiters],
43
+ );
44
+
45
+ return useCallback(
46
+ () =>
47
+ new Promise<void>((resolve) => {
48
+ setTimeout(() => {
49
+ if (!mountedRef.current) {
50
+ resolve();
51
+ return;
52
+ }
53
+
54
+ const ticket = nextTicketRef.current + 1;
55
+ nextTicketRef.current = ticket;
56
+ waitersRef.current.push({ ticket, resolve });
57
+ setRenderTicket(ticket);
58
+ }, 0);
59
+ }),
60
+ [],
61
+ );
62
+ };
63
+
64
+ const parseReplayContentLength = (headers: Headers): number => {
65
+ const raw = headers.get(REPLAY_CONTENT_LENGTH_HEADER);
66
+ if (raw == null) return 0;
67
+
68
+ const boundary = Number(raw);
69
+ return Number.isSafeInteger(boundary) && boundary > 0 ? boundary : 0;
70
+ };
71
+
72
+ // Gates replay bytes until isReplaying:true renders, then releases live bytes after isReplaying:false renders.
73
+ export const createReplayBoundaryStream = async (
74
+ response: Response,
75
+ {
76
+ setReplaying,
77
+ waitForRender: waitForReplayRender,
78
+ }: ReplayBoundaryStreamOptions,
79
+ ): Promise<ReadableStream<Uint8Array>> => {
80
+ const body = response.body as ReadableStream<Uint8Array>;
81
+ const replayContentLength = parseReplayContentLength(response.headers);
82
+
83
+ if (replayContentLength <= 0) {
84
+ return body;
85
+ }
86
+
87
+ setReplaying(true);
88
+ await waitForReplayRender();
89
+
90
+ const reader = body.getReader();
91
+ let bytesForwarded = 0;
92
+ let replayFinished = false;
93
+
94
+ const finishReplay = async () => {
95
+ if (replayFinished) return;
96
+ replayFinished = true;
97
+
98
+ // Let replay bytes drain before rendering live mode, then render live mode before releasing live bytes.
99
+ await waitForReplayRender();
100
+ setReplaying(false);
101
+ await waitForReplayRender();
102
+ };
103
+
104
+ return new ReadableStream<Uint8Array>({
105
+ async pull(controller) {
106
+ const { done, value } = await reader.read();
107
+
108
+ if (done) {
109
+ await finishReplay();
110
+ controller.close();
111
+ return;
112
+ }
113
+
114
+ if (replayFinished) {
115
+ controller.enqueue(value);
116
+ return;
117
+ }
118
+
119
+ const nextBytesForwarded = bytesForwarded + value.byteLength;
120
+
121
+ if (nextBytesForwarded < replayContentLength) {
122
+ bytesForwarded = nextBytesForwarded;
123
+ controller.enqueue(value);
124
+ return;
125
+ }
126
+
127
+ if (nextBytesForwarded === replayContentLength) {
128
+ controller.enqueue(value);
129
+ await finishReplay();
130
+ return;
131
+ }
132
+
133
+ const replayBytesInChunk = replayContentLength - bytesForwarded;
134
+
135
+ controller.enqueue(value.subarray(0, replayBytesInChunk));
136
+ await finishReplay();
137
+ controller.enqueue(value.subarray(replayBytesInChunk));
138
+ },
139
+ async cancel(reason) {
140
+ const wasFinished = replayFinished;
141
+ replayFinished = true;
142
+ if (!wasFinished) setReplaying(false);
143
+ await reader.cancel(reason);
144
+ },
145
+ });
146
+ };
@@ -27,6 +27,10 @@ import type {
27
27
  SendCommandsRequestBody,
28
28
  } from "./types";
29
29
  import { useCommandQueue } from "./commandQueue";
30
+ import {
31
+ createReplayBoundaryStream,
32
+ useReplayRenderWait,
33
+ } from "./replayBoundaryStream";
30
34
  import { useRunManager } from "./runManager";
31
35
  import { useConvertedState } from "./useConvertedState";
32
36
  import type { ToolExecutionStatus } from "@assistant-ui/core";
@@ -116,6 +120,8 @@ const useAssistantTransportThreadRuntime = <T>(
116
120
  const agentStateRef = useRef(options.initialState);
117
121
  const [, rerender] = useState(0);
118
122
  const resumeFlagRef = useRef(false);
123
+ const [isReplaying, setIsReplaying] = useState(false);
124
+ const waitForReplayRender = useReplayRenderWait();
119
125
  const parentIdRef = useRef<string | null | undefined>(undefined);
120
126
  const commandQueue = useCommandQueue({
121
127
  onQueue: () => runManager.schedule(),
@@ -127,6 +133,7 @@ const useAssistantTransportThreadRuntime = <T>(
127
133
  onRun: async (signal: AbortSignal) => {
128
134
  const isResume = resumeFlagRef.current;
129
135
  resumeFlagRef.current = false;
136
+ setIsReplaying(false);
130
137
  const commands: QueuedCommand[] = isResume ? [] : commandQueue.flush();
131
138
  if (commands.length === 0 && !isResume)
132
139
  throw new Error("No commands to send");
@@ -182,6 +189,11 @@ const useAssistantTransportThreadRuntime = <T>(
182
189
  throw new Error("Response body is null");
183
190
  }
184
191
 
192
+ const body = await createReplayBoundaryStream(response, {
193
+ setReplaying: setIsReplaying,
194
+ waitForRender: waitForReplayRender,
195
+ });
196
+
185
197
  // Select decoder based on protocol option
186
198
  const protocol = options.protocol ?? "data-stream";
187
199
  const decoder =
@@ -190,7 +202,7 @@ const useAssistantTransportThreadRuntime = <T>(
190
202
  : new DataStreamDecoder();
191
203
 
192
204
  let err: string | undefined;
193
- const stream = response.body.pipeThrough(decoder).pipeThrough(
205
+ const stream = body.pipeThrough(decoder).pipeThrough(
194
206
  new AssistantMessageAccumulator({
195
207
  initialMessage: createInitialMessage({
196
208
  unstable_state:
@@ -223,6 +235,7 @@ const useAssistantTransportThreadRuntime = <T>(
223
235
  },
224
236
  onFinish: options.onFinish,
225
237
  onCancel: () => {
238
+ setIsReplaying(false);
226
239
  const cmds = [
227
240
  ...commandQueue.state.inTransit,
228
241
  ...commandQueue.state.queued,
@@ -239,6 +252,7 @@ const useAssistantTransportThreadRuntime = <T>(
239
252
  });
240
253
  },
241
254
  onError: async (error) => {
255
+ setIsReplaying(false);
242
256
  const inTransitCmds = [...commandQueue.state.inTransit];
243
257
  const queuedCmds = [...commandQueue.state.queued];
244
258
 
@@ -288,6 +302,7 @@ const useAssistantTransportThreadRuntime = <T>(
288
302
  messages: converted.messages,
289
303
  state: converted.state,
290
304
  isRunning: converted.isRunning,
305
+ isLoading: isReplaying,
291
306
  adapters: options.adapters,
292
307
  unstable_enableToolInvocations: true,
293
308
  setToolStatuses,
@@ -45,7 +45,7 @@ export type Unstable_UseMentionAdapterOptions = {
45
45
  /** Categorized mentions for drill-down navigation. */
46
46
  readonly categories?: readonly Unstable_MentionCategory[];
47
47
  /**
48
- * How tools registered via `useAssistantTool` integrate.
48
+ * How tools registered in model context integrate.
49
49
  * - `false`: exclude.
50
50
  * - `true`: include (default when no `items`/`categories`; as a category
51
51
  * if `categories` is set, flat otherwise).
@@ -75,7 +75,7 @@ export type Unstable_MentionDirective = {
75
75
  * @deprecated Under active development and might change without notice.
76
76
  *
77
77
  * Creates a spreadable `{ adapter, directive }` bundle for `@` mentions.
78
- * Supports tools registered via `useAssistantTool`, explicit items, or both —
78
+ * Supports tools registered in model context, explicit items, or both —
79
79
  * flat or categorized.
80
80
  *
81
81
  * @example