@opengeni/react 0.23.0 → 0.25.5

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 (62) hide show
  1. package/dist/{chunk-UQS7OQDE.js → chunk-F4CFWKVI.js} +334 -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-UAHPKLB6.js → chunk-MP6237JB.js} +571 -975
  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-CATATY77.js → chunk-TMH6HZWF.js} +8 -7
  14. package/dist/chunk-TMH6HZWF.js.map +1 -0
  15. package/dist/{chunk-6XUS5VFM.js → chunk-YU5PGUK7.js} +6 -4
  16. package/dist/{chunk-6XUS5VFM.js.map → chunk-YU5PGUK7.js.map} +1 -1
  17. package/dist/{composer-CVm5uoUI.d.ts → composer-DoC4veX1.d.ts} +6 -63
  18. package/dist/composer.d.ts +3 -2
  19. package/dist/composer.js +4 -3
  20. package/dist/fleet-decision-projection-SLNZMS42.js +241 -0
  21. package/dist/fleet-decision-projection-SLNZMS42.js.map +1 -0
  22. package/dist/fleet-decision-row-GGCAT4E4.js +351 -0
  23. package/dist/fleet-decision-row-GGCAT4E4.js.map +1 -0
  24. package/dist/index.d.ts +17 -259
  25. package/dist/index.js +1042 -5058
  26. package/dist/index.js.map +1 -1
  27. package/dist/machines.js +3 -2
  28. package/dist/session-Du_FsrZ1.d.ts +264 -0
  29. package/dist/session-ui-BqQDH7YV.d.ts +183 -0
  30. package/dist/session-ui.d.ts +7 -0
  31. package/dist/session-ui.js +17 -0
  32. package/dist/session-ui.js.map +1 -0
  33. package/dist/session.d.ts +4 -2
  34. package/dist/session.js +26 -9
  35. package/dist/{use-composer-By1wn3MM.d.ts → use-composer-BnEzheSk.d.ts} +8 -2
  36. package/dist/use-file-attachments-C0kpHrs9.d.ts +76 -0
  37. package/dist/use-turn-queue-3rkJwjFv.d.ts +290 -0
  38. package/package.json +6 -2
  39. package/src/components/composer.tsx +7 -6
  40. package/src/components/message-timeline.tsx +94 -54
  41. package/src/hooks/use-composer.ts +470 -73
  42. package/src/hooks/use-file-attachments.ts +96 -3
  43. package/src/hooks/use-goal.ts +23 -5
  44. package/src/hooks/use-session-events.ts +5 -0
  45. package/src/hooks/use-session-lineage.ts +1 -1
  46. package/src/hooks/use-session.ts +9 -6
  47. package/src/hooks/use-workspace-sessions.ts +30 -5
  48. package/src/session-ui.ts +13 -0
  49. package/src/session.ts +15 -0
  50. package/src/timeline/activity-rail.tsx +18 -2
  51. package/src/timeline/fleet-decision-projection.ts +366 -0
  52. package/src/timeline/fleet-decision-row.tsx +412 -0
  53. package/src/timeline/index.ts +2 -0
  54. package/src/timeline/projection.ts +19 -8
  55. package/src/timeline/tool-display-name.ts +16 -0
  56. package/src/timeline/tool-renderers.tsx +1 -1
  57. package/src/timeline/types.ts +68 -1
  58. package/dist/chunk-CATATY77.js.map +0 -1
  59. package/dist/chunk-I3BJZIG5.js.map +0 -1
  60. package/dist/chunk-UAHPKLB6.js.map +0 -1
  61. package/dist/chunk-UQS7OQDE.js.map +0 -1
  62. package/dist/session-Db00TRwl.d.ts +0 -446
@@ -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,20 +120,31 @@ 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;
146
+ const sendBlockedRef = useRef(options.sendBlocked);
147
+ sendBlockedRef.current = options.sendBlocked;
36
148
  const liveExtrasVersion = JSON.stringify(resolveSendExtras(options.sendExtras));
