@opengeni/react 3.8.0-canary.0 → 4.0.1-canary.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 (111) hide show
  1. package/README.md +11 -24
  2. package/dist/artifacts.js +7 -7
  3. package/dist/{browser-viewer-SS46LUAC.js → browser-viewer-NTD6UIPR.js} +3 -3
  4. package/dist/{chunk-VEPNNDNG.js → chunk-24M4YBCL.js} +4 -4
  5. package/dist/chunk-5M6VTFJ5.js +171 -0
  6. package/dist/chunk-5M6VTFJ5.js.map +1 -0
  7. package/dist/{chunk-IOLVN74D.js → chunk-6J56T3MG.js} +15 -1
  8. package/dist/chunk-6J56T3MG.js.map +1 -0
  9. package/dist/{chunk-ELIYWBZR.js → chunk-BV5A4OTE.js} +4 -4
  10. package/dist/{chunk-3HIF5IFC.js → chunk-HO6X4FHG.js} +2597 -1030
  11. package/dist/chunk-HO6X4FHG.js.map +1 -0
  12. package/dist/{chunk-BXJOQFCD.js → chunk-HUMIRRYI.js} +84 -221
  13. package/dist/chunk-HUMIRRYI.js.map +1 -0
  14. package/dist/{chunk-ZFOFY5ST.js → chunk-NLTS7JHI.js} +16 -31
  15. package/dist/chunk-NLTS7JHI.js.map +1 -0
  16. package/dist/chunk-NOSYBS2D.js +208 -0
  17. package/dist/chunk-NOSYBS2D.js.map +1 -0
  18. package/dist/chunk-YBM6CQRU.js +292 -0
  19. package/dist/chunk-YBM6CQRU.js.map +1 -0
  20. package/dist/{chunk-BQGIXRKV.js → chunk-Z7WBDB4E.js} +416 -290
  21. package/dist/chunk-Z7WBDB4E.js.map +1 -0
  22. package/dist/chunk-ZYRGEKWS.js +216 -0
  23. package/dist/chunk-ZYRGEKWS.js.map +1 -0
  24. package/dist/components/composer.d.ts +4 -2
  25. package/dist/components/human-input-form.d.ts +3 -1
  26. package/dist/components/human-input-surface.d.ts +3 -2
  27. package/dist/components/message-timeline.d.ts +9 -2
  28. package/dist/components/session-conversation.d.ts +2 -0
  29. package/dist/components/timeline-annotation-chrome.d.ts +25 -0
  30. package/dist/components/timeline-annotation-layout.d.ts +42 -0
  31. package/dist/components/timeline-annotation-markers.d.ts +8 -0
  32. package/dist/components/timeline-annotation-reveal-context.d.ts +4 -0
  33. package/dist/components/timeline-annotation-shared.d.ts +31 -0
  34. package/dist/components/timeline-annotations-dialog.d.ts +1 -1
  35. package/dist/components/timeline-annotations.d.ts +8 -2
  36. package/dist/composer.js +3 -4
  37. package/dist/{computer-viewer-FWEZISW4.js → computer-viewer-7TEZC6Y2.js} +3 -3
  38. package/dist/hooks/use-codex-accounts.d.ts +11 -1
  39. package/dist/hooks/use-composer.d.ts +2 -0
  40. package/dist/hooks/use-session-events.d.ts +2 -2
  41. package/dist/index.d.ts +1 -1
  42. package/dist/index.js +50 -37
  43. package/dist/index.js.map +1 -1
  44. package/dist/interaction.js +3 -3
  45. package/dist/machines.js +1 -1
  46. package/dist/older-history.d.ts +3 -2
  47. package/dist/{platform-activity-row-BJHUHXAT.js → platform-activity-row-27LKAWZD.js} +2 -2
  48. package/dist/platform-activity-row-27LKAWZD.js.map +1 -0
  49. package/dist/{queue-surface-implementation-XL2A6PBM.js → queue-surface-implementation-LK3S4BIW.js} +5 -2
  50. package/dist/{queue-surface-implementation-XL2A6PBM.js.map → queue-surface-implementation-LK3S4BIW.js.map} +1 -1
  51. package/dist/realtime.js +3 -3
  52. package/dist/session-ui.js +11 -11
  53. package/dist/session.js +2 -2
  54. package/dist/timeline/types.d.ts +3 -0
  55. package/dist/timeline-annotation-markers-DRSTLEM6.js +165 -0
  56. package/dist/timeline-annotation-markers-DRSTLEM6.js.map +1 -0
  57. package/dist/timeline-annotation-selection-IHLUGVND.js +236 -0
  58. package/dist/timeline-annotation-selection-IHLUGVND.js.map +1 -0
  59. package/dist/timeline-annotations-dialog-6Q53N5FG.js +262 -0
  60. package/dist/timeline-annotations-dialog-6Q53N5FG.js.map +1 -0
  61. package/package.json +2 -6
  62. package/src/components/chat-composer.tsx +5 -2
  63. package/src/components/composer.tsx +40 -5
  64. package/src/components/copy-button.tsx +1 -0
  65. package/src/components/human-input-form.tsx +185 -53
  66. package/src/components/human-input-surface.tsx +3 -0
  67. package/src/components/machines-dashboard.tsx +1 -16
  68. package/src/components/message-timeline.tsx +397 -302
  69. package/src/components/session-commands-panel.tsx +7 -1
  70. package/src/components/session-conversation.tsx +4 -1
  71. package/src/components/timeline-annotation-chrome.tsx +183 -0
  72. package/src/components/timeline-annotation-layout.ts +268 -0
  73. package/src/components/timeline-annotation-markers.tsx +185 -0
  74. package/src/components/timeline-annotation-reveal-context.ts +9 -0
  75. package/src/components/timeline-annotation-selection.tsx +99 -54
  76. package/src/components/timeline-annotation-shared.ts +245 -0
  77. package/src/components/timeline-annotations-dialog.tsx +172 -128
  78. package/src/components/timeline-annotations.tsx +178 -45
  79. package/src/hooks/use-codex-accounts.ts +32 -5
  80. package/src/hooks/use-composer.ts +17 -0
  81. package/src/hooks/use-session-events.ts +16 -277
  82. package/src/index.ts +0 -1
  83. package/src/older-history.ts +22 -4
  84. package/src/timeline/platform-activity-row.tsx +4 -1
  85. package/src/timeline/projection.ts +99 -9
  86. package/src/timeline/types.ts +3 -0
  87. package/styles/compiled.css +86 -22
  88. package/dist/chat.d.ts +0 -37
  89. package/dist/chat.js +0 -315
  90. package/dist/chat.js.map +0 -1
  91. package/dist/chunk-3HIF5IFC.js.map +0 -1
  92. package/dist/chunk-BQGIXRKV.js.map +0 -1
  93. package/dist/chunk-BXJOQFCD.js.map +0 -1
  94. package/dist/chunk-F2CFHN3Y.js +0 -1415
  95. package/dist/chunk-F2CFHN3Y.js.map +0 -1
  96. package/dist/chunk-HQV2I5HV.js +0 -124
  97. package/dist/chunk-HQV2I5HV.js.map +0 -1
  98. package/dist/chunk-IOLVN74D.js.map +0 -1
  99. package/dist/chunk-W3OUAH6M.js +0 -170
  100. package/dist/chunk-W3OUAH6M.js.map +0 -1
  101. package/dist/chunk-ZFOFY5ST.js.map +0 -1
  102. package/dist/platform-activity-row-BJHUHXAT.js.map +0 -1
  103. package/dist/timeline-annotation-selection-3L7LHGG2.js +0 -187
  104. package/dist/timeline-annotation-selection-3L7LHGG2.js.map +0 -1
  105. package/dist/timeline-annotations-dialog-U7EVLR75.js +0 -196
  106. package/dist/timeline-annotations-dialog-U7EVLR75.js.map +0 -1
  107. package/src/chat.tsx +0 -393
  108. /package/dist/{browser-viewer-SS46LUAC.js.map → browser-viewer-NTD6UIPR.js.map} +0 -0
  109. /package/dist/{chunk-VEPNNDNG.js.map → chunk-24M4YBCL.js.map} +0 -0
  110. /package/dist/{chunk-ELIYWBZR.js.map → chunk-BV5A4OTE.js.map} +0 -0
  111. /package/dist/{computer-viewer-FWEZISW4.js.map → computer-viewer-7TEZC6Y2.js.map} +0 -0
