import { QUESTION_NAV_MARGIN_PX, questionNavTarget } from "../timeline/question-nav-model"; import { timelineGroupIndexAtSequence } from "../timeline/focus-sequence"; import { RollingActivity } from "../timeline/rolling-activity"; import { clusterIsSettled, compactedLandmarkCount, durationBetween, flattenActivityItems, isPreparingWork, readableWorkDefaultOpen, readableWorkHasDetails, readableWorkShowsPreview, readableWorkStatus, } from "../timeline/work-presentation"; import { useErrorMessage } from "../lib/error-message"; import { formatElapsed } from "../timeline/turn-summary"; import { ActivityNoteTextContext } from "../timeline/activity-rail"; import { TurnStepsAction, TurnStepsLoader } from "../timeline/turn-steps"; import { timelineGroupContainsPresentedImage } from "../timeline/presented-image"; import { isTimelineSearchTarget, TimelineSearchRevealContext, useTimelineSearchNavigation, type TimelineSearchTarget, } from "./timeline-search"; import { GenieLoadingOptionsContext, type GenieLoadingOptions } from "../timeline/genie-loading"; import { ChildSessionLink } from "./child-session-link"; import { useStartupDetails } from "../timeline/startup-preference"; import { compactionFailureDetails, compactionSkipSubtitle, compactionSkipTitle, } from "../timeline/compaction-copy"; import type { DraftTimelineAnnotation, MediaGenerationResult, SessionEvent, SessionStatus, } from "@opengeni/sdk"; import { dequal } from "dequal/lite"; import { ArrowDownIcon, ArrowRightIcon, ArrowUpIcon, BotIcon, CheckCircle2Icon, CheckIcon, ChevronDownIcon, ChevronRightIcon, CornerDownRightIcon, PauseCircleIcon, PauseIcon, MessageCircleQuestionIcon, PencilLineIcon, PlayIcon, RefreshCwIcon, ShrinkIcon, TargetIcon, Trash2Icon, TriangleAlertIcon, XCircleIcon, } from "lucide-react"; import type { ComponentType, CSSProperties } from "react"; import { AnimatePresence, motion, useReducedMotion } from "motion/react"; import { Component, Fragment, Suspense, createContext, lazy, memo, useCallback, useContext, useEffect, useId, useLayoutEffect, useMemo, useRef, useState, type ReactNode, } from "react"; import { cn } from "../lib/cn"; import { MessageSenderContext, MessageSenderSlot, type RenderMessageSender, } from "../timeline/message-sender"; import { formatClockTime, formatRelativeTime, truncate } from "../lib/format"; import { prefersReducedMotion } from "../lib/motion"; import { invokeOlderHistoryLoaderWithReceiptCapture, type OlderHistoryLoadReceipt, type OlderHistoryLoader, } from "../older-history"; import { Markdown, type MarkdownProps } from "./markdown"; import { linkifyText } from "./linkified-text"; import { OpenGeniLinkProvider, type OpenGeniLinkResolver } from "./open-geni-links"; import { TimelineBeforeLayout, captureTimelineAnchor, timelineAnchorCorrection, timelineHasReader, type TimelineAnchor, } from "./timeline-anchor"; import { useReadingProgress } from "./timeline-reading-progress"; import { animateTimelineSettlement, captureTimelineSettlement, type TimelineSettlement, } from "./timeline-settlement"; import { UserMessageBody, UserMessageDisclosureProvider, type UserMessageDisclosureContextValue, type UserMessageDisclosureLabels, } from "./user-message-body"; import { createTipFollowState, readerScrollUpPx, tipFollowCancel, tipFollowCompensateViewportShrink, tipFollowObserveContentShrink, tipFollowStep, supportsScrollEndEvent, TIP_FOLLOW_READER_UP_EPS_PX, TIP_FOLLOW_SHRINK_EPS_PX, type TipFollowContentShrinkBaseline, type TipFollowState, } from "./tip-follow"; import { ActivityRail, buildTimeline, defaultToolRegistry, groupTimeline, LightboxProvider, type ActivityItem, type AgentMessageItem, type AuthNeededItem, type ContextCompactionItem, type GoalItem, type HumanInputItem, type MachineInputBatchItem, type NoticeItem, type TimelineGroup, type TimelineItem, type TimelineAnnotationSourceDescriptor, type RetainedArtifactLoader, type RetainedScreenshotLoader, type VideoArtifactPlaybackLoader, type ToolRegistry, type TurnSummaryOptions, type TurnSummaryStatus, type UserMessageItem, type FoldRestingState, type WorkerCompletionItem, FoldMemoryProvider, inheritFoldRestingState, TurnSummary, useFoldMemory, useTurnSettleOpen, } from "../timeline"; import { CopyHoverFrame } from "./copy-button"; import { SecretAnswerChip } from "./secret-ref-chip"; import { secretAnswerLabel } from "../human-input-model"; import { TimelineAnnotationSourceRootContext } from "./timeline-annotation-reveal-context"; import { GeneratedVideoPlayer } from "./generated-video-player"; import { MACHINE_INPUT_META, agentMemberSessionId, agentMemberText, agentMemberTitle, cleanMachineInputSummary, isAgentMember, machineInputBatchLabel, machineInputSummaryIsUseful, readableMachineInputSource, } from "./machine-input-display"; import { AgentRow, AgentRowSection, AgentRowText } from "../timeline/agent-row"; import { AgentIdentityProvider, useAgentIdentity, type ResolveSessionTitle, } from "../timeline/agent-identity"; import { namedAgentTitle, type AgentTitleParts } from "../timeline/platform-activity-presentation"; import { SESSION_STATUS_META, StatusDot } from "./session-status"; import { noticeDisplayText, noticeIsResolvedApproval, noticeTone, } from "../timeline/notice-presentation"; import { TimelineComputeLabelProvider } from "../timeline/compute-label"; import { ProviderLogoProvider } from "../timeline/provider-logo"; import { EntranceAnimationProvider, useEntranceAnimation } from "../timeline/entrance"; import { AllowanceExhaustedRow, AllowancePresentationProvider, type RenderAllowanceExhausted, } from "../timeline/allowance-exhausted-row"; import { SessionUsageLimitRow } from "../timeline/session-usage-limit-row"; import { LearningRefusalRow } from "../timeline/learning-refusal-row"; import type { AllowanceLabels } from "../usage/allowance-copy"; import { SeenActivityIdsProvider } from "../timeline/seen-activity-ids"; import { TimelineAnnotationCards } from "./timeline-annotations"; import { TooltipProvider } from "./tooltip"; import { ShimmerText } from "../timeline/shimmer-text"; import { useOpenGeniText, type OpenGeniText } from "../messages/runtime"; const TimelineAnnotationSelection = lazy(() => import("./timeline-annotation-selection")); const TimelineAnnotationMarkers = lazy(() => import("./timeline-annotation-markers")); export type MessageTimelineProps = { /** Exact durable search hit; clearing it removes highlighting without moving the reader. */ searchTarget?: TimelineSearchTarget | null | undefined; /** * A durable event sequence to open on once (an inbox item's or notification's * moment): the timeline lands on the row that holds it, or the nearest row * before it, instead of the latest message. Load the window around it first. */ focusSequence?: number | null | undefined; /** Localized user-message disclosure actions, including custom UserMessageBody renderers. */ userMessageDisclosureLabels?: UserMessageDisclosureLabels | undefined; /** Raw session events (projected internally) … */ events?: SessionEvent[] | undefined; /** … or pre-projected items (e.g. from `useSessionEvents().timeline`). */ items?: TimelineItem[] | undefined; /** Current session status (reserved; tip "Working…" chrome removed for now). */ status?: SessionStatus | null | undefined; /** Host-owned controls beside Copy and the timestamp on settled message rows. */ renderMessageActions?: ((item: AgentMessageItem | UserMessageItem) => ReactNode) | undefined; /** * Who sent a person's message, drawn above its bubble. Called only for * messages that carry a sender; return null for the viewer's own messages * or when the host has no identity to show. Omit to show no senders. */ renderMessageSender?: RenderMessageSender | undefined; /** Plug a markdown renderer for message bodies (e.g. streamdown). */ renderMessageText?: | (( text: string, item: AgentMessageItem | UserMessageItem, context: { searchTarget: TimelineSearchTarget | null }, ) => ReactNode) | undefined; /** * Render assistant `opengeni-html` / `opengeni-site` fences in the default * message renderer (`SessionConversation` supplies the inline Site preview). */ renderInteractiveBlock?: MarkdownProps["renderInteractiveBlock"]; /** Drill into a spawned worker session. */ onOpenSession?: ((sessionId: string) => void) | undefined; /** * Name the agents this session spawns, messages, and hears from. Return the * current display title for a session id, or null when unknown; the * timeline falls back to the title given at spawn, then to generic copy. * Keep the function identity stable across renders. */ resolveSessionTitle?: ResolveSessionTitle | undefined; /** * Deep-link a memory row (a `memory.saved` / `memory.corrected` step) to its * record in the host's memory pane. Opt-in, exactly like `onReconnect`: the * library draws no "View in memory" affordance without a handler — the memory * row is then non-interactive rich content. This is the switch that makes the * deep-link a first-party Opengeni capability without other SDK consumers * opting into it. The app supplies it (it owns routing to the memory pane). */ onMemoryClick?: ((memoryId: string) => void) | undefined; /** * Start the reconnect flow when a tool needs its connection reauthorized. The * app supplies this (it owns the SDK client + workspace): it typically kicks * off `startConnectionOAuth` and redirects, or routes to credential entry. * Rejecting surfaces a calm inline error on the card; the library never draws * a Reconnect button without a handler to run it. */ onReconnect?: ((item: AuthNeededItem) => void | Promise) | undefined; /** Host-owned inline connection setup. Return undefined to use the default recovery card. */ renderAuthNeeded?: ((item: AuthNeededItem) => ReactNode | undefined) | undefined; /** * Replace the "usage limit reached" row shown when a workspace or member * usage allowance stops work (an `allowance_exhausted` refusal). Receives the * typed refusal (scope, reset time) and the default row; return undefined to * keep the default. Use it to point people at your own plan or admin page. */ renderAllowanceExhausted?: RenderAllowanceExhausted | undefined; /** Replace the words of the default "usage limit reached" row. */ allowanceExhaustedLabels?: Partial | undefined; /** * Decide which durable authentication notices this timeline presents. * Defaults to showing every notice. Embedded hosts can suppress notices for * credentials they manage elsewhere without discarding the underlying event. */ shouldRenderAuthNeeded?: ((item: AuthNeededItem) => boolean) | undefined; /** * Resolve a provider domain (from a reconnect card) to a logo URL the host * serves itself — the app maps it through its catalog + `catalogAssetUrl`. * Return null/undefined to fall back to a calm monogram. The library never * fetches an off-origin favicon (CSP + privacy); an unresolved logo is a * monogram, not an external image. */ resolveProviderLogo?: ((providerDomain: string) => string | null | undefined) | undefined; /** * The tool-renderer registry that resolves how each tool call is drawn. * Defaults to {@link defaultToolRegistry}; pass a registry from * `createDefaultToolRegistry({ entries })` to add custom tool renderers. */ toolRegistry?: ToolRegistry | undefined; /** * Open Opengeni object links the agent writes in replies and progress notes: * `artifact:`, `sandbox:`, editable artifacts, and Sites. Return * a host URL (`{ href }`) or action (`{ open }`); unhandled targets render as * unavailable text instead of a console link that 404s inside the host. * Applies to the default message renderer and to any `Markdown` a custom * `renderMessageText` renders. `SessionConversation` supplies file and * sandbox downloads by default. */ resolveLink?: OpenGeniLinkResolver | undefined; /** Resolve opaque retained screenshot receipts through the authenticated host SDK. */ loadRetainedScreenshot?: RetainedScreenshotLoader | undefined; /** Resolve permanent workspace image/file receipts through the authenticated host SDK. */ loadRetainedArtifact?: RetainedArtifactLoader | undefined; /** Mint short-lived native playback sources for retained generated videos. */ loadVideoArtifactPlayback?: VideoArtifactPlaybackLoader | undefined; /** * Display name of the session's active compute target (Connected Machine or * cloud sandbox). When set, exec_command collapsed previews prefix `on {label}`. */ computeLabel?: string | null | undefined; /** Customize collapsed turn facets for this timeline instance. */ genieLoading?: GenieLoadingOptions | undefined; turnSummary?: TurnSummaryOptions | undefined; /** Follow new events when pinned to the bottom. Defaults to true. */ autoFollow?: boolean | undefined; /** Capture a same-row text selection into the host's canonical composer draft. */ onAnnotate?: ((annotation: DraftTimelineAnnotation) => void) | undefined; /** * Hide the floating "Back to your message" pill while the timeline viewport * is shorter than this (px), e.g. a narrow embed above a decision card. * Defaults to 0 (always available). */ questionNavMinViewportHeight?: number | undefined; /** Composer draft quotes currently attached to the next send. */ draftAnnotations?: readonly DraftTimelineAnnotation[] | undefined; /** Open the composer review list for one numbered draft badge. */ onDraftAnnotationSelect?: ((id: string) => void) | undefined; /** Older durable history exists above the current window (see useSessionEvents). */ hasOlder?: boolean | undefined; /** An older window is being fetched; shows the quiet top shimmer. */ loadingOlder?: boolean | undefined; /** * Called when older history should backfill. Existing void, synchronous-value, * and arbitrary-promise callbacks remain supported. Receipt-aware loaders * preserve committed-page direction through wrappers and bounded windows. */ onLoadOlder?: OlderHistoryLoader | undefined; /** Jump to the durable session start (bounded oldest window, no middle). */ onJumpToStart?: (() => void | Promise) | undefined; /** True while the oldest window is loading. */ loadingOldest?: boolean | undefined; /** Newer durable history exists below the current (history) window. */ hasNewer?: boolean | undefined; /** A newer history page is being fetched. */ loadingNewer?: boolean | undefined; /** Page forward through history. Return the request promise to enable inline error/retry. */ onLoadNewer?: (() => unknown) | undefined; /** * Reload the live tip window. When omitted, Jump to latest only re-pins and * scrolls the in-memory window. */ onJumpToLatest?: (() => void | Promise) | undefined; /** @deprecated Retained for source compatibility; contextual prompt navigation * uses only mounted prompts and never invokes this global resolver. */ onJumpToLatestQuestion?: (() => Promise) | undefined; /** Host-owned content appended after timeline groups, such as startup progress. */ trailingState?: ReactNode | undefined; emptyState?: ReactNode | undefined; className?: string | undefined; }; /** * Scroll ownership, from first principles. Everything the events hook has * loaded is mounted — no tip-lock window, no per-frame progressive reveal. * (The in-memory window is already byte/count-bounded by useSessionEvents, and * rows are memoized, so a full mount is cheap; the drip-feed machinery this * replaces was the "content is hidden, then pops in in batches" wobble.) * * Scroll invariant (tip-follow camera — see `./tip-follow.ts`): * - Load/remount: hidden until tip is hard-snapped across a short settle; then * reveal. Live tip: DOM growth advances the pinned viewport by the same * amount, so rendered content is visible immediately. Only debt that already * existed before the growth goes through the camera ease. * - One continuous follow while hot (faster τ when behind); sleeps when cold. * - While pinned, tip-debt from growth/collapse must NEVER unpin — only * wheel/keys/pointer-armed scroll-up, or a settled scrollend away from the * tip while the tip-follow camera is idle (Vimium / unfocused PageUp). * Height shrink compensates scrollTop by Δh (collapse owns motion); tip-ease * pauses briefly so the two don't fight. Programmatic camera writes are * tagged so their scroll echoes never count as leave. * - overflow-anchor off while pinned so the browser cannot instant-correct. * - Scrolled up → history prepends restore via the retained group anchor * (offsetTop delta); loadOlder can truncate the tip, so scrollHeight delta * alone is wrong. Late layout while unpinned stays browser-owned. */ const PIN_THRESHOLD_PX = 48; /** * A pinned timeline can still have meaningful pre-existing debt while the * tip-follow camera catches up. Surface the existing explicit jump once that * debt is large enough to be useful, without flashing the control for ordinary * line-sized streaming movement. */ const JUMP_TO_LATEST_CATCHUP_DEBT_PX = 240; /** * Floating timeline navigation: two identical small round arrow buttons at fixed * spots, centered on the conversation. Back to your message floats 12px below * the top edge, or just below an expanded work-header strip while that strip is * pinned (32px, 44px on coarse pointers) so the strip stays a full-width * target. Jump to latest floats 12px above the bottom edge. They never dodge * the content beneath them (floating over a sliver of text is fine), so they * cannot wander as rows stream in or the host resizes. Coarse pointers get a * larger invisible hit area instead of a larger button. */ const NAV_BUTTON_CLASS = "pointer-events-auto relative inline-flex size-8 items-center justify-center rounded-full border border-og-border bg-og-surface-3/90 text-og-fg-muted shadow-og-md backdrop-blur hover:border-og-border-strong hover:text-og-fg focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-og-accent pointer-coarse:before:absolute pointer-coarse:before:-inset-1.5 pointer-coarse:before:content-['']"; const NAV_FADE = { initial: { opacity: 0 }, animate: { opacity: 1 }, exit: { opacity: 0 }, transition: { duration: 0.15, ease: "easeOut" }, } as const; /** * Prefetch older history when the top sentinel is this far from the viewport. * After a page loads we stay cool until the reader leaves this band (scrolls * down into content) — never re-fire from continued scroll toward y=0. */ const OLDER_PREFETCH_MARGIN_PX = 400; const OLDER_PREFETCH_ROOT_MARGIN = `${OLDER_PREFETCH_MARGIN_PX}px 0px 0px 0px`; const PRIMARY_ACTION_CLASS = "inline-flex w-full shrink-0 items-center justify-center gap-1.5 rounded-og-md border border-og-primary-border bg-og-primary text-og-primary-fg px-3 py-1.5 text-og-menu font-medium sm:w-auto"; const MESSAGE_BUBBLE_CLASS = "w-fit max-w-full min-w-0 rounded-og-lg rounded-br-og-xs border border-og-border bg-og-surface-2 px-4 py-2.5 text-og-md leading-6 text-og-fg"; const WAITING_PILL_CLASS = "border-og-status-waiting/35 bg-og-status-waiting/10 text-og-status-waiting"; const LOADING_CHIP_CLASS = "pointer-events-none inline-flex items-center rounded-full border border-og-border bg-og-surface-3/90 px-3 py-1 text-og-control font-medium shadow-og-md backdrop-blur"; /** How long live progress notes take to fold into (or out of) their work row. */ const LIVE_NOTE_FOLD_MS = 220; /** The timeline list's `gap-5`; a folded note gives it back with `-mt-5`. */ const LIVE_NOTE_GAP_PX = 20; /** Longest a bottom-space loan waits for an opening work list to grow into it. */ const LIVE_NOTE_RESERVE_MS = 700; /** Lets a live work disclosure report whether it is open (see `reportLiveFold`). */ const LiveNoteFoldContext = createContext<((workId: string, open: boolean) => void) | null>(null); // State: 0 underfill retry, 1 prefetch pending, 2 compatibility-settled, // 3 explicit no-progress waiting to become an underfill retry. // The tuple identity is the exact request ownership fence. The boundary may rebase // forward while that request is pending when a bounded live-tail append evicts // its former oldest row. First-party loaders mark the exact owner when their // older page commits; retained direction is the fallback for other hosts. type OlderLoadAttempt = [ boundary: string | undefined, state?: number, receipt?: OlderHistoryLoadReceipt, ]; function invokeOlderLoad( load: OlderHistoryLoader, noProgress: () => void, attempt: OlderLoadAttempt, preserveTail = false, ): 1 | undefined { try { // Receipt creation is captured synchronously through legacy wrappers such // as `() => void loadOlder()`, even when the wrapper discards the return. const result = invokeOlderHistoryLoaderWithReceiptCapture( load, (receipt) => { attempt[2] = receipt; }, preserveTail, ) as OlderHistoryLoadReceipt | PromiseLike | unknown; const receipt = attempt[2] ?? (typeof (result as { committed?: unknown } | undefined)?.committed === "boolean" ? (result as OlderHistoryLoadReceipt) : undefined); if (receipt) { attempt[2] = receipt; void receipt.then( (value) => value === false && !receipt.committed && noProgress(), noProgress, ); return 1; } if (typeof (result as PromiseLike | undefined)?.then != "function") { return; } void (result as PromiseLike).then( (value) => value === false && noProgress(), noProgress, ); } catch { noProgress(); } return 1; } /** * Pinned = the viewport bottom is within PIN_THRESHOLD_PX of the content * bottom. When the scroll range itself is shorter than the threshold, the * whole range would count as "at the bottom" and the reader could never unpin * to reach older history — so the effective threshold shrinks to the range, * making the very top of a short window count as scrolled up. A window that * cannot scroll at all is always pinned. */ function maxScrollOf(node: HTMLElement): number { // Browser layout guarantees scrollHeight is at least clientHeight. return node.scrollHeight - node.clientHeight; } /** * Wheel bubbled from a nested overflow scroller that can still move up — not * timeline intent (code blocks / notice `
`).
 */
function wheelConsumedByNestedScrollable(event: {
  deltaY: number;
  target: EventTarget | null;
  currentTarget: EventTarget | null;
}): boolean {
  if (event.deltaY >= 0) {
    return false;
  }
  let el = event.target instanceof Element ? event.target : null;
  const root = event.currentTarget instanceof Element ? event.currentTarget : null;
  while (el && el !== root) {
    if (el instanceof HTMLElement) {
      const style = getComputedStyle(el);
      const overflowY = style.overflowY;
      if (
        (overflowY === "auto" || overflowY === "scroll" || overflowY === "overlay") &&
        el.scrollHeight > el.clientHeight + 1 &&
        el.scrollTop > 0
      ) {
        return true;
      }
    }
    el = el.parentElement;
  }
  return false;
}

function isNearBottom(node: HTMLElement): boolean {
  const maxScroll = maxScrollOf(node);
  if (maxScroll <= 1) {
    return true;
  }
  const gap = maxScroll - node.scrollTop;
  return gap < Math.min(PIN_THRESHOLD_PX, maxScroll);
}

/** Content-space top of a timeline group, or null when it is not mounted. */
function contentTopOf(node: HTMLElement, groupKey: string): number | null {
  const element = node.querySelector(`[data-og-group-key="${cssEscapeAttribute(groupKey)}"]`);
  if (!(element instanceof HTMLElement)) {
    return null;
  }
  return element.getBoundingClientRect().top - node.getBoundingClientRect().top + node.scrollTop;
}

/** The question the reader is inside of, once its start has scrolled out of view. */
/**
 * `belowHeader`: an expanded work header is pinned to the top of the scrollport,
 * so the up arrow sits just below that strip instead of covering it.
 */
type QuestionNav = { key: string; belowHeader: boolean };

function readQuestionNav(node: HTMLElement): QuestionNav | null {
  const view = node.getBoundingClientRect();
  const prompts = [...node.querySelectorAll("[data-og-prompt]")].flatMap((prompt) => {
    const key = prompt.dataset.ogGroupKey;
    if (!key) return [];
    const bounds = prompt.getBoundingClientRect();
    return [{ key, top: bounds.top, bottom: bounds.bottom }];
  });
  const key = questionNavTarget(prompts, { top: view.top, height: view.height });
  return key ? { key, belowHeader: workHeaderPinned(node, view) } : null;
}

/** True while an expanded outer work header is stuck to the scrollport's top edge. */
function workHeaderPinned(node: HTMLElement, view: DOMRect): boolean {
  for (const header of node.querySelectorAll(
    '[data-og-work-header="outer"][data-state="open"]',
  )) {
    const box = header.getBoundingClientRect();
    if (Math.abs(box.top - view.top) <= 1 && box.bottom > view.top) return true;
  }
  return false;
}

function sameQuestionNav(a: QuestionNav | null, b: QuestionNav | null): boolean {
  return a === b || (!!a && !!b && a.key === b.key && a.belowHeader === b.belowHeader);
}

/** Escape a value for use inside a CSS attribute selector. */
function cssEscapeAttribute(value: string): string {
  if (typeof CSS !== "undefined" && typeof CSS.escape === "function") {
    return CSS.escape(value);
  }
  return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
}

/**
 * The session timeline: chat messages with streaming deltas, collapsed
 * activity clusters (reasoning, tool calls, sandbox work), spawned-worker
 * cards, goal markers, and status transitions. Owns stick-to-bottom scrolling
 * with a "jump to latest" affordance when the reader scrolls back.
 */
export function MessageTimeline({
  resolveLink,
  renderInteractiveBlock,
  userMessageDisclosureLabels,
  searchTarget,
  focusSequence,
  events,
  items,
  status: _status,
  renderMessageActions,
  renderMessageSender,
  renderMessageText,
  onOpenSession,
  resolveSessionTitle,
  onMemoryClick,
  onReconnect,
  renderAuthNeeded,
  renderAllowanceExhausted,
  allowanceExhaustedLabels,
  shouldRenderAuthNeeded,
  resolveProviderLogo,
  toolRegistry = defaultToolRegistry,
  loadRetainedScreenshot,
  loadRetainedArtifact,
  loadVideoArtifactPlayback,
  computeLabel = null,
  genieLoading,
  turnSummary,
  autoFollow = true,
  onAnnotate,
  questionNavMinViewportHeight = 0,
  draftAnnotations,
  onDraftAnnotationSelect,
  hasOlder = false,
  loadingOlder = false,
  onLoadOlder,
  onJumpToStart,
  loadingOldest = false,
  hasNewer = false,
  loadingNewer = false,
  onLoadNewer,
  onJumpToLatest,
  trailingState,
  emptyState,
  className,
}: MessageTimelineProps) {
  const t = useOpenGeniText();
  const formatError = useErrorMessage();
  const resolvedItems = useMemo(() => {
    const projectedItems = items ?? buildTimeline(t, events ?? []);
    if (!shouldRenderAuthNeeded) {
      return projectedItems;
    }
    return projectedItems.filter(
      (item) => item.kind !== "auth-needed" || shouldRenderAuthNeeded(item),
    );
  }, [items, events, shouldRenderAuthNeeded, t]);
  const searchItem = searchTarget
    ? resolvedItems.find((item) => isTimelineSearchTarget(item, searchTarget))
    : undefined;
  const searchRevealKey =
    searchItem && searchTarget
      ? JSON.stringify([
          searchItem.id,
          searchTarget.query,
          searchTarget.occurrence ?? 0,
          searchTarget.offset ?? null,
        ])
      : null;
  const searchTargetRef = useRef(searchTarget);
  searchTargetRef.current = searchTarget;
  const renderSearchableMessageText = useMemo(
    () =>
      renderMessageText
        ? (text: string, item: AgentMessageItem | UserMessageItem) =>
            renderMessageText(text, item, {
              searchTarget: searchItem?.id === item.id ? (searchTarget ?? null) : null,
            })
        : (text: string, item: AgentMessageItem | UserMessageItem) => {
            const body = (
              
                {text}
              
            );
            return item.kind === "user-message" ? (
              
                {body}
              
            ) : (
              body
            );
          },
    [renderInteractiveBlock, renderMessageText, searchItem?.id, searchTarget],
  );
  // Event-window identity is independent of projected rows (partial messages
  // can acquire a different first-delta id when older text arrives).
  const sourceItems = events ?? items;
  const olderBoundaryKey = sourceItems?.[0]?.id;
  const newerBoundaryKey = `${events?.[0]?.sessionId ?? ""}:${olderBoundaryKey ?? ""}`;
  const newerScopeRef = useRef(newerBoundaryKey);
  newerScopeRef.current = newerBoundaryKey;
  const newerAttemptRef = useRef<{ pending: boolean } | null>(null);
  const newerRetryButtonRef = useRef(null);
  const [newerRetryPending, setNewerRetryPending] = useState(false);
  const [newerFailure, setNewerFailure] = useState<{ key: string; message: string } | null>(null);
  useEffect(() => {
    newerScopeRef.current = newerBoundaryKey;
    newerAttemptRef.current = null;
    setNewerFailure(null);
    setNewerRetryPending(false);
    return () => {
      newerScopeRef.current = "";
      newerAttemptRef.current = null;
    };
  }, [newerBoundaryKey]);
  const requestNewer = useCallback(
    (explicitRetry = false) => {
      if (
        newerScopeRef.current !== newerBoundaryKey ||
        !onLoadNewer ||
        loadingNewer ||
        (newerAttemptRef.current && (!explicitRetry || newerAttemptRef.current.pending))
      ) {
        return;
      }
      const attempt = { pending: true };
      newerAttemptRef.current = attempt;
      if (explicitRetry) setNewerRetryPending(true);
      const isCurrent = () =>
        newerAttemptRef.current === attempt && newerScopeRef.current === newerBoundaryKey;
      // Both synchronous host errors and rejected promises belong to this
      // boundary. Retain the failed attempt so observers cannot hot-retry it.
      void Promise.resolve()
        .then(() => (isCurrent() ? onLoadNewer() : undefined))
        .then(
          () => {
            if (!isCurrent()) return;
            newerAttemptRef.current = null;
            // The successful page removes the recovery control. Return focus
            // to the reading surface without moving the reader's viewport.
            if (document.activeElement === newerRetryButtonRef.current) {
              scrollRef.current?.focus({ preventScroll: true });
            }
            setNewerFailure(null);
            setNewerRetryPending(false);
          },
          (reason: unknown) => {
            if (!isCurrent()) return;
            attempt.pending = false;
            setNewerRetryPending(false);
            setNewerFailure({
              key: newerBoundaryKey,
              message: formatError(reason),
            });
          },
        );
    },
    [onLoadNewer, loadingNewer, newerBoundaryKey, formatError],
  );
  const previousSourceIdsRef = useRef(new Set());
  const previousSourceBoundaryRef = useRef(undefined);
  const readingAnchorRef = useRef(null);
  const settlementRef = useRef(null);
  const settlementAnimationRef = useRef(null);
  const stopSettlement = useCallback(() => {
    settlementAnimationRef.current?.cancel();
    settlementAnimationRef.current = null;
  }, []);
  const olderPageBudgetRef = useRef(0);
  const [olderDemand, setOlderDemand] = useState(0);
  const readableTurns = turnSummary?.rolling === true;
  const projectedGroups = useMemo(
    () => groupTimeline(t, resolvedItems, { readableTurns }),
    [resolvedItems, readableTurns, t],
  );
  // A new timeline revision (including session/turn replacement) owns layout.
  // Never leave an old decorative transform running on its retained rows.
  useLayoutEffect(() => stopSettlement, [projectedGroups, stopSettlement]);
  const annotationSources = useMemo(() => {
    const sources = new Map();
    for (const item of resolvedItems) {
      if (
        (item.kind === "user-message" ||
          item.kind === "agent-message" ||
          item.kind === "tool-call") &&
        item.annotationSource
      ) {
        sources.set(item.annotationSource.eventId, item.annotationSource);
      }
    }
    return sources;
  }, [resolvedItems]);
  const scrollRef = useRef(null);
  const topSentinelRef = useRef(null);
  const bottomSentinelRef = useRef(null);
  const previousBulkFirstKeyRef = useRef(undefined);
  const [pinned, setPinned] = useState(true);
  const { groups: allGroups, release: releaseProgress } = useReadingProgress(
    projectedGroups,
    autoFollow && pinned && !hasNewer,
    scrollRef.current,
  );
  const [canSkipTipCatchup, setCanSkipTipCatchup] = useState(false);
  const canSkipTipCatchupRef = useRef(false);
  const [bulkActive, setBulkActive] = useState(true);
  // Older history prefetch is user-driven: a window shorter than the viewport
  // + rootMargin would otherwise keep the top sentinel intersecting and fetch
  // history forever while the reader sits at the tip. Arm on first scroll-up.
  const olderPrefetchArmedRef = useRef(false);
  const [olderPrefetchArmed, setOlderPrefetchArmed] = useState(false);
  // A collapsed history tail can be shorter than the viewport. In that state
  // there is no upward scroll range, so reader intent can never arm the top
  // sentinel. Request one older page per loaded window until history either
  // fills the viewport or the host reports that no older rows remain. Each
  // attempt owns the oldest loaded item boundary across BOTH the automatic
  // underfill and reader-driven sentinel paths. Live-tail appends do not
  // advance older pagination or release that exact owner.
  const olderLoadAttemptRef = useRef(null);
  const [underfillSettledAttempt, setUnderfillSettledAttempt] = useState(
    null,
  );
  const underfillRetryReadyRef = useRef(false);
  const resizeFollowRafRef = useRef(null);
  const questionNavFrameRef = useRef(null);
  const [questionNav, setQuestionNav] = useState(null);
  const questionNavMinViewportRef = useRef(questionNavMinViewportHeight);
  questionNavMinViewportRef.current = questionNavMinViewportHeight;
  const scheduleQuestionNavRef = useRef<() => void>(() => {});
  // Unpinned is not the same as away from the tip: focusing a control in the
  // conversation (Copy, a connection card) hands the view to the reader while
  // they are still at the bottom. Offer the jump only once there is something
  // below them, or a newer window / catch-up to skip.
  const [awayFromTip, setAwayFromTip] = useState(false);
  const jumpPillShown = autoFollow && ((!pinned && awayFromTip) || hasNewer || canSkipTipCatchup);
  const firstGroupKey = allGroups[0] ? timelineGroupKey(allGroups[0]) : null;
  // Content stays invisible until the tip is hard-parked across a short
  // post-commit settle (two rAFs). That absorbs sync late layout while hidden
  // so load/remount does not ease into the tip — live tip-follow is unchanged
  // once revealed. A flash of the window's TOP is still structurally impossible.
  // The accepted-create handoff uses the reserved local id "c" so its known
  // first message is visible immediately; loaded histories still park first.
  const [revealed, setRevealed] = useState(resolvedItems[0]?.id === "c");
  // Mirror `pinned` into a ref, written ONLY by applyPinned, so the
  // ResizeObserver rAF (a stable closure) reads the live value and a snap can
  // never race a just-unpinned reader across a pending React commit.
  const pinnedRef = useRef(true);
  // History windows (`hasNewer`) have a bottom that is not the live tip.
  // Pin/follow must ignore that floor — otherwise loadNewer appends yank the
  // reader to the new page bottom. LoadOlder prepends already stay put because
  // the reader is unpinned and scroll anchoring / delta correction owns place.
  const hasNewerRef = useRef(hasNewer);
  hasNewerRef.current = hasNewer;
  // Jump-to-latest pressed while a history window is showing: the pin must
  // wait for the tip window to actually land (`hasNewer` → false) — pinning
  // immediately would snap to the bottom of the CURRENT history page and
  // page-crawl forward through the gap.
  const wantPinRef = useRef(false);
  // Jump-to-start pressed: consume on the commit that swaps the window so the
  // scroll-to-top write races neither the old DOM nor the prepend correction.
  const pendingJumpToStartRef = useRef(false);
  // Identifies the newest Jump-to-start click so a settling promise callback
  // from an earlier click can never clear a re-click's pending flag.
  const jumpToStartSeqRef = useRef(0);
  // Prepend detection: the oldest loaded item's id changes exactly when older
  // history lands (including the merge-into-first-group case where the first
  // GROUP key is retained). Item ids, not group keys, are the durable signal.
  const previousFirstItemIdRef = useRef(null);
  const previousScrollHeightRef = useRef(0);
  // Per-commit place memory for unpinned prepend restore (see layout effect).
  // Paired with max/height/client for clamp-conservation reader-intent math.
  const lastScrollTopRef = useRef(0);
  const lastMaxScrollRef = useRef(0);
  const lastScrollHeightRef = useRef(0);
  const lastClientHeightRef = useRef(0);
  /**
   * Armed by pointerdown on the scroller. Immediate geometric scroll-up unpins
   * while armed (scrollbar / touch drag). Wheel/keys unpin directly. Extension
   * jumps (Vimium) settle via scrollend while the camera is idle.
   */
  const readerIntentArmRef = useRef(false);
  /** Gesture-start geometry survives commits before the native scroll event. */
  const readerIntentStartRef = useRef<{ scrollTop: number; maxScroll: number } | null>(null);
  /**
   * Count of camera/snap scrollTop writes whose scroll echoes are not yet
   * consumed. A boolean was wrong when the browser coalesced two writes into
   * one scroll event (or fired two) — use a count, and clear to 0 on echo.
   */
  const programmaticScrollRef = useRef(0);
  /**
   * Disclosure height changes are not reader navigation. While an unpinned
   * Show more/less state is active, its clamp/native-anchor scroll echoes must
   * never geometrically re-enable bottom-follow. A later real reader navigation
   * or explicit Jump to latest releases this fence.
   */
  const disclosureKeepsUnpinnedRef = useRef(false);
  /**
   * Unarmed scroll-away observed; waiting for scrollend (or rAF fallback).
   * Blocks layout tip-follow so a stream token cannot yank before leave settles.
   */
  const pendingReaderLeaveRef = useRef(false);
  /** Fallback leave check when `scrollend` is missing (one rAF, not a timer). */
  const leaveFallbackRafRef = useRef(null);
  // Resting fold state per durable group id (see fold-memory.ts). Outlives the
  // deliberate chip remounts (activity→turn wrap, nested key flips) so a fold
  // that already settled closed — or that the reader closed — never reopens.
  const foldMemoryRef = useRef>(new Map());
  // Live work disclosures that are open. While open, that turn's progress
  // notes read inside the disclosure, so their copies above it fold away.
  const [openLiveFolds, setOpenLiveFolds] = useState>(() => new Set());
  const openLiveFoldsRef = useRef>(openLiveFolds);
  const liveNoteIdsByWorkRef = useRef>(new Map());
  const liveFoldMotionRef = useRef<{ frame: number | null; finish: () => void } | null>(null);
  // The bottom-space loan currently on the scroller, by owner.
  const liveFoldReserveRef = useRef(null);
  // writeScrollTop is declared further down; the fold reaches it through this.
  const writeScrollTopRef = useRef<(node: HTMLElement, top: number) => void>(() => {});
  const reportLiveFold = useCallback((workId: string, open: boolean) => {
    const current = openLiveFoldsRef.current;
    if (current.has(workId) === open) return;
    const next = new Set(current);
    if (open) next.add(workId);
    else next.delete(workId);
    openLiveFoldsRef.current = next;
    liveFoldMotionRef.current?.finish();
    const scroller = scrollRef.current;
    const rows = (liveNoteIdsByWorkRef.current.get(workId) ?? []).flatMap((id) => {
      const row = scroller?.querySelector(
        `[data-og-live-note-id="${cssEscapeAttribute(id)}"]`,
      );
      return row ? [row] : [];
    });
    if (!scroller || rows.length === 0) {
      setOpenLiveFolds(next);
      return;
    }
    const header = scroller.querySelector(
      `[data-og-work-id="${cssEscapeAttribute(workId)}"] [data-og-work-header="outer"]`,
    );
    const viewTop = scroller.getBoundingClientRect().top;
    const headerTop = header?.getBoundingClientRect().top;
    if (
      !open &&
      header &&
      headerTop !== undefined &&
      (headerTop < viewTop || headerTop > viewTop + scroller.clientHeight)
    ) {
      // Closed from the row's sticky header after reading down the list: the
      // row is no longer where the reader is. Like any big collapse, land on
      // it in one step — the notes return above and the closed row rests at
      // the bottom edge — rather than animating content the reader can't see.
      setOpenLiveFolds(next);
      requestFrame(() =>
        requestFrame(() => {
          const row = header.closest("[data-og-timeline-group-anchor]");
          if (!row?.isConnected) return;
          const box = row.getBoundingClientRect();
          const top = scroller.getBoundingClientRect().top;
          const bottom = top + scroller.clientHeight;
          // + the scroller's pb-6, as at the true end.
          if (box.bottom > bottom || box.top < top)
            writeScrollTopRef.current(scroller, scroller.scrollTop + box.bottom - bottom + 24);
          scheduleQuestionNavRef.current();
        }),
      );
      return;
    }
    // Opening a live row near the bottom drops its one-line preview while its
    // list grows from nothing; closing shrinks the list before the notes have
    // grown back. For that moment the page is shorter than where the reader
    // is, so the browser would clamp the scroll and everything would drop.
    // This runs before that frame paints: keep the reader's position and lend
    // one screen of space at the bottom (the shortfall keeps changing through
    // the commit) until the content has grown back into it.
    const readerTop = lastScrollTopRef.current;
    const reserve = readerTop > 0.5 ? scroller.clientHeight : 0;
    const loan = {};
    if (reserve > 0) {
      liveFoldReserveRef.current = loan;
      scroller.style.paddingBottom = `calc(1.5rem + ${reserve}px)`;
      if (Math.abs(scroller.scrollTop - readerTop) > 0.5)
        writeScrollTopRef.current(scroller, readerTop);
    }
    const releaseReserve = (deadline: number) => {
      // Only this motion's loan; a newer toggle may have replaced it.
      if (reserve === 0 || liveFoldReserveRef.current !== loan) return;
      const needed = scroller.scrollTop + scroller.clientHeight - (scroller.scrollHeight - reserve);
      if (needed > 0.5 && performance.now() < deadline && scroller.isConnected) {
        requestFrame(() => releaseReserve(deadline));
        return;
      }
      liveFoldReserveRef.current = null;
      scroller.style.removeProperty("padding-bottom");
    };
    // The notes fold into (or out of) the work row in one short motion. The
    // row glides to where it should rest: opening, into the first note's place
    // (or the top edge, when that note was above view) so its list starts just
    // beneath; closing, down below its returning notes but never past the
    // bottom edge. When everything is in view that is exactly what happens on
    // its own; otherwise the scroll is written in the same frame as the
    // heights, so it cannot lag. Native scroll anchoring is suspended for the
    // motion; inline styles pin the current look across the state commit, end
    // exactly at the classes' resting geometry, and are then removed.
    const workRow = header?.closest("[data-og-timeline-group-anchor]") ?? null;
    const rowStart = workRow?.getBoundingClientRect().top;
    const viewBottom = viewTop + scroller.clientHeight;
    let landing =
      open && rowStart !== undefined
        ? Math.max(rows[0]!.getBoundingClientRect().top, viewTop)
        : null;
    const heights = rows.map((row) => (open ? row.offsetHeight : 0));
    const apply = (progress: number) => {
      const shown = open ? 1 - progress : progress;
      rows.forEach((note, index) => {
        // The list gap travels inside the row as padding (margin cancels it),
        // so a shrinking row never overlaps the one above it mid-motion.
        note.style.marginTop = `${-LIVE_NOTE_GAP_PX}px`;
        note.style.paddingTop = `${LIVE_NOTE_GAP_PX * shown}px`;
        note.style.height = `${(heights[index]! + LIVE_NOTE_GAP_PX) * shown}px`;
        note.style.opacity = String(shown);
      });
      if (workRow && rowStart !== undefined && landing !== null) {
        const drift =
          workRow.getBoundingClientRect().top - (rowStart + (landing - rowStart) * progress);
        if (Math.abs(drift) > 0.5) writeScrollTopRef.current(scroller, scroller.scrollTop + drift);
      }
    };
    for (const row of rows) {
      row.style.boxSizing = "border-box";
      row.style.overflow = "hidden";
      row.style.overflowAnchor = "none";
    }
    scroller.style.overflowAnchor = "none";
    apply(0);
    const fold: { frame: number | null; finish: () => void } = {
      frame: null,
      finish: () => {
        if (fold.frame != null) cancelFrame(fold.frame);
        for (const row of rows) {
          for (const property of [
            "height",
            "margin-top",
            "padding-top",
            "opacity",
            "overflow",
            "overflow-anchor",
            "box-sizing",
          ])
            row.style.removeProperty(property);
        }
        scroller.style.removeProperty("overflow-anchor");
        releaseReserve(performance.now() + LIVE_NOTE_RESERVE_MS);
        // The row may now be pinned to the top without any scroll: re-place
        // the up arrow (it sits below a pinned work header).
        scheduleQuestionNavRef.current();
        if (liveFoldMotionRef.current === fold) liveFoldMotionRef.current = null;
      },
    };
    liveFoldMotionRef.current = fold;
    const duration = prefersReducedMotion() ? 0 : LIVE_NOTE_FOLD_MS;
    let began: number | null = null;
    const step = (now: number) => {
      fold.frame = null;
      if (!rows.every((row) => row.isConnected)) return fold.finish();
      if (!open && began === null) {
        // Unfolding: the natural heights are known only once the folded
        // class is gone; the inline styles still hold them at zero.
        rows.forEach((note, index) => (heights[index] = note.scrollHeight));
        if (workRow && rowStart !== undefined) {
          const returning = heights.reduce((sum, height) => sum + height + LIVE_NOTE_GAP_PX, 0);
          // The closed row is its header plus one preview line (its list is
          // still collapsing now, so the row's own height is not final yet).
          const closedRow = 2 * (header?.getBoundingClientRect().height ?? 0);
          const lowest = viewBottom - closedRow - 24;
          landing = Math.min(rowStart + returning, Math.max(lowest, rowStart));
        }
      }
      began ??= now;
      const linear = duration === 0 ? 1 : Math.min(1, (now - began) / duration);
      apply(1 - (1 - linear) ** 3);
      if (linear < 1) fold.frame = requestFrame(step);
      else fold.finish();
    };
    fold.frame = requestFrame(step);
    setOpenLiveFolds(next);
  }, []);
  useEffect(
    () => () => {
      liveFoldMotionRef.current?.finish();
    },
    [],
  );
  const userMessageDisclosureMemoryRef = useRef>(new Map());
  const seenActivityIdsRef = useRef>(new Set());
  const firstItemGroupKeyRef = useRef(null);
  const firstItemGroupOffsetTopRef = useRef(null);
  const firstItemContentTopRef = useRef(null);
  const firstItemId = resolvedItems[0]?.id ?? null;
  // Pagination ownership follows the oldest committed input, before host
  // filtering and grouping. A page containing only suppressed auth notices
  // still advances this receipt, while live-tail appends leave it unchanged.
  // Promise fulfillment alone does neither, so delayed prepends stay fenced.
  const underfillRetryReady = (underfillRetryReadyRef.current = !!(
    underfillSettledAttempt &&
    underfillSettledAttempt === olderLoadAttemptRef.current &&
    hasOlder &&
    onLoadOlder &&
    !loadingOlder
  ));
  // Bulk paints (the initial tail window, a prepended older window — detected
  // by the first group key changing) must not run per-row entrance animations.
  const firstKeyChangedForBulk =
    previousBulkFirstKeyRef.current !== undefined &&
    previousBulkFirstKeyRef.current !== firstGroupKey;
  const bulkRender = allGroups.length > 0 && (bulkActive || firstKeyChangedForBulk);
  const groups = useStableTimelineGroupKeys(allGroups, !bulkRender);
  // Each live progress note → whether its turn's work disclosure is open.
  const [liveNoteFolds, liveNoteIdsByWork] = useMemo(() => {
    const folds = new Map();
    const byWork = new Map();
    for (const { group } of groups) {
      if (group.kind !== "activity" || !group.work?.liveNoteIds) continue;
      byWork.set(group.id, group.work.liveNoteIds);
      for (const id of group.work.liveNoteIds) folds.set(id, openLiveFolds.has(group.id));
    }
    return [folds, byWork] as const;
  }, [groups, openLiveFolds]);
  useLayoutEffect(() => {
    liveNoteIdsByWorkRef.current = liveNoteIdsByWork;
  }, [liveNoteIdsByWork]);
  const turnsWithOutput = useMemo(
    () =>
      new Set(
        resolvedItems.flatMap((item) =>
          "turnId" in item &&
          item.turnId &&
          (item.kind === "tool-call" ||
            ((item.kind === "agent-message" || item.kind === "reasoning") && item.text.trim()))
            ? [item.turnId]
            : [],
        ),
      ),
    [resolvedItems],
  );

  const applyCanSkipTipCatchup = useCallback((value: boolean) => {
    if (canSkipTipCatchupRef.current !== value) {
      canSkipTipCatchupRef.current = value;
      setCanSkipTipCatchup(value);
    }
  }, []);

  // The ONLY writer of the pinned flag. Ref and state move together, so
  // behavior (refs read by rAF callbacks) and rendering (the anchor class,
  // the Jump-to-latest button) can never desync.
  const applyPinned = useCallback(
    (value: boolean) => {
      if (pinnedRef.current !== value) {
        pinnedRef.current = value;
        setPinned(value);
      }
      if (!value) {
        applyCanSkipTipCatchup(false);
      }
    },
    [applyCanSkipTipCatchup],
  );

  const rearmOlderPrefetchAfterLeavingTop = useCallback((node: HTMLElement) => {
    if (node.scrollTop > OLDER_PREFETCH_MARGIN_PX) {
      const state = olderLoadAttemptRef.current?.[1];
      if (state === 2 || state === 3) {
        olderLoadAttemptRef.current = null;
      }
    }
  }, []);

  const revealedRef = useRef(revealed);
  revealedRef.current = revealed;

  // Pure tip-follow camera. Pin intent uses clamp conservation, not timers.
  const followRef = useRef(createTipFollowState());
  const followFrameRef = useRef(null);
  const contentShrinkBaselineRef = useRef(null);

  const syncScrollBaseline = useCallback((node: HTMLElement) => {
    lastScrollTopRef.current = node.scrollTop;
    lastMaxScrollRef.current = maxScrollOf(node);
    lastScrollHeightRef.current = node.scrollHeight;
    lastClientHeightRef.current = node.clientHeight;
  }, []);

  const writeScrollTop = useCallback((node: HTMLElement, top: number) => {
    const next = Math.max(0, top);
    const before = node.scrollTop;
    if (before === next) {
      return;
    }
    // Unpinned camera/anchor writes invalidate return intent. A pinned
    // pointer gesture still needs its cumulative leave budget across writes.
    if (!pinnedRef.current || !readerIntentArmRef.current) readerIntentStartRef.current = null;
    programmaticScrollRef.current += 1;
    node.scrollTop = next;
    if (node.scrollTop === before) {
      // The engine floored a sub-device-pixel write to a no-op: no scroll echo
      // will ever fire. Counting it would leak the echo count and silently eat
      // a later REAL reader scroll as programmatic.
      programmaticScrollRef.current -= 1;
    }
  }, []);
  writeScrollTopRef.current = writeScrollTop;

  const cancelLeaveFallback = useCallback(() => {
    if (leaveFallbackRafRef.current != null) {
      cancelFrame(leaveFallbackRafRef.current);
      leaveFallbackRafRef.current = null;
    }
  }, []);

  const clearPendingReaderLeave = useCallback(() => {
    pendingReaderLeaveRef.current = false;
    cancelLeaveFallback();
  }, [cancelLeaveFallback]);

  const clearReaderIntent = useCallback(() => {
    readerIntentArmRef.current = false;
    readerIntentStartRef.current = null;
  }, []);

  const stopFollow = useCallback(() => {
    contentShrinkBaselineRef.current = null;
    followRef.current = tipFollowCancel(followRef.current);
    if (followFrameRef.current != null) {
      cancelFrame(followFrameRef.current);
      followFrameRef.current = null;
    }
  }, []);

  /** Reader left the tip — wheel, keyboard, pointer-armed scroll-up, or scrollend. */
  const releasePinFromReader = useCallback(() => {
    if (!autoFollow || !pinnedRef.current || hasNewerRef.current) {
      return;
    }
    clearReaderIntent();
    clearPendingReaderLeave();
    stopFollow();
    applyPinned(false);
    if (wantPinRef.current) {
      wantPinRef.current = false;
    }
    if (!olderPrefetchArmedRef.current) {
      olderPrefetchArmedRef.current = true;
      setOlderPrefetchArmed(true);
    }
  }, [autoFollow, applyPinned, clearPendingReaderLeave, clearReaderIntent, stopFollow]);

  useEffect(() => {
    if (!readableTurns) return;
    const node = scrollRef.current;
    if (!node) return;
    const onSelection = () => {
      const selection = node.ownerDocument.getSelection();
      if (
        selection &&
        !selection.isCollapsed &&
        node.contains(selection.anchorNode) &&
        node.contains(selection.focusNode)
      ) {
        stopSettlement();
        releasePinFromReader();
        disclosureKeepsUnpinnedRef.current = true;
      }
    };
    node.ownerDocument.addEventListener("selectionchange", onSelection);
    return () => node.ownerDocument.removeEventListener("selectionchange", onSelection);
  }, [readableTurns, releasePinFromReader, stopSettlement]);

  /**
   * Settled away from the tip while the camera is idle — Vimium / unfocused
   * PageUp. Folds are recovered by layout tip-follow before this fires at tip.
   */
  const releasePinAfterScrollSettled = useCallback(
    (node: HTMLElement) => {
      if (!autoFollow || !pinnedRef.current || hasNewerRef.current) {
        return;
      }
      if (programmaticScrollRef.current > 0) {
        return;
      }
      if (followRef.current.running || followFrameRef.current != null) {
        return;
      }
      if (isNearBottom(node) || maxScrollOf(node) <= 1) {
        clearPendingReaderLeave();
        return;
      }
      releasePinFromReader();
      rearmOlderPrefetchAfterLeavingTop(node);
    },
    [autoFollow, clearPendingReaderLeave, rearmOlderPrefetchAfterLeavingTop, releasePinFromReader],
  );

  const scheduleLeaveFallback = useCallback(() => {
    // Prefer scrollend when the engine supports it.
    if (supportsScrollEndEvent()) {
      return;
    }
    cancelLeaveFallback();
    leaveFallbackRafRef.current = requestFrame(() => {
      leaveFallbackRafRef.current = null;
      const current = scrollRef.current;
      if (current) {
        releasePinAfterScrollSettled(current);
      }
    });
  }, [cancelLeaveFallback, releasePinAfterScrollSettled]);

  const requestEarlierFromReader = () => {
    // Upward navigation replaces any earlier downward gesture, even when
    // already unpinned and the engine never delivered scrollend.
    clearReaderIntent();
    releasePinFromReader();
    wantPinRef.current = false;
    // A stationary upward gesture is still demand. Successful short/folded
    // pages may never create enough range to leave the prefetch band.
    olderPageBudgetRef.current = 8;
    if (olderLoadAttemptRef.current?.[1] === 2) {
      olderLoadAttemptRef.current = null;
      setOlderDemand((value) => value + 1);
    }
  };
  const touchPositionRef = useRef<{ x: number; y: number } | null>(null);

  const onWheel = (event: {
    deltaY: number;
    deltaX: number;
    target: EventTarget | null;
    currentTarget: EventTarget | null;
  }) => {
    // Nested overflow (code / notice pre) or mostly-horizontal pan: not
    // timeline reader intent. A real timeline wheel in either direction
    // releases the disclosure fence; downward movement may then re-pin
    // naturally when it reaches the bottom.
    if (Math.abs(event.deltaY) <= Math.abs(event.deltaX)) {
      return;
    }
    if (wheelConsumedByNestedScrollable(event)) {
      return;
    }
    stopSettlement();
    disclosureKeepsUnpinnedRef.current = false;
    programmaticScrollRef.current = 0;
    if (event.deltaY >= 0) {
      const node = scrollRef.current;
      readerIntentStartRef.current = node
        ? { scrollTop: node.scrollTop, maxScroll: maxScrollOf(node) }
        : null;
      return;
    }
    requestEarlierFromReader();
  };

  /** Touch / stylus / mouse drag on the scroller — explicit leave (not layout). */
  const onPointerDown = (event: {
    button: number;
    pointerType: string;
    target: EventTarget | null;
    currentTarget: EventTarget | null;
  }) => {
    // Primary button / touch / pen only. Ignore right-click etc.
    if (event.button && event.pointerType === "mouse") {
      return;
    }
    stopSettlement();
    // Clicks on chips/buttons/links must not arm — their settle collapse
    // also drops scrollTop and would false-unpin. Drag on prose/scroller may.
    if (
      event.target instanceof Element &&
      event.target.closest("button, a, input, textarea, select, [role='button']")
    ) {
      return;
    }
    disclosureKeepsUnpinnedRef.current = false;
    programmaticScrollRef.current = 0;
    const node =
      event.currentTarget instanceof HTMLElement ? event.currentTarget : scrollRef.current;
    readerIntentArmRef.current = true;
    readerIntentStartRef.current = node
      ? { scrollTop: node.scrollTop, maxScroll: maxScrollOf(node) }
      : null;
  };

  const onKeyDown = (event: { key: string; currentTarget: EventTarget | null }) => {
    if (
      event.key === "ArrowUp" ||
      event.key === "ArrowDown" ||
      event.key === "PageUp" ||
      event.key === "PageDown" ||
      event.key === "Home" ||
      event.key === "End"
    ) {
      stopSettlement();
      disclosureKeepsUnpinnedRef.current = false;
      // Reader navigation supersedes a pending camera/anchor scroll echo,
      // including downward keys that do not explicitly release the pin.
      programmaticScrollRef.current = 0;
      const node = scrollRef.current;
      readerIntentStartRef.current = node
        ? { scrollTop: node.scrollTop, maxScroll: maxScrollOf(node) }
        : null;
    }
    if (event.key !== "ArrowUp" && event.key !== "PageUp" && event.key !== "Home") {
      return;
    }
    requestEarlierFromReader();
  };

  const snapToBottom = useCallback(
    (node: HTMLElement) => {
      clearReaderIntent();
      stopFollow();
      cancelLeaveFallback();
      writeScrollTop(node, Math.max(0, node.scrollHeight - node.clientHeight));
      syncScrollBaseline(node);
      followRef.current = {
        ...followRef.current,
        lastHeight: node.scrollHeight,
        lastClientHeight: node.clientHeight,
        cameraTop: null,
      };
      applyCanSkipTipCatchup(false);
    },
    [
      applyCanSkipTipCatchup,
      cancelLeaveFallback,
      clearReaderIntent,
      stopFollow,
      syncScrollBaseline,
      writeScrollTop,
    ],
  );

  const beginUserMessageDisclosureChange = useCallback(
    (messageBody: HTMLElement, disclosureControl: HTMLElement) => {
      const node = scrollRef.current;
      if (!node || !node.contains(messageBody)) {
        return null;
      }
      const keepBottom = autoFollow && pinnedRef.current && !hasNewerRef.current;
      if (keepBottom) {
        return () => {
          const current = scrollRef.current;
          if (current) {
            snapToBottom(current);
          }
        };
      }

      disclosureKeepsUnpinnedRef.current = true;

      const scrollerRect = node.getBoundingClientRect();
      const group = messageBody.closest("[data-og-timeline-group-anchor]");
      const groupRect = group?.getBoundingClientRect();
      // Expanding from a visible message top keeps the beginning in place.
      // Collapsing after reading deep in the message keeps the disclosure
      // control in place because the message top is already above the viewport.
      const anchor =
        group &&
        groupRect &&
        groupRect.top >= scrollerRect.top - 1 &&
        groupRect.top < scrollerRect.bottom
          ? group
          : disclosureControl;
      const beforeTop = anchor.getBoundingClientRect().top - scrollerRect.top;

      return () => {
        const current = scrollRef.current;
        if (!current || !current.contains(anchor)) {
          return;
        }
        const currentScrollerTop = current.getBoundingClientRect().top;
        const afterTop = anchor.getBoundingClientRect().top - currentScrollerTop;
        const delta = afterTop - beforeTop;
        if (Math.abs(delta) > 0.5) {
          writeScrollTop(current, current.scrollTop + delta);
        }
        applyPinned(false);
        syncScrollBaseline(current);
      };
    },
    [applyPinned, autoFollow, snapToBottom, syncScrollBaseline, writeScrollTop],
  );

  const userMessageDisclosureContext = useMemo(
    () => ({
      expandedByMessageId: userMessageDisclosureMemoryRef.current,
      beginChange: beginUserMessageDisclosureChange,
      labels: {
        showMore: userMessageDisclosureLabels?.showMore,
        showLess: userMessageDisclosureLabels?.showLess,
      },
    }),
    [
      beginUserMessageDisclosureChange,
      userMessageDisclosureLabels?.showMore,
      userMessageDisclosureLabels?.showLess,
    ],
  );
  const timelineGroupEntryContext = useMemo(
    () => ({
      userMessageDisclosureContext,
      behavior: {
        renderMessageActions,
        renderMessageText: renderSearchableMessageText,
        onOpenSession,
        onMemoryClick,
        onReconnect,
        renderAuthNeeded,
        resolveProviderLogo,
        toolRegistry,
        loadRetainedScreenshot,
        loadRetainedArtifact,
        loadVideoArtifactPlayback,
        genieLoading,
        turnSummary,
      },
    }),
    [
      loadRetainedArtifact,
      loadRetainedScreenshot,
      loadVideoArtifactPlayback,
      onMemoryClick,
      onOpenSession,
      onReconnect,
      renderAuthNeeded,
      renderMessageActions,
      renderSearchableMessageText,
      resolveProviderLogo,
      toolRegistry,
      genieLoading,
      turnSummary,
      userMessageDisclosureContext,
    ],
  );

  const releaseSearchPin = useCallback(() => {
    applyPinned(false);
    stopFollow();
    setRevealed(true);
  }, [applyPinned, stopFollow]);
  useTimelineSearchNavigation(
    scrollRef,
    searchItem?.id,
    searchTarget,
    releaseSearchPin,
    resolvedItems,
  );

  const requestOlderIfUnderfilled = useCallback(
    (node: HTMLElement, retry?: OlderLoadAttempt) => {
      let currentAttempt = olderLoadAttemptRef.current;
      if (retry && (currentAttempt !== retry || retry[0] !== olderBoundaryKey)) {
        // AnimatePresence may retain the exiting button briefly. Its stale
        // handler cannot replace a newer exact owner.
        return;
      }
      const underfilled = maxScrollOf(node) <= 1;
      if (!retry && underfilled && currentAttempt?.[1] === 3) {
        // A receipted prefetch already declined or failed while the viewport
        // was still scrollable. Collapse may later remove the scroll range;
        // promote that exact owner to Retry without issuing another request.
        currentAttempt[1] = 0;
        setUnderfillSettledAttempt(currentAttempt);
        return;
      }
      if (!retry && underfilled && currentAttempt?.[1] === 2) {
        // A settled ordinary prefetch owns only this visit to the top band.
        // If its resulting window cannot scroll, yield to automatic underfill
        // so the reader is not stranded behind a cooldown they cannot exit.
        currentAttempt = olderLoadAttemptRef.current = null;
      }
      // clientHeight=0 is pre-layout/headless, not evidence that the rendered
      // history underfills a real viewport.
      if (
        node.clientHeight <= 1 ||
        (!retry && !underfilled) ||
        !hasOlder ||
        loadingOlder ||
        !onLoadOlder ||
        (currentAttempt && !retry)
      ) {
        return;
      }
      const attempt: OlderLoadAttempt = (olderLoadAttemptRef.current = [olderBoundaryKey]);
      setUnderfillSettledAttempt(null);
      // Every underfill request owns the ordinary sentinel too, even if reader
      // intent had not armed it yet when this short-window request began.
      const noProgress = () => {
        if (scrollRef.current && olderLoadAttemptRef.current === attempt) {
          setUnderfillSettledAttempt(attempt);
        }
      };
      // Legacy fire-and-forget callbacks keep the one-shot behavior. Hosts
      // that return the real promise opt into safe rejection/no-progress retry;
      // exact `false` is the first-party request-not-accepted receipt.
      // All other fulfillment retains this exact owner until its prepend
      // boundary commits; promise settlement alone cannot prove progress.
      invokeOlderLoad(onLoadOlder, noProgress, attempt, !retry);
    },
    [hasOlder, loadingOlder, olderBoundaryKey, onLoadOlder],
  );
  const scheduleQuestionNav = useCallback(() => {
    if (!readableTurns || questionNavFrameRef.current != null) {
      return;
    }
    questionNavFrameRef.current = requestFrame(() => {
      questionNavFrameRef.current = null;
      const node = scrollRef.current;
      // A short viewport (a narrow embed above a decision card) has no room
      // for a floating pill that would cover the very rows being read.
      // clientHeight <= 1 is pre-layout/headless, not a short viewport.
      const roomy =
        node !== null &&
        (node.clientHeight <= 1 || node.clientHeight >= questionNavMinViewportRef.current);
      const next = node && roomy ? readQuestionNav(node) : null;
      setQuestionNav((current) => (sameQuestionNav(current, next) ? current : next));
    });
  }, [readableTurns]);
  scheduleQuestionNavRef.current = scheduleQuestionNav;
  useEffect(
    () => () => {
      if (questionNavFrameRef.current != null) {
        cancelFrame(questionNavFrameRef.current);
        questionNavFrameRef.current = null;
      }
    },
    [],
  );
  const jumpToQuestion = (key: string) => {
    const node = scrollRef.current;
    const top = node ? contentTopOf(node, key) : null;
    if (!node || top === null) {
      return;
    }
    // Explicit reader navigation: leave the tip and park the question on top.
    releasePinFromReader();
    wantPinRef.current = false;
    disclosureKeepsUnpinnedRef.current = false;
    writeScrollTop(node, Math.max(0, top - QUESTION_NAV_MARGIN_PX));
    // Hand keyboard/accessibility focus to the actual destination. This also
    // gives native scroll anchoring the new question rather than an old work
    // row when the bounded context subsequently gains a later history page.
    node
      .querySelector(`[data-og-group-key="${cssEscapeAttribute(key)}"]`)
      ?.focus({ preventScroll: true });
    syncScrollBaseline(node);
    scheduleQuestionNav();
  };

  // Land once on the moment an inbox item or a notification points at.
  // Deliberately dependency-free: it retries after every commit until the
  // target row is laid out, and the ref guard makes the state update run once
  // per focus sequence, so it cannot chain renders.
  const focusedSequenceRef = useRef(null);
  // oxlint-disable-next-line react-hooks/exhaustive-deps -- retries each commit; ref-guarded to run once
  useLayoutEffect(() => {
    if (!focusSequence || focusedSequenceRef.current === focusSequence) return;
    const index = timelineGroupIndexAtSequence(
      groups.map((entry) => entry.group),
      focusSequence,
    );
    const key = index >= 0 ? groups[index]?.key : undefined;
    const node = scrollRef.current;
    if (!key || !node || contentTopOf(node, key) === null) return;
    focusedSequenceRef.current = focusSequence;
    setRevealed(true);
    jumpToQuestion(key);
  });

  const driveFollowRef = useRef<(node: HTMLElement, now?: number) => void>(
    requestOlderIfUnderfilled as (node: HTMLElement, now?: number) => void,
  );
  const driveFollow = useCallback(
    (node: HTMLElement, nowMs?: number) => {
      if (!pinnedRef.current || hasNewerRef.current) {
        stopFollow();
        return;
      }
      // Reader/extension leave in flight — do not yank back before scrollend.
      if (pendingReaderLeaveRef.current) {
        stopFollow();
        return;
      }
      cancelLeaveFallback();
      // Prefer the rAF timestamp so ease integrates against vsync (and tests
      // can advance a synthetic clock via requestAnimationFrame callbacks).
      const now =
        typeof nowMs === "number"
          ? nowMs
          : typeof performance !== "undefined"
            ? performance.now()
            : Date.now();
      let previousHeight = followRef.current.lastHeight;
      const previousObservedHeight = lastScrollHeightRef.current;
      if (
        contentShrinkBaselineRef.current &&
        previousObservedHeight > 0 &&
        node.scrollHeight > previousObservedHeight
      ) {
        // A reversal ends the collapse sequence. If it remains below the held
        // baseline, adopt the recovered height; if it grew beyond the baseline,
        // leave that baseline for tipFollowStep to observe as real growth.
        contentShrinkBaselineRef.current = null;
        if (node.scrollHeight < previousHeight) {
          followRef.current = {
            ...followRef.current,
            lastHeight: node.scrollHeight,
            cameraTop: null,
          };
          previousHeight = node.scrollHeight;
        }
      }
      const shrinkObservation = tipFollowObserveContentShrink(
        contentShrinkBaselineRef.current,
        previousHeight,
        lastScrollTopRef.current,
        node.scrollHeight,
        node.clientHeight,
      );
      contentShrinkBaselineRef.current = shrinkObservation.baseline;
      // Settle-collapse: compensate Δh from the pre-shrink baseline (browser
      // may already have clamped — don't double-subtract). Keep the follow rAF
      // alive so when collapse ends (or stream resumes) we ease instead of a
      // hard stop → flick. Do NOT tip-ease on the same frame as a real shrink
      // (that fight was the top-of-viewport flicker).
      if (shrinkObservation.compensatedScrollTop !== null) {
        let nextTop = shrinkObservation.compensatedScrollTop;
        // A chrome/composer dock can land on the same frame as a settle-fold
        // (the "turn blocked" moment). Compensate BOTH in one write — adopting
        // the shrunk clientHeight below without gluing left the chrome height
        // behind as cold tip debt.
        const previousClient = followRef.current.lastClientHeight;
        if (previousClient > 0 && node.clientHeight < previousClient - TIP_FOLLOW_SHRINK_EPS_PX) {
          nextTop = tipFollowCompensateViewportShrink(
            nextTop,
            previousClient,
            node.clientHeight,
            node.scrollHeight,
          );
        }
        writeScrollTop(node, nextTop);
        syncScrollBaseline(node);
        followRef.current = {
          ...followRef.current,
          lastHeight: node.scrollHeight,
          lastClientHeight: node.clientHeight,
          running: true,
          lastTs: now,
          // A direct glue write re-based the camera — drop any stale fraction.
          cameraTop: null,
        };
        if (followFrameRef.current == null) {
          followFrameRef.current = requestFrame((frameNow) => {
            followFrameRef.current = null;
            const current = scrollRef.current;
            if (current) {
              driveFollowRef.current(current, frameNow);
            }
          });
        }
        return;
      }
      const result = tipFollowStep(followRef.current, {
        scrollTop: node.scrollTop,
        scrollHeight: node.scrollHeight,
        clientHeight: node.clientHeight,
        now,
        pinned: true,
        reducedMotion: prefersReducedMotion(),
        revealed: revealedRef.current,
      });
      followRef.current = result.state;
      writeScrollTop(node, result.scrollTop);
      syncScrollBaseline(node);
      applyCanSkipTipCatchup(
        result.state.running &&
          maxScrollOf(node) - node.scrollTop >= JUMP_TO_LATEST_CATCHUP_DEBT_PX,
      );
      if (result.state.running) {
        cancelLeaveFallback();
        if (followFrameRef.current == null) {
          followFrameRef.current = requestFrame((frameNow) => {
            followFrameRef.current = null;
            const current = scrollRef.current;
            if (current) {
              driveFollowRef.current(current, frameNow);
            }
          });
        }
      } else if (followFrameRef.current != null) {
        cancelFrame(followFrameRef.current);
        followFrameRef.current = null;
      }
    },
    [applyCanSkipTipCatchup, cancelLeaveFallback, stopFollow, syncScrollBaseline, writeScrollTop],
  );
  driveFollowRef.current = driveFollow;

  useEffect(() => stopFollow, [stopFollow]);
  useEffect(() => () => cancelLeaveFallback(), [cancelLeaveFallback]);

  // The single post-commit scroll authority. Runs after EVERY commit (no dep
  // list): any commit may change content height, and the decision is cheap.
  // Also the ONLY writer of the prepend-correction baselines
  // (previousFirstItemIdRef / previousScrollHeightRef / group offset maps).
  // oxlint-disable-next-line react-hooks/exhaustive-deps -- Deliberately runs after every commit.
  useLayoutEffect(() => {
    const node = scrollRef.current;
    if (!node) {
      return;
    }
    const previousFirstItemId = previousFirstItemIdRef.current;
    const previousFirstItemGroupKey = firstItemGroupKeyRef.current;
    const previousFirstItemGroupOffsetTop = firstItemGroupOffsetTopRef.current;
    const previousItemContentTop = firstItemContentTopRef.current;
    const previousScrollTop = lastScrollTopRef.current;
    const previousMaxScroll = lastMaxScrollRef.current;
    const wasAtLiveTailBeforeCommit =
      previousMaxScroll <= 1 ||
      previousMaxScroll - previousScrollTop < Math.min(PIN_THRESHOLD_PX, previousMaxScroll);
    const firstItemChanged = !!previousFirstItemId && firstItemId !== previousFirstItemId;
    const sourceChanged = olderBoundaryKey !== previousSourceBoundaryRef.current;
    const retainedSource =
      sourceItems?.some((item) => previousSourceIdsRef.current.has(item.id)) ?? false;
    const prepended = sourceChanged && retainedSource;
    previousSourceBoundaryRef.current = olderBoundaryKey;
    previousSourceIdsRef.current = new Set(sourceItems?.map((item) => item.id));
    const readingAnchor = readingAnchorRef.current;
    readingAnchorRef.current = null;
    const settlement = settlementRef.current;
    settlementRef.current = null;
    const attempt = olderLoadAttemptRef.current;
    const committedZeroOverlapOlderReplacement = !!(
      attempt?.[2]?.committed &&
      sourceChanged &&
      !retainedSource
    );
    const restorePrependAnchor = () => {
      const correction = readingAnchor && timelineAnchorCorrection(node, readingAnchor);
      if (correction != null) {
        if (Math.abs(correction) > 1) writeScrollTop(node, node.scrollTop + correction);
        return;
      }
      // Keep the reader on the same retained rows. Prefer the exact first-item
      // content coordinate (needed when a prepend merges inside one group),
      // then the retained group offset, then scrollHeight as a final fallback.
      // If native anchoring already applied the same shift, leave scrollTop.
      let delta: number | null = null;
      if (previousFirstItemId && previousItemContentTop != null) {
        const itemEl = node.querySelector(
          `[data-og-item="${cssEscapeAttribute(previousFirstItemId)}"]`,
        );
        if (itemEl instanceof HTMLElement) {
          const scrollerTop = node.getBoundingClientRect().top;
          const currentItemTop = itemEl.getBoundingClientRect().top - scrollerTop + node.scrollTop;
          delta = Math.round(currentItemTop - previousItemContentTop);
        }
      }
      const anchorKey = previousFirstItemGroupKey;
      const anchorEl =
        anchorKey != null
          ? node.querySelector(`[data-og-group-key="${cssEscapeAttribute(anchorKey)}"]`)
          : null;
      if (
        delta == null &&
        anchorEl instanceof HTMLElement &&
        previousFirstItemGroupOffsetTop != null
      ) {
        const moved = Math.round(anchorEl.offsetTop - previousFirstItemGroupOffsetTop);
        if (moved) {
          delta = moved;
        }
      }
      if (delta == null) {
        const heightDelta = Math.round(node.scrollHeight - previousScrollHeightRef.current);
        if (heightDelta > 0) {
          delta = heightDelta;
        }
      }
      if (delta != null) {
        const expected = previousScrollTop + delta;
        if (Math.abs(node.scrollTop - expected) > 2) {
          writeScrollTop(node, expected);
        }
      }
    };
    if (pendingJumpToStartRef.current && firstItemChanged) {
      // The oldest window landed — jump against the NEW DOM, and skip the
      // prepend correction (it would shift the reader away from the top).
      pendingJumpToStartRef.current = false;
      stopFollow();
      writeScrollTop(node, 0);
    } else if (wantPinRef.current && !hasNewer) {
      // Jump-to-latest was pressed on a history window and the tip window is
      // in THIS commit — consume pre-paint so the first tip frame is already
      // at the bottom (post-paint consumption flashed one clamped frame).
      wantPinRef.current = false;
      if (autoFollow) {
        applyPinned(true);
        snapToBottom(node);
      }
    } else if (committedZeroOverlapOlderReplacement) {
      // An oldest-directed bounded page can replace every previously mounted
      // row. Its receipt is the only causal proof that this is backward
      // progress rather than live-tail forward eviction. Anchor at the bottom
      // seam before recording this window's scroll baselines so an unpinned
      // reader stays adjacent to the history they were reading.
      clearPendingReaderLeave();
      stopFollow();
      writeScrollTop(node, maxScrollOf(node));
      if (attempt?.[1] === 1 && pinnedRef.current) {
        applyPinned(false);
      }
    } else if (prepended) {
      const olderAttemptState = olderLoadAttemptRef.current?.[1];
      const underfillOwned =
        !!olderLoadAttemptRef.current &&
        olderAttemptState !== 1 &&
        olderAttemptState !== 2 &&
        olderAttemptState !== 3;
      if (
        autoFollow &&
        pinnedRef.current &&
        !hasNewer &&
        !pendingReaderLeaveRef.current &&
        (wasAtLiveTailBeforeCommit || underfillOwned)
      ) {
        // Still following the live tip: underfill, or a prefetch the reader
        // started and then returned from. Park at the new tip. A stale pin
        // while they are actually up in a short window (gap is not inside
        // PIN_THRESHOLD of maxScroll, so wasAtLiveTail is false) restores.
        clearPendingReaderLeave();
        snapToBottom(node);
      } else {
        restorePrependAnchor();
        if (autoFollow && pinnedRef.current && !hasNewer) {
          // Geometry or a pending extension/programmatic leave proves the
          // reader was browsing history even if the pin ref has not settled.
          stopFollow();
          clearPendingReaderLeave();
          applyPinned(false);
        }
      }
    } else if (!pinnedRef.current && readingAnchor && readableTurns) {
      // A live work row moves behind newly streamed prose. Correct only the
      // residual movement native anchoring did not absorb, using the reader's
      // retained paragraph/control rather than the moving row's outer box.
      const correction = timelineAnchorCorrection(node, readingAnchor);
      if (correction !== null && Math.abs(correction) > 1) {
        writeScrollTop(node, node.scrollTop + correction);
      }
      const focused = readingAnchor.find((anchor) => anchor.focused)?.element;
      if (
        focused &&
        node.contains(focused) &&
        node.ownerDocument.activeElement === node.ownerDocument.body
      ) {
        focused.focus({ preventScroll: true });
      }
    } else if (autoFollow && pinnedRef.current && !hasNewer) {
      // Load/remount (still hidden): hard-park. Live tip after reveal: ease.
      // Pending unarmed leave: tip *growth* must not yank (Vimium during stream).
      // Flat/shrink commits (fold) still recover — height did not grow under us.
      if (!revealedRef.current) {
        snapToBottom(node);
      } else if (pendingReaderLeaveRef.current) {
        if (node.scrollHeight <= lastScrollHeightRef.current) {
          clearPendingReaderLeave();
          driveFollow(node);
        }
      } else {
        driveFollow(node);
      }
    }
    // After a prepend, if restore left us below the top prefetch band,
    // re-arm so a later approach can load again. Still cooling while parked
    // inside the band (short pages) — that stops the y=0 load loop.
    if (prepended && !pinnedRef.current && node.scrollTop > OLDER_PREFETCH_MARGIN_PX) {
      rearmOlderPrefetchAfterLeavingTop(node);
    }
    scheduleQuestionNav();
    previousFirstItemIdRef.current = firstItemId;
    previousScrollHeightRef.current = node.scrollHeight;
    syncScrollBaseline(node);
    // offsetTop queries are O(groups); skip while pinned at the live tip
    // (every stream token used to remeasure the whole timeline).
    firstItemGroupKeyRef.current = groups[0]?.key ?? null;
    const needOffsets =
      prepended ||
      firstItemChanged ||
      !pinnedRef.current ||
      hasNewer ||
      firstItemContentTopRef.current == null;
    if (needOffsets) {
      const committedFirstGroupKey = firstItemGroupKeyRef.current;
      const firstGroupEl = committedFirstGroupKey
        ? node.querySelector(`[data-og-group-key="${cssEscapeAttribute(committedFirstGroupKey)}"]`)
        : null;
      firstItemGroupOffsetTopRef.current =
        firstGroupEl instanceof HTMLElement ? firstGroupEl.offsetTop : null;
      const firstItemEl = firstItemId
        ? node.querySelector(`[data-og-item="${cssEscapeAttribute(firstItemId)}"]`)
        : null;
      firstItemContentTopRef.current =
        firstItemId && firstItemEl instanceof HTMLElement
          ? firstItemEl.getBoundingClientRect().top -
            node.getBoundingClientRect().top +
            node.scrollTop
          : null;
    }

    if (settlement) settlementAnimationRef.current = animateTimelineSettlement(settlement);

    // Promise settlement is not itself permission to retry. A receipt-marked
    // accepted page retires its exact owner on this commit even when projection
    // is empty or merges into the same first item. Without that mark, retain
    // the compatibility fallback: a retained prior boundary proves prepend,
    // while a missing prior boundary is forward eviction and merely rebases.
    if (!attempt) {
      return;
    }
    if (!attempt[2]?.committed && attempt[0] === olderBoundaryKey) {
      return;
    }
    if (
      !attempt[2]?.committed &&
      attempt[0] &&
      !sourceItems?.find((entry) => entry.id === attempt[0])
    ) {
      attempt[0] = olderBoundaryKey;
      return;
    }
    if (!attempt[1] || !hasOlder) {
      setUnderfillSettledAttempt((olderLoadAttemptRef.current = null));
      return;
    }
    // Boundary progress retires the request owner. A late settlement from
    // that completed prefetch cannot mutate the new window's cooldown owner.
    olderLoadAttemptRef.current = [olderBoundaryKey, 2];
    // Bound automatic work, but let continued upward input replenish demand.
    // Only a committed first-party receipt proves that another page is safe.
    if (
      attempt[2]?.committed &&
      !pinnedRef.current &&
      hasOlder &&
      node.scrollTop <= OLDER_PREFETCH_MARGIN_PX &&
      olderPageBudgetRef.current > 0
    ) {
      olderPageBudgetRef.current -= 1;
      olderLoadAttemptRef.current = null;
      setOlderDemand((value) => value + 1);
    }
    rearmOlderPrefetchAfterLeavingTop(node);
    requestOlderIfUnderfilled(node);
  });

  // First paint / session remount: keep the scroller hidden, snap to tip for
  // two animation frames (late sync layout), then reveal. Does not change the
  // live tip-follow law used once `revealed` is true.
  useLayoutEffect(() => {
    if (revealed || !allGroups.length) {
      return;
    }
    let cancelled = false;
    let frame2 = 0;
    const park = () => {
      const node = scrollRef.current;
      if (node && autoFollow && pinnedRef.current && !hasNewerRef.current) {
        snapToBottom(node);
      }
    };
    park();
    const frame1 = requestFrame(() => {
      if (cancelled) {
        return;
      }
      park();
      frame2 = requestFrame(() => {
        if (cancelled) {
          return;
        }
        park();
        setRevealed(true);
      });
    });
    return () => {
      cancelled = true;
      cancelFrame(frame1);
      if (frame2) {
        cancelFrame(frame2);
      }
    };
  }, [revealed, allGroups.length, autoFollow, snapToBottom]);

  // A cleared timeline (stream identity change) re-arms the reveal + prefetch
  // gate and returns to bottom-follow for the next session's first paint.
  useLayoutEffect(() => {
    if (allGroups.length > 0) {
      return;
    }
    if (revealed) {
      setRevealed(false);
    }
    if (olderPrefetchArmedRef.current) {
      olderPrefetchArmedRef.current = false;
      setOlderPrefetchArmed(false);
    }
    wantPinRef.current = false;
    pendingJumpToStartRef.current = false;
    previousFirstItemIdRef.current = null;
    previousScrollHeightRef.current = 0;
    lastScrollTopRef.current = 0;
    lastMaxScrollRef.current = 0;
    lastScrollHeightRef.current = 0;
    lastClientHeightRef.current = 0;
    firstItemGroupKeyRef.current = null;
    firstItemGroupOffsetTopRef.current = null;
    firstItemContentTopRef.current = null;
    foldMemoryRef.current.clear();
    userMessageDisclosureMemoryRef.current.clear();
    disclosureKeepsUnpinnedRef.current = false;
    clearReaderIntent();
    contentShrinkBaselineRef.current = null;
    seenActivityIdsRef.current.clear();
    applyPinned(true);
  }, [allGroups.length, revealed, applyPinned, clearReaderIntent]);

  // Parent commits cover the initial/history-loading cases. Disclosure state
  // changes are child-local, so the ResizeObserver below owns dynamic collapse
  // and expansion after mount.
  useEffect(() => {
    const node = scrollRef.current;
    if (node) {
      requestOlderIfUnderfilled(node);
    }
  });

  // Clear the bulk-paint marker a frame after it renders, so rows appended
  // live (streams, new turns) animate exactly as before.
  useLayoutEffect(() => {
    previousBulkFirstKeyRef.current = firstGroupKey;
    if (!bulkRender) {
      return;
    }
    setBulkActive(true);
    const frame = requestFrame(() => setBulkActive(false));
    return () => cancelFrame(frame);
  }, [bulkRender, firstGroupKey]);

  // Prefetch older history only after the reader scrolls up from the tip.
  // Once armed, the sentinel trips early so backfill is usually rendered
  // (and its scroll delta corrected) before the reader reaches it. Gated so
  // a short prepend that leaves the sentinel intersecting cannot loop.
  useEffect(() => {
    const root = scrollRef.current;
    const target = topSentinelRef.current;
    if (
      !root ||
      !target ||
      !olderPrefetchArmed ||
      !hasOlder ||
      loadingOlder ||
      !onLoadOlder ||
      typeof IntersectionObserver === "undefined"
    ) {
      return;
    }
    const observer = new IntersectionObserver(
      (entries) => {
        const intersecting = entries.some((entry) => entry.isIntersecting);
        if (!intersecting) {
          // Left the top band. A settled ordinary prefetch releases its exact
          // owner here; a still-pending request remains cooling so a quick
          // leave/re-enter cannot overlap it.
          const attempt = olderLoadAttemptRef.current;
          if (attempt?.[1] === 2 || attempt?.[1] === 3) {
            olderLoadAttemptRef.current = null;
          }
          return;
        }
        // Both automatic underfill and ordinary prefetch use one boundary
        // owner. In particular, live growth cannot arm a second sentinel load
        // while the short-window request that preceded it is still pending.
        if (olderLoadAttemptRef.current) {
          return;
        }
        const attempt: OlderLoadAttempt = (olderLoadAttemptRef.current = [olderBoundaryKey, 1]);
        const noProgress = () => {
          if (!scrollRef.current || olderLoadAttemptRef.current !== attempt) {
            return;
          }
          attempt[1] = 3;
          if (root.scrollTop > OLDER_PREFETCH_MARGIN_PX) {
            olderLoadAttemptRef.current = null;
          } else if (maxScrollOf(root) <= 1) {
            attempt[1] = 0;
            setUnderfillSettledAttempt(attempt);
          }
        };
        // Preserve legacy fire-and-forget top-band retries: the callback has
        // synchronously returned, but this visit remains cooling until exit.
        // Only rejection/throw/exact `false` is a no-progress receipt. Other
        // fulfillment retains the pending owner until boundary commit;
        // otherwise a delayed prepend could overlap a same-boundary request.
        if (
          !invokeOlderLoad(onLoadOlder, noProgress, attempt) &&
          olderLoadAttemptRef.current === attempt
        ) {
          olderLoadAttemptRef.current[1] = 2;
          requestOlderIfUnderfilled(root);
        }
      },
      { root, rootMargin: OLDER_PREFETCH_ROOT_MARGIN },
    );
    observer.observe(target);
    return () => observer.disconnect();
  }, [
    firstGroupKey,
    hasOlder,
    loadingOlder,
    olderBoundaryKey,
    olderPrefetchArmed,
    olderDemand,
    onLoadOlder,
    requestOlderIfUnderfilled,
  ]);

  // History view: page forward when the reader nears the bottom of the current
  // non-tip window. Does not pull the whole gap — one density-bounded page.
  useEffect(() => {
    const root = scrollRef.current;
    const target = bottomSentinelRef.current;
    if (
      !root ||
      !target ||
      !hasNewer ||
      loadingNewer ||
      !onLoadNewer ||
      typeof IntersectionObserver === "undefined"
    ) {
      return;
    }
    const observer = new IntersectionObserver(
      (entries) => {
        // An underfilled history window has both sentinels visible. Advancing
        // it automatically would undo an explicit older-page navigation.
        if (maxScrollOf(root) > 1 && entries.some((entry) => entry.isIntersecting)) {
          requestNewer();
        }
      },
      { root, rootMargin: "0px 0px 1200px 0px" },
    );
    observer.observe(target);
    return () => observer.disconnect();
  }, [hasNewer, loadingNewer, onLoadNewer, requestNewer, firstGroupKey]);

  // Late layout that React commits cannot see (images decoding, fonts, code
  // blocks) grows content without a commit. While pinned, soft-follow the tip;
  // unpinned: do nothing — chasing those shifts was the wobble. Coalesce RO
  // into one rAF.
  useEffect(() => {
    const node = scrollRef.current;
    const inner = node?.firstElementChild;
    if (!node || !inner || typeof ResizeObserver === "undefined") {
      return;
    }
    const observer = new ResizeObserver(() => {
      if (resizeFollowRafRef.current != null) {
        return;
      }
      resizeFollowRafRef.current = requestFrame(() => {
        resizeFollowRafRef.current = null;
        const current = scrollRef.current;
        if (!current) {
          return;
        }
        // A viewport that shrank below the pill's minimum hides it now, not
        // on the next scroll.
        if (questionNavMinViewportRef.current > 0) scheduleQuestionNavRef.current();
        requestOlderIfUnderfilled(current);
        if (!autoFollow || !pinnedRef.current || hasNewerRef.current) {
          return;
        }
        // Still unveiling the first tip frame: hard-park (no ease settle).
        if (!revealedRef.current) {
          snapToBottom(current);
          return;
        }
        driveFollow(current);
      });
    });
    observer.observe(inner);
    // The scroller's own box moves the bottom too (window resize, composer
    // growing): clientHeight changes with no inner resize and no commit.
    observer.observe(node);
    return () => {
      observer.disconnect();
      if (resizeFollowRafRef.current != null) {
        cancelFrame(resizeFollowRafRef.current);
        resizeFollowRafRef.current = null;
      }
    };
  }, [autoFollow, driveFollow, requestOlderIfUnderfilled, snapToBottom]);

  // Entering a non-tip history window: drop any live pin so the page bottom
  // cannot re-stick follow across loadNewer. Leaving it (the tip window
  // landed): honor a pending Jump-to-latest, or re-pin a reader already parked
  // at what just became the live bottom — paging forward to the tip must not
  // strand them unpinned watching new content grow below.
  useEffect(() => {
    // Closing find is paint-only; it must not trigger this history-to-tip rule.
    if (searchTargetRef.current) return;
    if (hasNewer) {
      stopFollow();
      applyPinned(false);
      return;
    }
    const node = scrollRef.current;
    if (!node) {
      return;
    }
    if (wantPinRef.current) {
      wantPinRef.current = false;
      if (autoFollow) {
        applyPinned(true);
        snapToBottom(node);
      }
      return;
    }
    if (autoFollow && !pinnedRef.current && isNearBottom(node)) {
      applyPinned(true);
    }
  }, [hasNewer, autoFollow, applyPinned, snapToBottom, stopFollow]);

  // After the scroll authority above has settled each commit, record whether
  // the reader is away from the tip (content can land below an unpinned
  // reader without any scroll event).
  // oxlint-disable-next-line react-hooks/exhaustive-deps -- Deliberately runs after every commit.
  useLayoutEffect(() => {
    const node = scrollRef.current;
    if (node && !pinnedRef.current) setAwayFromTip(!isNearBottom(node));
  });

  // Pinned: layout/camera recover tip debt; wheel/keys/pointer-arm unpin
  // immediately; extension jumps settle via scrollend (or one-rAF fallback).
  // Do not tip-follow-yank an in-flight unarmed scroll-away — that ate Vimium.
  const onScroll = () => {
    const node = scrollRef.current;
    if (!node) {
      return;
    }
    // Only an unpinned reader's distance matters for the jump.
    if (!pinnedRef.current) setAwayFromTip(!isNearBottom(node));
    scheduleQuestionNav();
    const previousTop = lastScrollTopRef.current;
    const previousMaxScroll = lastMaxScrollRef.current;
    const nextTop = node.scrollTop;
    const nextMaxScroll = maxScrollOf(node);
    const nextHeight = node.scrollHeight;
    const readerUp = readerScrollUpPx(previousTop, nextTop, previousMaxScroll, nextMaxScroll);
    const maxFell = nextMaxScroll < previousMaxScroll - 1;
    const readerArmed = readerIntentArmRef.current;
    const readerIntentStart = readerIntentStartRef.current;
    const cumulativeReaderUp =
      readerArmed && readerIntentStart
        ? readerScrollUpPx(
            readerIntentStart.scrollTop,
            nextTop,
            readerIntentStart.maxScroll,
            nextMaxScroll,
          )
        : readerUp;
    const heightShrunk =
      followRef.current.lastHeight > 0 &&
      nextHeight < followRef.current.lastHeight - TIP_FOLLOW_SHRINK_EPS_PX;
    // Consume all pending camera-write echoes (browsers may coalesce writes).
    const programmatic = programmaticScrollRef.current > 0;
    if (programmatic) {
      programmaticScrollRef.current = 0;
    }
    if (disclosureKeepsUnpinnedRef.current) {
      stopFollow();
      applyPinned(false);
      syncScrollBaseline(node);
      return;
    }

    if (programmatic && !pinnedRef.current) {
      // Restore/camera writes while reading history are not a return to the tip.
      // Still expire a pending Jump-to-latest latch: the in-window jump itself
      // is a programmatic snap, and a later reader scroll-away can arrive
      // before that echo is consumed. Skipping this left the latch armed and
      // snapped the reader when hasNewer later flipped false.
      syncScrollBaseline(node);
      if (wantPinRef.current && !isNearBottom(node)) {
        wantPinRef.current = false;
      }
      return;
    }

    if (autoFollow && pinnedRef.current && !hasNewer) {
      // Fold / composer / SessionChrome: viewport shrink raises maxScroll without
      // growing content. Must hit tipFollow before we adopt the new clientHeight
      // (the near-bottom branch used to poison lastClientHeight and skip glue).
      const previousClient = followRef.current.lastClientHeight;
      const viewportShrunk =
        previousClient > 0 && node.clientHeight < previousClient - TIP_FOLLOW_SHRINK_EPS_PX;
      // Fold / composer content shrink: compensate before baseline sync so
      // driveFollow still sees the pre-shrink scrollTop (avoid double-subtract).
      if (heightShrunk || maxFell || viewportShrunk) {
        clearReaderIntent();
        clearPendingReaderLeave();
        driveFollow(node);
        return;
      }
      if (programmatic) {
        // Camera-write echo: consume it, sync the SHELL baselines only. The
        // camera's growth baselines (lastHeight / lastClientHeight) belong to
        // tipFollowStep — adopting them here made every echo "consume" growth
        // that arrived without a commit (motion/Radix height animations of
        // nested tools, late layout). Echoes fire before rAF callbacks, so the
        // step saw frameGrowth=0, never heated, and the cold ~42px/s settle
        // let bursty growth park the tip under the chrome.
        syncScrollBaseline(node);
        return;
      }
      syncScrollBaseline(node);
      const nearBottomPinned = isNearBottom(node);
      if (nearBottomPinned) {
        clearPendingReaderLeave();
      }
      // Pointer-dragged scroll-up away from tip. Layout churn never arms this.
      if (readerArmed && cumulativeReaderUp > TIP_FOLLOW_READER_UP_EPS_PX && !nearBottomPinned) {
        clearReaderIntent();
        requestEarlierFromReader();
        rearmOlderPrefetchAfterLeavingTop(node);
        return;
      }
      // Tip grew under a still viewport (no reader-up): track the new growth
      // immediately and ease only any debt that already existed.
      // Reader/extension scroll-up in progress: do not yank — scrollend decides.
      // Near-bottom with tipDebt≈0 stays the quiet path (do not broaden follow
      // inside PIN_THRESHOLD — that fights small intentional scroll-ups).
      if (!nearBottomPinned && readerUp <= TIP_FOLLOW_READER_UP_EPS_PX) {
        if (!pendingReaderLeaveRef.current) {
          driveFollow(node);
        }
      } else if (!nearBottomPinned && readerUp > TIP_FOLLOW_READER_UP_EPS_PX) {
        pendingReaderLeaveRef.current = true;
        scheduleLeaveFallback();
      }
      // Near-bottom reader jiggle: stay quiet, and leave the camera's growth
      // baselines alone — adopting them here stole the heat of growth the
      // step had not seen yet (the next driveFollow then settled cold and
      // parked short inside the pin band).
      return;
    }

    syncScrollBaseline(node);
    if (programmatic) {
      // Anchor restoration is never permission to resume tip-follow.
      return;
    }
    const nearBottom = isNearBottom(node);

    // Re-pin only when the reader moved toward/at the tip. Prepend restore
    // and overflow-anchor raise scrollTop by roughly the same amount as
    // maxScroll; treating that as a scroll-down
    // re-pinned a compact-tail history reader (their preserved gap falls
    // inside PIN_THRESHOLD once the window is tall) and snapped them back.
    // A commit may already have adopted this scroll position into the layout
    // baseline. Explicit wheel/key/pointer intent retains its own start so
    // returning to the tip still re-pins after that interleaving.
    // Passive wheel handlers may run after compositor scrolling; retain the
    // earlier layout baseline when it still contains that reader movement.
    const returnStart =
      readerIntentStart && readerIntentStart.scrollTop <= previousTop ? readerIntentStart : null;
    const inserted = Math.max(0, nextMaxScroll - (returnStart?.maxScroll ?? previousMaxScroll));
    const towardTip = nextTop - (returnStart?.scrollTop ?? previousTop) - inserted;
    // Explicit intent may reach the exact live bottom despite intervening
    // streaming growth. Keep subtracting that growth: native anchoring alone
    // conserves the gap and must never qualify as reader movement.
    const explicitReturnToBottom = returnStart !== null && nextMaxScroll - nextTop <= 1;
    const nextPinned =
      !hasNewer && nearBottom && towardTip > 0.5 && (inserted <= 1 || explicitReturnToBottom);
    if (!nextPinned) {
      stopFollow();
    }
    const focused = node.ownerDocument.activeElement;
    if (
      nextPinned &&
      focused instanceof HTMLElement &&
      focused.hasAttribute("data-og-prompt") &&
      node.contains(focused)
    ) {
      // Question navigation focuses its destination. Returning to the live
      // tip releases that stale reader ownership before the commit snapshot.
      node.focus({ preventScroll: true });
    }
    applyPinned(nextPinned);
    // A far-from-bottom scroll while a Jump-to-latest is pending is the reader
    // changing their mind: drop the latch, or a stale one (host rejected or
    // never flipped hasNewer) would fire a surprise pin + snap whenever the
    // reader later pages to the tip themselves. Our own snaps land AT the
    // bottom, so their echoes read nearBottom and keep a live latch.
    if (wantPinRef.current && !nearBottom) {
      wantPinRef.current = false;
    }
    if (nextPinned) {
      clearReaderIntent();
      return;
    }
    if (readerIntentStart) {
      // This scroll already accounted for the gesture's movement. Retain a
      // fresh start for continuous input across the next commit, not stale
      // movement that could turn a later native clamp into a return to tip.
      readerIntentStartRef.current = { scrollTop: nextTop, maxScroll: nextMaxScroll };
    }
    if (!olderPrefetchArmedRef.current) {
      olderPrefetchArmedRef.current = true;
      setOlderPrefetchArmed(true);
    }
    // Re-arm older prefetch only after leaving the top band (scroll down into
    // content). Never re-arm/load from continued scroll toward y=0.
    rearmOlderPrefetchAfterLeavingTop(node);
  };

  const onScrollEnd = () => {
    const node = scrollRef.current;
    if (!node) {
      return;
    }
    cancelLeaveFallback();
    // Engines can emit scrollend between tiny pointer steps. Keep the pinned
    // gesture's cumulative leave budget until it actually leaves the tip.
    if (!pinnedRef.current || !readerIntentArmRef.current) clearReaderIntent();
    if (disclosureKeepsUnpinnedRef.current) {
      programmaticScrollRef.current = 0;
      stopFollow();
      applyPinned(false);
      syncScrollBaseline(node);
      return;
    }
    if (programmaticScrollRef.current > 0) {
      programmaticScrollRef.current = 0;
      syncScrollBaseline(node);
      return;
    }
    releasePinAfterScrollSettled(node);
  };
  // React 18 has no `onScrollEnd` prop: it warns and never calls it, while
  // `supportsScrollEndEvent()` still makes the camera wait for scrollend. A
  // native listener (re-bound each commit, so it always sees this render's
  // handler and scroller) works on every supported React version.
  useEffect(() => {
    const node = scrollRef.current;
    if (!node) return;
    node.addEventListener("scrollend", onScrollEnd);
    return () => node.removeEventListener("scrollend", onScrollEnd);
  });

  const timeline = (
    
      
        
          
            
              
                
                  
                    
                      
{onAnnotate ? ( ) : null} {/* Pinned: anchoring off so the tip-follow camera owns the motion. Unpinned: native scroll anchoring holds the reader's place. */}
{ stopSettlement(); const touch = event.touches.length === 1 ? event.touches[0] : undefined; touchPositionRef.current = touch ? { x: touch.clientX, y: touch.clientY } : null; }} onTouchMove={(event) => { const touch = event.touches[0]; const previous = touchPositionRef.current; if (!touch || !previous || event.touches.length !== 1) { touchPositionRef.current = null; return; } const deltaX = previous.x - touch.clientX; const deltaY = previous.y - touch.clientY; if (Math.max(Math.abs(deltaX), Math.abs(deltaY)) < 4) return; touchPositionRef.current = { x: touch.clientX, y: touch.clientY }; onWheel({ deltaX, deltaY, target: event.target, currentTarget: event.currentTarget, }); }} onTouchEnd={() => { touchPositionRef.current = null; }} onTouchCancel={() => { touchPositionRef.current = null; }} onClickCapture={(event) => { const target = event.target instanceof Element ? event.target.closest("button[aria-expanded]") : null; if (target) { if (target.hasAttribute("data-og-work-header")) { releaseProgress( target .closest("[data-og-group-key]") ?.getAttribute("data-og-group-key") ?? null, ); } releasePinFromReader(); disclosureKeepsUnpinnedRef.current = true; } }} onFocusCapture={(event) => { if (readableTurns && event.target !== event.currentTarget) { stopSettlement(); releasePinFromReader(); disclosureKeepsUnpinnedRef.current = true; } }} onPointerDown={onPointerDown} onKeyDown={onKeyDown} style={ groups.length > 0 && !revealed ? { visibility: "hidden" } : undefined } className={cn( // tabIndex=-1 is programmatic only — never paint a focus ring on // the whole scroller (click + Shift used to flash a blue outline). "min-h-0 flex-1 overflow-y-auto overscroll-contain px-4 pt-16 pb-6 sm:px-6 outline-hidden", autoFollow && pinned && !hasNewer ? "[overflow-anchor:none]" : "[overflow-anchor:auto]", )} > { if (readableTurns && timelineHasReader(scrollRef.current)) { releasePinFromReader(); } readingAnchorRef.current = !pinnedRef.current && scrollRef.current ? captureTimelineAnchor(scrollRef.current) : null; settlementRef.current = readableTurns && autoFollow && pinnedRef.current && revealedRef.current && !hasNewer && !pendingReaderLeaveRef.current && !disclosureKeepsUnpinnedRef.current && !prefersReducedMotion() && scrollRef.current ? captureTimelineSettlement(scrollRef.current, groups) : null; }} >
{loadingOlder || loadingOldest || (hasOlder && onJumpToStart && olderPrefetchArmed) || underfillRetryReady ? ( // Reserved top gutter: controls scroll with history and cannot // cover a disclosure. Visibility never changes content height. {loadingOlder || loadingOldest ? ( {loadingOldest ? t("messageTimeline.jumpingToStart") : t("messageTimeline.loadingEarlierActivity")} ) : null} {hasOlder && !loadingOlder && !loadingOldest && (underfillRetryReady || onJumpToStart) ? ( ) : null} ) : null} {!groups.length ? (emptyState ?? (

{t("messageTimeline.noActivityYet")}

)) : null} {hasOlder && olderPrefetchArmed ? ( // Overlaid, not a layout row: mounting/unmounting the sentinel // must never shift content (that shift was itself a wobble). // End at the scroll origin above the pt-16 gutter so the // observer and scrollTop cooldown share the same 400px band. {draftAnnotations && draftAnnotations.length > 0 ? ( ) : null}
{questionNav ? ( jumpToQuestion(questionNav.key)} className={NAV_BUTTON_CLASS} > {t("messageTimeline.backToYourMessage")} ) : null}
{loadingNewer ? ( {t("messageTimeline.loadingLaterActivity")} ) : null} {jumpPillShown ? ( { // Returning to the tip explicitly releases reader-owned // prose. Clear only this timeline's selection before the // synchronous ownership check on the next commit. const viewport = scrollRef.current; const selection = viewport?.ownerDocument.getSelection(); if ( selection && !selection.isCollapsed && (viewport?.contains(selection.anchorNode) || viewport?.contains(selection.focusNode)) ) selection.removeAllRanges(); disclosureKeepsUnpinnedRef.current = false; if (hasNewer) { // Do not pin against the current history page — its bottom // is not the tip. The pin + snap run when the tip window // actually lands (`hasNewer` flips false). wantPinRef.current = true; const node = scrollRef.current; if (onJumpToLatest) { void Promise.resolve(onJumpToLatest()).then( () => { // Covers a host that flipped hasNewer before // resolving; otherwise the tip-window commit // consumes the flag. const current = scrollRef.current; if ( current && wantPinRef.current && !hasNewerRef.current ) { wantPinRef.current = false; applyPinned(true); snapToBottom(current); } }, () => { // The tip reload failed (ordinary network error): // an armed latch would fire a surprise snap when // the reader later pages to the tip themselves. wantPinRef.current = false; }, ); } else if (node) { // No tip reload available: jump within the in-memory // window so the newer sentinel can page forward; the // latch pins if the tip window eventually lands. snapToBottom(node); } return; } const node = scrollRef.current; if (node) { applyPinned(true); snapToBottom(node); } }} className={NAV_BUTTON_CLASS} > {t("messageTimeline.jumpToLatest")} ) : null}
); // Agent-authored object links resolve through the host, never the console. return ( {timeline} ); } type KeyedTimelineGroup = { group: TimelineGroup; key: string; entranceEnabled: boolean; }; function timelineGroupsRenderEqual(previous: TimelineGroup, next: TimelineGroup): boolean { try { return dequal(previous, next); } catch { // Consumer-owned `unknown` payloads may be proxies/getters. Equality is an // optimization only; a hostile comparator surface must fall back to the // authoritative new group rather than taking down the timeline render. return false; } } /** * Projection can legitimately change a group's content-derived key while * retaining its existing rows. The common pagination case is an older activity * item merging into the first activity group; live appends grow the same group * from the other side. Match the new authoritative groups to the previous * committed groups by their durable item IDs so both the React key and the * progressive-window anchor survive either change. */ function useStableTimelineGroupKeys( allGroups: TimelineGroup[], entranceEnabled: boolean, ): KeyedTimelineGroup[] { const previousRef = useRef([]); const keyedGroups = useMemo(() => { const previousByItemId = new Map(); for (const previous of previousRef.current) { for (const itemId of timelineGroupItemIds(previous.group)) { // A live progress note is both its own row and listed in its work // history. Its own row owns the id, so the note keeps its identity. if (previous.group.kind === "item" || !previousByItemId.has(itemId)) { previousByItemId.set(itemId, previous); } } } const usedKeys = new Set(); return allGroups.map((group, index) => { const itemIds = timelineGroupItemIds(group); let retainedGroup: KeyedTimelineGroup | undefined; for (const itemId of itemIds) { const previous = previousByItemId.get(itemId); // Retain only same-kind matches. Activity → turn wrap must NOT keep the // activity chip's React key: that reused a collapsed TurnSummary and // skipped the settle beat (insta-collapse / content flash). const startupCompletion = previous?.group.kind === "activity" && previous.group.items.every( (item) => item.kind === "startup-phase" || (item.kind === "reasoning" && !item.text.trim()), ) && group.kind === "turn" && group.outcome === "complete" && group.groups.every( (child) => child.kind === "activity" && child.items.every((item) => item.kind === "startup-phase"), ); if ( previous && (previous.group.kind === group.kind || startupCompletion) && !usedKeys.has(previous.key) ) { retainedGroup = previous; break; } } const canonicalKey = timelineGroupKey(group); let key = retainedGroup?.key ?? canonicalKey; let collision = 0; while (usedKeys.has(key)) { key = `${canonicalKey}:${index}:${collision}`; collision += 1; } usedKeys.add(key); return { group: retainedGroup && timelineGroupsRenderEqual(retainedGroup.group, group) ? retainedGroup.group : group, key, entranceEnabled: retainedGroup?.entranceEnabled ?? entranceEnabled, }; }); }, [allGroups, entranceEnabled]); useLayoutEffect(() => { previousRef.current = keyedGroups; }, [keyedGroups]); return keyedGroups; } function requestFrame(callback: FrameRequestCallback): number { if (typeof requestAnimationFrame === "function") { return requestAnimationFrame(callback); } return window.setTimeout(() => callback(performance.now()), 16); } function cancelFrame(id: number): void { if (typeof cancelAnimationFrame === "function") { cancelAnimationFrame(id); return; } window.clearTimeout(id); } type TimelineGroupRenderBoundaryProps = { children: ReactNode; resetKeys: readonly unknown[]; }; type TimelineGroupRenderBoundaryState = { failed: boolean; resetKeys: readonly unknown[]; }; function timelineRenderResetKeysChanged( previous: readonly unknown[], next: readonly unknown[], ): boolean { return ( previous.length !== next.length || previous.some((key, index) => !Object.is(key, next[index])) ); } /** * A malformed historical payload or consumer renderer must not take down the * entire conversation. Keep the boundary outside the row component so React * can replace an invalid element type (error #130) with a bounded fallback. */ function TimelineGroupRenderError() { const t = useOpenGeniText(); return (
); } class TimelineGroupRenderBoundary extends Component< TimelineGroupRenderBoundaryProps, TimelineGroupRenderBoundaryState > { state: TimelineGroupRenderBoundaryState = { failed: false, resetKeys: this.props.resetKeys, }; static getDerivedStateFromError(): Partial { return { failed: true }; } static getDerivedStateFromProps( props: TimelineGroupRenderBoundaryProps, state: TimelineGroupRenderBoundaryState, ): Partial | null { if (timelineRenderResetKeysChanged(state.resetKeys, props.resetKeys)) { return { failed: false, resetKeys: props.resetKeys }; } return null; } render(): ReactNode { if (this.state.failed) { return ; } return this.props.children; } } type TimelineGroupEntryProps = { groupKey: string; group: TimelineGroup; nextGroup?: TimelineGroup | undefined; startupDismissed: boolean; entranceEnabled: boolean; liveEntranceEnabled?: boolean | undefined; /** * Present for a live progress note: true while its turn's work disclosure is * open (the note then reads inside it), false while it reads here. */ noteFolded?: boolean | undefined; context: TimelineGroupEntryContext; }; type TimelineGroupEntryContext = { userMessageDisclosureContext: UserMessageDisclosureContextValue; behavior: TimelineGroupBehaviorProps; }; type TimelineGroupBehaviorProps = { renderMessageActions: MessageTimelineProps["renderMessageActions"]; renderMessageText: | ((text: string, item: AgentMessageItem | UserMessageItem) => ReactNode) | undefined; onOpenSession: MessageTimelineProps["onOpenSession"]; onMemoryClick: MessageTimelineProps["onMemoryClick"]; onReconnect: MessageTimelineProps["onReconnect"]; renderAuthNeeded: MessageTimelineProps["renderAuthNeeded"]; resolveProviderLogo: MessageTimelineProps["resolveProviderLogo"]; toolRegistry: ToolRegistry; loadRetainedScreenshot: MessageTimelineProps["loadRetainedScreenshot"]; loadRetainedArtifact: MessageTimelineProps["loadRetainedArtifact"]; loadVideoArtifactPlayback: MessageTimelineProps["loadVideoArtifactPlayback"]; genieLoading: MessageTimelineProps["genieLoading"]; turnSummary: MessageTimelineProps["turnSummary"]; }; /** * Keep the complete settled-row shell behind one shallow memo boundary. A * prepend still reconciles the keyed list, but render-equivalent suffix groups * skip their providers, error boundaries, disclosure wrappers, and row trees. */ const TimelineGroupEntry = memo(function TimelineGroupEntry({ groupKey, group, nextGroup, startupDismissed, entranceEnabled, liveEntranceEnabled, noteFolded, context, }: TimelineGroupEntryProps) { const reducedMotion = useReducedMotion(); const { behavior, userMessageDisclosureContext } = context; const contextCompactionCount = group.kind === "turn" ? (group.contextCompactionCount ?? 0) : group.kind === "activity" && nextGroup?.kind === "item" && nextGroup.item.kind === "context-compaction" && nextGroup.item.phase === "compacted" ? 1 : 0; const content = ( 0 ? contextCompactionCount : undefined} /> ); return (
{/* Item groups never switch the preparation/content key. Keep their DOM shell without mounting inert presence and motion lifecycles for every historical message in a prepend. */} {group.kind === "item" ? (
{content}
) : ( item.kind === "startup-phase" || (item.kind === "reasoning" && !item.text.trim()), ) ? "preparation" : "content" } initial={false} exit={{ opacity: 0, height: 0 }} transition={{ duration: reducedMotion ? 0 : 0.2 }} > {content} )}
); }); // The full loaded window stays mounted, so settled history rows must be cheap // on every commit: the stable-key projection reuses render-equivalent group // objects, so memo skips them. Changed projection content receives a new group // object, and behavior/callback changes are separate props, so ordinary // streaming and host updates still invalidate immediately. const TimelineGroupView = memo(function TimelineGroupView({ group, renderMessageActions, renderMessageText, onOpenSession, onMemoryClick, onReconnect, renderAuthNeeded, resolveProviderLogo, toolRegistry, loadRetainedScreenshot, loadRetainedArtifact, loadVideoArtifactPlayback, turnSummary, insideTurn = false, nestClusterChips = false, foldLiveCluster = false, startupDismissed = false, trailingAgentText, contextCompactionCount, }: { group: TimelineGroup; renderMessageActions?: ((item: AgentMessageItem | UserMessageItem) => ReactNode) | undefined; renderMessageText?: | ((text: string, item: AgentMessageItem | UserMessageItem) => ReactNode) | undefined; onOpenSession?: ((sessionId: string) => void) | undefined; onMemoryClick?: ((memoryId: string) => void) | undefined; onReconnect?: ((item: AuthNeededItem) => void | Promise) | undefined; /** Host-owned inline connection setup. Return undefined to use the default recovery card. */ renderAuthNeeded?: ((item: AuthNeededItem) => ReactNode | undefined) | undefined; resolveProviderLogo?: ((providerDomain: string) => string | null | undefined) | undefined; toolRegistry: ToolRegistry; loadRetainedScreenshot?: RetainedScreenshotLoader | undefined; loadRetainedArtifact?: RetainedArtifactLoader | undefined; loadVideoArtifactPlayback?: VideoArtifactPlaybackLoader | undefined; turnSummary?: TurnSummaryOptions | undefined; /** A completed cluster of a still-RUNNING turn (not the live tail) folds behind a neutral chip — the one place activity without an outcome still folds, bounding the DOM of days-long autonomous turns. */ foldLiveCluster?: boolean; startupDismissed?: boolean; /** Rendering inside an expanded turn group: the outer chip already owns the failure surface, so nested chips stay tinted but quiet (no repeated failure text, no auto-open) — one loud error, N calm sub-expands. */ insideTurn?: boolean; /** * Parent turn has ≥2 foldable activity clusters. Only then do we wrap settled * clusters in nested chips — a single cluster under "N steps" is redundant. * During outer settle chrome, nested chips stay force-open (structure kept, * height stable) instead of flat-mapping to bare rails. */ nestClusterChips?: boolean; /** * Final agent answer extracted as a sibling after a settled turn — folded * into "Copy turn" so the chip copies the whole assistant reply, not only * mid-turn narration still inside the fold. */ trailingAgentText?: string | undefined; /** Secondary chip facet when this fold sits next to a compaction landmark. */ contextCompactionCount?: number | undefined; }) { const t = useOpenGeniText(); const startupDetails = useStartupDetails(); const enter = useEntranceAnimation(); const settleChrome = useTurnSettleOpen(); const previousSingleActivity = useRef(undefined); useLayoutEffect(() => { const work = group.kind === "activity" ? group.items.filter((item) => item.kind !== "startup-phase") : []; previousSingleActivity.current = work.length === 1 ? work[0] : undefined; }, [group]); const foldMemory = useFoldMemory(); const reportLiveFold = useContext(LiveNoteFoldContext); // Settled (or live-fold) activity clusters get a chip. Inside an expanded // turn that is the second layer — quiet nested chips under the outer turn // summary when contiguous activity naturally clusters (≥2 only). const containsPresentedImage = timelineGroupContainsPresentedImage(group); const hasRememberedImageFold = group.kind === "activity" && containsPresentedImage && foldMemory?.has(group.id); // Primary images stay visible through live narration and the turn wrap. // Manual collapse still belongs to TurnSummary's existing fold memory. const activityShouldFold = group.kind === "activity" && !containsPresentedImage && !!(group.outcome || (foldLiveCluster && clusterIsSettled(group))); // Latch live→folded so a top-level shell that was already mounted open can // start the settle beat without remounting bare rail → wrapper. const liveActivitySettle = useLiveSettleFold(activityShouldFold && !insideTurn); const turnDefaultOpen = !insideTurn && group.kind === "turn" && (group.outcome === "failed" || timelineGroupContainsAuthNeeded(group) || containsPresentedImage); // activity-* → turn-* remount: carry resting state so settleFold does not // re-open a chip the reader already watched collapse. if (group.kind === "turn" && foldMemory && !insideTurn) { inheritFoldRestingState( foldMemory, group.id, group.groups.flatMap((child) => (child.kind === "activity" ? [child.id] : [])), ); } const settleFold = !turnSummary?.rolling && (group.kind === "turn" ? !!(enter && !insideTurn && !turnDefaultOpen) : liveActivitySettle); switch (group.kind) { case "activity": if (group.work) { const preparing = isPreparingWork(group, { startupDetails, startupDismissed }); if (preparing) { // Preparation is primary, never hidden in an activity disclosure. // Keep work.startedAt unchanged for the handoff and settled duration. return ( ); } const workStatus = readableWorkStatus(t, { ...group, work: group.work }); const status: TurnSummaryStatus = workStatus.kind === "working" ? { ...workStatus, preview: readableWorkShowsPreview(group) ? ( ) : undefined, } : workStatus.kind === "waiting" ? { kind: "waiting", label: workStatus.label, since: workStatus.since, // One quiet line under the row saying what the wait needs, // kept visible when the work is expanded; never a second // warning banner. guidance: workStatus.detail ? ( {workStatus.detail} ) : undefined, } : group.work.cycles?.summary ? { ...workStatus, // Folded quiet cycles say what the agent is waiting on. preview: (

{group.work.cycles.summary}

), } : workStatus; return ( // Marks this work row for the live-note fold (list keys can be carried // over from an earlier row, so they are not the work id).
reportLiveFold(group.id, open) : undefined } > {group.work.steps ? ( ) : null} {renderFoldedGroups( group.work.details, { renderMessageActions, renderMessageText, onOpenSession, onMemoryClick, onReconnect, renderAuthNeeded, resolveProviderLogo, toolRegistry, loadRetainedScreenshot, loadRetainedArtifact, loadVideoArtifactPlayback, turnSummary, }, true, )}
); } // Preparation is one quiet surface, not a fold with eight technical steps. // A later turn of the same exchange keeps its status row instead. if ( !startupDetails && !insideTurn && !group.outcome && group.items.every( (item) => item.kind === "startup-phase" || (item.kind === "reasoning" && !item.text.trim()), ) ) { return ( ); } if ( turnSummary?.rolling && insideTurn && !startupDetails && !hasRememberedImageFold && group.items.filter((item) => item.kind !== "startup-phase").length === 1 ) { return ( ); } if (insideTurn) { // Nested chips whenever the parent has ≥2 clusters. During outer settle // chrome they stay force-open so structure is visible and height stays // stable through collapse — never flat-map to bare rails (that flash // was the "inner steps vanish then reappear nested" bug). Key flips // when chrome clears so they remount closed inside the already-hidden // parent (safe; mid-collapse remount of closed chips was the snap). // foldKey memory overrides the force-open: a cluster that already // settled closed pre-wrap was showing as a CHIP, so mounting it closed // is both the stable height and the honest state — force-opening it // was the "already-collapsed cluster auto-expands at the end" reopen. const visibleActivity = group.items.filter((item) => item.kind !== "startup-phase"); const singleThought = !startupDetails && visibleActivity.length === 1 && visibleActivity[0]?.kind === "reasoning"; // Untouched images stay primary output, but a reader-owned image fold // must keep its shell across a multi-cluster wrap. A bare rail would // bypass the remembered choice; retaining either state also lets the // reader reopen and close the same chip after settlement. const useNestedChip = nestClusterChips && (activityShouldFold || hasRememberedImageFold) && !singleThought; if (!useNestedChip) { return ( ); } return ( ); } // Always the same TurnSummary shell while live so mid-turn fold only // flips settleFold (collapse) instead of remounting bare rail → wrapper. return ( ) : undefined } foldKey={group.id} facets={turnSummary?.facets} settleFold={settleFold} contextCompactionCount={contextCompactionCount} > ); case "turn": { const activityItems = flattenActivityItems(group.groups); if ( !startupDetails && group.outcome === "complete" && group.groups.every( (child) => child.kind === "activity" && child.items.every( (item) => item.kind === "startup-phase" && item.status === "complete", ), ) ) return ; const turnCopyText = collectTurnCopyText(group.groups, trailingAgentText); const body = renderFoldedGroups(group.groups, { renderMessageActions, renderMessageText, onOpenSession, onMemoryClick, onReconnect, renderAuthNeeded, resolveProviderLogo, toolRegistry, loadRetainedScreenshot, loadRetainedArtifact, loadVideoArtifactPlayback, turnSummary, }); const durationMs = durationBetween(group.startedAt, group.endedAt); return ( {insideTurn ? ( // A nested turn is already on an ancestor rail — its body just stacks // flush (the bare-node body already indents it), so no second rule.
{body}
) : ( {body} )}
); } case "item": return ( ); } }); type FoldedGroupBehavior = { renderMessageActions?: ((item: AgentMessageItem | UserMessageItem) => ReactNode) | undefined; renderMessageText?: | ((text: string, item: AgentMessageItem | UserMessageItem) => ReactNode) | undefined; onOpenSession?: ((sessionId: string) => void) | undefined; onMemoryClick?: ((memoryId: string) => void) | undefined; onReconnect?: ((item: AuthNeededItem) => void | Promise) | undefined; renderAuthNeeded?: ((item: AuthNeededItem) => ReactNode | undefined) | undefined; resolveProviderLogo?: ((providerDomain: string) => string | null | undefined) | undefined; toolRegistry: ToolRegistry; loadRetainedScreenshot?: RetainedScreenshotLoader | undefined; loadRetainedArtifact?: RetainedArtifactLoader | undefined; loadVideoArtifactPlayback?: VideoArtifactPlaybackLoader | undefined; turnSummary?: TurnSummaryOptions | undefined; }; /** * A search hit inside a nested work row opens that row only. The reveal key * is provided per top-level group, so without narrowing every nested row of * a folded quiet-cycle run would open at once. */ function NestedSearchReveal({ group, children }: { group: TimelineGroup; children: ReactNode }) { const revealKey = useContext(TimelineSearchRevealContext); const itemId = useMemo(() => { if (!revealKey) return null; try { const [id] = JSON.parse(revealKey) as unknown[]; return typeof id === "string" ? id : null; } catch { return null; } }, [revealKey]); const value = itemId && timelineGroupItemIds(group).includes(itemId) ? revealKey : null; return ( {children} ); } function renderFoldedGroups( groups: readonly TimelineGroup[], behavior: FoldedGroupBehavior, readableWork = false, ) { // Second-layer chips only when there are natural multi-cluster seams: // otherwise the outer turn chip alone is enough ("N steps" wrapping one // more "N steps" was the redundant double fold). const nestClusters = foldableActivityClusterCount(groups) >= 2; return groups.map((child, index) => ( {readableWork ? (
0 && (child.kind !== "activity" || groups[index - 1]?.kind !== "activity") ? "mt-3" : undefined } >
) : ( )}
)); } /** * Body under a turn/activity chip. Remount flashes are gated by the timeline * seen-activity-id map (not by killing entrance): FoldBody used to force * entrance off, which made every live tool pop with no fade. */ function FoldBody({ children }: { children: ReactNode }) { return <>{children}; } /** Stable left rule for turn/activity bodies — always present so settle wrap never inserts or removes the rail chrome. */ function TurnRailFrame({ children, compact = false }: { children: ReactNode; compact?: boolean }) { return (
{children}
); } /** * True once THIS component instance has seen its group transition from * unfolded to folded — i.e. the reader watched the rows live and the fold is * new information worth choreographing. Latched: TurnSummary captures the flag * at its own mount (the flip render), so later prop churn is inert. History * that mounts already folded initializes folded and never latches. */ function useLiveSettleFold(folded: boolean): boolean { const previousFoldedRef = useRef(folded); const latchedRef = useRef(false); if (!previousFoldedRef.current && folded) { latchedRef.current = true; } useLayoutEffect(() => { previousFoldedRef.current = folded; }); return latchedRef.current; } function timelineGroupKey(group: TimelineGroup): string { switch (group.kind) { case "item": return group.item.kind === "user-message" && group.item.reconciliationKey ? group.item.reconciliationKey : group.item.id; case "activity": return group.id; case "turn": return group.id; } } function timelineGroupContainsAuthNeeded(group: TimelineGroup): boolean { switch (group.kind) { case "item": return group.item.kind === "auth-needed"; case "activity": return false; case "turn": return group.groups.some(timelineGroupContainsAuthNeeded); } } function timelineGroupItemIds(group: TimelineGroup): string[] { switch (group.kind) { case "item": return [group.item.id]; case "activity": return [ ...(group.work?.details ?? []).flatMap(timelineGroupItemIds), ...group.items.map((item) => item.id), ]; case "turn": return group.groups.flatMap(timelineGroupItemIds); } } /** The agent has moved PAST a cluster only when what follows is more agent progress — new activity, a settled turn, or narration. A waiting notice (approval pause), a pending queued message, a goal pill, or nothing at all do NOT advance the story, and folding on them would hide exactly the work the reader needs in view. */ function isAgentProgress(next: TimelineGroup | undefined): boolean { if (!next) { return false; } return ( next.kind === "activity" || next.kind === "turn" || (next.kind === "item" && next.item.kind === "agent-message" && next.item.text.trim().length > 0) ); } /** No item still running or streaming — the only state safe to fold live. Position alone is a broken proxy: a pending queued message (or any trailing item) can sit after the ACTIVE cluster, which must never fold mid-work. */ /** Settled activity clusters that could become nested chips under a turn. */ function foldableActivityClusterCount(groups: readonly TimelineGroup[]): number { let count = 0; for (const child of groups) { if (child.kind === "activity" && (child.outcome || clusterIsSettled(child))) { count += 1; } } return count; } /** Assistant prose inside a turn fold (mid-turn narration), joined for copy. */ function collectAgentMessageText(groups: readonly TimelineGroup[]): string { const parts: string[] = []; for (const group of groups) { if (group.kind === "item" && group.item.kind === "agent-message") { const text = group.item.text.trim(); if (text.length > 0) { parts.push(text); } } else if (group.kind === "activity") { // Commentary folded into clusters is still the assistant's own prose. const nested = collectAgentMessageText(group.work?.details ?? []); if (nested.length > 0) { parts.push(nested); } for (const item of group.items) { const text = item.kind === "agent-message" ? item.text.trim() : ""; if (text.length > 0) { parts.push(text); } } } else if (group.kind === "turn") { const nested = collectAgentMessageText(group.groups); if (nested.length > 0) { parts.push(nested); } } } return parts.join("\n\n"); } /** Non-null turnIds projected into a turn body (activity + nested messages). */ function collectTurnIdsFromGroups(groups: readonly TimelineGroup[]): Set { const ids = new Set(); for (const group of groups) { if (group.kind === "item") { const turnId = "turnId" in group.item ? group.item.turnId : null; if (typeof turnId === "string" && turnId.length > 0) { ids.add(turnId); } } else if (group.kind === "activity") { for (const nested of collectTurnIdsFromGroups(group.work?.details ?? [])) { ids.add(nested); } for (const item of group.items) { if (item.turnId) { ids.add(item.turnId); } } } else if (group.kind === "turn") { for (const nested of collectTurnIdsFromGroups(group.groups)) { ids.add(nested); } } } return ids; } /** * Settled turns lift the final agent answer out as a sibling group. Include it * in "Copy turn" when present so the chip copies the full assistant reply. * Fenced by turnId so the next turn's answer is never stolen. */ export function trailingAgentTextAfterTurn( group: TimelineGroup, next: TimelineGroup | undefined, ): string | undefined { if (group.kind !== "turn") { return undefined; } if (next?.kind === "item" && next.item.kind === "agent-message") { const turnIds = collectTurnIdsFromGroups(group.groups); // Degenerate body with no turnIds: do not guess — safer than lifting wrong. if (!turnIds.size) { return undefined; } if (!next.item.turnId || !turnIds.has(next.item.turnId)) { return undefined; } const text = next.item.text.trim(); return text.length > 0 ? text : undefined; } return undefined; } function collectTurnCopyText( groups: readonly TimelineGroup[], trailingAgentText: string | undefined, ): string | undefined { const parts = [collectAgentMessageText(groups), trailingAgentText?.trim() ?? ""].filter( (part) => part.length > 0, ); if (!parts.length) { return undefined; } return parts.join("\n\n"); } /* --- single rows ------------------------------------------------------------ */ /** * Render one non-activity timeline item (chat message, status divider, goal * landmark, notice). Exported so the component demo draws the EXACT same rows as * the live app — no forked bubble/goal markup. */ export function TimelineRow({ item, renderMessageActions, renderMessageText, onReconnect, renderAuthNeeded, resolveProviderLogo, onOpenSession, loadVideoArtifactPlayback, }: { item: TimelineItem; renderMessageActions?: ((item: AgentMessageItem | UserMessageItem) => ReactNode) | undefined; renderMessageText?: | ((text: string, item: AgentMessageItem | UserMessageItem) => ReactNode) | undefined; onReconnect?: ((item: AuthNeededItem) => void | Promise) | undefined; /** Host-owned inline connection setup. Return undefined to use the default recovery card. */ renderAuthNeeded?: ((item: AuthNeededItem) => ReactNode | undefined) | undefined; resolveProviderLogo?: ((providerDomain: string) => string | null | undefined) | undefined; onOpenSession?: ((sessionId: string) => void) | undefined; loadVideoArtifactPlayback?: VideoArtifactPlaybackLoader | undefined; }) { switch (item.kind) { case "user-message": return ( ); case "human-input": return ; case "agent-message": return ( ); case "worker-completion": return ; case "session-status": return ; case "goal": return ; case "machine-input-batch": return ( ); case "notice": return ; case "context-compaction": return ; case "turn-steps": return ; case "auth-needed": return ( renderAuthNeeded?.(item) ?? ( ) ); default: return null; } } const COMPACTION_TRIGGER_LABEL = ( t: OpenGeniText, ): Record, string> => ({ auto: t("messageTimeline.auto"), operator: t("messageTimeline.manual"), proactive: t("messageTimeline.auto"), overflow: t("messageTimeline.overflow"), }); function CompactionRow({ item }: { item: ContextCompactionItem }) { const t = useOpenGeniText(); const enter = useEntranceAnimation(); const detailsId = useId(); const [detailsOpen, setDetailsOpen] = useState(false); const trigger = item.trigger && item.phase !== "started" ? COMPACTION_TRIGGER_LABEL(t)[item.trigger] : null; const before = item.estimatedTokensBefore != null ? Math.round(item.estimatedTokensBefore).toLocaleString(t.locale) : null; const after = item.estimatedTokensAfter != null ? Math.round(item.estimatedTokensAfter).toLocaleString(t.locale) : null; const title = item.phase === "started" ? t("messageTimeline.compactingConversationHistory") : item.phase === "compacted" ? before && after ? t("messageTimeline.conversationHistoryCompactedBeforeAfter", { before, after }) : t("messageTimeline.conversationHistoryCompacted") : compactionSkipTitle(t, item.skipReason); const subtitle = item.phase === "compacted" ? t("messageTimeline.chatHistoryAboveIsUnchanged") : item.phase === "skipped" ? compactionSkipSubtitle(t, item.skipReason, item.providerRejection) : null; // Provider identifiers are diagnostics: one short line up front, the rest on demand. const details = compactionFailureDetails(t, item); const failed = item.phase === "skipped" && item.skipReason === "summarization_failed"; const pill = failed ? "border-og-status-failed/35 bg-og-status-failed/10 text-og-status-failed" : item.phase === "started" ? WAITING_PILL_CLASS : NEUTRAL_PILL; return (
{title} {trigger ? ` · ${trigger}` : ""} {subtitle || details ? ( {subtitle ? ( {subtitle} ) : null} {subtitle && details && failed ? " " : null} {details ? ( ) : null} ) : null}
{details ? ( ) : null}
); } /** Hover-reveal clock beside the copy control (sent / finished). */ function MessageFooterTime({ occurredAt }: { occurredAt: string }) { const t = useOpenGeniText(); return ( ); } function UserMessageRow({ item, renderMessageActions, renderMessageText, }: { item: UserMessageItem; renderMessageActions?: ((item: AgentMessageItem | UserMessageItem) => ReactNode) | undefined; renderMessageText?: | ((text: string, item: AgentMessageItem | UserMessageItem) => ReactNode) | undefined; }) { const t = useOpenGeniText(); const enter = useEntranceAnimation(); const deliveryFailed = item.delivery?.state === "failed"; return (
`${annotation.quote}\n${annotation.note}`) .join("\n\n") } label={t("messageTimeline.copyMessage")} className="w-fit max-w-full min-w-0" trailing={ <> {renderMessageActions?.(item)} } >
{item.text ? (
{renderMessageText ? ( renderMessageText(item.text, item) ) : ( {item.text} )}
) : null} {(item.annotations?.length ?? 0) > 0 ? ( ) : null}
{deliveryFailed ? (
{item.delivery?.onEdit ? ( ) : null} {item.delivery?.onRetry ? ( ) : null} {item.delivery?.onRemove ? ( ) : null}
) : null}
); } function HumanInputConversationRow({ item }: { item: HumanInputItem }) { const t = useOpenGeniText(); const enter = useEntranceAnimation(); const multipleQuestions = Math.max(item.questions.length, item.answers.length) > 1; const questionNumberById = new Map( item.questions.map((question, index) => [question.id, index + 1]), ); const settledLabel = item.response.outcome === "answered" ? t("messageTimeline.youAnswered") : item.response.outcome === "skipped" ? t("messageTimeline.skipped") : item.response.outcome === "expired" ? t("messageTimeline.expired") : t("messageTimeline.cancelled"); const copyText = humanInputConversationCopyText(t, item, settledLabel); return (

{t("messageTimeline.agentAsked")}

{item.questions.length > 0 ? ( item.questions.map((question, index) => (
{question.label ? (

{multipleQuestions ? ( {index + 1}. ) : null} {multipleQuestions ? " " : null} {question.label}

) : null}

{!question.label && multipleQuestions ? ( {index + 1}. ) : null} {!question.label && multipleQuestions ? " " : null} {linkifyText(question.prompt)}

)) ) : (

{t("messageTimeline.theAgentRequestedStructuredInput")}

)}
} >

{settledLabel}

{item.response.outcome === "answered" ? (
{item.answers.length > 0 ? ( item.answers.map((answer, answerIndex) => (
{multipleQuestions ? (

{questionNumberById.get(answer.questionId) ?? answerIndex + 1}. {" "} {answer.label}

) : null}
{answer.secret ? ( ) : answer.values.length > 1 ? (
    {answer.values.map((value) => (
  • {value}
  • ))}
) : (

{answer.values[0] || t("messageTimeline.answered")}

)}
)) ) : (

{t("messageTimeline.answered")}

)}
) : null}
); } function humanInputConversationCopyText( t: OpenGeniText, item: HumanInputItem, settledLabel: string, ): string { const questions = item.questions .map( (question, index) => `${item.questions.length > 1 ? `${index + 1}. ` : ""}${question.label || t("messageTimeline.question")}: ${question.prompt}`, ) .join("\n\n"); const questionNumberById = new Map( item.questions.map((question, index) => [question.id, index + 1]), ); const answers = item.answers .map( (answer, index) => `${ item.questions.length > 1 ? `${questionNumberById.get(answer.questionId) ?? index + 1}. ` : "" }${answer.label}: ${ answer.secret ? `${secretAnswerLabel(t, answer.secret)} (${answer.secret.reference})` : answer.values.join(", ") || t("messageTimeline.answered") }`, ) .join("\n\n"); return [questions, `${settledLabel}${answers ? `\n\n${answers}` : ""}`] .filter(Boolean) .join("\n\n"); } function AgentMessageRow({ item, renderMessageActions, renderMessageText, }: { item: AgentMessageItem; renderMessageActions?: ((item: AgentMessageItem | UserMessageItem) => ReactNode) | undefined; renderMessageText?: | ((text: string, item: AgentMessageItem | UserMessageItem) => ReactNode) | undefined; }) { const t = useOpenGeniText(); const enter = useEntranceAnimation(); // No streaming caret: it fought the trailing block layout (inline ↔ block) // and snapped on exit. Live text carries the stream via tip ink. const body = renderMessageText ? ( renderMessageText(item.text, item) ) : ( {item.text} ); // While streaming, copy is still useful (current text) but keep chrome calm — // stamp only after the message finishes (occurredAt tracks completion). return ( {renderMessageActions?.(item)} ) } >
{body}
); } /** * A worker session reporting back to its manager in older history: the child's * completion arrived as a `user.message` carrying a `childCompletion` payload. * It draws as the same named agent row as every other inbound agent update: * the outcome and goal on the row, the report, evidence, and any paused * reason behind the disclosure, and a deep link into the child. */ type WorkerCompletionMeta = { title: AgentTitleParts; icon: ComponentType<{ className?: string }>; failed: boolean; }; function workerCompletionMeta( t: OpenGeniText, item: WorkerCompletionItem, name: string | null, ): WorkerCompletionMeta { if (item.childStatus === "failed") { return { title: namedAgentTitle( name, (agent) => t("messageTimeline.nameFailed", { name: agent }), t("messageTimeline.workerFailed"), ), icon: XCircleIcon, failed: true, }; } if (item.goalStatus === "paused") { return { title: namedAgentTitle( name, (agent) => t("messageTimeline.namePaused", { name: agent }), t("messageTimeline.workerPaused"), ), icon: PauseCircleIcon, failed: false, }; } if (item.goalStatus === "completed") { return { title: namedAgentTitle( name, (agent) => t("messageTimeline.nameCompletedItsGoal", { name: agent }), t("messageTimeline.workerCompleted"), ), icon: CheckCircle2Icon, failed: false, }; } return { title: namedAgentTitle( name, (agent) => t("messageTimeline.resultFromName", { name: agent }), t("messageTimeline.workerReportedBack"), ), icon: BotIcon, failed: false, }; } function WorkerCompletionRow({ item, onOpenSession, }: { item: WorkerCompletionItem; onOpenSession?: ((sessionId: string) => void) | undefined; }) { const t = useOpenGeniText(); const enter = useEntranceAnimation(); const identity = useAgentIdentity(); const meta = workerCompletionMeta(t, item, identity.titleFor(item.childSessionId)); const Icon = meta.icon; // "Paused because" only when the outcome actually IS a pause — completion // payloads can carry a leftover pausedReason/rationale from earlier in the // worker's life, and a completed row must not show a pause section. const showPausedReason = item.childStatus !== "failed" && item.goalStatus === "paused" && !!item.pausedReason?.trim(); const details: { label: string; value: string; muted?: boolean }[] = [ ...(item.text.trim() ? [{ label: t("messageTimeline.report"), value: item.text.trim() }] : []), ...(item.evidence?.trim() ? [{ label: t("messageTimeline.evidence"), value: item.evidence.trim(), muted: true }] : []), ...(showPausedReason ? [ { label: t("messageTimeline.pausedBecause"), value: item.pausedReason!.trim(), muted: true, }, ] : []), ]; return (
} title={meta.title} preview={item.goalText ? truncate(item.goalText, 240) : null} previewLines={2} sessionId={item.childSessionId} onOpenSession={onOpenSession ?? identity.onOpenSession} failed={meta.failed} > {details.length > 0 ? ( <> {details.map((detail) => ( {detail.value} ))} ) : undefined}
); } function SessionStatusRow({ item, }: { item: { status: SessionStatus; blocked?: true; occurredAt: string; resolvedAt?: string; }; }) { const t = useOpenGeniText(); const enter = useEntranceAnimation(); const status = item.blocked ? "blocked" : item.status; const meta = SESSION_STATUS_META(t)[status]; return (
{item.resolvedAt ? null : } {item.resolvedAt ? t("messageTimeline.workResumed") : meta.label.toLowerCase()} ·{" "} {formatRelativeTime(t, item.resolvedAt ?? item.occurredAt)}
); } /** * The per-action presentation of a goal landmark pill. Each of the six goal * actions reads distinctly, but the palette stays quiet — color is spent only on * the two states that genuinely earn it, the rest are neutral pills set apart by * their glyph alone: * * completed success green (status-idle) check — the only "done" hue * paused attention waiting-tinted pause — a held goal asks to resume * set a landmark a quiet accent target — opening a fresh goal * resumed forward neutral play — motion picking back up * updated a revision neutral pencil — the goal text changed * continuation steady on neutral arrow — still tracking the same goal * * The pill class is the established badge convention (`text-X border-X/30 * bg-X/10`); neutral actions reuse the surface/border tokens so a clean run of * landmarks stays calm rather than a row of colored chips. */ type GoalMeta = { label: string; pill: string; icon: ComponentType<{ className?: string }> }; const NEUTRAL_PILL = "border-og-border bg-og-surface-1 text-og-fg-muted"; // Shared production chunks can be cyclic. Resolve icon bindings during render, // after their modules initialize, rather than permanently capturing undefined. function goalMeta(t: OpenGeniText, action: GoalItem["action"]): GoalMeta { const metadata: Record = { set: { label: t("messageTimeline.goalSet"), pill: "border-og-accent/30 bg-og-accent/10 text-og-accent", icon: TargetIcon, }, updated: { label: t("messageTimeline.goalUpdated"), pill: NEUTRAL_PILL, icon: PencilLineIcon }, completed: { label: t("messageTimeline.goalCompleted"), pill: "border-og-status-idle/30 bg-og-status-idle/10 text-og-status-idle", icon: CheckIcon, }, paused: { label: t("messageTimeline.goalPaused"), pill: WAITING_PILL_CLASS, icon: PauseIcon, }, resumed: { label: t("messageTimeline.goalResumed"), pill: NEUTRAL_PILL, icon: PlayIcon }, cleared: { label: t("messageTimeline.goalCleared"), pill: NEUTRAL_PILL, icon: Trash2Icon }, held: { label: t("messageTimeline.goalHeld"), pill: WAITING_PILL_CLASS, icon: PauseCircleIcon, }, continuation: { label: t("messageTimeline.continuingTowardTheGoal"), pill: NEUTRAL_PILL, icon: ArrowRightIcon, }, }; return metadata[action]; } /** * A goal landmark pill. Resolves its label, accent/tone, and glyph from * {@link goalMeta} so all six actions are visually distinguishable while the * palette stays restrained — see that table for the per-action rationale. */ function GoalRow({ item }: { item: GoalItem }) { const t = useOpenGeniText(); const enter = useEntranceAnimation(); const { label, pill, icon: Icon } = goalMeta(t, item.action); return (
{label} {item.text ? `: ${truncate(item.text, 90)}` : ""}
); } function MachineInputBatchRow({ item, onOpenSession, loadVideoArtifactPlayback, }: { item: MachineInputBatchItem; onOpenSession?: ((sessionId: string) => void) | undefined; loadVideoArtifactPlayback?: VideoArtifactPlaybackLoader | undefined; }) { const t = useOpenGeniText(); const enter = useEntranceAnimation(); const identity = useAgentIdentity(); const label = machineInputBatchLabel(t, item.members, identity.titleFor); const single = item.members.length === 1 ? item.members[0]! : null; if (single?.kind === "media_generation_result" && single.result) { return ( ); } const singleSummary = single ? cleanMachineInputSummary(single.summary) : ""; // An agent's words live in its named row behind the pill; the pill already // says who it is from, so nothing repeats beneath it. const showCollapsedSummary = !item.compact && single != null && !isAgentMember(single) && machineInputSummaryIsUseful(t, single.kind, singleSummary); return (
{label}

{t("messageTimeline.received")}{" "}

{groupMachineInputMembers(item.members).map((group) => group.kind === "agent" ? ( ) : ( ), )}
{showCollapsedSummary ? (

{truncate(singleSummary, 160)}

) : null}
); } type MachineInputMemberGroup = | { kind: "agent"; members: MachineInputBatchItem["members"] } | { kind: "other"; member: MachineInputBatchItem["members"][number] }; /** * Agent members get named rows; repeated progress notes from one agent fold * into its single row (latest note shown, earlier ones behind the disclosure). */ function groupMachineInputMembers( members: MachineInputBatchItem["members"], ): MachineInputMemberGroup[] { const groups: MachineInputMemberGroup[] = []; const progressBySource = new Map(); for (const member of members) { if (!isAgentMember(member)) { groups.push({ kind: "other", member }); continue; } if (member.kind === "child_progress") { const existing = progressBySource.get(member.sourceId); if (existing) { existing.push(member); continue; } const bucket = [member]; progressBySource.set(member.sourceId, bucket); groups.push({ kind: "agent", members: bucket }); continue; } groups.push({ kind: "agent", members: [member] }); } return groups; } function agentMemberIcon(member: MachineInputBatchItem["members"][number]): ReactNode { if (member.kind === "child_terminal_result" && member.classification === "failure") { return ; } if (member.kind === "child_paused") return ; return ; } /** One named row for an update sent by (or about) another agent. */ function AgentMemberRow({ members, onOpenSession, }: { members: MachineInputBatchItem["members"]; onOpenSession?: ((sessionId: string) => void) | undefined; }) { const t = useOpenGeniText(); const identity = useAgentIdentity(); const latest = members[members.length - 1]!; const sessionId = agentMemberSessionId(latest); const title = agentMemberTitle(t, latest, identity.titleFor(sessionId)); const texts = members.map((member) => agentMemberText(t, member)); const latestText = texts[texts.length - 1]!; const bodies = texts .map((text, index) => ({ id: members[index]!.id, body: text.body })) .filter((entry) => entry.body.length > 0); return ( 1 ? `+${members.length - 1} earlier` : null} sessionId={sessionId} onOpenSession={onOpenSession ?? identity.onOpenSession} failed={latest.kind === "child_terminal_result" && latest.classification === "failure"} > {bodies.length > 0 ? ( <> {bodies.map((entry) => ( {entry.body} ))} ) : undefined} ); } function MachineInputRow({ member, onOpenSession, loadVideoArtifactPlayback, }: { member: MachineInputBatchItem["members"][number]; onOpenSession?: ((sessionId: string) => void) | undefined; loadVideoArtifactPlayback?: VideoArtifactPlaybackLoader | undefined; }) { const t = useOpenGeniText(); if (member.kind === "media_generation_result" && member.result) { return ( ); } const source = readableMachineInputSource(member.sourceId); const summary = cleanMachineInputSummary(member.summary); return (
{MACHINE_INPUT_META(t)[member.kind]} {source && ( {t("messageTimeline.fromSource", { source })} )} {summary ? (

{truncate(summary, 320)}

) : null}
); } function VideoGenerationResultRow({ result, loadVideoArtifactPlayback, compact = false, }: { result: MediaGenerationResult; loadVideoArtifactPlayback?: VideoArtifactPlaybackLoader | undefined; compact?: boolean | undefined; }) { const t = useOpenGeniText(); const enter = useEntranceAnimation(); if (result.status !== "ready") { return (
{t("messageTimeline.videoGenerationFailed")}

{result.boundedPublicReason}

); } const { receipt } = result; const facts = receipt.video; return (
{loadVideoArtifactPlayback ? ( ) : (
)}

{t("messageTimeline.generatedVideo")}

{facts.width}×{facts.height} · {formatVideoDuration(facts.durationSeconds)} {facts.hasAudio ? " · Audio" : ""}

); } function formatVideoDuration(seconds: number): string { return `${Math.round(seconds * 10) / 10}s`; } function NoticeRow({ item }: { item: NoticeItem }) { const t = useOpenGeniText(); const enter = useEntranceAnimation(); if (item.allowance) return ; if (item.sessionUsageLimit) return ; if (item.learningRefusal) return ; if (item.recordedOutcome) { return (
{recordedWaitSummary(t, item)}

{item.text}

); } const presentedTone = noticeTone(item); const resolvedApproval = noticeIsResolvedApproval(item); const tone = presentedTone === "failed" ? "border-og-status-failed/35 bg-og-status-failed/10 text-og-status-failed" : presentedTone === "waiting" ? WAITING_PILL_CLASS : NEUTRAL_PILL; return (
{resolvedApproval ? (
); } /** * "Waited for 2 agents · 3m 5s" once later input ended the wait, otherwise * "Waiting for 2 agents · since 10:32" (with the date on a later day). The * reason stays behind the disclosure. */ function recordedWaitSummary(t: OpenGeniText, item: NoticeItem): ReactNode { const agents = item.waitingAgents ?? 0; const waitedMs = item.waitEndedAt ? durationBetween(item.occurredAt, item.waitEndedAt) : undefined; if (waitedMs !== undefined) { return withSlot( agents > 0 ? t("timeline.noticePresentation.waitedForAgentsElapsed", { count: agents, elapsed: SLOT }) : t("timeline.noticePresentation.waitedElapsed", { elapsed: SLOT }), , ); } // An open wait is historical once its day has passed; keep its date visible. const started = new Date(item.occurredAt); const sameDay = started.toDateString() === new Date().toDateString(); const text = item.waitEndedAt ? agents > 0 ? t("timeline.noticePresentation.waitedForAgentsSince", { count: agents, since: SLOT }) : t("timeline.noticePresentation.waitedSince", { since: SLOT }) : agents > 0 ? t("timeline.noticePresentation.waitingForAgentsSince", { count: agents, since: SLOT }) : t("timeline.noticePresentation.waitingSince", { since: SLOT }); return withSlot( text, , ); } /** Marks where a rendered element goes inside translated text. */ const SLOT = "\u0000"; function withSlot(text: string, node: ReactNode): ReactNode { const at = text.indexOf(SLOT); if (at < 0) { return ( <> {text} {node} ); } return ( <> {text.slice(0, at)} {node} {text.slice(at + SLOT.length)} ); } /** * The inline connection-recovery card: missing or lapsed access surfaces as a * calm, tappable affordance instead of a raw provider-domain error. The `reason` * only shapes human copy; no domain or enum code is shown as a label. * `onReconnect` (from the app, which owns the SDK client) starts the flow; * without it, a pre-minted authorization link is offered, or the card stays * informative. Recovery never claims to resume/replay the failed tool call. */ function AuthNeededRow({ item, onReconnect, resolveProviderLogo, }: { item: AuthNeededItem; onReconnect?: ((item: AuthNeededItem) => void | Promise) | undefined; resolveProviderLogo?: ((providerDomain: string) => string | null | undefined) | undefined; }) { const t = useOpenGeniText(); const enter = useEntranceAnimation(); const [busy, setBusy] = useState(false); const [failed, setFailed] = useState(false); const recommendation = item.capability ?? null; const provider = recommendation?.name ?? (item.serverId === "codex_apps" ? t("messageTimeline.codexApps") : providerLabel(t, item.providerDomain)); const unavailable = item.reason === "personal_authority_unavailable" || item.reason === "unsupported_auth" || item.reason === "resource_scope_unavailable"; const missing = item.reason === "missing_connection"; const actionLabel = recommendation ? recommendation.action === "connect" ? t("messageTimeline.connect") : t("messageTimeline.review") : missing ? t("messageTimeline.connect") : t("messageTimeline.reconnect"); const title = recommendation ? recommendation.action === "connect" ? t("messageTimeline.connectProvider", { provider }) : recommendation.action === "add_credentials" ? t("messageTimeline.setUpProvider", { provider }) : t("messageTimeline.enableProvider", { provider }) : unavailable ? t("messageTimeline.providerToolsUnavailable", { provider }) : `${actionLabel} ${provider}`; const reasonLine = recommendation?.rationale ?? authReasonLine(t, item.reason); const hostAuthorizationUrl = item.authoritySource === "host" ? item.authorizationUrl : null; const start = async () => { if (!onReconnect || busy) { return; } setBusy(true); setFailed(false); try { // On success the app redirects to consent (or routes to credential entry), // so this row unmounts; a resolve without navigation just relaxes the button. // The callback starts authorization only. It never resumes this tool call. await onReconnect(item); setBusy(false); } catch { setFailed(true); setBusy(false); } }; if (recommendation?.id === "api:connected-machine") { return (

{t("thisComputer.card.name")}

{t("thisComputer.card.hostSetupUnavailable")}

); } return (

{title}

{reasonLine}

{recommendation ? (

{t("messageTimeline.providerProviderdomain", { providerDomain: item.providerDomain, })}

) : null} {recommendation && recommendation.requiredVariables.length > 0 ? (

{t("messageTimeline.needsVariablesJoin", { join: recommendation.requiredVariables.join(", "), })}

) : null}
{hostAuthorizationUrl ? ( {actionLabel} ) : item.authoritySource !== "host" && !unavailable && onReconnect ? ( ) : item.authoritySource !== "host" && !unavailable && item.authorizationUrl ? ( {actionLabel} ) : null}
{!unavailable ? (

{recommendation ? t("messageTimeline.noAccessHasBeenGranted") : t( missing ? "messageTimeline.toolCallNotReplayedAfterConnecting" : "messageTimeline.toolCallNotReplayedAfterReconnecting", )}

) : null} {failed ? (

{t("messageTimeline.couldnTStart")}{" "} {missing ? t("messageTimeline.connecting") : t("messageTimeline.reconnecting")}{" "} {t("messageTimeline.providerTryAgain", { provider })}

) : null}
); } /** * The provider's logo in a rounded tile, from a URL the HOST serves itself * (resolved via `resolveProviderLogo` → the app's catalog assets). A missing or * failed image falls back to a calm letter monogram — same as the rest of the * app — so the card never shows a broken-image glyph and never reaches off-origin * for a favicon (CSP + privacy). */ function AuthProviderLogo({ src, label }: { src: string | null; label: string }) { const [failed, setFailed] = useState(false); // A resolver that only returns the URL after a lazy catalog fetch means `src` // can arrive on a later render; reset the error latch so it gets its attempt. useEffect(() => setFailed(false), [src]); const showImage = src && !failed; return ( {showImage ? ( setFailed(true)} /> ) : ( {monogram(label)} )} ); } /** First one or two initials for the monogram fallback — mirrors the app's `capabilityMonogram` so the reconnect tile reads like every other logo tile. */ function monogram(label: string): string { const words = label.trim().split(/\s+/).filter(Boolean); if (!words.length) { return "?"; } if (words.length === 1) { return words[0]!.slice(0, 2).toUpperCase(); } return (words[0]![0]! + words[1]![0]!).toUpperCase(); } /** "linear.app" -> "Linear": the first domain label, capitalized. A calm human name for the provider — never the raw domain shown as a label. */ function providerLabel(t: OpenGeniText, domain: string): string { const host = domain .trim() .replace(/^https?:\/\//, "") .replace(/^www\./, "") .split("/")[0] ?? ""; const first = host.split(".")[0] ?? host; if (!first) { return t("messageTimeline.thisService"); } return first.charAt(0).toUpperCase() + first.slice(1); } /** A calm, human helper line per reauth reason — the `reason` informs the copy but is never rendered as a raw enum/code. */ function authReasonLine(t: OpenGeniText, reason: AuthNeededItem["reason"]): string { switch (reason) { case "insufficient_scope": return t("messageTimeline.itNeedsAdditionalAccessTo"); case "missing_connection": return t("messageTimeline.itIsnTConnectedYet"); case "expired": case "refresh_failed": return t("messageTimeline.itsAccessExpired"); case "personal_authority_unavailable": return t("messageTimeline.thisAutomationWasNotGranted"); case "unsupported_auth": return t("messageTimeline.thisConnectionCannotAuthenticateThe"); case "resource_scope_unavailable": return t("messageTimeline.thisToolEndpointCannotEnforce"); default: return t("messageTimeline.itsConnectionNeedsAttention"); } }