@opengeni/react 0.25.0 → 0.26.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.
Files changed (46) hide show
  1. package/dist/{chunk-AVPU5PMC.js → chunk-F4CFWKVI.js} +332 -64
  2. package/dist/chunk-F4CFWKVI.js.map +1 -0
  3. package/dist/chunk-GQN2QIR2.js +3767 -0
  4. package/dist/chunk-GQN2QIR2.js.map +1 -0
  5. package/dist/{chunk-I3BJZIG5.js → chunk-HIWPQYWI.js} +2 -120
  6. package/dist/chunk-HIWPQYWI.js.map +1 -0
  7. package/dist/chunk-M7X4JZOD.js +121 -0
  8. package/dist/chunk-M7X4JZOD.js.map +1 -0
  9. package/dist/{chunk-SHOFILHJ.js → chunk-MP6237JB.js} +570 -984
  10. package/dist/chunk-MP6237JB.js.map +1 -0
  11. package/dist/chunk-OZDLELJQ.js +937 -0
  12. package/dist/chunk-OZDLELJQ.js.map +1 -0
  13. package/dist/{chunk-RDPDU4TA.js → chunk-TMH6HZWF.js} +3 -3
  14. package/dist/{chunk-6XUS5VFM.js → chunk-YU5PGUK7.js} +6 -4
  15. package/dist/{chunk-6XUS5VFM.js.map → chunk-YU5PGUK7.js.map} +1 -1
  16. package/dist/{composer-BXb0Q1HF.d.ts → composer-DoC4veX1.d.ts} +4 -75
  17. package/dist/composer.d.ts +2 -1
  18. package/dist/composer.js +4 -3
  19. package/dist/index.d.ts +14 -259
  20. package/dist/index.js +1016 -5140
  21. package/dist/index.js.map +1 -1
  22. package/dist/machines.js +3 -2
  23. package/dist/session-Du_FsrZ1.d.ts +264 -0
  24. package/dist/session-ui-BqQDH7YV.d.ts +183 -0
  25. package/dist/session-ui.d.ts +7 -0
  26. package/dist/session-ui.js +17 -0
  27. package/dist/session-ui.js.map +1 -0
  28. package/dist/session.d.ts +3 -1
  29. package/dist/session.js +26 -9
  30. package/dist/use-file-attachments-C0kpHrs9.d.ts +76 -0
  31. package/dist/{session-BEvtFWhe.d.ts → use-turn-queue-3rkJwjFv.d.ts} +3 -185
  32. package/package.json +6 -2
  33. package/src/components/message-timeline.tsx +92 -52
  34. package/src/hooks/use-composer.ts +456 -69
  35. package/src/hooks/use-file-attachments.ts +1 -1
  36. package/src/hooks/use-goal.ts +1 -1
  37. package/src/hooks/use-session-events.ts +5 -0
  38. package/src/hooks/use-session-lineage.ts +1 -1
  39. package/src/hooks/use-session.ts +9 -6
  40. package/src/session-ui.ts +13 -0
  41. package/src/session.ts +15 -0
  42. package/src/timeline/activity-rail.tsx +1 -1
  43. package/dist/chunk-AVPU5PMC.js.map +0 -1
  44. package/dist/chunk-I3BJZIG5.js.map +0 -1
  45. package/dist/chunk-SHOFILHJ.js.map +0 -1
  46. /package/dist/{chunk-RDPDU4TA.js.map → chunk-TMH6HZWF.js.map} +0 -0
@@ -1,15 +1,116 @@
1
1
  import {
2
2
  useEmbeddedSession,
3
3
  useSessionEventTrigger
4
- } from "./chunk-I3BJZIG5.js";
4
+ } from "./chunk-HIWPQYWI.js";
5
5
 
6
6
  // src/hooks/use-composer.ts
7
7
  import { useCallback, useEffect, useLayoutEffect, useRef, useState } from "react";
