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