@opengeni/react 7.4.0-canary.36855269964001 → 7.4.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 (157) hide show
  1. package/README.md +3 -48
  2. package/dist/artifacts-document.js +2 -3
  3. package/dist/artifacts-presentation.js +2 -3
  4. package/dist/artifacts-spreadsheet.js +2 -2
  5. package/dist/artifacts.d.ts +0 -1
  6. package/dist/artifacts.js +1402 -505
  7. package/dist/artifacts.js.map +1 -1
  8. package/dist/{browser-viewer-R6KK5VKR.js → browser-viewer-2E6R2Y4W.js} +4 -4
  9. package/dist/{chunk-WUISZ3WU.js → chunk-3E2AITTZ.js} +28 -32
  10. package/dist/{chunk-WUISZ3WU.js.map → chunk-3E2AITTZ.js.map} +1 -1
  11. package/dist/{chunk-PWUILT2U.js → chunk-5AZSK6MV.js} +2 -2
  12. package/dist/{chunk-Y2PRMYLC.js → chunk-DUEBTL3N.js} +3 -4
  13. package/dist/{chunk-Y2PRMYLC.js.map → chunk-DUEBTL3N.js.map} +1 -1
  14. package/dist/{chunk-7UAMYXJY.js → chunk-KNDIXZN6.js} +18 -25
  15. package/dist/{chunk-7UAMYXJY.js.map → chunk-KNDIXZN6.js.map} +1 -1
  16. package/dist/{chunk-P4G6K2E2.js → chunk-L4OSXQ6H.js} +884 -1119
  17. package/dist/chunk-L4OSXQ6H.js.map +1 -0
  18. package/dist/{chunk-LLIH7Q4B.js → chunk-S5JQ4R22.js} +19 -47
  19. package/dist/chunk-S5JQ4R22.js.map +1 -0
  20. package/dist/{chunk-A4UJB2MM.js → chunk-SEEVCT36.js} +2 -2
  21. package/dist/{chunk-UXJCQ5GH.js → chunk-UDD6DLLT.js} +1 -6
  22. package/dist/chunk-UDD6DLLT.js.map +1 -0
  23. package/dist/chunk-UGMKPUEM.js +299 -0
  24. package/dist/chunk-UGMKPUEM.js.map +1 -0
  25. package/dist/{chunk-ZCZHKJKZ.js → chunk-USXVQACU.js} +3 -3
  26. package/dist/{chunk-VABBF52G.js → chunk-V2ABIVYJ.js} +6 -70
  27. package/dist/chunk-V2ABIVYJ.js.map +1 -0
  28. package/dist/{chunk-TVO6GXR6.js → chunk-X4HIN6TV.js} +162 -71
  29. package/dist/chunk-X4HIN6TV.js.map +1 -0
  30. package/dist/{chunk-IIDEIBMC.js → chunk-XEJEOAOV.js} +38 -49
  31. package/dist/chunk-XEJEOAOV.js.map +1 -0
  32. package/dist/{chunk-ZKLCPBWN.js → chunk-Y3QTZN2F.js} +5 -5
  33. package/dist/{chunk-73BUNGLF.js → chunk-YZ3YYMOM.js} +1 -1
  34. package/dist/components/artifacts/editable-artifact-ui.d.ts +1 -2
  35. package/dist/components/artifacts/index.d.ts +0 -7
  36. package/dist/components/browser-select-control.d.ts +1 -3
  37. package/dist/components/message-timeline.d.ts +1 -18
  38. package/dist/components/open-geni-links.d.ts +0 -16
  39. package/dist/components/session-conversation.d.ts +1 -20
  40. package/dist/composer.js +3 -3
  41. package/dist/{computer-viewer-DJHPSP2J.js → computer-viewer-WMJE6OA3.js} +3 -3
  42. package/dist/connect.js +6 -7
  43. package/dist/connect.js.map +1 -1
  44. package/dist/hooks/latest-question.d.ts +1 -1
  45. package/dist/index.d.ts +2 -5
  46. package/dist/index.js +28 -39
  47. package/dist/index.js.map +1 -1
  48. package/dist/interaction.js +4 -4
  49. package/dist/{latest-question-KGCDK3XX.js → latest-question-QB4EUCW5.js} +3 -3
  50. package/dist/lib/format.d.ts +0 -3
  51. package/dist/machines.js +2 -2
  52. package/dist/{markdown-table-layout-QXSLK4BY.js → markdown-table-layout-PFUEZ2U4.js} +5 -37
  53. package/dist/markdown-table-layout-PFUEZ2U4.js.map +1 -0
  54. package/dist/{sandbox-files-4PMXDSDV.js → sandbox-files-PXPIZAFG.js} +2 -2
  55. package/dist/session-ui.d.ts +2 -5
  56. package/dist/session-ui.js +13 -23
  57. package/dist/session-ui.js.map +1 -1
  58. package/dist/session.js +3 -3
  59. package/dist/sites.js +289 -4
  60. package/dist/sites.js.map +1 -1
  61. package/dist/timeline/types.d.ts +1 -7
  62. package/dist/{timeline-annotation-selection-DMLMM2TX.js → timeline-annotation-selection-ODLVF4OI.js} +1 -6
  63. package/dist/timeline-annotation-selection-ODLVF4OI.js.map +1 -0
  64. package/package.json +3 -7
  65. package/src/artifacts.ts +0 -37
  66. package/src/components/artifacts/editable-artifact-ui.tsx +1 -3
  67. package/src/components/artifacts/editable-artifact-workbench.tsx +2 -5
  68. package/src/components/artifacts/editable-document.tsx +36 -34
  69. package/src/components/artifacts/editable-presentation.tsx +16 -21
  70. package/src/components/artifacts/index.ts +0 -41
  71. package/src/components/browser-select-control.tsx +1 -4
  72. package/src/components/browser-viewer.tsx +22 -81
  73. package/src/components/computer-viewer.tsx +23 -51
  74. package/src/components/markdown-table-layout.ts +5 -49
  75. package/src/components/markdown.tsx +2 -5
  76. package/src/components/message-timeline.tsx +434 -515
  77. package/src/components/open-geni-links.tsx +0 -24
  78. package/src/components/session-conversation.tsx +20 -131
  79. package/src/components/timeline-annotation-selection.tsx +0 -7
  80. package/src/hooks/use-available-models.ts +1 -1
  81. package/src/index.ts +0 -9
  82. package/src/lib/format.ts +0 -13
  83. package/src/session-ui.ts +0 -9
  84. package/src/timeline/projection.ts +4 -78
  85. package/src/timeline/types.ts +0 -7
  86. package/styles/compiled.css +0 -325
  87. package/dist/chat-interactive-block-VAGABN7J.js +0 -11
  88. package/dist/chunk-5CVXZA3Y.js +0 -292
  89. package/dist/chunk-5CVXZA3Y.js.map +0 -1
  90. package/dist/chunk-B3V6TG77.js +0 -41
  91. package/dist/chunk-B3V6TG77.js.map +0 -1
  92. package/dist/chunk-IIDEIBMC.js.map +0 -1
  93. package/dist/chunk-LLIH7Q4B.js.map +0 -1
  94. package/dist/chunk-P4G6K2E2.js.map +0 -1
  95. package/dist/chunk-RAIQYITC.js +0 -1772
  96. package/dist/chunk-RAIQYITC.js.map +0 -1
  97. package/dist/chunk-SCOTPE2I.js +0 -589
  98. package/dist/chunk-SCOTPE2I.js.map +0 -1
  99. package/dist/chunk-TVO6GXR6.js.map +0 -1
  100. package/dist/chunk-UXJCQ5GH.js.map +0 -1
  101. package/dist/chunk-VABBF52G.js.map +0 -1
  102. package/dist/chunk-WEHMPOXK.js +0 -39
  103. package/dist/chunk-WEHMPOXK.js.map +0 -1
  104. package/dist/chunk-XCHWDJ22.js +0 -107
  105. package/dist/chunk-XCHWDJ22.js.map +0 -1
  106. package/dist/components/artifacts/artifact-chrome.d.ts +0 -120
  107. package/dist/components/artifacts/artifact-sandbox.d.ts +0 -27
  108. package/dist/components/artifacts/chat-interactive-block.d.ts +0 -29
  109. package/dist/components/artifacts/deferred-chat-media.d.ts +0 -15
  110. package/dist/components/artifacts/editable-artifact-view.d.ts +0 -46
  111. package/dist/components/artifacts/latest-task-runner.d.ts +0 -15
  112. package/dist/components/artifacts/session-artifact-viewer.d.ts +0 -39
  113. package/dist/components/artifacts/site-view.d.ts +0 -23
  114. package/dist/components/timeline-pill-geometry.d.ts +0 -6
  115. package/dist/components/timeline-pill-placement.d.ts +0 -18
  116. package/dist/components/usage-limit-notice.d.ts +0 -49
  117. package/dist/components/usage-member-list.d.ts +0 -91
  118. package/dist/components/usage-meter.d.ts +0 -73
  119. package/dist/hooks/use-usage.d.ts +0 -40
  120. package/dist/markdown-table-layout-QXSLK4BY.js.map +0 -1
  121. package/dist/sandbox-files-4PMXDSDV.js.map +0 -1
  122. package/dist/timeline/allowance-exhausted-row.d.ts +0 -24
  123. package/dist/timeline-annotation-selection-DMLMM2TX.js.map +0 -1
  124. package/dist/timeline-pill-geometry-2G6EK47V.js +0 -73
  125. package/dist/timeline-pill-geometry-2G6EK47V.js.map +0 -1
  126. package/dist/usage/allowance-copy.d.ts +0 -25
  127. package/dist/usage/summary.d.ts +0 -35
  128. package/dist/usage.d.ts +0 -13
  129. package/dist/usage.js +0 -1073
  130. package/dist/usage.js.map +0 -1
  131. package/src/components/artifacts/artifact-chrome.tsx +0 -413
  132. package/src/components/artifacts/artifact-sandbox.tsx +0 -185
  133. package/src/components/artifacts/chat-interactive-block.tsx +0 -260
  134. package/src/components/artifacts/deferred-chat-media.tsx +0 -67
  135. package/src/components/artifacts/editable-artifact-view.tsx +0 -187
  136. package/src/components/artifacts/latest-task-runner.ts +0 -49
  137. package/src/components/artifacts/session-artifact-viewer.tsx +0 -376
  138. package/src/components/artifacts/site-view.tsx +0 -116
  139. package/src/components/timeline-pill-geometry.ts +0 -112
  140. package/src/components/timeline-pill-placement.ts +0 -81
  141. package/src/components/usage-limit-notice.tsx +0 -179
  142. package/src/components/usage-member-list.tsx +0 -792
  143. package/src/components/usage-meter.tsx +0 -382
  144. package/src/hooks/use-usage.ts +0 -100
  145. package/src/timeline/allowance-exhausted-row.tsx +0 -86
  146. package/src/usage/allowance-copy.ts +0 -64
  147. package/src/usage/summary.ts +0 -93
  148. package/src/usage.ts +0 -50
  149. /package/dist/{browser-viewer-R6KK5VKR.js.map → browser-viewer-2E6R2Y4W.js.map} +0 -0
  150. /package/dist/{chunk-PWUILT2U.js.map → chunk-5AZSK6MV.js.map} +0 -0
  151. /package/dist/{chunk-A4UJB2MM.js.map → chunk-SEEVCT36.js.map} +0 -0
  152. /package/dist/{chunk-ZCZHKJKZ.js.map → chunk-USXVQACU.js.map} +0 -0
  153. /package/dist/{chunk-ZKLCPBWN.js.map → chunk-Y3QTZN2F.js.map} +0 -0
  154. /package/dist/{chunk-73BUNGLF.js.map → chunk-YZ3YYMOM.js.map} +0 -0
  155. /package/dist/{chat-interactive-block-VAGABN7J.js.map → computer-viewer-WMJE6OA3.js.map} +0 -0
  156. /package/dist/{latest-question-KGCDK3XX.js.map → latest-question-QB4EUCW5.js.map} +0 -0
  157. /package/dist/{computer-viewer-DJHPSP2J.js.map → sandbox-files-PXPIZAFG.js.map} +0 -0
