@opengeni/react 3.8.0-canary.0 → 3.8.0-canary.1

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 (72) hide show
  1. package/README.md +6 -0
  2. package/dist/chat.js +1 -1
  3. package/dist/chunk-5M6VTFJ5.js +171 -0
  4. package/dist/chunk-5M6VTFJ5.js.map +1 -0
  5. package/dist/{chunk-IOLVN74D.js → chunk-6J56T3MG.js} +15 -1
  6. package/dist/chunk-6J56T3MG.js.map +1 -0
  7. package/dist/{chunk-BQGIXRKV.js → chunk-7BWFU6H4.js} +22 -4
  8. package/dist/chunk-7BWFU6H4.js.map +1 -0
  9. package/dist/{chunk-3HIF5IFC.js → chunk-7W6PVVTK.js} +63 -18
  10. package/dist/chunk-7W6PVVTK.js.map +1 -0
  11. package/dist/{chunk-ZFOFY5ST.js → chunk-NLTS7JHI.js} +16 -31
  12. package/dist/chunk-NLTS7JHI.js.map +1 -0
  13. package/dist/chunk-NOSYBS2D.js +208 -0
  14. package/dist/chunk-NOSYBS2D.js.map +1 -0
  15. package/dist/{chunk-F2CFHN3Y.js → chunk-YAIPFO5D.js} +2 -1
  16. package/dist/chunk-YAIPFO5D.js.map +1 -0
  17. package/dist/chunk-YBM6CQRU.js +292 -0
  18. package/dist/chunk-YBM6CQRU.js.map +1 -0
  19. package/dist/chunk-ZYRGEKWS.js +216 -0
  20. package/dist/chunk-ZYRGEKWS.js.map +1 -0
  21. package/dist/components/composer.d.ts +4 -2
  22. package/dist/components/message-timeline.d.ts +9 -2
  23. package/dist/components/timeline-annotation-chrome.d.ts +25 -0
  24. package/dist/components/timeline-annotation-layout.d.ts +42 -0
  25. package/dist/components/timeline-annotation-markers.d.ts +8 -0
  26. package/dist/components/timeline-annotation-reveal-context.d.ts +4 -0
  27. package/dist/components/timeline-annotation-shared.d.ts +31 -0
  28. package/dist/components/timeline-annotations-dialog.d.ts +1 -1
  29. package/dist/components/timeline-annotations.d.ts +8 -2
  30. package/dist/composer.js +2 -2
  31. package/dist/hooks/use-composer.d.ts +2 -0
  32. package/dist/index.js +9 -6
  33. package/dist/index.js.map +1 -1
  34. package/dist/interaction.js +6 -6
  35. package/dist/machines.js +1 -1
  36. package/dist/{queue-surface-implementation-XL2A6PBM.js → queue-surface-implementation-LK3S4BIW.js} +5 -2
  37. package/dist/{queue-surface-implementation-XL2A6PBM.js.map → queue-surface-implementation-LK3S4BIW.js.map} +1 -1
  38. package/dist/session-ui.js +8 -5
  39. package/dist/session.js +1 -1
  40. package/dist/timeline-annotation-markers-DRSTLEM6.js +165 -0
  41. package/dist/timeline-annotation-markers-DRSTLEM6.js.map +1 -0
  42. package/dist/timeline-annotation-selection-IHLUGVND.js +236 -0
  43. package/dist/timeline-annotation-selection-IHLUGVND.js.map +1 -0
  44. package/dist/timeline-annotations-dialog-6Q53N5FG.js +262 -0
  45. package/dist/timeline-annotations-dialog-6Q53N5FG.js.map +1 -0
  46. package/package.json +2 -2
  47. package/src/components/chat-composer.tsx +5 -2
  48. package/src/components/composer.tsx +40 -5
  49. package/src/components/copy-button.tsx +1 -0
  50. package/src/components/machines-dashboard.tsx +1 -16
  51. package/src/components/message-timeline.tsx +357 -297
  52. package/src/components/timeline-annotation-chrome.tsx +183 -0
  53. package/src/components/timeline-annotation-layout.ts +268 -0
  54. package/src/components/timeline-annotation-markers.tsx +185 -0
  55. package/src/components/timeline-annotation-reveal-context.ts +9 -0
  56. package/src/components/timeline-annotation-selection.tsx +99 -54
  57. package/src/components/timeline-annotation-shared.ts +245 -0
  58. package/src/components/timeline-annotations-dialog.tsx +172 -128
  59. package/src/components/timeline-annotations.tsx +178 -45
  60. package/src/hooks/use-composer.ts +17 -0
  61. package/styles/compiled.css +86 -22
  62. package/dist/chunk-3HIF5IFC.js.map +0 -1
  63. package/dist/chunk-BQGIXRKV.js.map +0 -1
  64. package/dist/chunk-F2CFHN3Y.js.map +0 -1
  65. package/dist/chunk-IOLVN74D.js.map +0 -1
  66. package/dist/chunk-W3OUAH6M.js +0 -170
  67. package/dist/chunk-W3OUAH6M.js.map +0 -1
  68. package/dist/chunk-ZFOFY5ST.js.map +0 -1
  69. package/dist/timeline-annotation-selection-3L7LHGG2.js +0 -187
  70. package/dist/timeline-annotation-selection-3L7LHGG2.js.map +0 -1
  71. package/dist/timeline-annotations-dialog-U7EVLR75.js +0 -196
  72. package/dist/timeline-annotations-dialog-U7EVLR75.js.map +0 -1
