@deuz-sdk/react 1.8.0 → 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,50 +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 [plan, setPlan] = useState(void 0);
35
- const [activity, setActivity] = useState([]);
36
- const uiRef = useRef(messages);
37
- const canonicalRef = useRef(
38
- options.initialMessages ? [...options.initialMessages] : []
39
- );
54
+ const [pendingToolCalls, setPendingToolCalls] = useState([]);
55
+ const snapshotRef = useRef(snapshot);
40
56
  const turnRef = useRef(void 0);
57
+ const parkedTurnRef = useRef(void 0);
58
+ const clientToolsRef = useRef([]);
59
+ const toolResultsRef = useRef(
60
+ []
61
+ );
41
62
  const approvalsRef = useRef([]);
42
63
  const verdictsRef = useRef([]);
43
64
  const abortRef = useRef(null);
44
65
  const lastEventIdRef = useRef(options.resume?.lastEventId);
45
- const pushMessage = useCallback((message) => {
46
- uiRef.current = [...uiRef.current, message];
47
- 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);
48
74
  }, []);
49
- const syncTurn = 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
- });
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;
57
93
  }
58
- setBudgetExceeded(turn.budgetExceeded);
59
- setDataParts(turn.dataParts);
60
- setCitations(turn.citations);
61
- setPlan(turn.plan);
62
- setActivity(turn.activity);
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([]);
63
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
+ );
64
180
  const stop = useCallback(() => {
65
181
  abortRef.current?.abort();
66
182
  }, []);
