@opengeni/react 0.20.0 → 0.25.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 (60) hide show
  1. package/dist/{chunk-6BXBGJ3B.js → chunk-6XUS5VFM.js} +2 -2
  2. package/dist/{chunk-MRSECXRP.js → chunk-AVPU5PMC.js} +87 -57
  3. package/dist/chunk-AVPU5PMC.js.map +1 -0
  4. package/dist/{chunk-HHFA4AFX.js → chunk-I3BJZIG5.js} +15 -1
  5. package/dist/chunk-I3BJZIG5.js.map +1 -0
  6. package/dist/{chunk-LAZ2A743.js → chunk-RDPDU4TA.js} +8 -7
  7. package/dist/chunk-RDPDU4TA.js.map +1 -0
  8. package/dist/{chunk-6UT5OC3P.js → chunk-SHOFILHJ.js} +130 -16
  9. package/dist/chunk-SHOFILHJ.js.map +1 -0
  10. package/dist/{chunk-EBTT3MYN.js → chunk-YDNWMKXO.js} +11 -1
  11. package/dist/chunk-YDNWMKXO.js.map +1 -0
  12. package/dist/{composer-DvUaa5ki.d.ts → composer-BXb0Q1HF.d.ts} +19 -5
  13. package/dist/composer.d.ts +3 -3
  14. package/dist/composer.js +3 -3
  15. package/dist/fleet-decision-projection-SLNZMS42.js +241 -0
  16. package/dist/fleet-decision-projection-SLNZMS42.js.map +1 -0
  17. package/dist/fleet-decision-row-GGCAT4E4.js +351 -0
  18. package/dist/fleet-decision-row-GGCAT4E4.js.map +1 -0
  19. package/dist/index.d.ts +14 -11
  20. package/dist/index.js +466 -244
  21. package/dist/index.js.map +1 -1
  22. package/dist/machines.d.ts +1 -1
  23. package/dist/machines.js +2 -2
  24. package/dist/{queue-surface-implementation-NQMV2ML3.js → queue-surface-implementation-3UW3MIIR.js} +9 -9
  25. package/dist/queue-surface-implementation-3UW3MIIR.js.map +1 -0
  26. package/dist/{session-Gd4iVPYw.d.ts → session-BEvtFWhe.d.ts} +71 -24
  27. package/dist/{session-context-DIFFlXKc.d.ts → session-context-rgrfElMl.d.ts} +7 -1
  28. package/dist/session.d.ts +3 -3
  29. package/dist/session.js +7 -3
  30. package/dist/{use-composer-BCfm4mzX.d.ts → use-composer-BnEzheSk.d.ts} +13 -20
  31. package/package.json +2 -2
  32. package/src/client.ts +4 -0
  33. package/src/components/composer.tsx +7 -6
  34. package/src/components/message-timeline.tsx +197 -25
  35. package/src/components/queue-draft-policy.ts +18 -0
  36. package/src/components/queue-surface-implementation.tsx +6 -10
  37. package/src/components/queue-surface.tsx +1 -0
  38. package/src/hooks/use-composer.ts +143 -83
  39. package/src/hooks/use-file-attachments.ts +95 -2
  40. package/src/hooks/use-goal.ts +22 -4
  41. package/src/hooks/use-session-mcp-approval-policy.ts +161 -0
  42. package/src/hooks/use-workspace-sessions.ts +30 -5
  43. package/src/index.ts +9 -0
  44. package/src/session-context.ts +25 -0
  45. package/src/session.ts +10 -0
  46. package/src/timeline/activity-rail.tsx +17 -1
  47. package/src/timeline/fleet-decision-projection.ts +366 -0
  48. package/src/timeline/fleet-decision-row.tsx +412 -0
  49. package/src/timeline/index.ts +2 -0
  50. package/src/timeline/projection.ts +19 -8
  51. package/src/timeline/tool-display-name.ts +16 -0
  52. package/src/timeline/tool-renderers.tsx +1 -1
  53. package/src/timeline/types.ts +76 -16
  54. package/dist/chunk-6UT5OC3P.js.map +0 -1
  55. package/dist/chunk-EBTT3MYN.js.map +0 -1
  56. package/dist/chunk-HHFA4AFX.js.map +0 -1
  57. package/dist/chunk-LAZ2A743.js.map +0 -1
  58. package/dist/chunk-MRSECXRP.js.map +0 -1
  59. package/dist/queue-surface-implementation-NQMV2ML3.js.map +0 -1
  60. /package/dist/{chunk-6BXBGJ3B.js.map → chunk-6XUS5VFM.js.map} +0 -0