@@ -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. */
@@ -389,6 +397,7 @@ export function MessageTimeline({
389
397
  events,
390
398
  items,
391
399
  status: _status,
400
+ renderMessageActions,
392
401
  renderMessageText,
393
402
  onOpenSession,
394
403
  onMemoryClick,
@@ -403,6 +412,8 @@ export function MessageTimeline({
403
412
  turnSummary,
404
413
  autoFollow = true,
405
414
  onAnnotate,
415
+ draftAnnotations,
416
+ onDraftAnnotationSelect,
406
417
  hasOlder = false,
407
418
  loadingOlder = false,
408
419
  onLoadOlder,
@@ -889,6 +900,7 @@ export function MessageTimeline({
889
900
  () => ({
890
901
  userMessageDisclosureContext,
891
902
  behavior: {
903
+ renderMessageActions,
892
904
  renderMessageText,
893
905
  onOpenSession,
894
906
  onMemoryClick,
@@ -908,6 +920,7 @@ export function MessageTimeline({
908
920
  onMemoryClick,
909
921
  onOpenSession,
910
922
  onReconnect,
923
+ renderMessageActions,
911
924
  renderMessageText,
912
925
  resolveProviderLogo,
913
926
  toolRegistry,
@@ -1785,303 +1798,316 @@ export function MessageTimeline({
1785
1798
  <TimelineComputeLabelProvider value={computeLabel ?? null}>
1786
1799
  <EntranceAnimationProvider value={false}>
1787
1800
  <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.
1801
+ <TimelineAnnotationSourceRootContext.Provider value={scrollRef}>
1802
+ <div className={cn("og-root relative flex min-h-0 flex-col", className)}>
1803
+ {onAnnotate ? (
1804
+ <Suspense fallback={null}>
1805
+ <TimelineAnnotationSelection
1806
+ rootRef={scrollRef}
1807
+ sources={annotationSources}
1808
+ onAnnotate={onAnnotate}
1809
+ />
1810
+ </Suspense>
1811
+ ) : null}
1812
+ {/* Pinned: anchoring off so the tip-follow camera owns the motion.
1799
1813
  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]")
1814
+ <div
1815
+ ref={scrollRef}
1816
+ data-og-timeline-scroller=""
1817
+ data-og-bottom-follow={autoFollow && pinned && !hasNewer ? "true" : "false"}
1818
+ tabIndex={-1}
1819
+ onScroll={onScroll}
1820
+ onScrollEnd={onScrollEnd}
1821
+ onWheel={onWheel}
1822
+ onTouchStart={(event) => {
1823
+ const touch = event.touches.length === 1 ? event.touches[0] : undefined;
1824
+ touchPositionRef.current = touch
1825
+ ? { x: touch.clientX, y: touch.clientY }
1842
1826
  : 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)
1827
+ }}
1828
+ onTouchMove={(event) => {
1829
+ const touch = event.touches[0];
1830
+ const previous = touchPositionRef.current;
1831
+ if (!touch || !previous || event.touches.length !== 1) {
1832
+ touchPositionRef.current = null;
1833
+ return;
1834
+ }
1835
+ const deltaX = previous.x - touch.clientX;
1836
+ const deltaY = previous.y - touch.clientY;
1837
+ if (Math.max(Math.abs(deltaX), Math.abs(deltaY)) < 4) return;
1838
+ touchPositionRef.current = { x: touch.clientX, y: touch.clientY };
1839
+ onWheel({
1840
+ deltaX,
1841
+ deltaY,
1842
+ target: event.target,
1843
+ currentTarget: event.currentTarget,
1844
+ });
1845
+ }}
1846
+ onTouchEnd={() => {
1847
+ touchPositionRef.current = null;
1848
+ }}
1849
+ onTouchCancel={() => {
1850
+ touchPositionRef.current = null;
1851
+ }}
1852
+ onClickCapture={(event) => {
1853
+ const target =
1854
+ event.target instanceof Element
1855
+ ? event.target.closest("button[aria-expanded]")
1865
1856
  : null;
1857
+ if (target) {
1858
+ releasePinFromReader();
1859
+ disclosureKeepsUnpinnedRef.current = true;
1860
+ }
1866
1861
  }}
1862
+ onPointerDown={onPointerDown}
1863
+ onKeyDown={onKeyDown}
1864
+ style={groups.length > 0 && !revealed ? { visibility: "hidden" } : undefined}
1865
+ className={cn(
1866
+ // tabIndex=-1 is programmatic only — never paint a focus ring on
1867
+ // the whole scroller (click + Shift used to flash a blue outline).
1868
+ "min-h-0 flex-1 overflow-y-auto overscroll-contain px-4 pt-16 pb-6 sm:px-6 outline-hidden",
1869
+ autoFollow && pinned && !hasNewer
1870
+ ? "[overflow-anchor:none]"
1871
+ : "[overflow-anchor:auto]",
1872
+ )}
1867
1873
  >
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…"}
1874
+ <TimelineBeforeLayout
1875
+ capture={() => {
1876
+ readingAnchorRef.current =
1877
+ !pinnedRef.current && scrollRef.current
1878
+ ? captureTimelineAnchor(scrollRef.current)
1879
+ : null;
1880
+ }}
1881
+ >
1882
+ <div className="relative mx-auto flex w-full max-w-3xl flex-col gap-5">
1883
+ <AnimatePresence>
1884
+ {loadingOlder ||
1885
+ loadingOldest ||
1886
+ (hasOlder && onJumpToStart && olderPrefetchArmed) ||
1887
+ underfillRetryReady ? (
1888
+ // Reserved top gutter: controls scroll with history and cannot
1889
+ // cover a disclosure. Visibility never changes content height.
1890
+ <motion.div
1891
+ initial={{ opacity: 0, y: -6 }}
1892
+ animate={{ opacity: 1, y: 0 }}
1893
+ exit={{ opacity: 0, y: -6 }}
1894
+ transition={{ duration: 0.15, ease: "easeOut" }}
1895
+ data-og-loading-older=""
1896
+ aria-live="polite"
1897
+ className="pointer-events-none absolute inset-x-0 -top-11 z-10 flex justify-center"
1898
+ >
1899
+ {loadingOlder || loadingOldest ? (
1900
+ <span className={LOADING_CHIP_CLASS}>
1901
+ <span className="og-shimmer-text">
1902
+ {loadingOldest
1903
+ ? "Jumping to start…"
1904
+ : "Loading earlier activity…"}
1905
+ </span>
1891
1906
  </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;
1907
+ ) : null}
1908
+ {hasOlder &&
1909
+ !loadingOlder &&
1910
+ !loadingOldest &&
1911
+ (underfillRetryReady || onJumpToStart) ? (
1912
+ <button
1913
+ type="button"
1914
+ data-og-retry={underfillRetryReady || undefined}
1915
+ data-og-jump-to-start={!underfillRetryReady || undefined}
1916
+ onClick={() => {
1917
+ const node = scrollRef.current;
1918
+ if (underfillRetryReady) {
1919
+ if (node && underfillRetryReadyRef.current) {
1920
+ // AnimatePresence retains this handler during
1921
+ // exit. Current authorization plus the exact
1922
+ // attempt check prevent stale dispatch.
1923
+ requestOlderIfUnderfilled(node, underfillSettledAttempt!);
1925
1924
  }
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(() => {
1925
+ return;
1926
+ }
1927
+ applyPinned(false);
1928
+ pendingJumpToStartRef.current = true;
1929
+ const seq = ++jumpToStartSeqRef.current;
1930
+ void Promise.resolve(onJumpToStart!()).then(
1931
+ () => {
1932
+ // The commit that swaps in the oldest window consumes
1933
+ // the flag against the new DOM; this write covers the
1934
+ // already-committed order and the no-window-change
1935
+ // case (jumping within the current window).
1936
+ const scroller = scrollRef.current ?? node;
1937
+ if (scroller) {
1938
+ scroller.scrollTop = 0;
1939
+ }
1940
+ // A host may resolve without ever changing the
1941
+ // window (already on the oldest page). Any swap
1942
+ // commit runs its layout effect before the next
1943
+ // frame, so a flag still armed by then is the
1944
+ // no-change case — clear it, or a LATER prepend
1945
+ // would spuriously jump the reader to the top.
1946
+ requestFrame(() => {
1947
+ if (jumpToStartSeqRef.current === seq) {
1948
+ pendingJumpToStartRef.current = false;
1949
+ }
1950
+ });
1951
+ },
1952
+ () => {
1933
1953
  if (jumpToStartSeqRef.current === seq) {
1934
1954
  pendingJumpToStartRef.current = false;
1935
1955
  }
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>
1956
+ },
1957
+ );
1958
+ }}
1959
+ className="pointer-events-auto rounded-full border border-og-border px-3 py-1.5 text-og-control"
1960
+ >
1961
+ {underfillRetryReady
1962
+ ? "Retry earlier activity"
1963
+ : "Jump to start"}
1964
+ </button>
1965
+ ) : null}
1966
+ </motion.div>
1967
+ ) : null}
1968
+ </AnimatePresence>
1969
+ {!groups.length
1970
+ ? (emptyState ?? (
1971
+ <p className="py-10 text-center text-og-menu text-og-fg-subtle">
1972
+ No activity yet.
1973
+ </p>
1974
+ ))
1975
+ : null}
1976
+ {hasOlder && olderPrefetchArmed ? (
1977
+ // Overlaid, not a layout row: mounting/unmounting the sentinel
1978
+ // must never shift content (that shift was itself a wobble).
1979
+ // End at the scroll origin above the pt-16 gutter so the
1980
+ // observer and scrollTop cooldown share the same 400px band.
1981
+ <div
1982
+ ref={topSentinelRef}
1983
+ data-og-top-sentinel=""
1984
+ data-og-timeline-chrome=""
1985
+ aria-hidden="true"
1986
+ className="pointer-events-none absolute inset-x-0 -top-16 h-px -translate-y-full"
1987
+ />
1951
1988
  ) : 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}
1989
+ {groups.map(({ group, key, entranceEnabled }, index) => {
1990
+ return (
1991
+ <TimelineGroupEntry
1992
+ key={key}
1993
+ groupKey={key}
1994
+ group={group}
1995
+ nextGroup={groups[index + 1]?.group}
1996
+ entranceEnabled={entranceEnabled}
1997
+ liveEntranceEnabled={
1998
+ group.kind === "activity" ? !bulkRender : undefined
1999
+ }
2000
+ context={timelineGroupEntryContext}
2001
+ />
2002
+ );
2003
+ })}
2004
+ {groups.length > 0 && trailingState ? (
2005
+ <div data-og-timeline-trailing-state="">{trailingState}</div>
2006
+ ) : null}
2007
+ {hasNewer ? (
2008
+ <div
2009
+ ref={bottomSentinelRef}
2010
+ data-og-bottom-sentinel=""
2011
+ data-og-timeline-chrome=""
2012
+ aria-hidden="true"
2013
+ className="h-px w-full"
1985
2014
  />
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>
2015
+ ) : null}
2016
+ </div>
2017
+ </TimelineBeforeLayout>
2018
+ </div>
2019
+ {draftAnnotations && draftAnnotations.length > 0 ? (
2020
+ <Suspense fallback={null}>
2021
+ <TimelineAnnotationMarkers
2022
+ rootRef={scrollRef}
2023
+ annotations={draftAnnotations}
2024
+ onSelect={onDraftAnnotationSelect}
2025
+ />
2026
+ </Suspense>
2019
2027
  ) : 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) {
2028
+
2029
+ <AnimatePresence>
2030
+ {loadingNewer ? (
2031
+ <motion.div
2032
+ initial={{ opacity: 0, y: 6 }}
2033
+ animate={{ opacity: 1, y: 0 }}
2034
+ exit={{ opacity: 0, y: 6 }}
2035
+ transition={{ duration: 0.15, ease: "easeOut" }}
2036
+ data-og-loading-newer=""
2037
+ aria-live="polite"
2038
+ className="pointer-events-none absolute inset-x-0 bottom-14 z-10 flex justify-center"
2039
+ >
2040
+ <span className={LOADING_CHIP_CLASS}>
2041
+ <span className="og-shimmer-text">Loading later activity…</span>
2042
+ </span>
2043
+ </motion.div>
2044
+ ) : null}
2045
+ </AnimatePresence>
2046
+ <AnimatePresence>
2047
+ {((!pinned && autoFollow) || hasNewer || canSkipTipCatchup) && autoFollow ? (
2048
+ <motion.button
2049
+ type="button"
2050
+ data-og-jump-to-latest=""
2051
+ initial={{ opacity: 0, y: 8 }}
2052
+ animate={{ opacity: 1, y: 0 }}
2053
+ exit={{ opacity: 0, y: 8 }}
2054
+ transition={{ duration: 0.15, ease: "easeOut" }}
2055
+ onClick={() => {
2056
+ disclosureKeepsUnpinnedRef.current = false;
2057
+ if (hasNewer) {
2058
+ // Do not pin against the current history page — its bottom
2059
+ // is not the tip. The pin + snap run when the tip window
2060
+ // actually lands (`hasNewer` flips false).
2061
+ wantPinRef.current = true;
2062
+ const node = scrollRef.current;
2063
+ if (onJumpToLatest) {
2064
+ void Promise.resolve(onJumpToLatest()).then(
2065
+ () => {
2066
+ // Covers a host that flipped hasNewer before
2067
+ // resolving; otherwise the tip-window commit
2068
+ // consumes the flag.
2069
+ const current = scrollRef.current;
2070
+ if (current && wantPinRef.current && !hasNewerRef.current) {
2071
+ wantPinRef.current = false;
2072
+ applyPinned(true);
2073
+ snapToBottom(current);
2074
+ }
2075
+ },
2076
+ () => {
2077
+ // The tip reload failed (ordinary network error):
2078
+ // an armed latch would fire a surprise snap when
2079
+ // the reader later pages to the tip themselves.
2046
2080
  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.
2081
+ },
2082
+ );
2083
+ } else if (node) {
2084
+ // No tip reload available: jump within the in-memory
2085
+ // window so the newer sentinel can page forward; the
2086
+ // latch pins if the tip window eventually lands.
2087
+ snapToBottom(node);
2088
+ }
2089
+ return;
2090
+ }
2091
+ const node = scrollRef.current;
2092
+ if (node) {
2093
+ applyPinned(true);
2062
2094
  snapToBottom(node);
2063
2095
  }
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>
2096
+ }}
2097
+ className={cn(
2098
+ "absolute inset-x-0 bottom-4 mx-auto w-fit",
2099
+ "inline-flex items-center gap-1.5 rounded-full border border-og-border bg-og-surface-3/90 px-3 py-1.5",
2100
+ "text-og-control font-medium text-og-fg shadow-og-md backdrop-blur",
2101
+ "hover:border-og-border-strong",
2102
+ )}
2103
+ >
2104
+ <ArrowDownIcon className="size-3.5" />
2105
+ Jump to latest
2106
+ </motion.button>
2107
+ ) : null}
2108
+ </AnimatePresence>
2109
+ </div>
2110
+ </TimelineAnnotationSourceRootContext.Provider>
2085
2111
  </TooltipProvider>
2086
2112
  </EntranceAnimationProvider>
2087
2113
  </TimelineComputeLabelProvider>
@@ -2269,6 +2295,7 @@ type TimelineGroupEntryContext = {
2269
2295
  };
2270
2296
 
2271
2297
  type TimelineGroupBehaviorProps = {
2298
+ renderMessageActions: MessageTimelineProps["renderMessageActions"];
2272
2299
  renderMessageText: MessageTimelineProps["renderMessageText"];
2273
2300
  onOpenSession: MessageTimelineProps["onOpenSession"];
2274
2301
  onMemoryClick: MessageTimelineProps["onMemoryClick"];
@@ -2332,6 +2359,7 @@ const TimelineGroupEntry = memo(function TimelineGroupEntry({
2332
2359
  // streaming and host updates still invalidate immediately.
2333
2360
  const TimelineGroupView = memo(function TimelineGroupView({
2334
2361
  group,
2362
+ renderMessageActions,
2335
2363
  renderMessageText,
2336
2364
  onOpenSession,
2337
2365
  onMemoryClick,
@@ -2349,6 +2377,7 @@ const TimelineGroupView = memo(function TimelineGroupView({
2349
2377
  contextCompactionCount,
2350
2378
  }: {
2351
2379
  group: TimelineGroup;
2380
+ renderMessageActions?: ((item: AgentMessageItem | UserMessageItem) => ReactNode) | undefined;
2352
2381
  renderMessageText?:
2353
2382
  | ((text: string, item: AgentMessageItem | UserMessageItem) => ReactNode)
2354
2383
  | undefined;
@@ -2507,6 +2536,7 @@ const TimelineGroupView = memo(function TimelineGroupView({
2507
2536
  key={key}
2508
2537
  resetKeys={[
2509
2538
  child,
2539
+ renderMessageActions,
2510
2540
  renderMessageText,
2511
2541
  onOpenSession,
2512
2542
  onMemoryClick,
@@ -2521,6 +2551,7 @@ const TimelineGroupView = memo(function TimelineGroupView({
2521
2551
  >
2522
2552
  <TimelineGroupView
2523
2553
  group={child}
2554
+ renderMessageActions={renderMessageActions}
2524
2555
  renderMessageText={renderMessageText}
2525
2556
  onOpenSession={onOpenSession}
2526
2557
  onMemoryClick={onMemoryClick}
@@ -2567,6 +2598,7 @@ const TimelineGroupView = memo(function TimelineGroupView({
2567
2598
  return (
2568
2599
  <TimelineRow
2569
2600
  item={group.item}
2601
+ renderMessageActions={renderMessageActions}
2570
2602
  renderMessageText={renderMessageText}
2571
2603
  onReconnect={onReconnect}
2572
2604
  resolveProviderLogo={resolveProviderLogo}
@@ -2818,6 +2850,7 @@ function durationBetween(startedAt: string, endedAt: string): number | undefined
2818
2850
  */
2819
2851
  export function TimelineRow({
2820
2852
  item,
2853
+ renderMessageActions,
2821
2854
  renderMessageText,
2822
2855
  onReconnect,
2823
2856
  resolveProviderLogo,
@@ -2825,6 +2858,7 @@ export function TimelineRow({
2825
2858
  loadVideoArtifactPlayback,
2826
2859
  }: {
2827
2860
  item: TimelineItem;
2861
+ renderMessageActions?: ((item: AgentMessageItem | UserMessageItem) => ReactNode) | undefined;
2828
2862
  renderMessageText?:
2829
2863
  | ((text: string, item: AgentMessageItem | UserMessageItem) => ReactNode)
2830
2864
  | undefined;
@@ -2835,11 +2869,23 @@ export function TimelineRow({
2835
2869
  }) {
2836
2870
  switch (item.kind) {
2837
2871
  case "user-message":
2838
- return <UserMessageRow item={item} renderMessageText={renderMessageText} />;
2872
+ return (
2873
+ <UserMessageRow
2874
+ item={item}
2875
+ renderMessageActions={renderMessageActions}
2876
+ renderMessageText={renderMessageText}
2877
+ />
2878
+ );
2839
2879
  case "human-input":
2840
2880
  return <HumanInputConversationRow item={item} />;
2841
2881
  case "agent-message":
2842
- return <AgentMessageRow item={item} renderMessageText={renderMessageText} />;
2882
+ return (
2883
+ <AgentMessageRow
2884
+ item={item}
2885
+ renderMessageActions={renderMessageActions}
2886
+ renderMessageText={renderMessageText}
2887
+ />
2888
+ );
2843
2889
  case "worker-completion":
2844
2890
  return <WorkerCompletionRow item={item} onOpenSession={onOpenSession} />;
2845
2891
  case "session-status":
@@ -2965,9 +3011,11 @@ function MessageFooterTime({ occurredAt }: { occurredAt: string }) {
2965
3011
 
2966
3012
  function UserMessageRow({
2967
3013
  item,
3014
+ renderMessageActions,
2968
3015
  renderMessageText,
2969
3016
  }: {
2970
3017
  item: UserMessageItem;
3018
+ renderMessageActions?: ((item: AgentMessageItem | UserMessageItem) => ReactNode) | undefined;
2971
3019
  renderMessageText?:
2972
3020
  | ((text: string, item: AgentMessageItem | UserMessageItem) => ReactNode)
2973
3021
  | undefined;
@@ -2986,7 +3034,12 @@ function UserMessageRow({
2986
3034
  }
2987
3035
  label="Copy message"
2988
3036
  className="w-fit max-w-full min-w-0"
2989
- trailing={<MessageFooterTime occurredAt={item.occurredAt} />}
3037
+ trailing={
3038
+ <>
3039
+ {renderMessageActions?.(item)}
3040
+ <MessageFooterTime occurredAt={item.occurredAt} />
3041
+ </>
3042
+ }
2990
3043
  >
2991
3044
  <div className={MESSAGE_BUBBLE_CLASS}>
2992
3045
  {item.text ? (
@@ -3001,7 +3054,7 @@ function UserMessageRow({
3001
3054
  </div>
3002
3055
  ) : null}
3003
3056
  {(item.annotations?.length ?? 0) > 0 ? (
3004
- <TimelineAnnotationsChip
3057
+ <TimelineAnnotationCards
3005
3058
  annotations={item.annotations ?? []}
3006
3059
  className={item.text ? "mt-2" : undefined}
3007
3060
  />
@@ -3177,9 +3230,11 @@ function humanInputConversationCopyText(item: HumanInputItem, settledLabel: stri
3177
3230
 
3178
3231
  function AgentMessageRow({
3179
3232
  item,
3233
+ renderMessageActions,
3180
3234
  renderMessageText,
3181
3235
  }: {
3182
3236
  item: AgentMessageItem;
3237
+ renderMessageActions?: ((item: AgentMessageItem | UserMessageItem) => ReactNode) | undefined;
3183
3238
  renderMessageText?:
3184
3239
  | ((text: string, item: AgentMessageItem | UserMessageItem) => ReactNode)
3185
3240
  | undefined;
@@ -3195,17 +3250,22 @@ function AgentMessageRow({
3195
3250
  // While streaming, copy is still useful (current text) but keep chrome calm —
3196
3251
  // stamp only after the message finishes (occurredAt tracks completion).
3197
3252
  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>
3253
+ <CopyHoverFrame
3254
+ copyText={item.text}
3255
+ label="Copy message"
3256
+ align="start"
3257
+ className={cn(enter && "animate-og-enter", "min-w-0 text-og-md leading-7 text-og-fg")}
3258
+ trailing={
3259
+ item.streaming ? null : (
3260
+ <>
3261
+ {renderMessageActions?.(item)}
3262
+ <MessageFooterTime occurredAt={item.occurredAt} />
3263
+ </>
3264
+ )
3265
+ }
3266
+ >
3267
+ <div data-og-annotation-source-key={item.annotationSource?.eventId}>{body}</div>
3268
+ </CopyHoverFrame>
3209
3269
  );
3210
3270
  }
3211
3271