@@ -77,7 +193,7 @@ function useChat(options) {
77
193
  method: "POST",
78
194
  headers: { "content-type": "application/json", ...options.headers },
79
195
  body: JSON.stringify({
80
- messages: canonicalRef.current,
196
+ messages: snapshotRef.current.history.canonical,
81
197
  ...options.chatId !== void 0 ? { chatId: options.chatId } : {},
82
198
  ...approvalResponses?.length ? { approvalResponses } : {},
83
199
  ...options.body
@@ -87,15 +203,25 @@ function useChat(options) {
87
203
  approvalResponses = void 0;
88
204
  let turn = createAssistantTurn(genId());
89
205
  turnRef.current = turn;
90
- pushMessage(turn.message);
206
+ placedIdRef.current = void 0;
91
207
  syncTurn(turn);
92
- for await (const part of readDeuzStream(res)) {
93
- 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;
94
219
  turnRef.current = turn;
95
220
  syncTurn(turn);
96
221
  }
222
+ flush();
97
223
  if (turn.error !== void 0) throw new Error(turn.error);
98
- canonicalRef.current = [...canonicalRef.current, assistantMessageFromTurn(turn)];
224
+ appendCanonical(assistantMessageFromTurn(turn));
99
225
  turnRef.current = void 0;
100
226
  if (turn.approvals.length > 0) {
101
227
  approvalsRef.current = turn.approvals;
@@ -106,7 +232,14 @@ function useChat(options) {
106
232
  const toolCalls = turn.message.toolCalls ?? [];
107
233
  const serverResults = new Set(turn.serverResults);
108
234
  const clientPending = toolCalls.filter((t) => !serverResults.has(t.toolCallId));
109
- 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
+ }
110
243
  const results = [];
111
244
  for (const call of clientPending) {
112
245
  try {
@@ -135,10 +268,18 @@ function useChat(options) {
135
268
  }
136
269
  syncTurn(turn);
137
270
  }
138
- canonicalRef.current = [...canonicalRef.current, clientToolResultMessage(results)];
271
+ appendCanonical(clientToolResultMessage(results));
139
272
  }
140
273
  } catch (err) {
141
- 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
+ }
142
283
  const e = err instanceof Error ? err : new Error(String(err));
143
284
  setError(e);
144
285
  setStatus("error");
@@ -146,6 +287,7 @@ function useChat(options) {
146
287
  return;
147
288
  } finally {
148
289
  setStatus((s) => s === "error" ? s : "idle");
290
+ flush();
149
291
  }
150
292
  },
151
293
  [
@@ -157,47 +299,112 @@ function useChat(options) {
157
299
  options.generateId,
158
300
  options.onToolCall,
159
301
  options.onError,
160
- pushMessage,
302
+ options.onHttpError,
303
+ appendCanonical,
304
+ fold,
305
+ flush,
161
306
  syncTurn
162
307
  ]
163
308
  );
164
309
  const sendMessage = useCallback(
165
- async (text) => {
166
- canonicalRef.current = [...canonicalRef.current, { role: "user", content: text }];
167
- pushMessage({ id: genId(), role: "user", content: text });
168
- approvalsRef.current = [];
169
- verdictsRef.current = [];
170
- 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();
171
323
  await run();
172
324
  },
173
- [run, pushMessage, options.generateId]
325
+ [run, commit, clearGates, flush, options.generateId]
174
326
  );
175
327
  const regenerate = useCallback(async () => {
176
- const cut = dropTrailingAssistant({ ui: uiRef.current, canonical: canonicalRef.current });
177
- uiRef.current = cut.ui;
178
- canonicalRef.current = cut.canonical;
179
- setMessages(cut.ui);
180
- approvalsRef.current = [];
181
- verdictsRef.current = [];
182
- setPendingApprovals([]);
328
+ const previous = snapshotRef.current;
329
+ commit({ ...previous, history: dropTrailingAssistant(previous.history) });
330
+ clearGates();
331
+ flush();
183
332
  await run();
184
- }, [run]);
333
+ }, [run, commit, clearGates, flush]);
185
334
  const editAndResend = useCallback(
186
- async (messageId, text) => {
187
- const cut = branchBeforeUserMessage(
188
- { ui: uiRef.current, canonical: canonicalRef.current },
189
- messageId
190
- );
335
+ async (messageId, input) => {
336
+ const previous = snapshotRef.current;
337
+ const cut = branchBeforeUserMessage(previous.history, messageId);
191
338
  if (!cut) return;
192
- uiRef.current = cut.ui;
193
- canonicalRef.current = cut.canonical;
194
- setMessages(cut.ui);
195
- approvalsRef.current = [];
196
- verdictsRef.current = [];
197
- setPendingApprovals([]);
198
- 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();
199
356
  },
200
- [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]
201
408
  );
202
409
  const addToolApprovalResponse = useCallback(
203
410
  async (response) => {
@@ -219,79 +426,118 @@ function useChat(options) {
219
426
  },
220
427
  [run]
221
428
  );
222
- const reconnect = useCallback(async () => {
223
- const resume = options.resume;
224
- if (!resume) return;
225
- const controller = new AbortController();
226
- abortRef.current = controller;
227
- setStatus("streaming");
228
- setError(void 0);
229
- try {
230
- const cursor = lastEventIdRef.current;
231
- let turn;
232
- if (turnRef.current && cursor !== void 0) {
233
- turn = turnRef.current;
234
- } else {
235
- turn = createAssistantTurn(genId());
236
- if (!turnRef.current) pushMessage(turn.message);
237
- }
238
- turnRef.current = turn;
239
- const parts = connectDeuzStream(resume.endpoint, {
240
- ...cursor !== void 0 ? { lastEventId: cursor } : {},
241
- onCursor: (id) => {
242
- lastEventIdRef.current = id;
243
- },
244
- signal: controller.signal,
245
- ...options.fetch ? { fetch: options.fetch } : {},
246
- ...options.headers ? { headers: options.headers } : {}
247
- });
248
- for await (const part of parts) {
249
- 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
+ }
250
446
  turnRef.current = turn;
251
- syncTurn(turn);
252
- }
253
- if (turn.error !== void 0) throw new Error(turn.error);
254
- canonicalRef.current = [...canonicalRef.current, assistantMessageFromTurn(turn)];
255
- turnRef.current = void 0;
256
- if (turn.approvals.length > 0) {
257
- approvalsRef.current = turn.approvals;
258
- verdictsRef.current = [];
259
- 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();
260
492
  }
261
- } catch (err) {
262
- if (controller.signal.aborted) return;
263
- const e = err instanceof Error ? err : new Error(String(err));
264
- setError(e);
265
- setStatus("error");
266
- options.onError?.(e);
267
- return;
268
- } finally {
269
- setStatus((s) => s === "error" ? s : "idle");
270
- }
271
- }, [
272
- options.resume,
273
- options.fetch,
274
- options.headers,
275
- options.generateId,
276
- options.onError,
277
- pushMessage,
278
- syncTurn
279
- ]);
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]);
280
513
  return {
281
- messages,
514
+ messages: snapshot.history.ui,
515
+ history: snapshot.history,
282
516
  status,
283
517
  error,
284
518
  pendingApprovals,
285
- ...cost !== void 0 ? { cost } : {},
286
- ...budgetExceeded !== void 0 ? { budgetExceeded } : {},
287
- dataParts,
288
- citations,
289
- ...plan !== void 0 ? { plan } : {},
290
- activity,
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 } : {},
291
533
  sendMessage,