@@ -20,6 +20,7 @@ import type { ComponentType } from "react";
20
20
  import { AnimatePresence, motion } from "motion/react";
21
21
  import { Collapsible } from "radix-ui";
22
22
  import {
23
+ memo,
23
24
  useCallback,
24
25
  useEffect,
25
26
  useLayoutEffect,
@@ -108,6 +109,8 @@ export type MessageTimelineProps = {
108
109
  className?: string | undefined;
109
110
  };
110
111
 
112
+ const INITIAL_MOUNTED_GROUPS = 1;
113
+
111
114
  /**
112
115
  * The session timeline: chat messages with streaming deltas, collapsed
113
116
  * activity clusters (reasoning, tool calls, sandbox work), spawned-worker
@@ -132,8 +135,10 @@ export function MessageTimeline({
132
135
  className,
133
136
  }: MessageTimelineProps) {
134
137
  const resolvedItems = useMemo(() => items ?? buildTimeline(events ?? []), [items, events]);
135
- const groups = useMemo(() => groupTimeline(resolvedItems), [resolvedItems]);
136
- const firstGroupKey = groups[0] ? timelineGroupKey(groups[0]) : null;
138
+ const allGroups = useMemo(() => groupTimeline(resolvedItems), [resolvedItems]);
139
+ const keyedGroups = useStableTimelineGroupKeys(allGroups);
140
+ const { mountedGroups: groups, mountingOlderGroups } = useProgressivelyMountedGroups(keyedGroups);
141
+ const firstGroupKey = allGroups[0] ? timelineGroupKey(allGroups[0]) : null;
137
142
 
138
143
  const scrollRef = useRef<HTMLDivElement | null>(null);
139
144
  const topSentinelRef = useRef<HTMLDivElement | null>(null);
@@ -178,7 +183,8 @@ export function MessageTimeline({
178
183
  const firstKeyChangedForBulk =
179
184
  previousBulkFirstKeyRef.current !== undefined &&
180
185
  previousBulkFirstKeyRef.current !== firstGroupKey;
181
- const bulkRender = groups.length > 0 && (bulkActive || firstKeyChangedForBulk);
186
+ const bulkRender =
187
+ allGroups.length > 0 && (bulkActive || firstKeyChangedForBulk || mountingOlderGroups);
182
188
 
183
189
  // Snapshot the topmost visible element and where it sits in the viewport, so a
184
190
  // later reflow can restore it to the same spot. Transient chrome (the backfill
@@ -221,10 +227,10 @@ export function MessageTimeline({
221
227
  // A cleared timeline (stream identity change) re-arms the reveal so the next
222
228
  // session also first paints at its bottom.
223
229
  useLayoutEffect(() => {
224
- if (groups.length === 0 && revealed) {
230
+ if (allGroups.length === 0 && revealed) {
225
231
  setRevealed(false);
226
232
  }
227
- }, [groups.length, revealed]);
233
+ }, [allGroups.length, revealed]);
228
234
 
229
235
  // Clear the bulk-paint marker a frame after it renders, so rows appended
230
236
  // live (streams, new turns) animate exactly as before.
@@ -234,9 +240,15 @@ export function MessageTimeline({
234
240
  return;
235
241
  }
236
242
  setBulkActive(true);
243
+ // Initial tails and prepended history mount from newest to oldest across
244
+ // frames. Keep every row born during that bulk window animation-free; only
245
+ // clear the marker after the authoritative group list is fully mounted.
246
+ if (mountingOlderGroups) {
247
+ return;
248
+ }
237
249
  const frame = requestFrame(() => setBulkActive(false));
238
250
  return () => cancelFrame(frame);
239
- }, [bulkRender, firstGroupKey]);
251
+ }, [bulkRender, firstGroupKey, mountingOlderGroups]);
240
252
 
241
253
  // Prefetch older history well before the reader reaches the top: the
242
254
  // sentinel sits above the first group and trips 1600px early, so backfill
@@ -248,6 +260,7 @@ export function MessageTimeline({
248
260
  if (
249
261
  !root ||
250
262
  !target ||
263
+ mountingOlderGroups ||
251
264
  !hasOlder ||
252
265
  loadingOlder ||
253
266
  !onLoadOlder ||
@@ -265,7 +278,7 @@ export function MessageTimeline({
265
278
  );
266
279
  observer.observe(target);
267
280
  return () => observer.disconnect();
268
- }, [hasOlder, loadingOlder, onLoadOlder, firstGroupKey]);
281
+ }, [hasOlder, loadingOlder, onLoadOlder, firstGroupKey, mountingOlderGroups]);
269
282
 
270
283
  // Scroll anchoring: when the content reflows (a fold expands/collapses, a
271
284
  // stream appends), keep following the bottom if pinned; otherwise pin the
@@ -338,7 +351,7 @@ export function MessageTimeline({
338
351
  <p className="py-10 text-center text-sm text-og-fg-subtle">No activity yet.</p>
339
352
  ))
340
353
  : null}
341
- {hasOlder ? (
354
+ {hasOlder && !mountingOlderGroups ? (
342
355
  <div
343
356
  ref={topSentinelRef}
344
357
  data-og-top-sentinel=""
@@ -352,9 +365,9 @@ export function MessageTimeline({
352
365
  <span className="og-shimmer-text font-medium">Loading earlier activity…</span>
353
366
  </div>
354
367
  ) : null}
355
- {groups.map((group, index) => (
368
+ {groups.map(({ group, key }, index) => (
356
369
  <TimelineGroupView
357
- key={timelineGroupKey(group)}
370
+ key={key}
358
371
  group={group}
359
372
  renderMessageText={renderMessageText}
360
373
  onOpenSession={onOpenSession}
@@ -362,7 +375,7 @@ export function MessageTimeline({
362
375
  onReconnect={onReconnect}
363
376
  resolveProviderLogo={resolveProviderLogo}
364
377
  toolRegistry={toolRegistry}
365
- foldLiveCluster={isAgentProgress(groups[index + 1])}
378
+ foldLiveCluster={isAgentProgress(groups[index + 1]?.group)}
366
379
  />
367
380
  ))}
368
381
  {working ? (
@@ -406,6 +419,141 @@ export function MessageTimeline({
406
419
  );
407
420
  }
408
421
 
422
+ type MountedGroupWindow = {
423
+ groupKeys: string[];
424
+ visibleStart: number;
425
+ };
426
+
427
+ type KeyedTimelineGroup = {
428
+ group: TimelineGroup;
429
+ key: string;
430
+ };
431
+
432
+ /**
433
+ * Projection can legitimately change a group's content-derived key while
434
+ * retaining its existing rows. The common pagination case is an older activity
435
+ * item merging into the first activity group; live appends grow the same group
436
+ * from the other side. Match the new authoritative groups to the previous
437
+ * committed groups by their durable item IDs so both the React key and the
438
+ * progressive-window anchor survive either change.
439
+ */
440
+ function useStableTimelineGroupKeys(allGroups: TimelineGroup[]): KeyedTimelineGroup[] {
441
+ const previousRef = useRef<KeyedTimelineGroup[]>([]);
442
+ const keyedGroups = useMemo(() => {
443
+ const previousByItemId = new Map<string, KeyedTimelineGroup>();
444
+ for (const previous of previousRef.current) {
445
+ for (const itemId of timelineGroupItemIds(previous.group)) {
446
+ previousByItemId.set(itemId, previous);
447
+ }
448
+ }
449
+
450
+ const usedKeys = new Set<string>();
451
+ return allGroups.map((group, index) => {
452
+ const itemIds = timelineGroupItemIds(group);
453
+ let retainedKey: string | undefined;
454
+ for (const itemId of itemIds) {
455
+ const previous = previousByItemId.get(itemId);
456
+ if (previous && !usedKeys.has(previous.key)) {
457
+ retainedKey = previous.key;
458
+ break;
459
+ }
460
+ }
461
+
462
+ const canonicalKey = timelineGroupKey(group);
463
+ let key = retainedKey ?? canonicalKey;
464
+ let collision = 0;
465
+ while (usedKeys.has(key)) {
466
+ key = `${canonicalKey}:${index}:${collision}`;
467
+ collision += 1;
468
+ }
469
+ usedKeys.add(key);
470
+ return { group, key };
471
+ });
472
+ }, [allGroups]);
473
+
474
+ useLayoutEffect(() => {
475
+ previousRef.current = keyedGroups;
476
+ }, [keyedGroups]);
477
+
478
+ return keyedGroups;
479
+ }
480
+
481
+ /**
482
+ * Bulk history is already projected into its authoritative order before this
483
+ * hook runs. Mount its newest group first, then prepend one older group per
484
+ * animation frame so low-end browsers can paint and service input between
485
+ * React/Markdown commits instead of doing the entire tail in one long task.
486
+ *
487
+ * A live append does not move the first mounted key, so it is included
488
+ * immediately in the mounted suffix. A history prepend moves that key deeper
489
+ * in the array; shifting `visibleStart` by that exact prefix keeps every
490
+ * existing row mounted while the new prefix is revealed. If projection
491
+ * coalesces the seam item itself away, the earliest surviving mounted key
492
+ * provides the same anchor. Replacements with no shared mounted key (for
493
+ * example a session switch or clear-view) start a fresh suffix.
494
+ */
495
+ function useProgressivelyMountedGroups(allGroups: KeyedTimelineGroup[]): {
496
+ mountedGroups: KeyedTimelineGroup[];
497
+ mountingOlderGroups: boolean;
498
+ } {
499
+ const previousGroupKeysRef = useRef<string[]>([]);
500
+ const groupKeys = useMemo(() => {
501
+ const nextKeys = allGroups.map((group) => group.key);
502
+ const previousKeys = previousGroupKeysRef.current;
503
+ return nextKeys.length === previousKeys.length &&
504
+ nextKeys.every((key, index) => key === previousKeys[index])
505
+ ? previousKeys
506
+ : nextKeys;
507
+ }, [allGroups]);
508
+ useLayoutEffect(() => {
509
+ previousGroupKeysRef.current = groupKeys;
510
+ }, [groupKeys]);
511
+ const [window, setWindow] = useState<MountedGroupWindow>(() => ({
512
+ groupKeys,
513
+ visibleStart: Math.max(0, allGroups.length - INITIAL_MOUNTED_GROUPS),
514
+ }));
515
+
516
+ const lastPossibleStart = Math.max(0, allGroups.length - INITIAL_MOUNTED_GROUPS);
517
+ let visibleStart = 0;
518
+ if (allGroups.length > 0) {
519
+ const currentIndexByKey = new Map(groupKeys.map((key, index) => [key, index]));
520
+ let retainedStart: number | undefined;
521
+ for (const key of window.groupKeys.slice(window.visibleStart)) {
522
+ const index = currentIndexByKey.get(key);
523
+ if (index !== undefined) {
524
+ retainedStart = index;
525
+ break;
526
+ }
527
+ }
528
+ visibleStart =
529
+ retainedStart === undefined ? lastPossibleStart : Math.min(retainedStart, lastPossibleStart);
530
+ }
531
+
532
+ useEffect(() => {
533
+ // Synchronize a prepend/replacement before scheduling its first reveal.
534
+ if (window.groupKeys !== groupKeys || window.visibleStart !== visibleStart) {
535
+ setWindow({ groupKeys, visibleStart });
536
+ return;
537
+ }
538
+ if (visibleStart === 0) {
539
+ return;
540
+ }
541
+ const frame = requestFrame(() => {
542
+ setWindow((current) =>
543
+ current.groupKeys === groupKeys && current.visibleStart > 0
544
+ ? { ...current, visibleStart: current.visibleStart - 1 }
545
+ : current,
546
+ );
547
+ });
548
+ return () => cancelFrame(frame);
549
+ }, [groupKeys, visibleStart, window.groupKeys, window.visibleStart]);
550
+
551
+ return {
552
+ mountedGroups: allGroups.slice(visibleStart),
553
+ mountingOlderGroups: visibleStart > 0,
554
+ };
555
+ }
556
+
409
557
  function requestFrame(callback: FrameRequestCallback): number {
410
558
  if (typeof requestAnimationFrame === "function") {
411
559
  return requestAnimationFrame(callback);
@@ -421,7 +569,11 @@ function cancelFrame(id: number): void {
421
569
  window.clearTimeout(id);
422
570
  }
423
571
 
424
- function TimelineGroupView({
572
+ // Progressive history mounting reuses the exact group objects from `allGroups`.
573
+ // Skip repainting those stable rows on every prefix reveal; live projection
574
+ // creates a new group object, and behavior/callback changes are separate props,
575
+ // so ordinary streaming and host updates still invalidate immediately.
576
+ const TimelineGroupView = memo(function TimelineGroupView({
425
577
  group,
426
578
  renderMessageText,
427
579
  onOpenSession,
@@ -531,7 +683,7 @@ function TimelineGroupView({
531
683
  />
532
684
  );
533
685
  }
534
- }
686
+ });
535
687
 
536
688
  function timelineGroupKey(group: TimelineGroup): string {
537
689
  switch (group.kind) {
@@ -544,6 +696,17 @@ function timelineGroupKey(group: TimelineGroup): string {
544
696
  }
545
697
  }
546
698
 
699
+ function timelineGroupItemIds(group: TimelineGroup): string[] {
700
+ switch (group.kind) {
701
+ case "item":
702
+ return [group.item.id];
703
+ case "activity":
704
+ return group.items.map((item) => item.id);
705
+ case "turn":
706
+ return group.groups.flatMap(timelineGroupItemIds);
707
+ }
708
+ }
709
+
547
710
  /** The agent has moved PAST a cluster only when what follows is more agent
548
711
  progress — new activity, a settled turn, or narration. A waiting notice
549
712
  (approval pause), a pending queued message, a goal pill, or nothing at all
@@ -568,8 +731,8 @@ function clusterIsSettled(group: Extract<TimelineGroup, { kind: "activity" }>):
568
731
  if (item.kind === "reasoning") {
569
732
  return !item.streaming;
570
733
  }
571
- // A memory write is a discrete, already-settled event — it has no running state.
572
- if (item.kind === "memory") {
734
+ // Memory writes and fleet observations are discrete, already-settled events.
735
+ if (item.kind === "memory" || item.kind === "fleet-decision") {
573
736
  return true;
574
737
  }
575
738
  return item.status !== "running";
@@ -998,12 +1161,12 @@ function NoticeRow({ item }: { item: NoticeItem }) {
998
1161
  }
999
1162
 
1000
1163
  /**
1001
- * The inline connection card: a lapsed connection surfaces as a calm, tappable
1002
- * reconnect affordance, while an incompatible host-owned endpoint/resource
1003
- * contract remains informative and non-actionable. The `reason` only shapes
1004
- * the helper copy; no domain or enum code is shown as a label.
1005
- * `onReconnect` (from the app, which owns the SDK client) runs the flow; without
1006
- * it, a pre-minted authorization link is offered, or the card stays informative.
1164
+ * The inline connection-recovery card: missing or lapsed access surfaces as a
1165
+ * calm, tappable affordance instead of a raw provider-domain error. The `reason`
1166
+ * only shapes human copy; no domain or enum code is shown as a label.
1167
+ * `onReconnect` (from the app, which owns the SDK client) starts the flow;
1168
+ * without it, a pre-minted authorization link is offered, or the card stays
1169
+ * informative. Recovery never claims to resume/replay the failed tool call.
1007
1170
  */
1008
1171
  function AuthNeededRow({
1009
1172
  item,
@@ -1020,6 +1183,8 @@ function AuthNeededRow({
1020
1183
  const provider = providerLabel(item.providerDomain);
1021
1184
  const unavailable =
1022
1185
  item.reason === "unsupported_auth" || item.reason === "resource_scope_unavailable";
1186
+ const missing = item.reason === "missing_connection";
1187
+ const actionLabel = missing ? "Connect" : "Reconnect";
1023
1188
 
1024
1189
  const start = async () => {
1025
1190
  if (!onReconnect || busy) {
@@ -1030,6 +1195,7 @@ function AuthNeededRow({
1030
1195
  try {
1031
1196
  // On success the app redirects to consent (or routes to credential entry),
1032
1197
  // so this row unmounts; a resolve without navigation just relaxes the button.
1198
+ // The callback starts authorization only. It never resumes this tool call.
1033
1199
  await onReconnect(item);
1034
1200
  setBusy(false);
1035
1201
  } catch {
@@ -1048,7 +1214,7 @@ function AuthNeededRow({
1048
1214
  />
1049
1215
  <div className="min-w-0">
1050
1216
  <p className="truncate text-og-md font-medium text-og-fg">
1051
- {unavailable ? `${provider} tools unavailable` : `Reconnect ${provider}`}
1217
+ {unavailable ? `${provider} tools unavailable` : `${actionLabel} ${provider}`}
1052
1218
  </p>
1053
1219
  <p className="truncate text-og-sm text-og-fg-subtle">{authReasonLine(item.reason)}</p>
1054
1220
  </div>
@@ -1064,7 +1230,7 @@ function AuthNeededRow({
1064
1230
  )}
1065
1231
  >
1066
1232
  <RefreshCwIcon className={cn("size-3.5", busy && "animate-og-spin")} aria-hidden />
1067
- {busy ? "Reconnecting…" : "Reconnect"}
1233
+ {busy ? "Opening…" : actionLabel}
1068
1234
  </button>
1069
1235
  ) : !unavailable && item.authorizationUrl ? (
1070
1236
  <a
@@ -1077,13 +1243,19 @@ function AuthNeededRow({
1077
1243
  )}
1078
1244
  >
1079
1245
  <RefreshCwIcon className="size-3.5" aria-hidden />
1080
- Reconnect
1246
+ {actionLabel}
1081
1247
  </a>
1082
1248
  ) : null}
1083
1249
  </div>
1250
+ {!unavailable ? (
1251
+ <p className="px-1 text-og-xs text-og-fg-subtle">
1252
+ This tool call wasn't replayed. After {missing ? "connecting" : "reconnecting"}, send a
1253
+ new message to try again.
1254
+ </p>
1255
+ ) : null}
1084
1256
  {failed ? (
1085
1257
  <p className="px-1 text-og-xs text-og-status-failed">
1086
- Couldn't start reconnecting {provider}. Try again.
1258
+ Couldn't start {missing ? "connecting" : "reconnecting"} {provider}. Try again.
1087
1259
  </p>
1088
1260
  ) : null}
1089
1261
  </div>
@@ -0,0 +1,18 @@
1
+ import type { ComposerState } from "../hooks/use-composer";
2
+
3
+ /**
4
+ * Decide whether checking out a queued prompt would replace current composer
5
+ * content. `hasDraftContent` reads the synchronous draft refs, so this stays
6
+ * correct even when a controlled input update has not rendered yet.
7
+ */
8
+ export function requestQueueDraftEdit(
9
+ composer: Pick<ComposerState, "hasDraftContent">,
10
+ confirmReplacement: () => void,
11
+ editImmediately: () => void,
12
+ ): void {
13
+ if (composer.hasDraftContent()) {
14
+ confirmReplacement();
15
+ } else {
16
+ editImmediately();
17
+ }
18
+ }
@@ -40,6 +40,7 @@ import {
40
40
  import { DropdownMenu } from "radix-ui";
41
41
  import type { ComposerState } from "../hooks/use-composer";
42
42
  import type { QueueMutationKind, UseTurnQueueResult } from "../hooks/use-turn-queue";
43
+ import { requestQueueDraftEdit } from "./queue-draft-policy";
43
44
  import { QueueErrorAlert, QueueStoppingStatus } from "./queue-surface-state";
44
45
 
45
46
  /** The sole human prompt queue: compact above Goal, Agents, and composer. */
@@ -217,16 +218,11 @@ export function QueueSurface({
217
218
  const requestEdit = useCallback(
218
219
  (turn: SessionTurn) => {
219
220
  if (!composer || !canEditInComposer) return;
220
- const draftDirty =
221
- composer.value.length > 0 ||
222
- composer.restoredResources.length > 0 ||
223
- (composer.draft?.tools.length ?? 0) > 0 ||
224
- (composer.draft?.sourceTurnId !== null && composer.draft?.sourceTurnId !== undefined);
225
- if (draftDirty) {
226
- setReplaceDraftFor(turn.id);
227
- } else {
228
- void edit(turn, false);
229
- }
221
+ requestQueueDraftEdit(
222
+ composer,
223
+ () => setReplaceDraftFor(turn.id),
224
+ () => void edit(turn, false),
225
+ );
230
226
  },
231
227
  [canEditInComposer, composer, edit],
232
228
  );
@@ -2,6 +2,7 @@ import { Loader2Icon } from "lucide-react";
2
2
  import { lazy, Suspense, type ComponentType } from "react";
3
3
 
4
4
  import type { QueueSurfaceProps } from "./queue-surface-implementation";
5
+ export { requestQueueDraftEdit } from "./queue-draft-policy";
5
6
  import { EmptyQueueStateSurface } from "./queue-surface-state";
6
7
 
7
8
  export type { QueueSurfaceProps } from "./queue-surface-implementation";