37
149
  const targetKeyRef = useRef(targetKey);
38
150
  useLayoutEffect(() => {
@@ -40,15 +152,17 @@ function useComposer(sessionId, options = {}) {
40
152
  targetKeyRef.current = targetKey;
41
153
  targetGeneration.current += 1;
42
154
  draftReadGeneration.current += 1;
43
- pendingClientEventId.current = null;
44
- localEditRevision.current = 0;
45
- 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 ?? "";
46
160
  draftRef.current = null;
47
- restoredResourcesRef.current = [];
161
+ restoredResourcesRef.current = shadow?.resources ?? [];
48
162
  lastSavedSignature.current = null;
49
163
  saveChain.current = Promise.resolve();
50
164
  setStateTargetKey(targetKey);
51
- setValue("");
165
+ setValue(shadow?.text ?? "");
52
166
  setSending(false);
53
167
  setPausing(false);
54
168
  setResuming(false);
@@ -57,13 +171,12 @@ function useComposer(sessionId, options = {}) {
57
171
  setDraftLoading(Boolean(sessionId) && durableDrafts);
58
172
  setDraftSaving(false);
59
173
  setDraftConflict(null);
60
- setRestoredResources([]);
61
- }, [durableDrafts, sessionId, targetKey]);
174
+ setRestoredResources(shadow?.resources ?? []);
175
+ }, [durableDrafts, pendingOperationKey, sessionId, targetKey]);
62
176
  const applyDraft = useCallback(
63
177
  (next) => {
64
178
  if (targetKeyRef.current !== targetKey) return;
65
179
  if (!durableDrafts) {
66
- pendingClientEventId.current = null;
67
180
  localEditRevision.current += 1;
68
181
  valueRef.current = next.text;
69
182
  restoredResourcesRef.current = next.resources;
@@ -72,6 +185,17 @@ function useComposer(sessionId, options = {}) {
72
185
  setDraft(null);
73
186
  setValue(next.text);
74
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
+ );
75
199
  setDraftConflict(null);
76
200
  return;
77
201
  }
@@ -80,14 +204,24 @@ function useComposer(sessionId, options = {}) {
80
204
  restoredResourcesRef.current = next.resources;
81
205
  lastSavedSignature.current = draftSignature(draftPayload(next));
82
206
  localEditRevision.current += 1;
83
- pendingClientEventId.current = null;
84
207
  setDraft(next);
85
208
  setValue(next.text);
86
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
+ );
87
221
  setDraftConflict(null);
88
- onDraftApplied?.(next);
222
+ onDraftAppliedRef.current?.(next);
89
223
  },
90
- [durableDrafts, onDraftApplied, targetKey]
224
+ [durableDrafts, pendingOperationKey, targetKey]
91
225
  );
92
226
  const loadDraft = useCallback(
93
227
  async (replaceLocal) => {
@@ -121,13 +255,20 @@ function useComposer(sessionId, options = {}) {
121
255
  draftRef.current = fetched;
122
256
  setDraft(fetched);
123
257
  setDraftConflict(null);
124
- 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) {
125
266
  valueRef.current = fetched.text;
126
267
  restoredResourcesRef.current = fetched.resources;
127
268
  lastSavedSignature.current = draftSignature(draftPayload(fetched));
128
269
  setValue(fetched.text);
129
270
  setRestoredResources(fetched.resources);
130
- onDraftApplied?.(fetched);
271
+ onDraftAppliedRef.current?.(fetched);
131
272
  }
132
273
  }
133
274
  } catch (cause) {
@@ -140,7 +281,7 @@ function useComposer(sessionId, options = {}) {
140
281
  }
141
282
  }
142
283
  },
143
- [client, durableDrafts, onDraftApplied, sessionId, targetKey, workspaceId]
284
+ [client, durableDrafts, sessionId, targetKey, workspaceId]
144
285
  );