@@ -106,6 +106,7 @@ import {
106
106
  useTurnSettleOpen,
107
107
  } from "../timeline";
108
108
  import { CopyHoverFrame } from "./copy-button";
109
+ import { TimelineAnnotationSourceRootContext } from "./timeline-annotation-reveal-context";
109
110
  import { GeneratedVideoPlayer } from "./generated-video-player";
110
111
  import {
111
112
  MACHINE_INPUT_META,
@@ -118,10 +119,11 @@ import { SESSION_STATUS_META, StatusDot } from "./session-status";
118
119
  import { TimelineComputeLabelProvider } from "../timeline/compute-label";
119
120
  import { EntranceAnimationProvider, useEntranceAnimation } from "../timeline/entrance";
120
121
  import { SeenActivityIdsProvider } from "../timeline/seen-activity-ids";
121
- import { TimelineAnnotationsChip } from "./timeline-annotations";
122
+ import { TimelineAnnotationCards } from "./timeline-annotations";
122
123
  import { TooltipProvider } from "./tooltip";
123
124
 
124
125
  const TimelineAnnotationSelection = lazy(() => import("./timeline-annotation-selection"));
126
+ const TimelineAnnotationMarkers = lazy(() => import("./timeline-annotation-markers"));
125
127
 
126
128
  export type MessageTimelineProps = {
127
129
  /** Raw session events (projected internally) … */
@@ -130,6 +132,8 @@ export type MessageTimelineProps = {
130
132
  items?: TimelineItem[] | undefined;
131
133
  /** Current session status (reserved; tip "Working…" chrome removed for now). */
132
134
  status?: SessionStatus | null | undefined;
135
+ /** Host-owned controls beside Copy and the timestamp on settled message rows. */
136
+ renderMessageActions?: ((item: AgentMessageItem | UserMessageItem) => ReactNode) | undefined;
133
137
  /** Plug a markdown renderer for message bodies (e.g. streamdown). */
134
138
  renderMessageText?:
135
139
  | ((text: string, item: AgentMessageItem | UserMessageItem) => ReactNode)
@@ -190,6 +194,10 @@ export type MessageTimelineProps = {
190
194
  autoFollow?: boolean | undefined;
191
195
  /** Capture a same-row text selection into the host's canonical composer draft. */
192
196
  onAnnotate?: ((annotation: DraftTimelineAnnotation) => void) | undefined;
197
+ /** Composer draft quotes currently attached to the next send. */
198
+ draftAnnotations?: readonly DraftTimelineAnnotation[] | undefined;
199
+ /** Open the composer review list for one numbered draft badge. */
200
+ onDraftAnnotationSelect?: ((id: string) => void) | undefined;
193
201
  /** Older durable history exists above the current window (see useSessionEvents). */
194
202
  hasOlder?: boolean | undefined;
195
203
  /** An older window is being fetched; shows the quiet top shimmer. */
@@ -285,13 +293,18 @@ function invokeOlderLoad(
285
293
  load: OlderHistoryLoader,
286
294
  noProgress: () => void,
287
295
  attempt: OlderLoadAttempt,
296
+ preserveTail = false,
288
297
  ): 1 | undefined {
289
298
  try {
290
299
  // Receipt creation is captured synchronously through legacy wrappers such
291
300
  // as `() => void loadOlder()`, even when the wrapper discards the return.
292
- const result = invokeOlderHistoryLoaderWithReceiptCapture(load, (receipt) => {
293
- attempt[2] = receipt;
294
- }) as OlderHistoryLoadReceipt | PromiseLike<unknown> | unknown;
301
+ const result = invokeOlderHistoryLoaderWithReceiptCapture(
302
+ load,
303
+ (receipt) => {
304
+ attempt[2] = receipt;
305
+ },
306
+ preserveTail,
307
+ ) as OlderHistoryLoadReceipt | PromiseLike<unknown> | unknown;
295
308
  const receipt =
296
309
  attempt[2] ??
297
310
  (typeof (result as { committed?: unknown } | undefined)?.committed === "boolean"
@@ -389,6 +402,7 @@ export function MessageTimeline({
389
402
  events,
390
403
  items,
391
404
  status: _status,
405
+ renderMessageActions,
392
406
  renderMessageText,
393
407
  onOpenSession,
394
408
  onMemoryClick,
@@ -403,6 +417,8 @@ export function MessageTimeline({
403
417
  turnSummary,
404
418
  autoFollow = true,
405
419
  onAnnotate,
420
+ draftAnnotations,
421
+ onDraftAnnotationSelect,
406
422
  hasOlder = false,
407
423
  loadingOlder = false,
408
424
  onLoadOlder,
@@ -889,6 +905,7 @@ export function MessageTimeline({
889
905
  () => ({
890
906
  userMessageDisclosureContext,
891
907
  behavior: {
908
+ renderMessageActions,
892
909
  renderMessageText,
893
910
  onOpenSession,
894
911
  onMemoryClick,
@@ -908,6 +925,7 @@ export function MessageTimeline({
908
925
  onMemoryClick,
909
926
  onOpenSession,
910
927
  onReconnect,
928
+ renderMessageActions,
911
929
  renderMessageText,
912
930
  resolveProviderLogo,
913
931
  toolRegistry,
@@ -965,7 +983,7 @@ export function MessageTimeline({
965
983
  // exact `false` is the first-party request-not-accepted receipt.
966
984
  // All other fulfillment retains this exact owner until its prepend
967
985
  // boundary commits; promise settlement alone cannot prove progress.
968
- invokeOlderLoad(onLoadOlder, noProgress, attempt);
986
+ invokeOlderLoad(onLoadOlder, noProgress, attempt, !retry);
969
987
  },
970
988
  [hasOlder, loadingOlder, olderBoundaryKey, onLoadOlder],
971
989
  );
@@ -1523,7 +1541,9 @@ export function MessageTimeline({
1523
1541
  }
1524
1542
  const observer = new IntersectionObserver(
1525
1543
  (entries) => {
1526
- if (entries.some((entry) => entry.isIntersecting)) {
1544
+ // An underfilled history window has both sentinels visible. Advancing
1545
+ // it automatically would undo an explicit older-page navigation.
1546
+ if (maxScrollOf(root) > 1 && entries.some((entry) => entry.isIntersecting)) {
1527
1547
  onLoadNewer();
1528
1548
  }
1529
1549
  },
@@ -1785,303 +1805,318 @@ export function MessageTimeline({
1785
1805
  <TimelineComputeLabelProvider value={computeLabel ?? null}>
1786
1806
  <EntranceAnimationProvider value={false}>
1787
1807
  <TooltipProvider delayDuration={400}>
1788
- <div className={cn("og-root relative flex min-h-0 flex-col", className)}>
1789
- {onAnnotate ? (
1790
- <Suspense fallback={null}>
1791
- <TimelineAnnotationSelection
1792
- rootRef={scrollRef}
1793
- sources={annotationSources}
1794
- onAnnotate={onAnnotate}
1795
- />
1796
- </Suspense>
1797
- ) : null}
1798
- {/* Pinned: anchoring off so the tip-follow camera owns the motion.
1808
+ <TimelineAnnotationSourceRootContext.Provider value={scrollRef}>
1809
+ <div className={cn("og-root relative flex min-h-0 flex-col", className)}>
1810
+ {onAnnotate ? (
1811
+ <Suspense fallback={null}>
1812
+ <TimelineAnnotationSelection
1813
+ rootRef={scrollRef}
1814
+ sources={annotationSources}
1815
+ onAnnotate={onAnnotate}
1816
+ />
1817
+ </Suspense>
1818
+ ) : null}
1819
+ {/* Pinned: anchoring off so the tip-follow camera owns the motion.
1799
1820
  Unpinned: native scroll anchoring holds the reader's place. */}
1800
- <div
1801
- ref={scrollRef}
1802
- data-og-timeline-scroller=""
1803
- data-og-bottom-follow={autoFollow && pinned && !hasNewer ? "true" : "false"}
1804
- tabIndex={-1}
1805
- onScroll={onScroll}
1806
- onScrollEnd={onScrollEnd}
1807
- onWheel={onWheel}
1808
- onTouchStart={(event) => {
1809
- const touch = event.touches.length === 1 ? event.touches[0] : undefined;
1810
- touchPositionRef.current = touch
1811
- ? { x: touch.clientX, y: touch.clientY }
1812
- : null;
1813
- }}
1814
- onTouchMove={(event) => {
1815
- const touch = event.touches[0];
1816
- const previous = touchPositionRef.current;
1817
- if (!touch || !previous || event.touches.length !== 1) {
1818
- touchPositionRef.current = null;
1819
- return;
1820
- }
1821
- const deltaX = previous.x - touch.clientX;
1822
- const deltaY = previous.y - touch.clientY;
1823
- if (Math.max(Math.abs(deltaX), Math.abs(deltaY)) < 4) return;
1824
- touchPositionRef.current = { x: touch.clientX, y: touch.clientY };
1825
- onWheel({
1826
- deltaX,
1827
- deltaY,
1828
- target: event.target,
1829
- currentTarget: event.currentTarget,
1830
- });
1831
- }}
1832
- onTouchEnd={() => {
1833
- touchPositionRef.current = null;
1834
- }}
1835
- onTouchCancel={() => {
1836
- touchPositionRef.current = null;
1837
- }}
1838
- onClickCapture={(event) => {
1839
- const target =
1840
- event.target instanceof Element
1841
- ? event.target.closest("button[aria-expanded]")
1821
+ <div
1822
+ ref={scrollRef}
1823
+ data-og-timeline-scroller=""
1824
+ data-og-bottom-follow={autoFollow && pinned && !hasNewer ? "true" : "false"}
1825
+ tabIndex={-1}
1826
+ onScroll={onScroll}
1827
+ onScrollEnd={onScrollEnd}
1828
+ onWheel={onWheel}
1829
+ onTouchStart={(event) => {
1830
+ const touch = event.touches.length === 1 ? event.touches[0] : undefined;
1831
+ touchPositionRef.current = touch
1832
+ ? { x: touch.clientX, y: touch.clientY }
1842
1833
  : null;
1843
- if (target) {
1844
- releasePinFromReader();
1845
- disclosureKeepsUnpinnedRef.current = true;
1846
- }
1847
- }}
1848
- onPointerDown={onPointerDown}
1849
- onKeyDown={onKeyDown}
1850
- style={groups.length > 0 && !revealed ? { visibility: "hidden" } : undefined}
1851
- className={cn(
1852
- // tabIndex=-1 is programmatic only — never paint a focus ring on
1853
- // the whole scroller (click + Shift used to flash a blue outline).
1854
- "min-h-0 flex-1 overflow-y-auto overscroll-contain px-4 pt-16 pb-6 sm:px-6 outline-hidden",
1855
- autoFollow && pinned && !hasNewer
1856
- ? "[overflow-anchor:none]"
1857
- : "[overflow-anchor:auto]",
1858
- )}
1859
- >
1860
- <TimelineBeforeLayout
1861
- capture={() => {
1862
- readingAnchorRef.current =
1863
- !pinnedRef.current && scrollRef.current
1864
- ? captureTimelineAnchor(scrollRef.current)
1834
+ }}
1835
+ onTouchMove={(event) => {
1836
+ const touch = event.touches[0];
1837
+ const previous = touchPositionRef.current;
1838
+ if (!touch || !previous || event.touches.length !== 1) {
1839
+ touchPositionRef.current = null;
1840
+ return;
1841
+ }
1842
+ const deltaX = previous.x - touch.clientX;
1843
+ const deltaY = previous.y - touch.clientY;
1844
+ if (Math.max(Math.abs(deltaX), Math.abs(deltaY)) < 4) return;
1845
+ touchPositionRef.current = { x: touch.clientX, y: touch.clientY };
1846
+ onWheel({
1847
+ deltaX,
1848
+ deltaY,
1849
+ target: event.target,
1850
+ currentTarget: event.currentTarget,
1851
+ });
1852
+ }}
1853
+ onTouchEnd={() => {
1854
+ touchPositionRef.current = null;
1855
+ }}
1856
+ onTouchCancel={() => {
1857
+ touchPositionRef.current = null;
1858
+ }}
1859
+ onClickCapture={(event) => {
1860
+ const target =
1861
+ event.target instanceof Element
1862
+ ? event.target.closest("button[aria-expanded]")
1865
1863
  : null;
1864
+ if (target) {
1865
+ releasePinFromReader();
1866
+ disclosureKeepsUnpinnedRef.current = true;
1867
+ }
1866
1868
  }}
1869
+ onPointerDown={onPointerDown}
1870
+ onKeyDown={onKeyDown}
1871
+ style={groups.length > 0 && !revealed ? { visibility: "hidden" } : undefined}
1872
+ className={cn(
1873
+ // tabIndex=-1 is programmatic only — never paint a focus ring on
1874
+ // the whole scroller (click + Shift used to flash a blue outline).
1875
+ "min-h-0 flex-1 overflow-y-auto overscroll-contain px-4 pt-16 pb-6 sm:px-6 outline-hidden",
1876
+ autoFollow && pinned && !hasNewer
1877
+ ? "[overflow-anchor:none]"
1878
+ : "[overflow-anchor:auto]",
1879
+ )}
1867
1880
  >
1868
- <div className="relative mx-auto flex w-full max-w-3xl flex-col gap-5">
1869
- <AnimatePresence>
1870
- {loadingOlder ||
1871
- loadingOldest ||
1872
- (hasOlder && onJumpToStart && olderPrefetchArmed) ||
1873
- underfillRetryReady ? (
1874
- // Reserved top gutter: controls scroll with history and cannot
1875
- // cover a disclosure. Visibility never changes content height.
1876
- <motion.div
1877
- initial={{ opacity: 0, y: -6 }}
1878
- animate={{ opacity: 1, y: 0 }}
1879
- exit={{ opacity: 0, y: -6 }}
1880
- transition={{ duration: 0.15, ease: "easeOut" }}
1881
- data-og-loading-older=""
1882
- aria-live="polite"
1883
- className="pointer-events-none absolute inset-x-0 -top-11 z-10 flex justify-center"
1884
- >
1885
- {loadingOlder || loadingOldest ? (
1886
- <span className={LOADING_CHIP_CLASS}>
1887
- <span className="og-shimmer-text">
1888
- {loadingOldest
1889
- ? "Jumping to start…"
1890
- : "Loading earlier activity…"}
1881
+ <TimelineBeforeLayout
1882
+ capture={() => {
1883
+ readingAnchorRef.current =
1884
+ !pinnedRef.current && scrollRef.current
1885
+ ? captureTimelineAnchor(scrollRef.current)
1886
+ : null;
1887
+ }}
1888
+ >
1889
+ <div className="relative mx-auto flex w-full max-w-3xl flex-col gap-5">
1890
+ <AnimatePresence>
1891
+ {loadingOlder ||
1892
+ loadingOldest ||
1893
+ (hasOlder && onJumpToStart && olderPrefetchArmed) ||
1894
+ underfillRetryReady ? (
1895
+ // Reserved top gutter: controls scroll with history and cannot
1896
+ // cover a disclosure. Visibility never changes content height.
1897
+ <motion.div
1898
+ initial={{ opacity: 0, y: -6 }}
1899
+ animate={{ opacity: 1, y: 0 }}
1900
+ exit={{ opacity: 0, y: -6 }}
1901
+ transition={{ duration: 0.15, ease: "easeOut" }}
1902
+ data-og-loading-older=""
1903
+ aria-live="polite"
1904
+ className="pointer-events-none absolute inset-x-0 -top-11 z-10 flex justify-center"
1905
+ >
1906
+ {loadingOlder || loadingOldest ? (
1907
+ <span className={LOADING_CHIP_CLASS}>
1908
+ <span className="og-shimmer-text">
1909
+ {loadingOldest
1910
+ ? "Jumping to start…"
1911
+ : "Loading earlier activity…"}
1912
+ </span>
1891
1913
  </span>
1892
- </span>
1893
- ) : null}
1894
- {hasOlder &&
1895
- !loadingOlder &&
1896
- !loadingOldest &&
1897
- (underfillRetryReady || onJumpToStart) ? (
1898
- <button
1899
- type="button"
1900
- data-og-retry={underfillRetryReady || undefined}
1901
- data-og-jump-to-start={!underfillRetryReady || undefined}
1902
- onClick={() => {
1903
- const node = scrollRef.current;
1904
- if (underfillRetryReady) {
1905
- if (node && underfillRetryReadyRef.current) {
1906
- // AnimatePresence retains this handler during
1907
- // exit. Current authorization plus the exact
1908
- // attempt check prevent stale dispatch.
1909
- requestOlderIfUnderfilled(node, underfillSettledAttempt!);
1910
- }
1911
- return;
1912
- }
1913
- applyPinned(false);
1914
- pendingJumpToStartRef.current = true;
1915
- const seq = ++jumpToStartSeqRef.current;
1916
- void Promise.resolve(onJumpToStart!()).then(
1917
- () => {
1918
- // The commit that swaps in the oldest window consumes
1919
- // the flag against the new DOM; this write covers the
1920
- // already-committed order and the no-window-change
1921
- // case (jumping within the current window).
1922
- const scroller = scrollRef.current ?? node;
1923
- if (scroller) {
1924
- scroller.scrollTop = 0;
1914
+ ) : null}
1915
+ {hasOlder &&
1916
+ !loadingOlder &&
1917
+ !loadingOldest &&
1918
+ (underfillRetryReady || onJumpToStart) ? (
1919
+ <button
1920
+ type="button"
1921
+ data-og-retry={underfillRetryReady || undefined}
1922
+ data-og-jump-to-start={!underfillRetryReady || undefined}
1923
+ onClick={() => {
1924
+ const node = scrollRef.current;
1925
+ if (underfillRetryReady) {
1926
+ if (node && underfillRetryReadyRef.current) {
1927
+ // AnimatePresence retains this handler during
1928
+ // exit. Current authorization plus the exact
1929
+ // attempt check prevent stale dispatch.
1930
+ requestOlderIfUnderfilled(node, underfillSettledAttempt!);
1925
1931
  }
1926
- // A host may resolve without ever changing the
1927
- // window (already on the oldest page). Any swap
1928
- // commit runs its layout effect before the next
1929
- // frame, so a flag still armed by then is the
1930
- // no-change case — clear it, or a LATER prepend
1931
- // would spuriously jump the reader to the top.
1932
- requestFrame(() => {
1932
+ return;
1933
+ }
1934
+ applyPinned(false);
1935
+ pendingJumpToStartRef.current = true;
1936
+ const seq = ++jumpToStartSeqRef.current;
1937
+ void Promise.resolve(onJumpToStart!()).then(
1938
+ () => {
1939
+ // The commit that swaps in the oldest window consumes
1940
+ // the flag against the new DOM; this write covers the
1941
+ // already-committed order and the no-window-change
1942
+ // case (jumping within the current window).
1943
+ const scroller = scrollRef.current ?? node;
1944
+ if (scroller) {
1945
+ scroller.scrollTop = 0;
1946
+ }
1947
+ // A host may resolve without ever changing the
1948
+ // window (already on the oldest page). Any swap
1949
+ // commit runs its layout effect before the next
1950
+ // frame, so a flag still armed by then is the
1951
+ // no-change case — clear it, or a LATER prepend
1952
+ // would spuriously jump the reader to the top.
1953
+ requestFrame(() => {
1954
+ if (jumpToStartSeqRef.current === seq) {
1955
+ pendingJumpToStartRef.current = false;
1956
+ }
1957
+ });
1958
+ },
1959
+ () => {
1933
1960
  if (jumpToStartSeqRef.current === seq) {
1934
1961
  pendingJumpToStartRef.current = false;
1935
1962
  }
1936
- });
1937
- },
1938
- () => {
1939
- if (jumpToStartSeqRef.current === seq) {
1940
- pendingJumpToStartRef.current = false;
1941
- }
1942
- },
1943
- );
1944
- }}
1945
- className="pointer-events-auto rounded-full border border-og-border px-3 py-1.5 text-og-control"
1946
- >
1947
- {underfillRetryReady ? "Retry earlier activity" : "Jump to start"}
1948
- </button>
1949
- ) : null}
1950
- </motion.div>
1963
+ },
1964
+ );
1965
+ }}
1966
+ className="pointer-events-auto rounded-full border border-og-border px-3 py-1.5 text-og-control"
1967
+ >
1968
+ {underfillRetryReady
1969
+ ? underfillSettledAttempt?.[2]?.tailPreserved
1970
+ ? "Load earlier activity"
1971
+ : "Retry earlier activity"
1972
+ : "Jump to start"}
1973
+ </button>
1974
+ ) : null}
1975
+ </motion.div>
1976
+ ) : null}
1977
+ </AnimatePresence>
1978
+ {!groups.length
1979
+ ? (emptyState ?? (
1980
+ <p className="py-10 text-center text-og-menu text-og-fg-subtle">
1981
+ No activity yet.
1982
+ </p>
1983
+ ))
1984
+ : null}
1985
+ {hasOlder && olderPrefetchArmed ? (
1986
+ // Overlaid, not a layout row: mounting/unmounting the sentinel
1987
+ // must never shift content (that shift was itself a wobble).
1988
+ // End at the scroll origin above the pt-16 gutter so the
1989
+ // observer and scrollTop cooldown share the same 400px band.
1990
+ <div
1991
+ ref={topSentinelRef}
1992
+ data-og-top-sentinel=""
1993
+ data-og-timeline-chrome=""
1994
+ aria-hidden="true"
1995
+ className="pointer-events-none absolute inset-x-0 -top-16 h-px -translate-y-full"
1996
+ />
1951
1997
  ) : null}
1952
- </AnimatePresence>
1953
- {!groups.length
1954
- ? (emptyState ?? (
1955
- <p className="py-10 text-center text-og-menu text-og-fg-subtle">
1956
- No activity yet.
1957
- </p>
1958
- ))
1959
- : null}
1960
- {hasOlder && olderPrefetchArmed ? (
1961
- // Overlaid, not a layout row: mounting/unmounting the sentinel
1962
- // must never shift content (that shift was itself a wobble).
1963
- // End at the scroll origin above the pt-16 gutter so the
1964
- // observer and scrollTop cooldown share the same 400px band.
1965
- <div
1966
- ref={topSentinelRef}
1967
- data-og-top-sentinel=""
1968
- data-og-timeline-chrome=""
1969
- aria-hidden="true"
1970
- className="pointer-events-none absolute inset-x-0 -top-16 h-px -translate-y-full"
1971
- />
1972
- ) : null}
1973
- {groups.map(({ group, key, entranceEnabled }, index) => {
1974
- return (
1975
- <TimelineGroupEntry
1976
- key={key}
1977
- groupKey={key}
1978
- group={group}
1979
- nextGroup={groups[index + 1]?.group}
1980
- entranceEnabled={entranceEnabled}
1981
- liveEntranceEnabled={
1982
- group.kind === "activity" ? !bulkRender : undefined
1983
- }
1984
- context={timelineGroupEntryContext}
1998
+ {groups.map(({ group, key, entranceEnabled }, index) => {
1999
+ return (
2000
+ <TimelineGroupEntry
2001
+ key={key}
2002
+ groupKey={key}
2003
+ group={group}
2004
+ nextGroup={groups[index + 1]?.group}
2005
+ entranceEnabled={entranceEnabled}
2006
+ liveEntranceEnabled={
2007
+ group.kind === "activity" ? !bulkRender : undefined
2008
+ }
2009
+ context={timelineGroupEntryContext}
2010
+ />
2011
+ );
2012
+ })}
2013
+ {groups.length > 0 && trailingState ? (
2014
+ <div data-og-timeline-trailing-state="">{trailingState}</div>
2015
+ ) : null}
2016
+ {hasNewer ? (
2017
+ <div
2018
+ ref={bottomSentinelRef}
2019
+ data-og-bottom-sentinel=""
2020
+ data-og-timeline-chrome=""
2021
+ aria-hidden="true"
2022
+ className="h-px w-full"
1985
2023
  />
1986
- );
1987
- })}
1988
- {groups.length > 0 && trailingState ? (
1989
- <div data-og-timeline-trailing-state="">{trailingState}</div>
1990
- ) : null}
1991
- {hasNewer ? (
1992
- <div
1993
- ref={bottomSentinelRef}
1994
- data-og-bottom-sentinel=""
1995
- data-og-timeline-chrome=""
1996
- aria-hidden="true"
1997
- className="h-px w-full"
1998
- />
1999
- ) : null}
2000
- </div>
2001
- </TimelineBeforeLayout>
2002
- </div>
2003
-
2004
- <AnimatePresence>
2005
- {loadingNewer ? (
2006
- <motion.div
2007
- initial={{ opacity: 0, y: 6 }}
2008
- animate={{ opacity: 1, y: 0 }}
2009
- exit={{ opacity: 0, y: 6 }}
2010
- transition={{ duration: 0.15, ease: "easeOut" }}
2011
- data-og-loading-newer=""
2012
- aria-live="polite"
2013
- className="pointer-events-none absolute inset-x-0 bottom-14 z-10 flex justify-center"
2014
- >
2015
- <span className={LOADING_CHIP_CLASS}>
2016
- <span className="og-shimmer-text">Loading later activity…</span>
2017
- </span>
2018
- </motion.div>
2024
+ ) : null}
2025
+ </div>
2026
+ </TimelineBeforeLayout>
2027
+ </div>
2028
+ {draftAnnotations && draftAnnotations.length > 0 ? (
2029
+ <Suspense fallback={null}>
2030
+ <TimelineAnnotationMarkers
2031
+ rootRef={scrollRef}
2032
+ annotations={draftAnnotations}
2033
+ onSelect={onDraftAnnotationSelect}
2034
+ />
2035
+ </Suspense>
2019
2036
  ) : null}
2020
- </AnimatePresence>
2021
- <AnimatePresence>
2022
- {((!pinned && autoFollow) || hasNewer || canSkipTipCatchup) && autoFollow ? (
2023
- <motion.button
2024
- type="button"
2025
- data-og-jump-to-latest=""
2026
- initial={{ opacity: 0, y: 8 }}
2027
- animate={{ opacity: 1, y: 0 }}
2028
- exit={{ opacity: 0, y: 8 }}
2029
- transition={{ duration: 0.15, ease: "easeOut" }}
2030
- onClick={() => {
2031
- disclosureKeepsUnpinnedRef.current = false;
2032
- if (hasNewer) {
2033
- // Do not pin against the current history page — its bottom
2034
- // is not the tip. The pin + snap run when the tip window
2035
- // actually lands (`hasNewer` flips false).
2036
- wantPinRef.current = true;
2037
- const node = scrollRef.current;
2038
- if (onJumpToLatest) {
2039
- void Promise.resolve(onJumpToLatest()).then(
2040
- () => {
2041
- // Covers a host that flipped hasNewer before
2042
- // resolving; otherwise the tip-window commit
2043
- // consumes the flag.
2044
- const current = scrollRef.current;
2045
- if (current && wantPinRef.current && !hasNewerRef.current) {
2037
+
2038
+ <AnimatePresence>
2039
+ {loadingNewer ? (
2040
+ <motion.div
2041
+ initial={{ opacity: 0, y: 6 }}
2042
+ animate={{ opacity: 1, y: 0 }}
2043
+ exit={{ opacity: 0, y: 6 }}
2044
+ transition={{ duration: 0.15, ease: "easeOut" }}
2045
+ data-og-loading-newer=""
2046
+ aria-live="polite"
2047
+ className="pointer-events-none absolute inset-x-0 bottom-14 z-10 flex justify-center"
2048
+ >
2049
+ <span className={LOADING_CHIP_CLASS}>
2050
+ <span className="og-shimmer-text">Loading later activity…</span>
2051
+ </span>
2052
+ </motion.div>
2053
+ ) : null}
2054
+ </AnimatePresence>
2055
+ <AnimatePresence>
2056
+ {((!pinned && autoFollow) || hasNewer || canSkipTipCatchup) && autoFollow ? (
2057
+ <motion.button
2058
+ type="button"
2059
+ data-og-jump-to-latest=""
2060
+ initial={{ opacity: 0, y: 8 }}
2061
+ animate={{ opacity: 1, y: 0 }}
2062
+ exit={{ opacity: 0, y: 8 }}
2063
+ transition={{ duration: 0.15, ease: "easeOut" }}
2064
+ onClick={() => {
2065
+ disclosureKeepsUnpinnedRef.current = false;
2066
+ if (hasNewer) {
2067
+ // Do not pin against the current history page — its bottom
2068
+ // is not the tip. The pin + snap run when the tip window
2069
+ // actually lands (`hasNewer` flips false).
2070
+ wantPinRef.current = true;
2071
+ const node = scrollRef.current;
2072
+ if (onJumpToLatest) {
2073
+ void Promise.resolve(onJumpToLatest()).then(
2074
+ () => {
2075
+ // Covers a host that flipped hasNewer before
2076
+ // resolving; otherwise the tip-window commit
2077
+ // consumes the flag.
2078
+ const current = scrollRef.current;
2079
+ if (current && wantPinRef.current && !hasNewerRef.current) {
2080
+ wantPinRef.current = false;
2081
+ applyPinned(true);
2082
+ snapToBottom(current);
2083
+ }
2084
+ },
2085
+ () => {
2086
+ // The tip reload failed (ordinary network error):
2087
+ // an armed latch would fire a surprise snap when
2088
+ // the reader later pages to the tip themselves.
2046
2089
  wantPinRef.current = false;
2047
- applyPinned(true);
2048
- snapToBottom(current);
2049
- }
2050
- },
2051
- () => {
2052
- // The tip reload failed (ordinary network error):
2053
- // an armed latch would fire a surprise snap when
2054
- // the reader later pages to the tip themselves.
2055
- wantPinRef.current = false;
2056
- },
2057
- );
2058
- } else if (node) {
2059
- // No tip reload available: jump within the in-memory
2060
- // window so the newer sentinel can page forward; the
2061
- // latch pins if the tip window eventually lands.
2090
+ },
2091
+ );
2092
+ } else if (node) {
2093
+ // No tip reload available: jump within the in-memory
2094
+ // window so the newer sentinel can page forward; the
2095
+ // latch pins if the tip window eventually lands.
2096
+ snapToBottom(node);
2097
+ }
2098
+ return;
2099
+ }
2100
+ const node = scrollRef.current;
2101
+ if (node) {
2102
+ applyPinned(true);
2062
2103
  snapToBottom(node);
2063
2104
  }
2064
- return;
2065
- }
2066
- const node = scrollRef.current;
2067
- if (node) {
2068
- applyPinned(true);
2069
- snapToBottom(node);
2070
- }
2071
- }}
2072
- className={cn(
2073
- "absolute inset-x-0 bottom-4 mx-auto w-fit",
2074
- "inline-flex items-center gap-1.5 rounded-full border border-og-border bg-og-surface-3/90 px-3 py-1.5",
2075
- "text-og-control font-medium text-og-fg shadow-og-md backdrop-blur",
2076
- "hover:border-og-border-strong",
2077
- )}
2078
- >
2079
- <ArrowDownIcon className="size-3.5" />
2080
- Jump to latest
2081
- </motion.button>
2082
- ) : null}
2083
- </AnimatePresence>
2084
- </div>
2105
+ }}
2106
+ className={cn(
2107
+ "absolute inset-x-0 bottom-4 mx-auto w-fit",
2108
+ "inline-flex items-center gap-1.5 rounded-full border border-og-border bg-og-surface-3/90 px-3 py-1.5",
2109
+ "text-og-control font-medium text-og-fg shadow-og-md backdrop-blur",
2110
+ "hover:border-og-border-strong",
2111
+ )}
2112
+ >
2113
+ <ArrowDownIcon className="size-3.5" />
2114
+ Jump to latest
2115
+ </motion.button>
2116
+ ) : null}
2117
+ </AnimatePresence>
2118
+ </div>
2119
+ </TimelineAnnotationSourceRootContext.Provider>
2085
2120
  </TooltipProvider>
2086
2121
  </EntranceAnimationProvider>
2087
2122
  </TimelineComputeLabelProvider>
@@ -2269,6 +2304,7 @@ type TimelineGroupEntryContext = {
2269
2304
  };
2270
2305
 
2271
2306
  type TimelineGroupBehaviorProps = {
2307
+ renderMessageActions: MessageTimelineProps["renderMessageActions"];
2272
2308
  renderMessageText: MessageTimelineProps["renderMessageText"];
2273
2309
  onOpenSession: MessageTimelineProps["onOpenSession"];
2274
2310
  onMemoryClick: MessageTimelineProps["onMemoryClick"];
@@ -2332,6 +2368,7 @@ const TimelineGroupEntry = memo(function TimelineGroupEntry({
2332
2368
  // streaming and host updates still invalidate immediately.
2333
2369
  const TimelineGroupView = memo(function TimelineGroupView({
2334
2370
  group,
2371
+ renderMessageActions,
2335
2372
  renderMessageText,
2336
2373
  onOpenSession,
2337
2374
  onMemoryClick,
@@ -2349,6 +2386,7 @@ const TimelineGroupView = memo(function TimelineGroupView({
2349
2386
  contextCompactionCount,
2350
2387
  }: {
2351
2388
  group: TimelineGroup;
2389
+ renderMessageActions?: ((item: AgentMessageItem | UserMessageItem) => ReactNode) | undefined;
2352
2390
  renderMessageText?:
2353
2391
  | ((text: string, item: AgentMessageItem | UserMessageItem) => ReactNode)
2354
2392
  | undefined;
@@ -2507,6 +2545,7 @@ const TimelineGroupView = memo(function TimelineGroupView({
2507
2545
  key={key}
2508
2546
  resetKeys={[
2509
2547
  child,
2548
+ renderMessageActions,
2510
2549
  renderMessageText,
2511
2550
  onOpenSession,
2512
2551
  onMemoryClick,
@@ -2521,6 +2560,7 @@ const TimelineGroupView = memo(function TimelineGroupView({
2521
2560
  >
2522
2561
  <TimelineGroupView
2523
2562
  group={child}
2563
+ renderMessageActions={renderMessageActions}
2524
2564
  renderMessageText={renderMessageText}
2525
2565
  onOpenSession={onOpenSession}
2526
2566
  onMemoryClick={onMemoryClick}
@@ -2567,6 +2607,7 @@ const TimelineGroupView = memo(function TimelineGroupView({
2567
2607
  return (
2568
2608
  <TimelineRow
2569
2609
  item={group.item}
2610
+ renderMessageActions={renderMessageActions}
2570
2611
  renderMessageText={renderMessageText}
2571
2612
  onReconnect={onReconnect}
2572
2613
  resolveProviderLogo={resolveProviderLogo}
@@ -2818,6 +2859,7 @@ function durationBetween(startedAt: string, endedAt: string): number | undefined
2818
2859
  */
2819
2860
  export function TimelineRow({
2820
2861
  item,
2862
+ renderMessageActions,
2821
2863
  renderMessageText,
2822
2864
  onReconnect,
2823
2865
  resolveProviderLogo,
@@ -2825,6 +2867,7 @@ export function TimelineRow({
2825
2867
  loadVideoArtifactPlayback,
2826
2868
  }: {
2827
2869
  item: TimelineItem;
2870
+ renderMessageActions?: ((item: AgentMessageItem | UserMessageItem) => ReactNode) | undefined;
2828
2871
  renderMessageText?:
2829
2872
  | ((text: string, item: AgentMessageItem | UserMessageItem) => ReactNode)
2830
2873
  | undefined;
@@ -2835,11 +2878,23 @@ export function TimelineRow({
2835
2878
  }) {
2836
2879
  switch (item.kind) {
2837
2880
  case "user-message":
2838
- return <UserMessageRow item={item} renderMessageText={renderMessageText} />;
2881
+ return (
2882
+ <UserMessageRow
2883
+ item={item}
2884
+ renderMessageActions={renderMessageActions}
2885
+ renderMessageText={renderMessageText}
2886
+ />
2887
+ );
2839
2888
  case "human-input":
2840
2889
  return <HumanInputConversationRow item={item} />;
2841
2890
  case "agent-message":
2842
- return <AgentMessageRow item={item} renderMessageText={renderMessageText} />;
2891
+ return (
2892
+ <AgentMessageRow
2893
+ item={item}
2894
+ renderMessageActions={renderMessageActions}
2895
+ renderMessageText={renderMessageText}
2896
+ />
2897
+ );
2843
2898
  case "worker-completion":
2844
2899
  return <WorkerCompletionRow item={item} onOpenSession={onOpenSession} />;
2845
2900
  case "session-status":
@@ -2965,9 +3020,11 @@ function MessageFooterTime({ occurredAt }: { occurredAt: string }) {
2965
3020
 
2966
3021
  function UserMessageRow({
2967
3022
  item,
3023
+ renderMessageActions,
2968
3024
  renderMessageText,
2969
3025
  }: {
2970
3026
  item: UserMessageItem;
3027
+ renderMessageActions?: ((item: AgentMessageItem | UserMessageItem) => ReactNode) | undefined;
2971
3028
  renderMessageText?:
2972
3029
  | ((text: string, item: AgentMessageItem | UserMessageItem) => ReactNode)
2973
3030
  | undefined;
@@ -2986,7 +3043,12 @@ function UserMessageRow({
2986
3043
  }
2987
3044
  label="Copy message"
2988
3045
  className="w-fit max-w-full min-w-0"
2989
- trailing={<MessageFooterTime occurredAt={item.occurredAt} />}
3046
+ trailing={
3047
+ <>
3048
+ {renderMessageActions?.(item)}
3049
+ <MessageFooterTime occurredAt={item.occurredAt} />
3050
+ </>
3051
+ }
2990
3052
  >
2991
3053
  <div className={MESSAGE_BUBBLE_CLASS}>
2992
3054
  {item.text ? (
@@ -3001,7 +3063,7 @@ function UserMessageRow({
3001
3063
  </div>
3002
3064
  ) : null}
3003
3065
  {(item.annotations?.length ?? 0) > 0 ? (
3004
- <TimelineAnnotationsChip
3066
+ <TimelineAnnotationCards
3005
3067
  annotations={item.annotations ?? []}
3006
3068
  className={item.text ? "mt-2" : undefined}
3007
3069
  />
@@ -3177,9 +3239,11 @@ function humanInputConversationCopyText(item: HumanInputItem, settledLabel: stri
3177
3239
 
3178
3240
  function AgentMessageRow({
3179
3241
  item,
3242
+ renderMessageActions,
3180
3243
  renderMessageText,
3181
3244
  }: {
3182
3245
  item: AgentMessageItem;
3246
+ renderMessageActions?: ((item: AgentMessageItem | UserMessageItem) => ReactNode) | undefined;
3183
3247
  renderMessageText?:
3184
3248
  | ((text: string, item: AgentMessageItem | UserMessageItem) => ReactNode)
3185
3249
  | undefined;
@@ -3195,17 +3259,22 @@ function AgentMessageRow({
3195
3259
  // While streaming, copy is still useful (current text) but keep chrome calm —
3196
3260
  // stamp only after the message finishes (occurredAt tracks completion).
3197
3261
  return (
3198
- <div data-og-annotation-source-key={item.annotationSource?.eventId}>
3199
- <CopyHoverFrame
3200
- copyText={item.text}
3201
- label="Copy message"
3202
- align="start"
3203
- className={cn(enter && "animate-og-enter", "min-w-0 text-og-md leading-7 text-og-fg")}
3204
- trailing={item.streaming ? null : <MessageFooterTime occurredAt={item.occurredAt} />}
3205
- >
3206
- {body}
3207
- </CopyHoverFrame>
3208
- </div>
3262
+ <CopyHoverFrame
3263
+ copyText={item.text}
3264
+ label="Copy message"
3265
+ align="start"
3266
+ className={cn(enter && "animate-og-enter", "min-w-0 text-og-md leading-7 text-og-fg")}
3267
+ trailing={
3268
+ item.streaming ? null : (
3269
+ <>
3270
+ {renderMessageActions?.(item)}
3271
+ <MessageFooterTime occurredAt={item.occurredAt} />
3272
+ </>
3273
+ )
3274
+ }
3275
+ >
3276
+ <div data-og-annotation-source-key={item.annotationSource?.eventId}>{body}</div>
3277
+ </CopyHoverFrame>
3209
3278
  );
3210
3279
  }
3211
3280
 
@@ -3642,6 +3711,32 @@ function formatVideoDuration(seconds: number): string {
3642
3711
 
3643
3712
  function NoticeRow({ item }: { item: NoticeItem }) {
3644
3713
  const enter = useEntranceAnimation();
3714
+ if (item.recordedOutcome) {
3715
+ return (
3716
+ <details
3717
+ className="group text-og-sm text-og-fg-muted"
3718
+ role="note"
3719
+ data-og-recorded-outcome="wait"
3720
+ >
3721
+ <summary className="flex cursor-pointer list-none items-center gap-2 py-1 [&::-webkit-details-marker]:hidden">
3722
+ <ChevronRightIcon
3723
+ aria-hidden
3724
+ className="size-3.5 transition-transform group-open:rotate-90"
3725
+ />
3726
+ <span>
3727
+ Wait recorded ·{" "}
3728
+ <time dateTime={item.occurredAt}>
3729
+ {new Date(item.occurredAt).toLocaleString(undefined, {
3730
+ dateStyle: "medium",
3731
+ timeStyle: "short",
3732
+ })}
3733
+ </time>
3734
+ </span>
3735
+ </summary>
3736
+ <p className="mt-1 whitespace-pre-wrap break-words pl-5 text-og-fg-muted">{item.text}</p>
3737
+ </details>
3738
+ );
3739
+ }
3645
3740
  const tone =
3646
3741
  item.tone === "failed"
3647
3742
  ? "border-og-status-failed/35 bg-og-status-failed/10 text-og-status-failed"