292
534
  stop,
293
535
  regenerate,
294
536
  editAndResend,
537
+ setHistory,
538
+ setMessages,
539
+ addToolResult,
540
+ clearError,
295
541
  addToolApprovalResponse,
296
542
  reconnect
297
543
  };
@@ -301,6 +547,40 @@ function useObject(options) {
301
547
  const [isLoading, setLoading] = useState(false);
302
548
  const [error, setError] = useState(void 0);
303
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
+ );
304
584
  const stop = useCallback(() => {
305
585
  abortRef.current?.abort();
306
586
  }, []);
@@ -311,7 +591,8 @@ function useObject(options) {
311
591
  abortRef.current = controller;
312
592
  setLoading(true);
313
593
  setError(void 0);
314
- setObject(void 0);
594
+ commit(void 0);
595
+ flush();
315
596
  try {
316
597
  const doFetch = options.fetch ?? fetch;
317
598
  const res = await doFetch(options.api, {
@@ -320,9 +601,11 @@ function useObject(options) {
320
601
  body: JSON.stringify({ input }),
321
602
  signal: controller.signal
322
603
  });
323
- for await (const part of readDeuzStream(res)) {
604
+ for await (const part of readDeuzStream(res, {
605
+ ...options.onHttpError ? { onHttpError: options.onHttpError } : {}
606
+ })) {
324
607
  if (part.type === "object-delta") {
325
- setObject(part.object);
608
+ commit(part.object);
326
609
  } else if (part.type === "error") {
327
610
  setError(new Error(part.message));
328
611
  break;
@@ -334,9 +617,10 @@ function useObject(options) {
334
617
  }
335
618
  } finally {
336
619
  setLoading(false);
620
+ flush();
337
621
  }
338
622
  },
339
- [options.api, options.fetch, options.headers]
623
+ [options.api, options.fetch, options.headers, options.onHttpError, commit, flush]
340
624
  );
341
625
  return { object, isLoading, error, submit, stop };
342
626
  }
@@ -364,6 +648,6 @@ function CostBadge({ cost, format }) {
364
648
  return /* @__PURE__ */ jsx("span", { "data-deuz": "cost-badge", children: `$${cost.costUsd.toFixed(4)}${saved}` });
365
649
  }
366
650
 
367
- export { CostBadge, ToolApprovalCard, useChat, useObject };
651
+ export { CostBadge, ToolApprovalCard, partsFromFiles, useChat, useObject };
368
652
  //# sourceMappingURL=index.js.map
369
653
  //# sourceMappingURL=index.js.map