145
286
  useEffect(() => {
146
287
  if (!sessionId || !durableDrafts) {
@@ -205,7 +346,7 @@ function useComposer(sessionId, options = {}) {
205
346
  } catch (cause) {
206
347
  if (targetKeyRef.current === ownedTargetKey && targetGeneration.current === ownedGeneration) {
207
348
  const problem = asError(cause);
208
- setDraftConflict(problem);
349
+ if (isDraftConflictError(problem)) setDraftConflict(problem);
209
350
  setError(problem);
210
351
  }
211
352
  } finally {
@@ -221,6 +362,24 @@ function useComposer(sessionId, options = {}) {
221
362
  [client, durableDrafts, sessionId, targetKey, workspaceId]
222
363
  );
223
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
+ }
224
383
  if (!durableDrafts || !sessionId || draftLoading || sending || !draftRef.current || draftConflict)
225
384
  return;
226
385
  const payload = currentDraftPayload();
@@ -233,6 +392,7 @@ function useComposer(sessionId, options = {}) {
233
392
  draftConflict,
234
393
  draftLoading,
235
394
  liveExtrasVersion,
395
+ pendingOperationKey,
236
396
  persistPayload,
237
397
  sending,
238
398
  sessionId
@@ -241,18 +401,110 @@ function useComposer(sessionId, options = {}) {
241
401
  async (delivery, explicit) => {
242
402
  const ownedTargetKey = targetKey;
243
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
+ }
244
410
  const draftAtSend = value;
245
411
  const rawText = explicit ?? draftAtSend;
246
412
  const hasText = rawText.trim().length > 0;
247
- const extras = resolveSendExtras(sendExtrasRef.current);
413
+ const extras = pending ? {} : resolveSendExtras(sendExtrasRef.current);
248
414
  const hasResources = restoredResources.length > 0 || (extras.resources?.length ?? 0) > 0;
249
- if (!hasText && !hasResources || !sessionId || sending || targetKeyRef.current !== ownedTargetKey) {
415
+ if (!pending && !hasText && !hasResources || !sessionId || sending || sendBlockedRef.current?.() === true || targetKeyRef.current !== ownedTargetKey) {
250
416
  return false;
251
417
  }
252
- 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
+ };
253
459
  setSending(true);
254
460
  setError(null);
255
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
+ }
256
508
  const sendText = hasText ? rawText : FILE_ONLY_MESSAGE_TEXT;
257
509
  const currentPayload = currentDraftPayload();
258
510
  const payload = currentPayload ? { ...currentPayload, text: sendText } : null;
@@ -262,50 +514,42 @@ function useComposer(sessionId, options = {}) {
262
514
  }
263
515
  const acknowledgedDraft = draftRef.current;
264
516
  const sendExtras = acknowledgedDraft?.toolsProvided === true && !Object.prototype.hasOwnProperty.call(extras, "tools") ? { ...extras, tools: acknowledgedDraft.tools } : extras;
517
+ pendingClientEventId.current ??= generateClientEventId();
265
518
  const input = composeSendInput(sendText, pendingClientEventId.current, sendExtras, {
266
519
  ...options.effectiveControl?.controlEtag ? { controlEtag: options.effectiveControl.controlEtag } : {},
267
520
  ...durableDrafts && draftRef.current ? { expectedDraftRevision: draftRef.current.revision } : {},
268
521
  resources: mergeResources(restoredResources, extras.resources ?? [])
269
522
  });
270
- if (delivery === "steer") {
271
- await client.steerMessage(workspaceId, sessionId, input);
272
- } else {
273
- 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;
274
547
  }
275
548
  if (targetKeyRef.current !== ownedTargetKey || targetGeneration.current !== ownedGeneration) {
276
549
  return false;
277
550
  }
278
- pendingClientEventId.current = null;
279
- const previousDraft = draftRef.current;
280
- if (previousDraft) {
281
- const cleared = {
282
- ...previousDraft,
283
- revision: 0,
284
- text: "",
285
- resources: [],
286
- sourceTurnId: null,
287
- sourceTurnVersion: null,
288
- updatedAt: null
289
- };
290
- draftRef.current = cleared;
291
- restoredResourcesRef.current = [];
292
- setDraft(cleared);
293
- setRestoredResources([]);
294
- lastSavedSignature.current = draftSignature(draftPayload(cleared));
295
- }
296
- if (explicit === void 0) {
297
- if (valueRef.current === draftAtSend) {
298
- valueRef.current = "";
299
- setValue("");
300
- }
301
- }
302
- onSent?.(sendText);
551
+ settleAccepted(operation);
303
552
  return true;
304
- } catch (cause) {
305
- if (targetKeyRef.current === ownedTargetKey && targetGeneration.current === ownedGeneration) {
306
- setError(cause instanceof Error ? cause : new Error(String(cause)));
307
- }
308
- return false;
309
553
  } finally {
310
554
  if (targetKeyRef.current === ownedTargetKey && targetGeneration.current === ownedGeneration) {
311
555
  setSending(false);
@@ -330,6 +574,7 @@ function useComposer(sessionId, options = {}) {
330
574
  const send = useCallback(async (text) => await dispatch("send", text), [dispatch]);
331
575
  const steer = useCallback(async (text) => await dispatch("steer", text), [dispatch]);
332
576
  const hasReadyResources = restoredResources.length > 0 || (resolveSendExtras(sendExtrasRef.current).resources?.length ?? 0) > 0;
577
+ const hasPendingOperation = pendingOperationRef.current !== null;
333
578
  const pause = useCallback(
334
579
  async (reason) => {
335
580
  const ownedTargetKey = targetKey;
@@ -430,12 +675,22 @@ function useComposer(sessionId, options = {}) {
430
675
  const updateValue = useCallback(
431
676
  (next) => {
432
677
  if (targetKeyRef.current !== targetKey) return;
433
- pendingClientEventId.current = null;
434
678
  localEditRevision.current += 1;
435
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
+ );
436
691
  setValue(next);
437
692
  },
438
- [targetKey]
693
+ [pendingOperationKey, targetKey]
439
694
  );
440
695
  const removeRestoredResource = useCallback(
441
696
  (index) => {
@@ -443,9 +698,17 @@ function useComposer(sessionId, options = {}) {
443
698
  localEditRevision.current += 1;
444
699
  const next = restoredResourcesRef.current.filter((_, candidate) => candidate !== index);
445
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
+ );
446
709
  setRestoredResources(next);
447
710
  },
448
- [targetKey]
711
+ [pendingOperationKey, targetKey]
449
712
  );
450
713
  const hasDraftContent = useCallback(() => {
451
714
  const current = draftRef.current;
@@ -498,7 +761,7 @@ function useComposer(sessionId, options = {}) {
498
761
  send,
499
762
  steer,
500
763
  sending: identityMatches ? sending : false,
501
- canSend: identityMatches && Boolean(sessionId) && !sending && (value.trim().length > 0 || hasReadyResources),
764
+ canSend: identityMatches && Boolean(sessionId) && !sending && sendBlockedRef.current?.() !== true && (hasPendingOperation || value.trim().length > 0 || hasReadyResources),
502
765
  pause,
503
766
  pausing: identityMatches ? pausing : false,
504
767
  resume,
@@ -541,9 +804,16 @@ function shouldSteerOnKey(event) {
541
804
  function generateClientEventId() {
542
805
  return globalThis.crypto.randomUUID();
543
806
  }
807
+ function isOutcomeUnknownError(cause) {
808
+ return typeof cause === "object" && cause !== null && cause.outcomeUnknown === true;
809
+ }
544
810
  function asError(cause) {
545
811
  return cause instanceof Error ? cause : new Error(String(cause));
546
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
+ }
547
817
  function draftPayload(draft) {
548
818
  return {
549
819
  expectedRevision: draft.revision,
@@ -588,4 +858,4 @@ export {
588
858
  shouldSubmitOnKey,
589
859
  shouldSteerOnKey
590
860
  };
591
- //# sourceMappingURL=chunk-UQS7OQDE.js.map
861
+ //# sourceMappingURL=chunk-F4CFWKVI.js.map