@deuz-sdk/react 1.7.1 → 1.9.0

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.
package/dist/index.cjs CHANGED
@@ -19,46 +19,166 @@ function foldPart(turn, part) {
19
19
  return next;
20
20
  }
21
21
  var defaultGenerateId = () => typeof crypto !== "undefined" && typeof crypto.randomUUID === "function" ? crypto.randomUUID() : `deuz-ui-${Math.random().toString(36).slice(2)}`;
22
+ async function partsFromFiles(files) {
23
+ if (!files) return [];
24
+ const list = Array.from(
25
+ files
26
+ );
27
+ return list.length === 0 ? [] : chat.filesToImageParts(list);
28
+ }
29
+ function loadCursor(resume) {
30
+ try {
31
+ return resume.cursor?.load();
32
+ } catch {
33
+ return void 0;
34
+ }
35
+ }
36
+ function saveCursor(resume, id) {
37
+ try {
38
+ resume.cursor?.save(id);
39
+ } catch {
40
+ }
41
+ }
22
42
  function useChat(options) {
23
43
  const genId = options.generateId ?? defaultGenerateId;
24
- const [messages, setMessages] = react.useState(
25
- () => chat.uiFromMessages(options.initialMessages ?? [], genId)
26
- );
44
+ const [snapshot, setSnapshot] = react.useState(() => ({
45
+ history: {
46
+ ui: chat.uiFromMessages(options.initialMessages ?? [], genId),
47
+ canonical: options.initialMessages ? [...options.initialMessages] : []
48
+ },
49
+ dataParts: [],
50
+ citations: [],
51
+ activity: []
52
+ }));
27
53
  const [status, setStatus] = react.useState("idle");
28
54
  const [error, setError] = react.useState(void 0);
29
55
  const [pendingApprovals, setPendingApprovals] = react.useState([]);
30
- const [cost, setCost] = react.useState(void 0);
31
- const [budgetExceeded, setBudgetExceeded] = react.useState(
32
- void 0
33
- );
34
- const [dataParts, setDataParts] = react.useState([]);
35
- const [citations, setCitations] = react.useState([]);
36
- const uiRef = react.useRef(messages);
37
- const canonicalRef = react.useRef(
38
- options.initialMessages ? [...options.initialMessages] : []
39
- );
56
+ const [pendingToolCalls, setPendingToolCalls] = react.useState([]);
57
+ const snapshotRef = react.useRef(snapshot);
40
58
  const turnRef = react.useRef(void 0);
59
+ const parkedTurnRef = react.useRef(void 0);
60
+ const clientToolsRef = react.useRef([]);
61
+ const toolResultsRef = react.useRef(
62
+ []
63
+ );
41
64
  const approvalsRef = react.useRef([]);
42
65
  const verdictsRef = react.useRef([]);
43
66
  const abortRef = react.useRef(null);
44
67
  const lastEventIdRef = react.useRef(options.resume?.lastEventId);
45
- const pushMessage = react.useCallback((message) => {
46
- uiRef.current = [...uiRef.current, message];
47
- setMessages(uiRef.current);
68
+ const placedIdRef = react.useRef(void 0);
69
+ const throttleMs = options.throttleMs ?? 0;
70
+ const flushTimerRef = react.useRef(void 0);
71
+ const dirtyRef = react.useRef(false);
72
+ const publish = react.useCallback(() => {
73
+ flushTimerRef.current = void 0;
74
+ dirtyRef.current = false;
75
+ setSnapshot(snapshotRef.current);
48
76
  }, []);
49
- const syncTurn = react.useCallback((turn) => {
50
- uiRef.current = [...uiRef.current.slice(0, -1), turn.message];
51
- setMessages(uiRef.current);
52
- if (turn.costUsd !== void 0) {
53
- setCost({
54
- costUsd: turn.costUsd,
55
- ...turn.cacheSavingsUsd !== void 0 ? { cacheSavingsUsd: turn.cacheSavingsUsd } : {}
56
- });
77
+ const commit = react.useCallback(
78
+ (next) => {
79
+ snapshotRef.current = next;
80
+ if (throttleMs <= 0) {
81
+ publish();
82
+ return;
83
+ }
84
+ dirtyRef.current = true;
85
+ if (flushTimerRef.current === void 0) {
86
+ flushTimerRef.current = setTimeout(publish, throttleMs);
87
+ }
88
+ },
89
+ [throttleMs, publish]
90
+ );
91
+ const flush = react.useCallback(() => {
92
+ if (flushTimerRef.current !== void 0) {
93
+ clearTimeout(flushTimerRef.current);
94
+ flushTimerRef.current = void 0;
57
95
  }
58
- setBudgetExceeded(turn.budgetExceeded);
59
- setDataParts(turn.dataParts);
60
- setCitations(turn.citations);
96
+ if (dirtyRef.current) publish();
97
+ }, [publish]);
98
+ react.useEffect(
99
+ () => () => {
100
+ if (flushTimerRef.current !== void 0) clearTimeout(flushTimerRef.current);
101
+ },
102
+ []
103
+ );
104
+ const syncTurn = react.useCallback(
105
+ (turn) => {
106
+ const previous = snapshotRef.current;
107
+ const placed = placedIdRef.current;
108
+ const ui = previous.history.ui;
109
+ const index = placed === void 0 ? -1 : ui.findIndex((m) => m.id === placed);
110
+ placedIdRef.current = turn.message.id;
111
+ commit({
112
+ ...previous,
113
+ history: {
114
+ // NEW array either way — the turn's message is never spliced in place.
115
+ ui: index === -1 ? [...ui, turn.message] : [...ui.slice(0, index), turn.message, ...ui.slice(index + 1)],
116
+ canonical: previous.history.canonical
117
+ },
118
+ // `cost` is CUMULATIVE for the chat, so an absent reading keeps the
119
+ // previous one (1.7 behaviour, verbatim); every other readout is
120
+ // turn-scoped and resets when the turn does.
121
+ cost: turn.costUsd !== void 0 ? {
122
+ costUsd: turn.costUsd,
123
+ ...turn.cacheSavingsUsd !== void 0 ? { cacheSavingsUsd: turn.cacheSavingsUsd } : {}
124
+ } : previous.cost,
125
+ budgetExceeded: turn.budgetExceeded,
126
+ dataParts: turn.dataParts,
127
+ citations: turn.citations,
128
+ plan: turn.plan,
129
+ activity: turn.activity,
130
+ verifications: turn.verifications,
131
+ warnings: turn.warnings,
132
+ falseFinishes: turn.falseFinishes,
133
+ subAgents: turn.subAgents,
134
+ steps: turn.steps,
135
+ usage: turn.usage,
136
+ finishReason: turn.finishReason
137
+ });
138
+ },
139
+ [commit]
140
+ );
141
+ const appendCanonical = react.useCallback(
142
+ (message) => {
143
+ const previous = snapshotRef.current;
144
+ commit({
145
+ ...previous,
146
+ history: {
147
+ ui: previous.history.ui,
148
+ canonical: [...previous.history.canonical, message]
149
+ }
150
+ });
151
+ },
152
+ [commit]
153
+ );
154
+ const clearGates = react.useCallback(() => {
155
+ approvalsRef.current = [];
156
+ verdictsRef.current = [];
157
+ clientToolsRef.current = [];
158
+ toolResultsRef.current = [];
159
+ parkedTurnRef.current = void 0;
160
+ setPendingApprovals([]);
161
+ setPendingToolCalls([]);
61
162
  }, []);
163
+ const fold = react.useCallback(
164
+ (turn, part) => {
165
+ const next = foldPart(turn, part);
166
+ const onData = options.onData;
167
+ if (onData && typeof part.type === "string" && part.type.startsWith("data-")) {
168
+ const data = part;
169
+ try {
170
+ onData({
171
+ name: data.type.slice("data-".length),
172
+ ...typeof data.id === "string" ? { id: data.id } : {},
173
+ payload: data.payload
174
+ });
175
+ } catch {
176
+ }
177
+ }
178
+ return next;
179
+ },
180
+ [options.onData]
181
+ );
62
182
  const stop = react.useCallback(() => {
63
183
  abortRef.current?.abort();
64
184
  }, []);
@@ -75,7 +195,7 @@ function useChat(options) {
75
195
  method: "POST",
76
196
  headers: { "content-type": "application/json", ...options.headers },
77
197
  body: JSON.stringify({
78
- messages: canonicalRef.current,
198
+ messages: snapshotRef.current.history.canonical,
79
199
  ...options.chatId !== void 0 ? { chatId: options.chatId } : {},
80
200
  ...approvalResponses?.length ? { approvalResponses } : {},
81
201
  ...options.body
@@ -85,15 +205,25 @@ function useChat(options) {
85
205
  approvalResponses = void 0;
86
206
  let turn = chat.createAssistantTurn(genId());
87
207
  turnRef.current = turn;
88
- pushMessage(turn.message);
208
+ placedIdRef.current = void 0;
89
209
  syncTurn(turn);
90
- for await (const part of ui.readDeuzStream(res)) {
91
- turn = foldPart(turn, part);
210
+ flush();
211
+ for await (const part of ui.readDeuzStream(res, {
212
+ ...options.onHttpError ? { onHttpError: options.onHttpError } : {}
213
+ })) {
214
+ turn = fold(turn, part);
215
+ turnRef.current = turn;
216
+ syncTurn(turn);
217
+ }
218
+ const sealed = chat.sealAssistantTurn(turn);
219
+ if (sealed !== turn) {
220
+ turn = sealed;
92
221
  turnRef.current = turn;
93
222
  syncTurn(turn);
94
223
  }
224
+ flush();
95
225
  if (turn.error !== void 0) throw new Error(turn.error);
96
- canonicalRef.current = [...canonicalRef.current, chat.assistantMessageFromTurn(turn)];
226
+ appendCanonical(chat.assistantMessageFromTurn(turn));
97
227
  turnRef.current = void 0;
98
228
  if (turn.approvals.length > 0) {
99
229
  approvalsRef.current = turn.approvals;
@@ -104,7 +234,14 @@ function useChat(options) {
104
234
  const toolCalls = turn.message.toolCalls ?? [];
105
235
  const serverResults = new Set(turn.serverResults);
106
236
  const clientPending = toolCalls.filter((t) => !serverResults.has(t.toolCallId));
107
- if (clientPending.length === 0 || !options.onToolCall) return;
237
+ if (clientPending.length === 0) return;
238
+ if (!options.onToolCall) {
239
+ parkedTurnRef.current = turn;
240
+ clientToolsRef.current = clientPending;
241
+ toolResultsRef.current = [];
242
+ setPendingToolCalls(clientPending);
243
+ return;
244
+ }
108
245
  const results = [];
109
246
  for (const call of clientPending) {
110
247
  try {
@@ -133,10 +270,18 @@ function useChat(options) {
133
270
  }
134
271
  syncTurn(turn);
135
272
  }
136
- canonicalRef.current = [...canonicalRef.current, chat.clientToolResultMessage(results)];
273
+ appendCanonical(chat.clientToolResultMessage(results));
137
274
  }
138
275
  } catch (err) {
139
- if (controller.signal.aborted) return;
276
+ if (controller.signal.aborted) {
277
+ const open = turnRef.current;
278
+ if (open) {
279
+ const sealed = chat.sealAssistantTurn(open);
280
+ turnRef.current = sealed;
281
+ if (sealed !== open) syncTurn(sealed);
282
+ }
283
+ return;
284
+ }
140
285
  const e = err instanceof Error ? err : new Error(String(err));
141
286
  setError(e);
142
287
  setStatus("error");
@@ -144,6 +289,7 @@ function useChat(options) {
144
289
  return;
145
290
  } finally {
146
291
  setStatus((s) => s === "error" ? s : "idle");
292
+ flush();
147
293
  }
148
294
  },
149
295
  [
@@ -155,47 +301,112 @@ function useChat(options) {
155
301
  options.generateId,
156
302
  options.onToolCall,
157
303
  options.onError,
158
- pushMessage,
304
+ options.onHttpError,
305
+ appendCanonical,
306
+ fold,
307
+ flush,
159
308
  syncTurn
160
309
  ]
161
310
  );
162
311
  const sendMessage = react.useCallback(
163
- async (text) => {
164
- canonicalRef.current = [...canonicalRef.current, { role: "user", content: text }];
165
- pushMessage({ id: genId(), role: "user", content: text });
166
- approvalsRef.current = [];
167
- verdictsRef.current = [];
168
- setPendingApprovals([]);
312
+ async (input) => {
313
+ const message = chat.userMessageFromInput(input);
314
+ const bubble = chat.uiFromMessages([message], genId)[0];
315
+ const previous = snapshotRef.current;
316
+ commit({
317
+ ...previous,
318
+ history: {
319
+ ui: bubble ? [...previous.history.ui, bubble] : previous.history.ui,
320
+ canonical: [...previous.history.canonical, message]
321
+ }
322
+ });
323
+ clearGates();
324
+ flush();
169
325
  await run();
170
326
  },
171
- [run, pushMessage, options.generateId]
327
+ [run, commit, clearGates, flush, options.generateId]
172
328
  );
173
329
  const regenerate = react.useCallback(async () => {
174
- const cut = chat.dropTrailingAssistant({ ui: uiRef.current, canonical: canonicalRef.current });
175
- uiRef.current = cut.ui;
176
- canonicalRef.current = cut.canonical;
177
- setMessages(cut.ui);
178
- approvalsRef.current = [];
179
- verdictsRef.current = [];
180
- setPendingApprovals([]);
330
+ const previous = snapshotRef.current;
331
+ commit({ ...previous, history: chat.dropTrailingAssistant(previous.history) });
332
+ clearGates();
333
+ flush();
181
334
  await run();
182
- }, [run]);
335
+ }, [run, commit, clearGates, flush]);
183
336
  const editAndResend = react.useCallback(
184
- async (messageId, text) => {
185
- const cut = chat.branchBeforeUserMessage(
186
- { ui: uiRef.current, canonical: canonicalRef.current },
187
- messageId
188
- );
337
+ async (messageId, input) => {
338
+ const previous = snapshotRef.current;
339
+ const cut = chat.branchBeforeUserMessage(previous.history, messageId);
189
340
  if (!cut) return;
190
- uiRef.current = cut.ui;
191
- canonicalRef.current = cut.canonical;
192
- setMessages(cut.ui);
193
- approvalsRef.current = [];
194
- verdictsRef.current = [];
195
- setPendingApprovals([]);
196
- await sendMessage(text);
341
+ commit({ ...previous, history: cut });
342
+ clearGates();
343
+ flush();
344
+ await sendMessage(input);
345
+ },
346
+ [sendMessage, commit, clearGates, flush]
347
+ );
348
+ const setHistory = react.useCallback(
349
+ (update) => {
350
+ const previous = snapshotRef.current;
351
+ const next = typeof update === "function" ? update(previous.history) : update;
352
+ commit({
353
+ ...previous,
354
+ history: { ui: [...next.ui], canonical: [...next.canonical] }
355
+ });
356
+ clearGates();
357
+ flush();
197
358
  },
198
- [sendMessage]
359
+ [commit, clearGates, flush]
360
+ );
361
+ const setMessages = react.useCallback(
362
+ (update) => {
363
+ setHistory((prev) => {
364
+ const ui = typeof update === "function" ? update(prev.ui) : update;
365
+ return { ui, canonical: chat.canonicalFromUI(ui) };
366
+ });
367
+ },
368
+ [setHistory]
369
+ );
370
+ const clearError = react.useCallback(() => {
371
+ setError(void 0);
372
+ setStatus((s) => s === "error" ? "idle" : s);
373
+ }, []);
374
+ const addToolResult = react.useCallback(
375
+ async (result) => {
376
+ const parked = clientToolsRef.current;
377
+ const call = parked.find((c) => c.toolCallId === result.toolCallId);
378
+ if (!call) return;
379
+ toolResultsRef.current = [
380
+ ...toolResultsRef.current.filter((r) => r.toolCallId !== result.toolCallId),
381
+ {
382
+ toolCallId: result.toolCallId,
383
+ result: result.output,
384
+ ...result.isError ? { isError: true } : {}
385
+ }
386
+ ];
387
+ const turn = parkedTurnRef.current;
388
+ if (turn) {
389
+ const next = chat.applyUIPart(turn, {
390
+ type: "tool-result",
391
+ toolCallId: result.toolCallId,
392
+ toolName: call.toolName,
393
+ output: result.output,
394
+ ...result.isError ? { isError: true } : {}
395
+ });
396
+ parkedTurnRef.current = next;
397
+ syncTurn(next);
398
+ flush();
399
+ }
400
+ const answered = toolResultsRef.current;
401
+ if (!parked.every((c) => answered.some((r) => r.toolCallId === c.toolCallId))) return;
402
+ clientToolsRef.current = [];
403
+ toolResultsRef.current = [];
404
+ parkedTurnRef.current = void 0;
405
+ setPendingToolCalls([]);
406
+ appendCanonical(chat.clientToolResultMessage(answered));
407
+ await run();
408
+ },
409
+ [run, appendCanonical, syncTurn, flush]
199
410
  );
200
411
  const addToolApprovalResponse = react.useCallback(
201
412
  async (response) => {
@@ -217,77 +428,118 @@ function useChat(options) {
217
428
  },
218
429
  [run]
219
430
  );
220
- const reconnect = react.useCallback(async () => {
221
- const resume = options.resume;
222
- if (!resume) return;
223
- const controller = new AbortController();
224
- abortRef.current = controller;
225
- setStatus("streaming");
226
- setError(void 0);
227
- try {
228
- const cursor = lastEventIdRef.current;
229
- let turn;
230
- if (turnRef.current && cursor !== void 0) {
231
- turn = turnRef.current;
232
- } else {
233
- turn = chat.createAssistantTurn(genId());
234
- if (!turnRef.current) pushMessage(turn.message);
235
- }
236
- turnRef.current = turn;
237
- const parts = ui.connectDeuzStream(resume.endpoint, {
238
- ...cursor !== void 0 ? { lastEventId: cursor } : {},
239
- onCursor: (id) => {
240
- lastEventIdRef.current = id;
241
- },
242
- signal: controller.signal,
243
- ...options.fetch ? { fetch: options.fetch } : {},
244
- ...options.headers ? { headers: options.headers } : {}
245
- });
246
- for await (const part of parts) {
247
- turn = foldPart(turn, part);
431
+ const runResume = react.useCallback(
432
+ async (silent) => {
433
+ const resume = options.resume;
434
+ if (!resume) return;
435
+ const controller = new AbortController();
436
+ abortRef.current = controller;
437
+ setStatus("streaming");
438
+ setError(void 0);
439
+ let folded = 0;
440
+ try {
441
+ const cursor = lastEventIdRef.current ?? loadCursor(resume);
442
+ const inFlight = turnRef.current;
443
+ let turn = inFlight;
444
+ if (turn === void 0 || cursor === void 0) {
445
+ if (inFlight === void 0) placedIdRef.current = void 0;
446
+ turn = chat.createAssistantTurn(genId());
447
+ }
248
448
  turnRef.current = turn;
249
- syncTurn(turn);
250
- }
251
- if (turn.error !== void 0) throw new Error(turn.error);
252
- canonicalRef.current = [...canonicalRef.current, chat.assistantMessageFromTurn(turn)];
253
- turnRef.current = void 0;
254
- if (turn.approvals.length > 0) {
255
- approvalsRef.current = turn.approvals;
256
- verdictsRef.current = [];
257
- setPendingApprovals(turn.approvals);
449
+ const parts = ui.connectDeuzStream(resume.endpoint, {
450
+ ...cursor !== void 0 ? { lastEventId: cursor } : {},
451
+ onCursor: (id) => {
452
+ lastEventIdRef.current = id;
453
+ saveCursor(resume, id);
454
+ },
455
+ signal: controller.signal,
456
+ ...options.fetch ? { fetch: options.fetch } : {},
457
+ ...options.headers ? { headers: options.headers } : {}
458
+ });
459
+ for await (const part of parts) {
460
+ folded++;
461
+ turn = fold(turn, part);
462
+ turnRef.current = turn;
463
+ syncTurn(turn);
464
+ }
465
+ if (folded === 0) {
466
+ turnRef.current = inFlight === void 0 ? void 0 : chat.sealAssistantTurn(inFlight);
467
+ return;
468
+ }
469
+ const sealed = chat.sealAssistantTurn(turn);
470
+ if (sealed !== turn) {
471
+ turn = sealed;
472
+ syncTurn(turn);
473
+ }
474
+ turnRef.current = turn;
475
+ if (turn.error !== void 0) throw new Error(turn.error);
476
+ appendCanonical(chat.assistantMessageFromTurn(turn));
477
+ turnRef.current = void 0;
478
+ if (turn.approvals.length > 0) {
479
+ approvalsRef.current = turn.approvals;
480
+ verdictsRef.current = [];
481
+ setPendingApprovals(turn.approvals);
482
+ }
483
+ } catch (err) {
484
+ if (controller.signal.aborted) return;
485
+ if (silent) return;
486
+ const e = err instanceof Error ? err : new Error(String(err));
487
+ setError(e);
488
+ setStatus("error");
489
+ options.onError?.(e);
490
+ return;
491
+ } finally {
492
+ setStatus((s) => s === "error" ? s : "idle");
493
+ flush();
258
494
  }
259
- } catch (err) {
260
- if (controller.signal.aborted) return;
261
- const e = err instanceof Error ? err : new Error(String(err));
262
- setError(e);
263
- setStatus("error");
264
- options.onError?.(e);
265
- return;
266
- } finally {
267
- setStatus((s) => s === "error" ? s : "idle");
268
- }
269
- }, [
270
- options.resume,
271
- options.fetch,
272
- options.headers,
273
- options.generateId,
274
- options.onError,
275
- pushMessage,
276
- syncTurn
277
- ]);
495
+ },
496
+ [
497
+ options.resume,
498
+ options.fetch,
499
+ options.headers,
500
+ options.generateId,
501
+ options.onError,
502
+ appendCanonical,
503
+ fold,
504
+ flush,
505
+ syncTurn
506
+ ]
507
+ );
508
+ const reconnect = react.useCallback(() => runResume(false), [runResume]);
509
+ const autoResumedRef = react.useRef(false);
510
+ react.useEffect(() => {
511
+ if (options.resume?.auto !== true || autoResumedRef.current) return;
512
+ autoResumedRef.current = true;
513
+ void runResume(true);
514
+ }, [options.resume?.auto, runResume]);
278
515
  return {
279
- messages,
516
+ messages: snapshot.history.ui,
517
+ history: snapshot.history,
280
518
  status,
281
519
  error,
282
520
  pendingApprovals,
283
- ...cost !== void 0 ? { cost } : {},
284
- ...budgetExceeded !== void 0 ? { budgetExceeded } : {},
285
- dataParts,
286
- citations,
521
+ pendingToolCalls,
522
+ ...snapshot.cost !== void 0 ? { cost: snapshot.cost } : {},
523
+ ...snapshot.budgetExceeded !== void 0 ? { budgetExceeded: snapshot.budgetExceeded } : {},
524
+ dataParts: snapshot.dataParts,
525
+ citations: snapshot.citations,
526
+ ...snapshot.plan !== void 0 ? { plan: snapshot.plan } : {},
527
+ activity: snapshot.activity,
528
+ ...snapshot.verifications !== void 0 ? { verifications: snapshot.verifications } : {},
529
+ ...snapshot.warnings !== void 0 ? { warnings: snapshot.warnings } : {},
530
+ ...snapshot.falseFinishes !== void 0 ? { falseFinishes: snapshot.falseFinishes } : {},
531
+ ...snapshot.subAgents !== void 0 ? { subAgents: snapshot.subAgents } : {},
532
+ ...snapshot.steps !== void 0 ? { steps: snapshot.steps } : {},
533
+ ...snapshot.usage !== void 0 ? { usage: snapshot.usage } : {},
534
+ ...snapshot.finishReason !== void 0 ? { finishReason: snapshot.finishReason } : {},
287
535
  sendMessage,
288
536
  stop,
289
537
  regenerate,
290
538
  editAndResend,
539
+ setHistory,
540
+ setMessages,
541
+ addToolResult,
542
+ clearError,
291
543
  addToolApprovalResponse,
292
544
  reconnect
293
545
  };
@@ -297,6 +549,40 @@ function useObject(options) {
297
549
  const [isLoading, setLoading] = react.useState(false);
298
550
  const [error, setError] = react.useState(void 0);
299
551
  const abortRef = react.useRef(null);
552
+ const throttleMs = options.throttleMs ?? 0;
553
+ const latestRef = react.useRef(void 0);
554
+ const timerRef = react.useRef(void 0);
555
+ const dirtyRef = react.useRef(false);
556
+ const publish = react.useCallback(() => {
557
+ timerRef.current = void 0;
558
+ dirtyRef.current = false;
559
+ setObject(latestRef.current);
560
+ }, []);
561
+ const commit = react.useCallback(
562
+ (next) => {
563
+ latestRef.current = next;
564
+ if (throttleMs <= 0) {
565
+ publish();
566
+ return;
567
+ }
568
+ dirtyRef.current = true;
569
+ if (timerRef.current === void 0) timerRef.current = setTimeout(publish, throttleMs);
570
+ },
571
+ [throttleMs, publish]
572
+ );
573
+ const flush = react.useCallback(() => {
574
+ if (timerRef.current !== void 0) {
575
+ clearTimeout(timerRef.current);
576
+ timerRef.current = void 0;
577
+ }
578
+ if (dirtyRef.current) publish();
579
+ }, [publish]);
580
+ react.useEffect(
581
+ () => () => {
582
+ if (timerRef.current !== void 0) clearTimeout(timerRef.current);
583
+ },
584
+ []
585
+ );
300
586
  const stop = react.useCallback(() => {
301
587
  abortRef.current?.abort();
302
588
  }, []);
@@ -307,7 +593,8 @@ function useObject(options) {
307
593
  abortRef.current = controller;
308
594
  setLoading(true);
309
595
  setError(void 0);
310
- setObject(void 0);
596
+ commit(void 0);
597
+ flush();
311
598
  try {
312
599
  const doFetch = options.fetch ?? fetch;
313
600
  const res = await doFetch(options.api, {
@@ -316,9 +603,11 @@ function useObject(options) {
316
603
  body: JSON.stringify({ input }),
317
604
  signal: controller.signal
318
605
  });
319
- for await (const part of ui.readDeuzStream(res)) {
606
+ for await (const part of ui.readDeuzStream(res, {
607
+ ...options.onHttpError ? { onHttpError: options.onHttpError } : {}
608
+ })) {
320
609
  if (part.type === "object-delta") {
321
- setObject(part.object);
610
+ commit(part.object);
322
611
  } else if (part.type === "error") {
323
612
  setError(new Error(part.message));
324
613
  break;
@@ -330,9 +619,10 @@ function useObject(options) {
330
619
  }
331
620
  } finally {
332
621
  setLoading(false);
622
+ flush();
333
623
  }
334
624
  },
335
- [options.api, options.fetch, options.headers]
625
+ [options.api, options.fetch, options.headers, options.onHttpError, commit, flush]
336
626
  );
337
627
  return { object, isLoading, error, submit, stop };
338
628
  }
@@ -362,6 +652,7 @@ function CostBadge({ cost, format }) {
362
652
 
363
653
  exports.CostBadge = CostBadge;
364
654
  exports.ToolApprovalCard = ToolApprovalCard;
655
+ exports.partsFromFiles = partsFromFiles;
365
656
  exports.useChat = useChat;
366
657
  exports.useObject = useObject;
367
658
  //# sourceMappingURL=index.cjs.map