@@ -63,7 +63,7 @@ import {
63
63
  type OlderHistoryLoadReceipt,
64
64
  type OlderHistoryLoader,
65
65
  } from "../older-history";
66
- import { Markdown, type MarkdownProps } from "./markdown";
66
+ import { Markdown } from "./markdown";
67
67
  import { OpenGeniLinkProvider, type OpenGeniLinkResolver } from "./open-geni-links";
68
68
  import {
69
69
  TimelineBeforeLayout,
@@ -73,7 +73,6 @@ import {
73
73
  type TimelineAnchor,
74
74
  } from "./timeline-anchor";
75
75
  import { useReadingProgress } from "./timeline-reading-progress";
76
- import { type TimelinePillPlacement, useTimelinePillPlacement } from "./timeline-pill-placement";
77
76
  import {
78
77
  animateTimelineSettlement,
79
78
  captureTimelineSettlement,
@@ -143,12 +142,6 @@ import {
143
142
  import { SESSION_STATUS_META, StatusDot } from "./session-status";
144
143
  import { TimelineComputeLabelProvider } from "../timeline/compute-label";
145
144
  import { EntranceAnimationProvider, useEntranceAnimation } from "../timeline/entrance";
146
- import {
147
- AllowanceExhaustedRow,
148
- AllowancePresentationProvider,
149
- type RenderAllowanceExhausted,
150
- } from "../timeline/allowance-exhausted-row";
151
- import type { AllowanceLabels } from "../usage/allowance-copy";
152
145
  import { SeenActivityIdsProvider } from "../timeline/seen-activity-ids";
153
146
  import { TimelineAnnotationCards } from "./timeline-annotations";
154
147
  import { TooltipProvider } from "./tooltip";
@@ -177,11 +170,6 @@ export type MessageTimelineProps = {
177
170
  context: { searchTarget: TimelineSearchTarget | null },
178
171
  ) => ReactNode)
179
172
  | undefined;
180
- /**
181
- * Render assistant `opengeni-html` / `opengeni-site` fences in the default
182
- * message renderer (`SessionConversation` supplies the inline Site preview).
183
- */
184
- renderInteractiveBlock?: MarkdownProps["renderInteractiveBlock"];
185
173
  /** Drill into a spawned worker session. */
186
174
  onOpenSession?: ((sessionId: string) => void) | undefined;
187
175
  /**
@@ -203,15 +191,6 @@ export type MessageTimelineProps = {
203
191
  onReconnect?: ((item: AuthNeededItem) => void | Promise<void>) | undefined;
204
192
  /** Host-owned inline connection setup. Return undefined to use the default recovery card. */
205
193
  renderAuthNeeded?: ((item: AuthNeededItem) => ReactNode | undefined) | undefined;
206
- /**
207
- * Replace the "usage limit reached" row shown when a workspace or member
208
- * usage allowance stops work (an `allowance_exhausted` refusal). Receives the
209
- * typed refusal (scope, reset time) and the default row; return undefined to
210
- * keep the default. Use it to point people at your own plan or admin page.
211
- */
212
- renderAllowanceExhausted?: RenderAllowanceExhausted | undefined;
213
- /** Replace the words of the default "usage limit reached" row. */
214
- allowanceExhaustedLabels?: Partial<AllowanceLabels> | undefined;
215
194
  /**
216
195
  * Decide which durable authentication notices this timeline presents.
217
196
  * Defaults to showing every notice. Embedded hosts can suppress notices for
@@ -332,20 +311,6 @@ const PIN_THRESHOLD_PX = 48;
332
311
  const JUMP_TO_LATEST_CATCHUP_DEBT_PX = 240;
333
312
  /** Breathing room above the question when following stops at an answer. */
334
313
  const QUESTION_NAV_MARGIN_PX = 12;
335
- // Floating pills rest at these spots and slide sideways, in this order, when
336
- // their resting spot would cover a control (see useTimelinePillPlacement).
337
- const JUMP_PILL_PLACEMENTS: readonly TimelinePillPlacement[] = ["center", "end", "start"];
338
- const QUESTION_PILL_PLACEMENTS: readonly TimelinePillPlacement[] = ["end", "center", "start"];
339
- const JUMP_PILL_PLACEMENT_CLASS: Record<TimelinePillPlacement, string> = {
340
- center: "inset-x-0 mx-auto",
341
- end: "right-4 sm:right-6",
342
- start: "left-4 sm:left-6",
343
- };
344
- const QUESTION_PILL_PLACEMENT_CLASS: Record<TimelinePillPlacement, string> = {
345
- center: "justify-center",
346
- end: "justify-end",
347
- start: "justify-start",
348
- };
349
314
  /**
350
315
  * Prefetch older history when the top sentinel is this far from the viewport.
351
316
  * After a page loads we stay cool until the reader leaves this band (scrolls
@@ -517,7 +482,6 @@ function cssEscapeAttribute(value: string): string {
517
482
  */
518
483
  export function MessageTimeline({
519
484
  resolveLink,
520
- renderInteractiveBlock,
521
485
  userMessageDisclosureLabels,
522
486
  searchTarget,
523
487
  events,
@@ -529,8 +493,6 @@ export function MessageTimeline({
529
493
  onMemoryClick,
530
494
  onReconnect,
531
495
  renderAuthNeeded,
532
- renderAllowanceExhausted,
533
- allowanceExhaustedLabels,
534
496
  shouldRenderAuthNeeded,
535
497
  resolveProviderLogo,
536
498
  toolRegistry = defaultToolRegistry,
@@ -593,9 +555,6 @@ export function MessageTimeline({
593
555
  <Markdown
594
556
  searchTarget={searchItem?.id === item.id ? searchTarget : null}
595
557
  streaming={item.kind === "agent-message" && item.streaming}
596
- renderInteractiveBlock={
597
- item.kind === "agent-message" ? renderInteractiveBlock : undefined
598
- }
599
558
  >
600
559
  {text}
601
560
  </Markdown>
@@ -608,7 +567,7 @@ export function MessageTimeline({
608
567
  body
609
568
  );
610
569
  },
611
- [renderInteractiveBlock, renderMessageText, searchItem?.id, searchTarget],
570
+ [renderMessageText, searchItem?.id, searchTarget],
612
571
  );
613
572
  // Event-window identity is independent of projected rows (partial messages
614
573
  // can acquire a different first-delta id when older text arrives).
@@ -741,21 +700,6 @@ export function MessageTimeline({
741
700
  const resizeFollowRafRef = useRef<number | null>(null);
742
701
  const questionNavFrameRef = useRef<number | null>(null);
743
702
  const [questionNav, setQuestionNav] = useState<QuestionNav | null>(null);
744
- const jumpPillRef = useRef<HTMLButtonElement | null>(null);
745
- const questionPillRef = useRef<HTMLDivElement | null>(null);
746
- const jumpPillShown = autoFollow && (!pinned || hasNewer || canSkipTipCatchup);
747
- const jumpPillPlacement = useTimelinePillPlacement({
748
- pillRef: jumpPillRef,
749
- scrollerRef: scrollRef,
750
- active: jumpPillShown,
751
- preference: JUMP_PILL_PLACEMENTS,
752
- });
753
- const questionPillPlacement = useTimelinePillPlacement({
754
- pillRef: questionPillRef,
755
- scrollerRef: scrollRef,
756
- active: questionNav !== null,
757
- preference: QUESTION_PILL_PLACEMENTS,
758
- });
759
703
  const [questionTarget, setQuestionTarget] = useState<number | null>(null);
760
704
  const [questionPending, setQuestionPending] = useState(false);
761
705
  const [questionError, setQuestionError] = useState<string | null>(null);
@@ -2243,484 +2187,460 @@ export function MessageTimeline({
2243
2187
  };
2244
2188
 
2245
2189
  const timeline = (
2246
- <AllowancePresentationProvider
2247
- render={renderAllowanceExhausted}
2248
- labels={allowanceExhaustedLabels}
2249
- >
2250
- <LightboxProvider>
2251
- <FoldMemoryProvider value={foldMemoryRef.current}>
2252
- <SeenActivityIdsProvider value={seenActivityIdsRef.current}>
2253
- <TimelineComputeLabelProvider value={computeLabel ?? null}>
2254
- <EntranceAnimationProvider value={false}>
2255
- <TooltipProvider delayDuration={400}>
2256
- <TimelineAnnotationSourceRootContext.Provider value={scrollRef}>
2257
- <div
2258
- className={cn("og-root relative flex min-h-0 flex-col", className)}
2259
- // Sticky insets start at the scroller's padded content edge
2260
- // (pt-16). Subtract exactly that padding so an expanded work
2261
- // header pins flush to the scrollport. Pinning it lower left a
2262
- // band of scrolling rows visible above the header, which then
2263
- // looked like it floated over the middle of the timeline. The
2264
- // floating question action sits below this strip instead.
2265
- style={{ "--og-work-header-top": "-4rem" } as CSSProperties}
2266
- >
2267
- {onAnnotate ? (
2268
- <Suspense fallback={null}>
2269
- <TimelineAnnotationSelection
2270
- rootRef={scrollRef}
2271
- sources={annotationSources}
2272
- onAnnotate={onAnnotate}
2273
- />
2274
- </Suspense>
2275
- ) : null}
2276
- {/* Pinned: anchoring off so the tip-follow camera owns the motion.
2190
+ <LightboxProvider>
2191
+ <FoldMemoryProvider value={foldMemoryRef.current}>
2192
+ <SeenActivityIdsProvider value={seenActivityIdsRef.current}>
2193
+ <TimelineComputeLabelProvider value={computeLabel ?? null}>
2194
+ <EntranceAnimationProvider value={false}>
2195
+ <TooltipProvider delayDuration={400}>
2196
+ <TimelineAnnotationSourceRootContext.Provider value={scrollRef}>
2197
+ <div
2198
+ className={cn("og-root relative flex min-h-0 flex-col", className)}
2199
+ // Sticky insets start at the scroller's padded content edge
2200
+ // (pt-16). Subtract exactly that padding so an expanded work
2201
+ // header pins flush to the scrollport. Pinning it lower left a
2202
+ // band of scrolling rows visible above the header, which then
2203
+ // looked like it floated over the middle of the timeline. The
2204
+ // floating question action sits below this strip instead.
2205
+ style={{ "--og-work-header-top": "-4rem" } as CSSProperties}
2206
+ >
2207
+ {onAnnotate ? (
2208
+ <Suspense fallback={null}>
2209
+ <TimelineAnnotationSelection
2210
+ rootRef={scrollRef}
2211
+ sources={annotationSources}
2212
+ onAnnotate={onAnnotate}
2213
+ />
2214
+ </Suspense>
2215
+ ) : null}
2216
+ {/* Pinned: anchoring off so the tip-follow camera owns the motion.
2277
2217
  Unpinned: native scroll anchoring holds the reader's place. */}
2278
- <div
2279
- ref={scrollRef}
2280
- data-og-timeline-scroller=""
2281
- data-og-bottom-follow={autoFollow && pinned && !hasNewer ? "true" : "false"}
2282
- tabIndex={-1}
2283
- onScroll={onScroll}
2284
- onScrollEnd={onScrollEnd}
2285
- onWheel={onWheel}
2286
- onTouchStart={(event) => {
2287
- stopSettlement();
2288
- const touch = event.touches.length === 1 ? event.touches[0] : undefined;
2289
- touchPositionRef.current = touch
2290
- ? { x: touch.clientX, y: touch.clientY }
2291
- : null;
2292
- }}
2293
- onTouchMove={(event) => {
2294
- const touch = event.touches[0];
2295
- const previous = touchPositionRef.current;
2296
- if (!touch || !previous || event.touches.length !== 1) {
2297
- touchPositionRef.current = null;
2298
- return;
2299
- }
2300
- const deltaX = previous.x - touch.clientX;
2301
- const deltaY = previous.y - touch.clientY;
2302
- if (Math.max(Math.abs(deltaX), Math.abs(deltaY)) < 4) return;
2303
- touchPositionRef.current = { x: touch.clientX, y: touch.clientY };
2304
- onWheel({
2305
- deltaX,
2306
- deltaY,
2307
- target: event.target,
2308
- currentTarget: event.currentTarget,
2309
- });
2310
- }}
2311
- onTouchEnd={() => {
2312
- touchPositionRef.current = null;
2313
- }}
2314
- onTouchCancel={() => {
2218
+ <div
2219
+ ref={scrollRef}
2220
+ data-og-timeline-scroller=""
2221
+ data-og-bottom-follow={autoFollow && pinned && !hasNewer ? "true" : "false"}
2222
+ tabIndex={-1}
2223
+ onScroll={onScroll}
2224
+ onScrollEnd={onScrollEnd}
2225
+ onWheel={onWheel}
2226
+ onTouchStart={(event) => {
2227
+ stopSettlement();
2228
+ const touch = event.touches.length === 1 ? event.touches[0] : undefined;
2229
+ touchPositionRef.current = touch
2230
+ ? { x: touch.clientX, y: touch.clientY }
2231
+ : null;
2232
+ }}
2233
+ onTouchMove={(event) => {
2234
+ const touch = event.touches[0];
2235
+ const previous = touchPositionRef.current;
2236
+ if (!touch || !previous || event.touches.length !== 1) {
2315
2237
  touchPositionRef.current = null;
2316
- }}
2317
- onClickCapture={(event) => {
2318
- const target =
2319
- event.target instanceof Element
2320
- ? event.target.closest("button[aria-expanded]")
2321
- : null;
2322
- if (target) {
2323
- if (target.hasAttribute("data-og-work-header")) {
2324
- releaseProgress(
2325
- target
2326
- .closest("[data-og-group-key]")
2327
- ?.getAttribute("data-og-group-key") ?? null,
2328
- );
2329
- }
2330
- releasePinFromReader();
2331
- disclosureKeepsUnpinnedRef.current = true;
2238
+ return;
2239
+ }
2240
+ const deltaX = previous.x - touch.clientX;
2241
+ const deltaY = previous.y - touch.clientY;
2242
+ if (Math.max(Math.abs(deltaX), Math.abs(deltaY)) < 4) return;
2243
+ touchPositionRef.current = { x: touch.clientX, y: touch.clientY };
2244
+ onWheel({
2245
+ deltaX,
2246
+ deltaY,
2247
+ target: event.target,
2248
+ currentTarget: event.currentTarget,
2249
+ });
2250
+ }}
2251
+ onTouchEnd={() => {
2252
+ touchPositionRef.current = null;
2253
+ }}
2254
+ onTouchCancel={() => {
2255
+ touchPositionRef.current = null;
2256
+ }}
2257
+ onClickCapture={(event) => {
2258
+ const target =
2259
+ event.target instanceof Element
2260
+ ? event.target.closest("button[aria-expanded]")
2261
+ : null;
2262
+ if (target) {
2263
+ if (target.hasAttribute("data-og-work-header")) {
2264
+ releaseProgress(
2265
+ target
2266
+ .closest("[data-og-group-key]")
2267
+ ?.getAttribute("data-og-group-key") ?? null,
2268
+ );
2332
2269
  }
2333
- }}
2334
- onFocusCapture={(event) => {
2335
- if (readableTurns && event.target !== event.currentTarget) {
2336
- stopSettlement();
2270
+ releasePinFromReader();
2271
+ disclosureKeepsUnpinnedRef.current = true;
2272
+ }
2273
+ }}
2274
+ onFocusCapture={(event) => {
2275
+ if (readableTurns && event.target !== event.currentTarget) {
2276
+ stopSettlement();
2277
+ releasePinFromReader();
2278
+ disclosureKeepsUnpinnedRef.current = true;
2279
+ }
2280
+ }}
2281
+ onPointerDown={onPointerDown}
2282
+ onKeyDown={onKeyDown}
2283
+ style={groups.length > 0 && !revealed ? { visibility: "hidden" } : undefined}
2284
+ className={cn(
2285
+ // tabIndex=-1 is programmatic only — never paint a focus ring on
2286
+ // the whole scroller (click + Shift used to flash a blue outline).
2287
+ "min-h-0 flex-1 overflow-y-auto overscroll-contain px-4 pt-16 pb-6 sm:px-6 outline-hidden",
2288
+ autoFollow && pinned && !hasNewer
2289
+ ? "[overflow-anchor:none]"
2290
+ : "[overflow-anchor:auto]",
2291
+ )}
2292
+ >
2293
+ <TimelineBeforeLayout
2294
+ capture={() => {
2295
+ if (readableTurns && timelineHasReader(scrollRef.current)) {
2337
2296
  releasePinFromReader();
2338
- disclosureKeepsUnpinnedRef.current = true;
2339
2297
  }
2298
+ readingAnchorRef.current =
2299
+ !pinnedRef.current && scrollRef.current
2300
+ ? captureTimelineAnchor(scrollRef.current)
2301
+ : null;
2302
+ settlementRef.current =
2303
+ readableTurns &&
2304
+ autoFollow &&
2305
+ pinnedRef.current &&
2306
+ revealedRef.current &&
2307
+ !hasNewer &&
2308
+ !pendingReaderLeaveRef.current &&
2309
+ !disclosureKeepsUnpinnedRef.current &&
2310
+ !prefersReducedMotion() &&
2311
+ scrollRef.current
2312
+ ? captureTimelineSettlement(scrollRef.current, groups)
2313
+ : null;
2340
2314
  }}
2341
- onPointerDown={onPointerDown}
2342
- onKeyDown={onKeyDown}
2343
- style={
2344
- groups.length > 0 && !revealed ? { visibility: "hidden" } : undefined
2345
- }
2346
- className={cn(
2347
- // tabIndex=-1 is programmatic only — never paint a focus ring on
2348
- // the whole scroller (click + Shift used to flash a blue outline).
2349
- "min-h-0 flex-1 overflow-y-auto overscroll-contain px-4 pt-16 pb-6 sm:px-6 outline-hidden",
2350
- autoFollow && pinned && !hasNewer
2351
- ? "[overflow-anchor:none]"
2352
- : "[overflow-anchor:auto]",
2353
- )}
2354
2315
  >
2355
- <TimelineBeforeLayout
2356
- capture={() => {
2357
- if (readableTurns && timelineHasReader(scrollRef.current)) {
2358
- releasePinFromReader();
2359
- }
2360
- readingAnchorRef.current =
2361
- !pinnedRef.current && scrollRef.current
2362
- ? captureTimelineAnchor(scrollRef.current)
2363
- : null;
2364
- settlementRef.current =
2365
- readableTurns &&
2366
- autoFollow &&
2367
- pinnedRef.current &&
2368
- revealedRef.current &&
2369
- !hasNewer &&
2370
- !pendingReaderLeaveRef.current &&
2371
- !disclosureKeepsUnpinnedRef.current &&
2372
- !prefersReducedMotion() &&
2373
- scrollRef.current
2374
- ? captureTimelineSettlement(scrollRef.current, groups)
2375
- : null;
2376
- }}
2377
- >
2378
- <div className="relative mx-auto flex w-full max-w-3xl flex-col gap-5">
2379
- <AnimatePresence>
2380
- {loadingOlder ||
2381
- loadingOldest ||
2382
- (hasOlder && onJumpToStart && olderPrefetchArmed) ||
2383
- underfillRetryReady ? (
2384
- // Reserved top gutter: controls scroll with history and cannot
2385
- // cover a disclosure. Visibility never changes content height.
2386
- <motion.div
2387
- initial={{ opacity: 0, y: -6 }}
2388
- animate={{ opacity: 1, y: 0 }}
2389
- exit={{ opacity: 0, y: -6 }}
2390
- transition={{ duration: 0.15, ease: "easeOut" }}
2391
- data-og-loading-older=""
2392
- aria-live="polite"
2393
- className={cn(
2394
- "pointer-events-none absolute inset-x-0 -top-11 z-10 flex",
2395
- // The scrolling history control and fixed question
2396
- // action must never share the same pointer region.
2397
- questionNav
2398
- ? "max-w-[calc(50%-0.5rem)] justify-start"
2399
- : "justify-center",
2400
- )}
2401
- >
2402
- {loadingOlder || loadingOldest ? (
2403
- <span className={LOADING_CHIP_CLASS}>
2404
- <span className="og-shimmer-text">
2405
- {loadingOldest
2406
- ? "Jumping to start…"
2407
- : "Loading earlier activity…"}
2408
- </span>
2316
+ <div className="relative mx-auto flex w-full max-w-3xl flex-col gap-5">
2317
+ <AnimatePresence>
2318
+ {loadingOlder ||
2319
+ loadingOldest ||
2320
+ (hasOlder && onJumpToStart && olderPrefetchArmed) ||
2321
+ underfillRetryReady ? (
2322
+ // Reserved top gutter: controls scroll with history and cannot
2323
+ // cover a disclosure. Visibility never changes content height.
2324
+ <motion.div
2325
+ initial={{ opacity: 0, y: -6 }}
2326
+ animate={{ opacity: 1, y: 0 }}
2327
+ exit={{ opacity: 0, y: -6 }}
2328
+ transition={{ duration: 0.15, ease: "easeOut" }}
2329
+ data-og-loading-older=""
2330
+ aria-live="polite"
2331
+ className={cn(
2332
+ "pointer-events-none absolute inset-x-0 -top-11 z-10 flex",
2333
+ // The scrolling history control and fixed question
2334
+ // action must never share the same pointer region.
2335
+ questionNav
2336
+ ? "max-w-[calc(50%-0.5rem)] justify-start"
2337
+ : "justify-center",
2338
+ )}
2339
+ >
2340
+ {loadingOlder || loadingOldest ? (
2341
+ <span className={LOADING_CHIP_CLASS}>
2342
+ <span className="og-shimmer-text">
2343
+ {loadingOldest
2344
+ ? "Jumping to start…"
2345
+ : "Loading earlier activity…"}
2409
2346
  </span>
2410
- ) : null}
2411
- {hasOlder &&
2412
- !loadingOlder &&
2413
- !loadingOldest &&
2414
- (underfillRetryReady || onJumpToStart) ? (
2415
- <button
2416
- type="button"
2417
- data-og-retry={underfillRetryReady || undefined}
2418
- data-og-jump-to-start={!underfillRetryReady || undefined}
2419
- onClick={() => {
2420
- const node = scrollRef.current;
2421
- if (underfillRetryReady) {
2422
- if (node && underfillRetryReadyRef.current) {
2423
- // AnimatePresence retains this handler during
2424
- // exit. Current authorization plus the exact
2425
- // attempt check prevent stale dispatch.
2426
- requestOlderIfUnderfilled(
2427
- node,
2428
- underfillSettledAttempt!,
2429
- );
2430
- }
2431
- return;
2347
+ </span>
2348
+ ) : null}
2349
+ {hasOlder &&
2350
+ !loadingOlder &&
2351
+ !loadingOldest &&
2352
+ (underfillRetryReady || onJumpToStart) ? (
2353
+ <button
2354
+ type="button"
2355
+ data-og-retry={underfillRetryReady || undefined}
2356
+ data-og-jump-to-start={!underfillRetryReady || undefined}
2357
+ onClick={() => {
2358
+ const node = scrollRef.current;
2359
+ if (underfillRetryReady) {
2360
+ if (node && underfillRetryReadyRef.current) {
2361
+ // AnimatePresence retains this handler during
2362
+ // exit. Current authorization plus the exact
2363
+ // attempt check prevent stale dispatch.
2364
+ requestOlderIfUnderfilled(node, underfillSettledAttempt!);
2432
2365
  }
2433
- applyPinned(false);
2434
- pendingJumpToStartRef.current = true;
2435
- const seq = ++jumpToStartSeqRef.current;
2436
- void Promise.resolve(onJumpToStart!()).then(
2437
- () => {
2438
- // The commit that swaps in the oldest window consumes
2439
- // the flag against the new DOM; this write covers the
2440
- // already-committed order and the no-window-change
2441
- // case (jumping within the current window).
2442
- const scroller = scrollRef.current ?? node;
2443
- if (scroller) {
2444
- scroller.scrollTop = 0;
2445
- }
2446
- // A host may resolve without ever changing the
2447
- // window (already on the oldest page). Any swap
2448
- // commit runs its layout effect before the next
2449
- // frame, so a flag still armed by then is the
2450
- // no-change case — clear it, or a LATER prepend
2451
- // would spuriously jump the reader to the top.
2452
- requestFrame(() => {
2453
- if (jumpToStartSeqRef.current === seq) {
2454
- pendingJumpToStartRef.current = false;
2455
- }
2456
- });
2457
- },
2458
- () => {
2366
+ return;
2367
+ }
2368
+ applyPinned(false);
2369
+ pendingJumpToStartRef.current = true;
2370
+ const seq = ++jumpToStartSeqRef.current;
2371
+ void Promise.resolve(onJumpToStart!()).then(
2372
+ () => {
2373
+ // The commit that swaps in the oldest window consumes
2374
+ // the flag against the new DOM; this write covers the
2375
+ // already-committed order and the no-window-change
2376
+ // case (jumping within the current window).
2377
+ const scroller = scrollRef.current ?? node;
2378
+ if (scroller) {
2379
+ scroller.scrollTop = 0;
2380
+ }
2381
+ // A host may resolve without ever changing the
2382
+ // window (already on the oldest page). Any swap
2383
+ // commit runs its layout effect before the next
2384
+ // frame, so a flag still armed by then is the
2385
+ // no-change case — clear it, or a LATER prepend
2386
+ // would spuriously jump the reader to the top.
2387
+ requestFrame(() => {
2459
2388
  if (jumpToStartSeqRef.current === seq) {
2460
2389
  pendingJumpToStartRef.current = false;
2461
2390
  }
2462
- },
2463
- );
2464
- }}
2465
- className="pointer-events-auto rounded-full border border-og-border px-3 py-1.5 text-og-control"
2466
- >
2467
- {underfillRetryReady
2468
- ? underfillSettledAttempt?.[2]?.tailPreserved
2469
- ? "Load earlier activity"
2470
- : "Retry earlier activity"
2471
- : "Jump to start"}
2472
- </button>
2473
- ) : null}
2474
- </motion.div>
2475
- ) : null}
2476
- </AnimatePresence>
2477
- {!groups.length
2478
- ? (emptyState ?? (
2479
- <p className="py-10 text-center text-og-menu text-og-fg-subtle">
2480
- No activity yet.
2481
- </p>
2482
- ))
2483
- : null}
2484
- {hasOlder && olderPrefetchArmed ? (
2485
- // Overlaid, not a layout row: mounting/unmounting the sentinel
2486
- // must never shift content (that shift was itself a wobble).
2487
- // End at the scroll origin above the pt-16 gutter so the
2488
- // observer and scrollTop cooldown share the same 400px band.
2489
- <div
2490
- ref={topSentinelRef}
2491
- data-og-top-sentinel=""
2492
- data-og-timeline-chrome=""
2493
- aria-hidden="true"
2494
- className="pointer-events-none absolute inset-x-0 -top-16 h-px -translate-y-full"
2495
- />
2496
- ) : null}
2497
- {groups.map(({ group, key, entranceEnabled }, index) => {
2498
- return (
2499
- <TimelineSearchRevealContext.Provider
2500
- key={key}
2501
- value={
2502
- searchItem &&
2503
- timelineGroupItemIds(group).includes(searchItem.id)
2504
- ? searchRevealKey
2505
- : null
2506
- }
2507
- >
2508
- <TimelineGroupEntry
2509
- groupKey={key}
2510
- group={group}
2511
- nextGroup={groups[index + 1]?.group}
2512
- startupDismissed={
2513
- group.kind === "activity" &&
2514
- group.items.some(
2515
- (item) => item.turnId && turnsWithOutput.has(item.turnId),
2516
- )
2517
- }
2518
- entranceEnabled={entranceEnabled}
2519
- liveEntranceEnabled={
2520
- group.kind === "activity" ? !bulkRender : undefined
2521
- }
2522
- context={timelineGroupEntryContext}
2523
- />
2524
- </TimelineSearchRevealContext.Provider>
2525
- );
2526
- })}
2527
- {groups.length > 0 && trailingState ? (
2528
- <div data-og-timeline-trailing-state="">{trailingState}</div>
2391
+ });
2392
+ },
2393
+ () => {
2394
+ if (jumpToStartSeqRef.current === seq) {
2395
+ pendingJumpToStartRef.current = false;
2396
+ }
2397
+ },
2398
+ );
2399
+ }}
2400
+ className="pointer-events-auto rounded-full border border-og-border px-3 py-1.5 text-og-control"
2401
+ >
2402
+ {underfillRetryReady
2403
+ ? underfillSettledAttempt?.[2]?.tailPreserved
2404
+ ? "Load earlier activity"
2405
+ : "Retry earlier activity"
2406
+ : "Jump to start"}
2407
+ </button>
2408
+ ) : null}
2409
+ </motion.div>
2529
2410
  ) : null}
2530
- {hasNewer && newerFailure?.key === newerBoundaryKey ? (
2531
- <div
2532
- data-og-newer-error=""
2533
- className="flex flex-col items-center gap-2 px-4 py-3 text-center text-og-menu text-og-fg-muted"
2534
- >
2535
- <p role="status" className="max-w-prose [overflow-wrap:anywhere]">
2536
- Couldn’t load later activity. {newerFailure.message}
2411
+ </AnimatePresence>
2412
+ {!groups.length
2413
+ ? (emptyState ?? (
2414
+ <p className="py-10 text-center text-og-menu text-og-fg-subtle">
2415
+ No activity yet.
2537
2416
  </p>
2538
- <button
2539
- ref={newerRetryButtonRef}
2540
- type="button"
2541
- data-og-retry-newer=""
2542
- className="min-h-11 rounded-og-md border border-og-border px-3 py-2 text-og-fg hover:bg-og-surface-2 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-og-accent"
2543
- aria-disabled={loadingNewer || newerRetryPending}
2544
- aria-busy={newerRetryPending}
2545
- onClick={() => requestNewer(true)}
2546
- >
2547
- {newerRetryPending
2548
- ? "Retrying later activity…"
2549
- : "Retry later activity"}
2550
- </button>
2551
- </div>
2552
- ) : null}
2553
- {hasNewer ? (
2554
- <div
2555
- ref={bottomSentinelRef}
2556
- data-og-bottom-sentinel=""
2557
- data-og-timeline-chrome=""
2558
- aria-hidden="true"
2559
- className="h-px w-full"
2560
- />
2561
- ) : null}
2562
- </div>
2563
- </TimelineBeforeLayout>
2564
- </div>
2565
- {draftAnnotations && draftAnnotations.length > 0 ? (
2566
- <Suspense fallback={null}>
2567
- <TimelineAnnotationMarkers
2568
- rootRef={scrollRef}
2569
- annotations={draftAnnotations}
2570
- onSelect={onDraftAnnotationSelect}
2571
- />
2572
- </Suspense>
2573
- ) : null}
2574
-
2575
- <AnimatePresence>
2576
- {questionNav ? (
2577
- <motion.div
2578
- key="question-nav"
2579
- initial={{ opacity: 0, y: -6 }}
2580
- animate={{ opacity: 1, y: 0 }}
2581
- exit={{ opacity: 0, y: -6 }}
2582
- transition={{ duration: 0.15, ease: "easeOut" }}
2583
- data-og-question-nav=""
2584
- // Below the pinned work-header strip (py-1.5 row, 44px on coarse
2585
- // pointers), never over it: the header stays a full-width target.
2586
- data-og-pill-placement={questionPillPlacement}
2587
- className={cn(
2588
- "pointer-events-none absolute inset-x-0 top-11 z-10 flex px-4 sm:px-6 pointer-coarse:top-14",
2589
- QUESTION_PILL_PLACEMENT_CLASS[questionPillPlacement],
2590
- )}
2591
- >
2592
- <motion.div
2593
- ref={questionPillRef}
2594
- layout="position"
2595
- transition={{ duration: 0.18, ease: "easeOut" }}
2596
- className="pointer-events-auto inline-flex max-w-[calc(50%-0.5rem)] items-center rounded-full border border-og-border bg-og-surface-3/90 text-og-control font-medium text-og-fg shadow-og-md backdrop-blur"
2417
+ ))
2418
+ : null}
2419
+ {hasOlder && olderPrefetchArmed ? (
2420
+ // Overlaid, not a layout row: mounting/unmounting the sentinel
2421
+ // must never shift content (that shift was itself a wobble).
2422
+ // End at the scroll origin above the pt-16 gutter so the
2423
+ // observer and scrollTop cooldown share the same 400px band.
2424
+ <div
2425
+ ref={topSentinelRef}
2426
+ data-og-top-sentinel=""
2427
+ data-og-timeline-chrome=""
2428
+ aria-hidden="true"
2429
+ className="pointer-events-none absolute inset-x-0 -top-16 h-px -translate-y-full"
2430
+ />
2431
+ ) : null}
2432
+ {groups.map(({ group, key, entranceEnabled }, index) => {
2433
+ return (
2434
+ <TimelineSearchRevealContext.Provider
2435
+ key={key}
2436
+ value={
2437
+ searchItem && timelineGroupItemIds(group).includes(searchItem.id)
2438
+ ? searchRevealKey
2439
+ : null
2440
+ }
2441
+ >
2442
+ <TimelineGroupEntry
2443
+ groupKey={key}
2444
+ group={group}
2445
+ nextGroup={groups[index + 1]?.group}
2446
+ startupDismissed={
2447
+ group.kind === "activity" &&
2448
+ group.items.some(
2449
+ (item) => item.turnId && turnsWithOutput.has(item.turnId),
2450
+ )
2451
+ }
2452
+ entranceEnabled={entranceEnabled}
2453
+ liveEntranceEnabled={
2454
+ group.kind === "activity" ? !bulkRender : undefined
2455
+ }
2456
+ context={timelineGroupEntryContext}
2457
+ />
2458
+ </TimelineSearchRevealContext.Provider>
2459
+ );
2460
+ })}
2461
+ {groups.length > 0 && trailingState ? (
2462
+ <div data-og-timeline-trailing-state="">{trailingState}</div>
2463
+ ) : null}
2464
+ {hasNewer && newerFailure?.key === newerBoundaryKey ? (
2465
+ <div
2466
+ data-og-newer-error=""
2467
+ className="flex flex-col items-center gap-2 px-4 py-3 text-center text-og-menu text-og-fg-muted"
2597
2468
  >
2469
+ <p role="status" className="max-w-prose [overflow-wrap:anywhere]">
2470
+ Couldn’t load later activity. {newerFailure.message}
2471
+ </p>
2598
2472
  <button
2473
+ ref={newerRetryButtonRef}
2599
2474
  type="button"
2600
- data-og-jump-to-question=""
2601
- onClick={() => void jumpToLatestQuestion()}
2602
- disabled={questionPending}
2603
- aria-busy={questionPending}
2604
- title={questionError ?? undefined}
2605
- className="inline-flex min-w-0 items-center gap-1.5 px-3 py-1.5 hover:text-og-fg pointer-coarse:min-h-11"
2475
+ data-og-retry-newer=""
2476
+ className="min-h-11 rounded-og-md border border-og-border px-3 py-2 text-og-fg hover:bg-og-surface-2 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-og-accent"
2477
+ aria-disabled={loadingNewer || newerRetryPending}
2478
+ aria-busy={newerRetryPending}
2479
+ onClick={() => requestNewer(true)}
2606
2480
  >
2607
- <ArrowUpIcon aria-hidden className="size-3.5" />
2608
- Latest question
2481
+ {newerRetryPending
2482
+ ? "Retrying later activity…"
2483
+ : "Retry later activity"}
2609
2484
  </button>
2610
- {questionError && (
2611
- <span role="status" className="px-2 text-og-xs">
2612
- {questionError}
2613
- </span>
2614
- )}
2615
- </motion.div>
2616
- </motion.div>
2617
- ) : null}
2618
- </AnimatePresence>
2619
- <AnimatePresence>
2620
- {loadingNewer ? (
2621
- <motion.div
2622
- initial={{ opacity: 0, y: 6 }}
2623
- animate={{ opacity: 1, y: 0 }}
2624
- exit={{ opacity: 0, y: 6 }}
2625
- transition={{ duration: 0.15, ease: "easeOut" }}
2626
- data-og-loading-newer=""
2627
- aria-live="polite"
2628
- className="pointer-events-none absolute inset-x-0 bottom-14 z-10 flex justify-center"
2629
- >
2630
- <span className={LOADING_CHIP_CLASS}>
2631
- <span className="og-shimmer-text">Loading later activity…</span>
2632
- </span>
2633
- </motion.div>
2634
- ) : null}
2635
- </AnimatePresence>
2636
- <AnimatePresence>
2637
- {jumpPillShown ? (
2638
- <motion.button
2639
- ref={jumpPillRef}
2640
- type="button"
2641
- data-og-jump-to-latest=""
2642
- data-og-pill-placement={jumpPillPlacement}
2643
- layout="position"
2644
- initial={{ opacity: 0, y: 8 }}
2645
- animate={{ opacity: 1, y: 0 }}
2646
- exit={{ opacity: 0, y: 8 }}
2647
- transition={{ duration: 0.15, ease: "easeOut" }}
2648
- onClick={() => {
2649
- // Returning to the tip explicitly releases reader-owned
2650
- // prose. Clear only this timeline's selection before the
2651
- // synchronous ownership check on the next commit.
2652
- const viewport = scrollRef.current;
2653
- const selection = viewport?.ownerDocument.getSelection();
2654
- if (
2655
- selection &&
2656
- !selection.isCollapsed &&
2657
- (viewport?.contains(selection.anchorNode) ||
2658
- viewport?.contains(selection.focusNode))
2659
- )
2660
- selection.removeAllRanges();
2661
- disclosureKeepsUnpinnedRef.current = false;
2662
- if (hasNewer) {
2663
- // Do not pin against the current history page — its bottom
2664
- // is not the tip. The pin + snap run when the tip window
2665
- // actually lands (`hasNewer` flips false).
2666
- wantPinRef.current = true;
2667
- const node = scrollRef.current;
2668
- if (onJumpToLatest) {
2669
- void Promise.resolve(onJumpToLatest()).then(
2670
- () => {
2671
- // Covers a host that flipped hasNewer before
2672
- // resolving; otherwise the tip-window commit
2673
- // consumes the flag.
2674
- const current = scrollRef.current;
2675
- if (current && wantPinRef.current && !hasNewerRef.current) {
2676
- wantPinRef.current = false;
2677
- applyPinned(true);
2678
- snapToBottom(current);
2679
- }
2680
- },
2681
- () => {
2682
- // The tip reload failed (ordinary network error):
2683
- // an armed latch would fire a surprise snap when
2684
- // the reader later pages to the tip themselves.
2685
- wantPinRef.current = false;
2686
- },
2687
- );
2688
- } else if (node) {
2689
- // No tip reload available: jump within the in-memory
2690
- // window so the newer sentinel can page forward; the
2691
- // latch pins if the tip window eventually lands.
2692
- snapToBottom(node);
2693
- }
2694
- return;
2695
- }
2485
+ </div>
2486
+ ) : null}
2487
+ {hasNewer ? (
2488
+ <div
2489
+ ref={bottomSentinelRef}
2490
+ data-og-bottom-sentinel=""
2491
+ data-og-timeline-chrome=""
2492
+ aria-hidden="true"
2493
+ className="h-px w-full"
2494
+ />
2495
+ ) : null}
2496
+ </div>
2497
+ </TimelineBeforeLayout>
2498
+ </div>
2499
+ {draftAnnotations && draftAnnotations.length > 0 ? (
2500
+ <Suspense fallback={null}>
2501
+ <TimelineAnnotationMarkers
2502
+ rootRef={scrollRef}
2503
+ annotations={draftAnnotations}
2504
+ onSelect={onDraftAnnotationSelect}
2505
+ />
2506
+ </Suspense>
2507
+ ) : null}
2508
+
2509
+ <AnimatePresence>
2510
+ {questionNav ? (
2511
+ <motion.div
2512
+ key="question-nav"
2513
+ initial={{ opacity: 0, y: -6 }}
2514
+ animate={{ opacity: 1, y: 0 }}
2515
+ exit={{ opacity: 0, y: -6 }}
2516
+ transition={{ duration: 0.15, ease: "easeOut" }}
2517
+ data-og-question-nav=""
2518
+ // Below the pinned work-header strip (py-1.5 row, 44px on coarse
2519
+ // pointers), never over it: the header stays a full-width target.
2520
+ className="pointer-events-none absolute inset-x-0 top-11 z-10 flex justify-end px-4 sm:px-6 pointer-coarse:top-14"
2521
+ >
2522
+ <div className="pointer-events-auto inline-flex max-w-[calc(50%-0.5rem)] items-center rounded-full border border-og-border bg-og-surface-3/90 text-og-control font-medium text-og-fg shadow-og-md backdrop-blur">
2523
+ <button
2524
+ type="button"
2525
+ data-og-jump-to-question=""
2526
+ onClick={() => void jumpToLatestQuestion()}
2527
+ disabled={questionPending}
2528
+ aria-busy={questionPending}
2529
+ title={questionError ?? undefined}
2530
+ className="inline-flex min-w-0 items-center gap-1.5 px-3 py-1.5 hover:text-og-fg pointer-coarse:min-h-11"
2531
+ >
2532
+ <ArrowUpIcon aria-hidden className="size-3.5" />
2533
+ Latest question
2534
+ </button>
2535
+ {questionError && (
2536
+ <span role="status" className="px-2 text-og-xs">
2537
+ {questionError}
2538
+ </span>
2539
+ )}
2540
+ </div>
2541
+ </motion.div>
2542
+ ) : null}
2543
+ </AnimatePresence>
2544
+ <AnimatePresence>
2545
+ {loadingNewer ? (
2546
+ <motion.div
2547
+ initial={{ opacity: 0, y: 6 }}
2548
+ animate={{ opacity: 1, y: 0 }}
2549
+ exit={{ opacity: 0, y: 6 }}
2550
+ transition={{ duration: 0.15, ease: "easeOut" }}
2551
+ data-og-loading-newer=""
2552
+ aria-live="polite"
2553
+ className="pointer-events-none absolute inset-x-0 bottom-14 z-10 flex justify-center"
2554
+ >
2555
+ <span className={LOADING_CHIP_CLASS}>
2556
+ <span className="og-shimmer-text">Loading later activity…</span>
2557
+ </span>
2558
+ </motion.div>
2559
+ ) : null}
2560
+ </AnimatePresence>
2561
+ <AnimatePresence>
2562
+ {((!pinned && autoFollow) || hasNewer || canSkipTipCatchup) && autoFollow ? (
2563
+ <motion.button
2564
+ type="button"
2565
+ data-og-jump-to-latest=""
2566
+ initial={{ opacity: 0, y: 8 }}
2567
+ animate={{ opacity: 1, y: 0 }}
2568
+ exit={{ opacity: 0, y: 8 }}
2569
+ transition={{ duration: 0.15, ease: "easeOut" }}
2570
+ onClick={() => {
2571
+ // Returning to the tip explicitly releases reader-owned
2572
+ // prose. Clear only this timeline's selection before the
2573
+ // synchronous ownership check on the next commit.
2574
+ const viewport = scrollRef.current;
2575
+ const selection = viewport?.ownerDocument.getSelection();
2576
+ if (
2577
+ selection &&
2578
+ !selection.isCollapsed &&
2579
+ (viewport?.contains(selection.anchorNode) ||
2580
+ viewport?.contains(selection.focusNode))
2581
+ )
2582
+ selection.removeAllRanges();
2583
+ disclosureKeepsUnpinnedRef.current = false;
2584
+ if (hasNewer) {
2585
+ // Do not pin against the current history page — its bottom
2586
+ // is not the tip. The pin + snap run when the tip window
2587
+ // actually lands (`hasNewer` flips false).
2588
+ wantPinRef.current = true;
2696
2589
  const node = scrollRef.current;
2697
- if (node) {
2698
- applyPinned(true);
2590
+ if (onJumpToLatest) {
2591
+ void Promise.resolve(onJumpToLatest()).then(
2592
+ () => {
2593
+ // Covers a host that flipped hasNewer before
2594
+ // resolving; otherwise the tip-window commit
2595
+ // consumes the flag.
2596
+ const current = scrollRef.current;
2597
+ if (current && wantPinRef.current && !hasNewerRef.current) {
2598
+ wantPinRef.current = false;
2599
+ applyPinned(true);
2600
+ snapToBottom(current);
2601
+ }
2602
+ },
2603
+ () => {
2604
+ // The tip reload failed (ordinary network error):
2605
+ // an armed latch would fire a surprise snap when
2606
+ // the reader later pages to the tip themselves.
2607
+ wantPinRef.current = false;
2608
+ },
2609
+ );
2610
+ } else if (node) {
2611
+ // No tip reload available: jump within the in-memory
2612
+ // window so the newer sentinel can page forward; the
2613
+ // latch pins if the tip window eventually lands.
2699
2614
  snapToBottom(node);
2700
2615
  }
2701
- }}
2702
- className={cn(
2703
- "absolute bottom-4 w-fit",
2704
- JUMP_PILL_PLACEMENT_CLASS[jumpPillPlacement],
2705
- "inline-flex items-center gap-1.5 rounded-full border border-og-border bg-og-surface-3/90 px-3 py-1.5",
2706
- "text-og-control font-medium text-og-fg shadow-og-md backdrop-blur",
2707
- "hover:border-og-border-strong",
2708
- )}
2709
- >
2710
- <ArrowDownIcon className="size-3.5" />
2711
- Jump to latest
2712
- </motion.button>
2713
- ) : null}
2714
- </AnimatePresence>
2715
- </div>
2716
- </TimelineAnnotationSourceRootContext.Provider>
2717
- </TooltipProvider>
2718
- </EntranceAnimationProvider>
2719
- </TimelineComputeLabelProvider>
2720
- </SeenActivityIdsProvider>
2721
- </FoldMemoryProvider>
2722
- </LightboxProvider>
2723
- </AllowancePresentationProvider>
2616
+ return;
2617
+ }
2618
+ const node = scrollRef.current;
2619
+ if (node) {
2620
+ applyPinned(true);
2621
+ snapToBottom(node);
2622
+ }
2623
+ }}
2624
+ className={cn(
2625
+ "absolute inset-x-0 bottom-4 mx-auto w-fit",
2626
+ "inline-flex items-center gap-1.5 rounded-full border border-og-border bg-og-surface-3/90 px-3 py-1.5",
2627
+ "text-og-control font-medium text-og-fg shadow-og-md backdrop-blur",
2628
+ "hover:border-og-border-strong",
2629
+ )}
2630
+ >
2631
+ <ArrowDownIcon className="size-3.5" />
2632
+ Jump to latest
2633
+ </motion.button>
2634
+ ) : null}
2635
+ </AnimatePresence>
2636
+ </div>
2637
+ </TimelineAnnotationSourceRootContext.Provider>
2638
+ </TooltipProvider>
2639
+ </EntranceAnimationProvider>
2640
+ </TimelineComputeLabelProvider>
2641
+ </SeenActivityIdsProvider>
2642
+ </FoldMemoryProvider>
2643
+ </LightboxProvider>
2724
2644
  );
2725
2645
  // Agent-authored object links resolve through the host, never the console.
2726
2646
  return <OpenGeniLinkProvider resolveLink={resolveLink}>{timeline}</OpenGeniLinkProvider>;
@@ -4628,7 +4548,6 @@ function formatVideoDuration(seconds: number): string {
4628
4548
 
4629
4549
  function NoticeRow({ item }: { item: NoticeItem }) {
4630
4550
  const enter = useEntranceAnimation();
4631
- if (item.allowance) return <AllowanceExhaustedRow refusal={item.allowance} />;
4632
4551
  if (item.recordedOutcome) {
4633
4552
  return (
4634
4553
  <details