8
+ var PENDING_COMPOSER_STORAGE_PREFIX = "opengeni.pending-composer.v1:";
9
+ var pendingComposerOperations = /* @__PURE__ */ new Map();
10
+ function pendingComposerOperationKey(workspaceId, sessionId) {
11
+ return sessionId ? `${workspaceId}\0${sessionId}` : null;
12
+ }
13
+ function pendingComposerStorage() {
14
+ try {
15
+ return typeof window === "undefined" ? null : window.sessionStorage;
16
+ } catch {
17
+ return null;
18
+ }
19
+ }
20
+ function pendingComposerStorageKey(key) {
21
+ return `${PENDING_COMPOSER_STORAGE_PREFIX}${encodeURIComponent(key)}`;
22
+ }
23
+ function resourceList(value) {
24
+ return Array.isArray(value) && value.every((candidate) => {
25
+ if (typeof candidate !== "object" || candidate === null) return false;
26
+ const resource = candidate;
27
+ return resource.kind === "file" ? typeof resource.fileId === "string" : resource.kind === "repository" && typeof resource.uri === "string" && typeof resource.ref === "string";
28
+ });
29
+ }
30
+ function readStoredPendingComposerOperation(key) {
31
+ const storage = key ? pendingComposerStorage() : null;
32
+ if (!storage || !key) return null;
33
+ try {
34
+ const parsed = JSON.parse(storage.getItem(pendingComposerStorageKey(key)) ?? "null");
35
+ if (typeof parsed !== "object" || parsed === null) return null;
36
+ const record = parsed;
37
+ const input = record.input;
38
+ const shadow = record.newerShadow;
39
+ if (typeof input !== "object" || input === null || typeof shadow !== "object" || shadow === null) {
40
+ return null;
41
+ }
42
+ const inputRecord = input;
43
+ const shadowRecord = shadow;
44
+ if (record.delivery !== "send" && record.delivery !== "steer" || typeof record.draftAtSend !== "string" || !resourceList(record.resourcesAtSend) || typeof shadowRecord.text !== "string" || !resourceList(shadowRecord.resources) || typeof record.clearDraftOnAccept !== "boolean" || typeof record.hasMcpCredentialUpdates !== "boolean" || typeof inputRecord.text !== "string" || typeof inputRecord.clientEventId !== "string" || "mcpCredentialUpdates" in inputRecord || "resources" in inputRecord && !resourceList(inputRecord.resources)) {
45
+ return null;
46
+ }
47
+ return record;
48
+ } catch {
49
+ return null;
50
+ }
51
+ }
52
+ function writePendingComposerOperation(key, operation) {
53
+ const storage = pendingComposerStorage();
54
+ if (!storage) return;
55
+ try {
56
+ storage.setItem(pendingComposerStorageKey(key), JSON.stringify(operation));
57
+ } catch {
58
+ }
59
+ }
60
+ function restorePendingComposerOperation(key) {
61
+ const stored = (key && pendingComposerOperations.get(key)) ?? readStoredPendingComposerOperation(key);
62
+ if (!stored) return null;
63
+ return {
64
+ ...stored,
65
+ input: stored.input,
66
+ newerShadow: stored.newerShadow ?? {
67
+ text: stored.draftAtSend,
68
+ resources: stored.resourcesAtSend
69
+ },
70
+ canRetry: !stored.hasMcpCredentialUpdates
71
+ };
72
+ }
73
+ function rememberPendingComposerOperation(key, operation) {
74
+ if (!key) return;
75
+ const { canRetry: _retry, ...safeOperation } = operation;
76
+ const { input: originalInput, ...withoutInput } = safeOperation;
77
+ const { mcpCredentialUpdates: _storedMcp, ...safeInput } = originalInput;
78
+ const stored = {
79
+ ...withoutInput,
80
+ input: safeInput,
81
+ hasMcpCredentialUpdates: operation.input.mcpCredentialUpdates !== void 0,
82
+ newerShadow: {
83
+ text: operation.newerShadow.text,
84
+ resources: [...operation.newerShadow.resources]
85
+ }
86
+ };
87
+ pendingComposerOperations.set(key, stored);
88
+ writePendingComposerOperation(key, stored);
89
+ }
90
+ function forgetPendingComposerOperation(key) {
91
+ if (!key) return;
92
+ pendingComposerOperations.delete(key);
93
+ const storage = pendingComposerStorage();
94
+ if (!storage) return;
95
+ try {
96
+ storage.removeItem(pendingComposerStorageKey(key));
97
+ } catch {
98
+ }
99
+ }
100
+ function updatePendingComposerShadow(key, operation, shadow) {
101
+ if (!key || !operation) return operation;
102
+ const next = { ...operation, newerShadow: { ...shadow, resources: [...shadow.resources] } };
103
+ rememberPendingComposerOperation(key, next);
104
+ return next;
105
+ }
8
106
  function useComposer(sessionId, options = {}) {
9
107
  const { client, workspaceId, registerSessionReconciler } = useEmbeddedSession(options);
10
108
  const durableDrafts = options.draftPersistence !== "disabled";
11
109
  const targetKey = `${workspaceId}\0${sessionId ?? ""}\0${durableDrafts ? "durable" : "disabled"}`;
12
- const [value, setValue] = useState("");
110
+ const pendingOperationKey = pendingComposerOperationKey(workspaceId, sessionId);
111
+ const initialPendingOperation = restorePendingComposerOperation(pendingOperationKey);
112
+ const initialShadow = initialPendingOperation?.newerShadow;
113
+ const [value, setValue] = useState(() => initialShadow?.text ?? "");
13
114
  const [stateTargetKey, setStateTargetKey] = useState(targetKey);
14
115
  const [sending, setSending] = useState(false);
15
116
  const [pausing, setPausing] = useState(false);
@@ -19,18 +120,27 @@ function useComposer(sessionId, options = {}) {
19
120
  const [draftLoading, setDraftLoading] = useState(Boolean(sessionId) && durableDrafts);
20
121
  const [draftSaving, setDraftSaving] = useState(false);
21
122
  const [draftConflict, setDraftConflict] = useState(null);
22
- const [restoredResources, setRestoredResources] = useState([]);
23
- const pendingClientEventId = useRef(null);
24
- const valueRef = useRef("");
123
+ const [restoredResources, setRestoredResources] = useState(
124
+ () => initialShadow?.resources ?? []
125
+ );
126
+ const pendingOperationRef = useRef(initialPendingOperation);
127
+ const pendingClientEventId = useRef(
128
+ initialPendingOperation?.input.clientEventId ?? null
129
+ );
130
+ const valueRef = useRef(initialShadow?.text ?? "");
25
131
  const draftRef = useRef(null);
26
- const restoredResourcesRef = useRef([]);
27
- const localEditRevision = useRef(0);
132
+ const restoredResourcesRef = useRef(initialShadow?.resources ?? []);
133
+ const localEditRevision = useRef(initialShadow ? 1 : 0);
28
134
  const targetGeneration = useRef(0);
29
135
  const draftReadGeneration = useRef(0);
30
136
  const lastSavedSignature = useRef(null);
31
137
  const saveChain = useRef(Promise.resolve());
32
138
  const onSent = options.onSent;
33
139
  const onDraftApplied = options.onDraftApplied;
140
+ const onDraftAppliedRef = useRef(onDraftApplied);
141
+ useLayoutEffect(() => {
142
+ onDraftAppliedRef.current = onDraftApplied;
143
+ }, [onDraftApplied]);
34
144
  const sendExtrasRef = useRef(options.sendExtras);
35
145
  sendExtrasRef.current = options.sendExtras;
36
146
  const sendBlockedRef = useRef(options.sendBlocked);
@@ -42,15 +152,17 @@ function useComposer(sessionId, options = {}) {
42
152
  targetKeyRef.current = targetKey;
43
153
  targetGeneration.current += 1;
44
154
  draftReadGeneration.current += 1;
45
- pendingClientEventId.current = null;
46
- localEditRevision.current = 0;
47
- valueRef.current = "";
155
+ pendingOperationRef.current = restorePendingComposerOperation(pendingOperationKey);
156
+ pendingClientEventId.current = pendingOperationRef.current?.input.clientEventId ?? null;
157
+ const shadow = pendingOperationRef.current?.newerShadow;
158
+ localEditRevision.current = shadow ? 1 : 0;
159
+ valueRef.current = shadow?.text ?? "";
48
160
  draftRef.current = null;
49
- restoredResourcesRef.current = [];
161
+ restoredResourcesRef.current = shadow?.resources ?? [];
50
162
  lastSavedSignature.current = null;
51
163
  saveChain.current = Promise.resolve();
52
164
  setStateTargetKey(targetKey);
53
- setValue("");
165
+ setValue(shadow?.text ?? "");
54
166
  setSending(false);
55
167
  setPausing(false);
56
168
  setResuming(false);
@@ -59,13 +171,12 @@ function useComposer(sessionId, options = {}) {
59
171
  setDraftLoading(Boolean(sessionId) && durableDrafts);
60
172
  setDraftSaving(false);
61
173
  setDraftConflict(null);
62
- setRestoredResources([]);
63
- }, [durableDrafts, sessionId, targetKey]);
174
+ setRestoredResources(shadow?.resources ?? []);
175
+ }, [durableDrafts, pendingOperationKey, sessionId, targetKey]);
64
176
  const applyDraft = useCallback(
65
177
  (next) => {
66
178
  if (targetKeyRef.current !== targetKey) return;
67
179
  if (!durableDrafts) {
68
- pendingClientEventId.current = null;
69
180
  localEditRevision.current += 1;
70
181
  valueRef.current = next.text;
71
182
  restoredResourcesRef.current = next.resources;
@@ -74,6 +185,17 @@ function useComposer(sessionId, options = {}) {
74
185
  setDraft(null);
75
186
  setValue(next.text);
76
187
  setRestoredResources(next.resources);
188
+ pendingOperationRef.current = updatePendingComposerShadow(
189
+ pendingOperationKey,
190
+ pendingOperationRef.current,
191
+ {
192
+ text: next.text,
193
+ resources: mergeResources(
194
+ next.resources,
195
+ resolveSendExtras(sendExtrasRef.current).resources ?? []
196
+ )
197
+ }
198
+ );
77
199
  setDraftConflict(null);
78
200
  return;
79
201
  }
@@ -82,14 +204,24 @@ function useComposer(sessionId, options = {}) {
82
204
  restoredResourcesRef.current = next.resources;
83
205
  lastSavedSignature.current = draftSignature(draftPayload(next));
84
206
  localEditRevision.current += 1;
85
- pendingClientEventId.current = null;
86
207
  setDraft(next);
87
208
  setValue(next.text);
88
209
  setRestoredResources(next.resources);
210
+ pendingOperationRef.current = updatePendingComposerShadow(
211
+ pendingOperationKey,
212
+ pendingOperationRef.current,
213
+ {
214
+ text: next.text,
215
+ resources: mergeResources(
216
+ next.resources,
217
+ resolveSendExtras(sendExtrasRef.current).resources ?? []
218
+ )
219
+ }
220
+ );
89
221
  setDraftConflict(null);
90
- onDraftApplied?.(next);
222
+ onDraftAppliedRef.current?.(next);
91
223
  },
92
- [durableDrafts, onDraftApplied, targetKey]
224
+ [durableDrafts, pendingOperationKey, targetKey]
93
225
  );
94
226
  const loadDraft = useCallback(
95
227
  async (replaceLocal) => {
@@ -123,13 +255,20 @@ function useComposer(sessionId, options = {}) {
123
255
  draftRef.current = fetched;
124
256
  setDraft(fetched);
125
257
  setDraftConflict(null);
126
- if (replaceLocal || !localWasDirtyAtStart && localAtStart === localEditRevision.current) {
258
+ const shadow = pendingOperationRef.current?.newerShadow;
259
+ if (shadow) {
260
+ valueRef.current = shadow.text;
261
+ restoredResourcesRef.current = shadow.resources;
262
+ localEditRevision.current ||= 1;
263
+ setValue(shadow.text);
264
+ setRestoredResources(shadow.resources);
265
+ } else if (replaceLocal || !localWasDirtyAtStart && localAtStart === localEditRevision.current) {
127
266
  valueRef.current = fetched.text;
128
267
  restoredResourcesRef.current = fetched.resources;
129
268
  lastSavedSignature.current = draftSignature(draftPayload(fetched));
130
269
  setValue(fetched.text);
131
270
  setRestoredResources(fetched.resources);
132
- onDraftApplied?.(fetched);
271
+ onDraftAppliedRef.current?.(fetched);
133
272
  }
134
273
  }
135
274
  } catch (cause) {
@@ -142,7 +281,7 @@ function useComposer(sessionId, options = {}) {
142
281
  }
143
282
  }
144
283
  },
145
- [client, durableDrafts, onDraftApplied, sessionId, targetKey, workspaceId]
284
+ [client, durableDrafts, sessionId, targetKey, workspaceId]
146
285
  );
147
286
  useEffect(() => {
148
287
  if (!sessionId || !durableDrafts) {
@@ -207,7 +346,7 @@ function useComposer(sessionId, options = {}) {
207
346
  } catch (cause) {
208
347
  if (targetKeyRef.current === ownedTargetKey && targetGeneration.current === ownedGeneration) {
209
348
  const problem = asError(cause);
210
- setDraftConflict(problem);
349
+ if (isDraftConflictError(problem)) setDraftConflict(problem);
211
350
  setError(problem);
212
351
  }
213
352
  } finally {
@@ -223,6 +362,24 @@ function useComposer(sessionId, options = {}) {
223
362
  [client, durableDrafts, sessionId, targetKey, workspaceId]
224
363
  );
225
364
  useEffect(() => {
365
+ const pending = pendingOperationRef.current;
366
+ if (pending) {
367
+ const shadow = {
368
+ text: valueRef.current,
369
+ resources: mergeResources(
370
+ restoredResourcesRef.current,
371
+ resolveSendExtras(sendExtrasRef.current).resources ?? []
372
+ )
373
+ };
374
+ if (pending.newerShadow.text !== shadow.text || JSON.stringify(pending.newerShadow.resources) !== JSON.stringify(shadow.resources)) {
375
+ pendingOperationRef.current = updatePendingComposerShadow(
376
+ pendingOperationKey,
377
+ pending,
378
+ shadow
379
+ );
380
+ }
381
+ return;
382
+ }
226
383
  if (!durableDrafts || !sessionId || draftLoading || sending || !draftRef.current || draftConflict)
227
384
  return;
228
385
  const payload = currentDraftPayload();
@@ -235,6 +392,7 @@ function useComposer(sessionId, options = {}) {
235
392
  draftConflict,
236
393
  draftLoading,
237
394
  liveExtrasVersion,
395
+ pendingOperationKey,
238
396
  persistPayload,
239
397
  sending,
240
398
  sessionId
@@ -243,18 +401,110 @@ function useComposer(sessionId, options = {}) {
243
401
  async (delivery, explicit) => {
244
402
  const ownedTargetKey = targetKey;
245
403
  const ownedGeneration = targetGeneration.current;
404
+ const operationKey = pendingComposerOperationKey(workspaceId, sessionId);
405
+ const pending = pendingOperationRef.current ?? restorePendingComposerOperation(operationKey);
406
+ if (pending && !pendingOperationRef.current) {
407
+ pendingOperationRef.current = pending;
408
+ pendingClientEventId.current = pending.input.clientEventId ?? null;
409
+ }
246
410
  const draftAtSend = value;
247
411
  const rawText = explicit ?? draftAtSend;
248
412
  const hasText = rawText.trim().length > 0;
249
- const extras = resolveSendExtras(sendExtrasRef.current);
413
+ const extras = pending ? {} : resolveSendExtras(sendExtrasRef.current);
250
414
  const hasResources = restoredResources.length > 0 || (extras.resources?.length ?? 0) > 0;
251
- if (!hasText && !hasResources || !sessionId || sending || sendBlockedRef.current?.() === true || targetKeyRef.current !== ownedTargetKey) {
415
+ if (!pending && !hasText && !hasResources || !sessionId || sending || sendBlockedRef.current?.() === true || targetKeyRef.current !== ownedTargetKey) {
252
416
  return false;
253
417
  }
254
- pendingClientEventId.current ??= generateClientEventId();
418
+ const clearPending = () => {
419
+ pendingOperationRef.current = null;
420
+ pendingClientEventId.current = null;
421
+ forgetPendingComposerOperation(operationKey);
422
+ };
423
+ const settleAccepted = (operation) => {
424
+ clearPending();
425
+ const draftWasUnchanged = valueRef.current === operation.draftAtSend;
426
+ const resourcesWereUnchanged = JSON.stringify(restoredResourcesRef.current) === JSON.stringify(operation.resourcesAtSend);
427
+ const previousDraft = draftRef.current;
428
+ if (previousDraft) {
429
+ const cleared = {
430
+ ...previousDraft,
431
+ revision: 0,
432
+ text: "",
433
+ resources: [],
434
+ sourceTurnId: null,
435
+ sourceTurnVersion: null,
436
+ updatedAt: null
437
+ };
438
+ draftRef.current = cleared;
439
+ setDraft(cleared);
440
+ lastSavedSignature.current = draftSignature(draftPayload(cleared));
441
+ }
442
+ if (resourcesWereUnchanged) {
443
+ restoredResourcesRef.current = [];
444
+ setRestoredResources([]);
445
+ }
446
+ if (operation.clearDraftOnAccept && draftWasUnchanged) {
447
+ valueRef.current = "";
448
+ setValue("");
449
+ }
450
+ onSent?.(operation.input.text, operation.input);
451
+ };
452
+ const deliver = async (operation) => {
453
+ if (operation.delivery === "steer") {
454
+ await client.steerMessage(workspaceId, sessionId, operation.input);
455
+ } else {
456
+ await client.sendMessage(workspaceId, sessionId, operation.input);
457
+ }
458
+ };
255
459
  setSending(true);
256
460
  setError(null);
257
461
  try {
462
+ if (pending) {
463
+ let accepted = false;
464
+ try {
465
+ const events = await client.listEvents(workspaceId, sessionId, {
466
+ includeTypes: ["user.message"],
467
+ limit: 100,
468
+ payloadMode: "none"
469
+ });
470
+ accepted = events.some(
471
+ (event) => event.type === "user.message" && event.clientEventId === pending.input.clientEventId
472
+ );
473
+ } catch (cause) {
474
+ if (targetKeyRef.current === ownedTargetKey && targetGeneration.current === ownedGeneration) {
475
+ setError(asError(cause));
476
+ }
477
+ return false;
478
+ }
479
+ if (targetKeyRef.current !== ownedTargetKey || targetGeneration.current !== ownedGeneration) {
480
+ return false;
481
+ }
482
+ if (accepted) {
483
+ settleAccepted(pending);
484
+ return true;
485
+ }
486
+ if (!pending.canRetry) {
487
+ setError(
488
+ new Error(
489
+ "OpenGeni cannot safely retry this uncertain request after remount; reconcile the session before sending again."
490
+ )
491
+ );
492
+ return false;
493
+ }
494
+ try {
495
+ await deliver(pending);
496
+ } catch (cause) {
497
+ if (targetKeyRef.current === ownedTargetKey && targetGeneration.current === ownedGeneration) {
498
+ setError(asError(cause));
499
+ }
500
+ return false;
501
+ }
502
+ if (targetKeyRef.current !== ownedTargetKey || targetGeneration.current !== ownedGeneration) {
503
+ return false;
504
+ }
505
+ settleAccepted(pending);
506
+ return true;
507
+ }
258
508
  const sendText = hasText ? rawText : FILE_ONLY_MESSAGE_TEXT;
259
509
  const currentPayload = currentDraftPayload();
260
510
  const payload = currentPayload ? { ...currentPayload, text: sendText } : null;
@@ -264,50 +514,42 @@ function useComposer(sessionId, options = {}) {
264
514
  }
265
515
  const acknowledgedDraft = draftRef.current;
266
516
  const sendExtras = acknowledgedDraft?.toolsProvided === true && !Object.prototype.hasOwnProperty.call(extras, "tools") ? { ...extras, tools: acknowledgedDraft.tools } : extras;
517
+ pendingClientEventId.current ??= generateClientEventId();
267
518
  const input = composeSendInput(sendText, pendingClientEventId.current, sendExtras, {
268
519
  ...options.effectiveControl?.controlEtag ? { controlEtag: options.effectiveControl.controlEtag } : {},
269
520
  ...durableDrafts && draftRef.current ? { expectedDraftRevision: draftRef.current.revision } : {},
270
521
  resources: mergeResources(restoredResources, extras.resources ?? [])
271
522
  });
272
- if (delivery === "steer") {
273
- await client.steerMessage(workspaceId, sessionId, input);
274
- } else {
275
- await client.sendMessage(workspaceId, sessionId, input);
523
+ const operation = {
524
+ delivery,
525
+ input,
526
+ draftAtSend,
527
+ resourcesAtSend: [...restoredResources],
528
+ newerShadow: {
529
+ text: draftAtSend,
530
+ resources: mergeResources(restoredResources, extras.resources ?? [])
531
+ },
532
+ clearDraftOnAccept: explicit === void 0,
533
+ canRetry: true
534
+ };
535
+ pendingOperationRef.current = operation;
536
+ rememberPendingComposerOperation(operationKey, operation);
537
+ try {
538
+ await deliver(operation);
539
+ } catch (cause) {
540
+ if (!isOutcomeUnknownError(cause)) {
541
+ clearPending();
542
+ }
543
+ if (targetKeyRef.current === ownedTargetKey && targetGeneration.current === ownedGeneration) {
544
+ setError(asError(cause));
545
+ }
546
+ return false;
276
547
  }
277
548
  if (targetKeyRef.current !== ownedTargetKey || targetGeneration.current !== ownedGeneration) {
278
549
  return false;
279
550
  }
280
- pendingClientEventId.current = null;
281
- const previousDraft = draftRef.current;
282
- if (previousDraft) {
283
- const cleared = {
284
- ...previousDraft,
285
- revision: 0,
286
- text: "",
287
- resources: [],
288
- sourceTurnId: null,
289
- sourceTurnVersion: null,
290
- updatedAt: null
291
- };
292
- draftRef.current = cleared;
293
- restoredResourcesRef.current = [];
294
- setDraft(cleared);
295
- setRestoredResources([]);
296
- lastSavedSignature.current = draftSignature(draftPayload(cleared));
297
- }
298
- if (explicit === void 0) {
299
- if (valueRef.current === draftAtSend) {
300
- valueRef.current = "";
301
- setValue("");
302
- }
303
- }
304
- onSent?.(sendText, input);
551
+ settleAccepted(operation);
305
552
  return true;
306
- } catch (cause) {
307
- if (targetKeyRef.current === ownedTargetKey && targetGeneration.current === ownedGeneration) {
308
- setError(cause instanceof Error ? cause : new Error(String(cause)));
309
- }
310
- return false;
311
553
  } finally {
312
554
  if (targetKeyRef.current === ownedTargetKey && targetGeneration.current === ownedGeneration) {
313
555
  setSending(false);
@@ -332,6 +574,7 @@ function useComposer(sessionId, options = {}) {
332
574
  const send = useCallback(async (text) => await dispatch("send", text), [dispatch]);
333
575
  const steer = useCallback(async (text) => await dispatch("steer", text), [dispatch]);
334
576
  const hasReadyResources = restoredResources.length > 0 || (resolveSendExtras(sendExtrasRef.current).resources?.length ?? 0) > 0;
577
+ const hasPendingOperation = pendingOperationRef.current !== null;
335
578
  const pause = useCallback(
336
579
  async (reason) => {
337
580
  const ownedTargetKey = targetKey;
@@ -432,12 +675,22 @@ function useComposer(sessionId, options = {}) {
432
675
  const updateValue = useCallback(
433
676
  (next) => {
434
677
  if (targetKeyRef.current !== targetKey) return;
435
- pendingClientEventId.current = null;
436
678
  localEditRevision.current += 1;
437
679
  valueRef.current = next;
680
+ pendingOperationRef.current = updatePendingComposerShadow(
681
+ pendingOperationKey,
682
+ pendingOperationRef.current,
683
+ {
684
+ text: next,
685
+ resources: mergeResources(
686
+ restoredResourcesRef.current,
687
+ resolveSendExtras(sendExtrasRef.current).resources ?? []
688
+ )
689
+ }
690
+ );
438
691
  setValue(next);
439
692
  },
440
- [targetKey]
693
+ [pendingOperationKey, targetKey]
441
694
  );
442
695
  const removeRestoredResource = useCallback(
443
696
  (index) => {
@@ -445,9 +698,17 @@ function useComposer(sessionId, options = {}) {
445
698
  localEditRevision.current += 1;
446
699
  const next = restoredResourcesRef.current.filter((_, candidate) => candidate !== index);
447
700
  restoredResourcesRef.current = next;
701
+ pendingOperationRef.current = updatePendingComposerShadow(
702
+ pendingOperationKey,
703
+ pendingOperationRef.current,
704
+ {
705
+ text: valueRef.current,
706
+ resources: mergeResources(next, resolveSendExtras(sendExtrasRef.current).resources ?? [])
707
+ }
708
+ );
448
709
  setRestoredResources(next);
449
710
  },
450
- [targetKey]
711
+ [pendingOperationKey, targetKey]
451
712
  );
452
713
  const hasDraftContent = useCallback(() => {
453
714
  const current = draftRef.current;
@@ -500,7 +761,7 @@ function useComposer(sessionId, options = {}) {
500
761
  send,
501
762
  steer,
502
763
  sending: identityMatches ? sending : false,
503
- canSend: identityMatches && Boolean(sessionId) && !sending && sendBlockedRef.current?.() !== true && (value.trim().length > 0 || hasReadyResources),
764
+ canSend: identityMatches && Boolean(sessionId) && !sending && sendBlockedRef.current?.() !== true && (hasPendingOperation || value.trim().length > 0 || hasReadyResources),
504
765
  pause,
505
766
  pausing: identityMatches ? pausing : false,
506
767
  resume,
@@ -543,9 +804,16 @@ function shouldSteerOnKey(event) {
543
804
  function generateClientEventId() {
544
805
  return globalThis.crypto.randomUUID();
545
806
  }
807
+ function isOutcomeUnknownError(cause) {
808
+ return typeof cause === "object" && cause !== null && cause.outcomeUnknown === true;
809
+ }
546
810
  function asError(cause) {
547
811
  return cause instanceof Error ? cause : new Error(String(cause));
548
812
  }
813
+ function isDraftConflictError(error) {
814
+ const apiError = error;
815
+ return apiError.status === 409 && apiError.outcomeUnknown === false && (apiError.code === void 0 || apiError.code === "conflict" || apiError.code === "idempotency_conflict");
816
+ }
549
817
  function draftPayload(draft) {
550
818
  return {
551
819
  expectedRevision: draft.revision,
@@ -590,4 +858,4 @@ export {
590
858
  shouldSubmitOnKey,
591
859
  shouldSteerOnKey
592
860
  };
593
- //# sourceMappingURL=chunk-AVPU5PMC.js.map
861
+ //# sourceMappingURL=chunk-F4CFWKVI.js.map