@opengeni/react 6.1.0 → 7.0.0-canary.35585322809001

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (206) hide show
  1. package/README.md +83 -2
  2. package/dist/artifacts-document.js +1 -1
  3. package/dist/artifacts-presentation.js +1 -1
  4. package/dist/artifacts-spreadsheet.js +1 -1
  5. package/dist/artifacts.js +3 -3
  6. package/dist/{browser-viewer-NTD6UIPR.js → browser-viewer-LGH4IP47.js} +3 -3
  7. package/dist/capability-catalog-row.d.ts +20 -0
  8. package/dist/capability-logo-fallback.d.ts +3 -0
  9. package/dist/{chunk-NOSYBS2D.js → chunk-2VHARTBL.js} +2 -1
  10. package/dist/{chunk-VRBL3F5E.js → chunk-5GAL23FF.js} +10 -2
  11. package/dist/{chunk-VRBL3F5E.js.map → chunk-5GAL23FF.js.map} +1 -1
  12. package/dist/chunk-5UONOQ3Y.js +635 -0
  13. package/dist/chunk-5UONOQ3Y.js.map +1 -0
  14. package/dist/{chunk-NYESIADM.js → chunk-AKOMC3VO.js} +3 -3
  15. package/dist/chunk-AKOMC3VO.js.map +1 -0
  16. package/dist/chunk-BRM6FVZA.js +302 -0
  17. package/dist/chunk-BRM6FVZA.js.map +1 -0
  18. package/dist/{chunk-TDSEILUH.js → chunk-D5NOQPLR.js} +1289 -1530
  19. package/dist/chunk-D5NOQPLR.js.map +1 -0
  20. package/dist/chunk-D7GAPJ3V.js +1558 -0
  21. package/dist/chunk-D7GAPJ3V.js.map +1 -0
  22. package/dist/{chunk-5M6VTFJ5.js → chunk-GBPCZH3Y.js} +2 -2
  23. package/dist/{chunk-YBM6CQRU.js → chunk-GEN54DU5.js} +4 -4
  24. package/dist/{chunk-UMECRJRV.js → chunk-HJ5VJ4JB.js} +21 -12
  25. package/dist/chunk-HJ5VJ4JB.js.map +1 -0
  26. package/dist/{chunk-R4KRSFGA.js → chunk-IP4BUBKH.js} +3 -3
  27. package/dist/{chunk-KANFU34B.js → chunk-IYUPN32E.js} +6 -2
  28. package/dist/chunk-IYUPN32E.js.map +1 -0
  29. package/dist/{chunk-L2NBTKRX.js → chunk-JKLGJQO5.js} +193 -12
  30. package/dist/chunk-JKLGJQO5.js.map +1 -0
  31. package/dist/{chunk-NLTS7JHI.js → chunk-JPHDWKZE.js} +5 -5
  32. package/dist/{chunk-ICA6R6TW.js → chunk-OWHPKAOV.js} +28 -20
  33. package/dist/chunk-OWHPKAOV.js.map +1 -0
  34. package/dist/{chunk-24M4YBCL.js → chunk-PNAVEK2U.js} +4 -4
  35. package/dist/{chunk-45XEUVTK.js → chunk-UXE5SSNN.js} +293 -463
  36. package/dist/chunk-UXE5SSNN.js.map +1 -0
  37. package/dist/{chunk-PEI6RANI.js → chunk-VR4Z73EU.js} +203 -9
  38. package/dist/chunk-VR4Z73EU.js.map +1 -0
  39. package/dist/{chunk-HUIKIDM5.js → chunk-XMXRI5JK.js} +65 -18
  40. package/dist/chunk-XMXRI5JK.js.map +1 -0
  41. package/dist/client.d.ts +1 -1
  42. package/dist/components/artifacts/spreadsheet-general-display.d.ts +24 -0
  43. package/dist/components/chat-composer.d.ts +13 -3
  44. package/dist/components/markdown.d.ts +4 -3
  45. package/dist/components/message-timeline.d.ts +7 -2
  46. package/dist/components/preview-loading.d.ts +2 -0
  47. package/dist/components/session-capability-frame.d.ts +24 -0
  48. package/dist/components/session-chrome.d.ts +1 -2
  49. package/dist/components/session-commands.d.ts +6 -0
  50. package/dist/components/session-connection-request.d.ts +8 -0
  51. package/dist/components/session-mcp-capability-card.d.ts +22 -0
  52. package/dist/components/timeline-search.d.ts +34 -0
  53. package/dist/composer.js +7 -7
  54. package/dist/connect-account-identity.d.ts +8 -0
  55. package/dist/connect-accounts.d.ts +5 -0
  56. package/dist/connect-chooser.d.ts +6 -0
  57. package/dist/connect-panel.d.ts +9 -2
  58. package/dist/connect-setup.d.ts +5 -1
  59. package/dist/connect.d.ts +5 -21
  60. package/dist/connect.js +1268 -584
  61. package/dist/connect.js.map +1 -1
  62. package/dist/connection-catalog.d.ts +6 -1
  63. package/dist/connection-discovery.d.ts +12 -0
  64. package/dist/connection-installed.d.ts +1 -0
  65. package/dist/connection-logo.d.ts +5 -2
  66. package/dist/connection-service-logo.d.ts +8 -0
  67. package/dist/connection-service-presentation.d.ts +13 -0
  68. package/dist/connection-skeleton.d.ts +5 -0
  69. package/dist/discovery-cache.d.ts +5 -0
  70. package/dist/hooks/use-codex-accounts.d.ts +11 -5
  71. package/dist/hooks/use-connect.d.ts +22 -0
  72. package/dist/hooks/use-session-events.d.ts +5 -0
  73. package/dist/hooks/use-session-mcp-approval-policy.d.ts +2 -2
  74. package/dist/hooks/use-workspace-sessions.d.ts +2 -0
  75. package/dist/index.d.ts +8 -2
  76. package/dist/index.js +771 -793
  77. package/dist/index.js.map +1 -1
  78. package/dist/interaction.js +2 -2
  79. package/dist/machines.js +2 -2
  80. package/dist/mcp-connection-status.d.ts +14 -0
  81. package/dist/{model-policy-picker-menu-3R336DP2.js → model-policy-picker-menu-RVOQ4ZZG.js} +4 -4
  82. package/dist/model-policy.d.ts +6 -2
  83. package/dist/model-policy.js +1 -1
  84. package/dist/{platform-activity-row-DUE2W74J.js → platform-activity-row-HLOH7FW4.js} +9 -4
  85. package/dist/platform-activity-row-HLOH7FW4.js.map +1 -0
  86. package/dist/plugin-discovery.d.ts +6 -0
  87. package/dist/{queue-surface-implementation-LK3S4BIW.js → queue-surface-implementation-3RIFK2BS.js} +4 -4
  88. package/dist/realtime.js +22 -22
  89. package/dist/realtime.js.map +1 -1
  90. package/dist/session-auth-recommendation.d.ts +5 -0
  91. package/dist/session-capability-policy.d.ts +12 -0
  92. package/dist/session-ui.d.ts +2 -0
  93. package/dist/session-ui.js +14 -12
  94. package/dist/session.d.ts +1 -0
  95. package/dist/session.js +6 -4
  96. package/dist/skill-discovery.d.ts +11 -0
  97. package/dist/timeline/compaction-copy.d.ts +10 -0
  98. package/dist/timeline/projection.d.ts +3 -1
  99. package/dist/timeline/turn-summary.d.ts +2 -2
  100. package/dist/timeline/types.d.ts +22 -1
  101. package/dist/{timeline-annotation-markers-DRSTLEM6.js → timeline-annotation-markers-OY6OWR3V.js} +2 -2
  102. package/dist/{timeline-annotation-selection-IHLUGVND.js → timeline-annotation-selection-BOXA2TIN.js} +2 -2
  103. package/dist/{timeline-annotations-dialog-6Q53N5FG.js → timeline-annotations-dialog-RGY3MHL3.js} +3 -3
  104. package/package.json +3 -3
  105. package/src/assets/README.md +8 -0
  106. package/src/assets/gmail.ico +0 -0
  107. package/src/assets/outlook.ico +0 -0
  108. package/src/capability-catalog-row.tsx +94 -0
  109. package/src/capability-logo-fallback.ts +22 -0
  110. package/src/client.ts +0 -9
  111. package/src/components/artifacts/document-editor.tsx +63 -6
  112. package/src/components/artifacts/presentation-editor.tsx +214 -9
  113. package/src/components/artifacts/spreadsheet-canvas.ts +3 -8
  114. package/src/components/artifacts/spreadsheet-general-display.ts +43 -0
  115. package/src/components/artifacts/spreadsheet-grid.tsx +10 -11
  116. package/src/components/chat-composer.tsx +26 -3
  117. package/src/components/composer.tsx +10 -4
  118. package/src/components/human-input-form.tsx +12 -2
  119. package/src/components/markdown.tsx +86 -4
  120. package/src/components/message-timeline.tsx +104 -36
  121. package/src/components/model-picker.tsx +11 -3
  122. package/src/components/model-policy-picker-menu.tsx +10 -3
  123. package/src/components/preview-loading.tsx +154 -0
  124. package/src/components/sandbox-workspace.tsx +20 -36
  125. package/src/components/session-capability-frame.tsx +163 -0
  126. package/src/components/session-chrome.tsx +46 -77
  127. package/src/components/session-commands.tsx +18 -0
  128. package/src/components/session-connection-request.tsx +97 -0
  129. package/src/components/session-conversation.tsx +5 -4
  130. package/src/components/session-mcp-capability-card.tsx +532 -0
  131. package/src/components/session-status.tsx +2 -2
  132. package/src/components/timeline-anchor.tsx +9 -8
  133. package/src/components/timeline-search.tsx +168 -0
  134. package/src/components/user-message-body.tsx +7 -0
  135. package/src/connect-account-identity.tsx +34 -0
  136. package/src/connect-accounts.tsx +114 -52
  137. package/src/connect-chooser.tsx +171 -39
  138. package/src/connect-panel.tsx +73 -9
  139. package/src/connect-setup.tsx +72 -25
  140. package/src/connect.ts +13 -22
  141. package/src/connection-assets.d.ts +4 -0
  142. package/src/connection-catalog.tsx +61 -39
  143. package/src/connection-discovery.tsx +235 -0
  144. package/src/connection-installed.tsx +2 -0
  145. package/src/connection-logo.tsx +18 -10
  146. package/src/connection-service-logo.tsx +55 -0
  147. package/src/connection-service-presentation.ts +46 -0
  148. package/src/connection-skeleton.tsx +16 -0
  149. package/src/conversation-timeline.ts +1 -0
  150. package/src/discovery-cache.ts +42 -0
  151. package/src/hooks/use-codex-accounts.ts +33 -14
  152. package/src/hooks/use-composer.ts +4 -5
  153. package/src/hooks/use-connect.ts +23 -0
  154. package/src/hooks/use-session-events.ts +114 -1
  155. package/src/hooks/use-session-mcp-approval-policy.ts +13 -5
  156. package/src/hooks/use-workspace-sessions.ts +10 -0
  157. package/src/index.ts +22 -2
  158. package/src/mcp-connection-status.ts +35 -0
  159. package/src/model-policy.ts +21 -0
  160. package/src/plugin-details.tsx +8 -5
  161. package/src/plugin-discovery.tsx +92 -72
  162. package/src/realtime/realtime-control.tsx +6 -8
  163. package/src/session-auth-recommendation.ts +48 -0
  164. package/src/session-capability-policy.ts +83 -0
  165. package/src/session-ui.ts +2 -0
  166. package/src/session.ts +1 -0
  167. package/src/skill-discovery.tsx +101 -56
  168. package/src/timeline/compaction-copy.ts +40 -0
  169. package/src/timeline/platform-activity-row.tsx +8 -3
  170. package/src/timeline/projection.ts +74 -2
  171. package/src/timeline/shared.tsx +6 -1
  172. package/src/timeline/startup-timings.tsx +1 -1
  173. package/src/timeline/turn-summary.tsx +17 -3
  174. package/src/timeline/types.ts +22 -2
  175. package/styles/compiled.css +325 -27
  176. package/styles/connect.css +90 -15
  177. package/styles/index.css +46 -0
  178. package/styles/session-capability.css +42 -0
  179. package/dist/chunk-45XEUVTK.js.map +0 -1
  180. package/dist/chunk-HQV2I5HV.js +0 -124
  181. package/dist/chunk-HQV2I5HV.js.map +0 -1
  182. package/dist/chunk-HUIKIDM5.js.map +0 -1
  183. package/dist/chunk-ICA6R6TW.js.map +0 -1
  184. package/dist/chunk-IHQII5WO.js +0 -886
  185. package/dist/chunk-IHQII5WO.js.map +0 -1
  186. package/dist/chunk-KANFU34B.js.map +0 -1
  187. package/dist/chunk-L2NBTKRX.js.map +0 -1
  188. package/dist/chunk-NYESIADM.js.map +0 -1
  189. package/dist/chunk-PEI6RANI.js.map +0 -1
  190. package/dist/chunk-TDSEILUH.js.map +0 -1
  191. package/dist/chunk-UMECRJRV.js.map +0 -1
  192. package/dist/hooks/use-packs.d.ts +0 -32
  193. package/dist/platform-activity-row-DUE2W74J.js.map +0 -1
  194. package/src/hooks/use-packs.ts +0 -157
  195. /package/dist/{browser-viewer-NTD6UIPR.js.map → browser-viewer-LGH4IP47.js.map} +0 -0
  196. /package/dist/{chunk-NOSYBS2D.js.map → chunk-2VHARTBL.js.map} +0 -0
  197. /package/dist/{chunk-5M6VTFJ5.js.map → chunk-GBPCZH3Y.js.map} +0 -0
  198. /package/dist/{chunk-YBM6CQRU.js.map → chunk-GEN54DU5.js.map} +0 -0
  199. /package/dist/{chunk-R4KRSFGA.js.map → chunk-IP4BUBKH.js.map} +0 -0
  200. /package/dist/{chunk-NLTS7JHI.js.map → chunk-JPHDWKZE.js.map} +0 -0
  201. /package/dist/{chunk-24M4YBCL.js.map → chunk-PNAVEK2U.js.map} +0 -0
  202. /package/dist/{model-policy-picker-menu-3R336DP2.js.map → model-policy-picker-menu-RVOQ4ZZG.js.map} +0 -0
  203. /package/dist/{queue-surface-implementation-LK3S4BIW.js.map → queue-surface-implementation-3RIFK2BS.js.map} +0 -0
  204. /package/dist/{timeline-annotation-markers-DRSTLEM6.js.map → timeline-annotation-markers-OY6OWR3V.js.map} +0 -0
  205. /package/dist/{timeline-annotation-selection-IHLUGVND.js.map → timeline-annotation-selection-BOXA2TIN.js.map} +0 -0
  206. /package/dist/{timeline-annotations-dialog-6Q53N5FG.js.map → timeline-annotations-dialog-RGY3MHL3.js.map} +0 -0
@@ -19,15 +19,19 @@ import ReactMarkdown, {
19
19
  type UrlTransform,
20
20
  } from "react-markdown";
21
21
  import remarkGfm from "remark-gfm";
22
+ import { PanelsTopLeftIcon } from "lucide-react";
23
+ import { ActivityDisclosure } from "../timeline/shared";
22
24
  import { cn } from "../lib/cn";
23
25
  import { tableElementToTsv } from "../lib/clipboard";
24
26
  import { prefersReducedMotion } from "../lib/motion";
25
27
  import { MOTION_INSPECT_SCALE } from "../lib/motion-inspect";
26
28
  import { CopyButton } from "./copy-button";
29
+ import { PreviewLoading } from "./preview-loading";
27
30
  import type { observeMarkdownTableLayout } from "./markdown-table-layout";
28
31
  import { softenStreamingMarkdown } from "./soften-streaming-markdown";
29
32
  import { createStreamReveal, rehypeStreamReveal, type StreamReveal } from "./stream-reveal";
30
33
  import { TooltipProvider } from "./tooltip";
34
+ import { searchMatchOffset, type TimelineSearchTarget } from "./timeline-search";
31
35
 
32
36
  /**
33
37
  * The default renderer for chat message bodies in {@link MessageTimeline}.
@@ -46,11 +50,14 @@ import { TooltipProvider } from "./tooltip";
46
50
  export type MarkdownInteractiveBlock = { kind: "html" | "site"; content: string };
47
51
  const InteractiveContext = createContext<{
48
52
  source: string;
53
+ streaming?: boolean | undefined;
49
54
  render?: (block: MarkdownInteractiveBlock) => ReactNode;
50
55
  renderImage?: (image: { src: string; alt: string }) => ReactNode;
51
56
  }>({ source: "" });
52
57
 
53
58
  export type MarkdownProps = {
59
+ /** Reveals a bounded source excerpt; closing Find retains it until explicit restore. */
60
+ searchTarget?: TimelineSearchTarget | null | undefined;
54
61
  renderImage?: ((image: { src: string; alt: string }) => ReactNode) | undefined;
55
62
  /** Host opt-in for assistant-authored interactive fences. */
56
63
  renderInteractiveBlock?: ((block: MarkdownInteractiveBlock) => ReactNode) | undefined;
@@ -406,7 +413,7 @@ function fenceLanguage(children: ReactNode): string | null {
406
413
  }
407
414
 
408
415
  function InteractiveCodeBlock({ children, node }: ComponentPropsWithoutRef<"pre"> & ExtraProps) {
409
- const { source, render } = useContext(InteractiveContext);
416
+ const { source, render, streaming } = useContext(InteractiveContext);
410
417
  const language = fenceLanguage(children);
411
418
  if (render && (language === "opengeni-html" || language === "opengeni-site")) {
412
419
  const start = node?.position?.start.offset;
@@ -428,7 +435,23 @@ function InteractiveCodeBlock({ children, node }: ComponentPropsWithoutRef<"pre"
428
435
  closing &&
429
436
  closing.length >= opening[1]!.length &&
430
437
  [...closing].every((c) => c === opening[1]![0]);
431
- if (!complete) return <p role="status">Preparing preview…</p>;
438
+ if (!complete) {
439
+ return (
440
+ <div role="status" aria-live="polite" aria-busy={streaming === true} className="my-3">
441
+ {streaming ? (
442
+ <PreviewLoading />
443
+ ) : (
444
+ <ActivityDisclosure
445
+ icon={<PanelsTopLeftIcon aria-hidden className="size-3.5" />}
446
+ title="Preview incomplete"
447
+ running={false}
448
+ expandable={false}
449
+ preview="Generation stopped before the preview was ready."
450
+ />
451
+ )}
452
+ </div>
453
+ );
454
+ }
432
455
  return (
433
456
  <>
434
457
  {render({
@@ -622,10 +645,11 @@ function MarkdownImpl({
622
645
  const interactiveContext = useMemo(
623
646
  () => ({
624
647
  source: children,
648
+ streaming,
625
649
  ...(renderInteractiveBlock ? { render: renderInteractiveBlock } : {}),
626
650
  ...(renderImage ? { renderImage } : {}),
627
651
  }),
628
- [children, renderInteractiveBlock, renderImage],
652
+ [children, streaming, renderInteractiveBlock, renderImage],
629
653
  );
630
654
 
631
655
  // `min-w-0` lets the prose shrink inside flex parents (message bubbles) so
@@ -656,4 +680,62 @@ function MarkdownImpl({
656
680
  }
657
681
 
658
682
  /** Memoized so streaming re-renders of the parent don't re-parse settled bodies. */
659
- export const Markdown = memo(MarkdownImpl);
683
+ export const Markdown = memo(function SearchableMarkdown(props: MarkdownProps) {
684
+ const [retained, setRetained] = useState<{ text: string; target: TimelineSearchTarget } | null>(
685
+ null,
686
+ );
687
+ if (
688
+ props.searchTarget &&
689
+ (retained?.target !== props.searchTarget || retained.text !== props.children)
690
+ ) {
691
+ setRetained({ text: props.children, target: props.searchTarget });
692
+ }
693
+ // Closing Find only removes the highlight. Retain the source window so a huge
694
+ // formatted message cannot replace it and unexpectedly move the reading point.
695
+ const target = props.searchTarget ?? (retained?.text === props.children ? retained.target : null);
696
+ if (!target) return <MarkdownImpl {...props} />;
697
+ const offset = searchMatchOffset(props.children, target);
698
+ if (offset < 0)
699
+ return props.searchTarget ? (
700
+ <div role="status">This match is no longer in the message source.</div>
701
+ ) : (
702
+ <MarkdownImpl {...props} />
703
+ );
704
+ // Bound mounted text even for multi-megabyte messages. Do not split a UTF-16 pair
705
+ // at excerpt edges; the selected range itself is validated against the source.
706
+ let start = Math.max(0, offset - 240);
707
+ let end = Math.min(props.children.length, offset + target.query.length + 240);
708
+ if (start > 0 && /[\uDC00-\uDFFF]/.test(props.children[start]!)) start--;
709
+ if (end < props.children.length && /[\uDC00-\uDFFF]/.test(props.children[end]!)) end++;
710
+ const MatchTag = props.searchTarget ? "mark" : "span";
711
+ return (
712
+ <div className={cn("og-markdown-body min-w-0 break-words", props.className)}>
713
+ <div data-og-annotation-chrome="" className="text-xs opacity-70">
714
+ Match in message source · Excerpt stays in place when Find closes
715
+ <button
716
+ type="button"
717
+ disabled={!!props.searchTarget}
718
+ title={props.searchTarget ? "Close Find to show the formatted message" : undefined}
719
+ className="ml-2 inline-flex min-h-7 items-center rounded-og-sm px-1.5 text-og-xs font-medium text-og-fg-muted outline-hidden hover:text-og-fg focus-visible:ring-2 focus-visible:ring-og-accent/45 pointer-coarse:min-h-11 disabled:opacity-50"
720
+ onClick={() => setRetained(null)}
721
+ >
722
+ Show formatted message
723
+ </button>
724
+ </div>
725
+ <div style={{ whiteSpace: "pre-wrap", overflowWrap: "anywhere" }}>
726
+ {start > 0 ? "…" : ""}
727
+ {props.children.slice(start, offset)}
728
+ <MatchTag
729
+ data-og-search-occurrence={target.occurrence ?? 0}
730
+ data-og-search-sequence={target.sequence}
731
+ data-og-search-query={target.query}
732
+ data-og-search-offset={offset}
733
+ >
734
+ {props.children.slice(offset, offset + target.query.length)}
735
+ </MatchTag>
736
+ {props.children.slice(offset + target.query.length, end)}
737
+ {end < props.children.length ? "…" : ""}
738
+ </div>
739
+ </div>
740
+ );
741
+ });
@@ -1,9 +1,16 @@
1
1
  import { RollingActivity } from "../timeline/rolling-activity";
2
+ import {
3
+ isTimelineSearchTarget,
4
+ TimelineSearchRevealContext,
5
+ useTimelineSearchNavigation,
6
+ type TimelineSearchTarget,
7
+ } from "./timeline-search";
2
8
  import { GenieLoadingOptionsContext, type GenieLoadingOptions } from "../timeline/genie-loading";
3
9
  import { ChildSessionLink } from "./child-session-link";
4
10
  import { useStartupDetails } from "../timeline/startup-preference";
5
11
  import { parseSandboxFileArtifactReceipt } from "@opengeni/sdk";
6
12
  import { unwrapMcpOutput } from "../timeline/parsers";
13
+ import { compactionSkipSubtitle } from "../timeline/compaction-copy";
7
14
  import { isRetainedImageContentType } from "../timeline/retained-image";
8
15
  import { mcpToolLeaf } from "../timeline/tool-display-name";
9
16
  import type {
@@ -134,6 +141,8 @@ const TimelineAnnotationSelection = lazy(() => import("./timeline-annotation-sel
134
141
  const TimelineAnnotationMarkers = lazy(() => import("./timeline-annotation-markers"));
135
142
 
136
143
  export type MessageTimelineProps = {
144
+ /** Exact durable search hit; clearing it removes highlighting without moving the reader. */
145
+ searchTarget?: TimelineSearchTarget | null | undefined;
137
146
  /** Localized user-message disclosure actions, including custom UserMessageBody renderers. */
138
147
  userMessageDisclosureLabels?: UserMessageDisclosureLabels | undefined;
139
148
  /** Raw session events (projected internally) … */
@@ -146,7 +155,11 @@ export type MessageTimelineProps = {
146
155
  renderMessageActions?: ((item: AgentMessageItem | UserMessageItem) => ReactNode) | undefined;
147
156
  /** Plug a markdown renderer for message bodies (e.g. streamdown). */
148
157
  renderMessageText?:
149
- | ((text: string, item: AgentMessageItem | UserMessageItem) => ReactNode)
158
+ | ((
159
+ text: string,
160
+ item: AgentMessageItem | UserMessageItem,
161
+ context: { searchTarget: TimelineSearchTarget | null },
162
+ ) => ReactNode)
150
163
  | undefined;
151
164
  /** Drill into a spawned worker session. */
152
165
  onOpenSession?: ((sessionId: string) => void) | undefined;
@@ -413,6 +426,7 @@ function cssEscapeAttribute(value: string): string {
413
426
  */
414
427
  export function MessageTimeline({
415
428
  userMessageDisclosureLabels,
429
+ searchTarget,
416
430
  events,
417
431
  items,
418
432
  status: _status,
@@ -457,6 +471,46 @@ export function MessageTimeline({
457
471
  (item) => item.kind !== "auth-needed" || shouldRenderAuthNeeded(item),
458
472
  );
459
473
  }, [items, events, shouldRenderAuthNeeded]);
474
+ const searchItem = searchTarget
475
+ ? resolvedItems.find((item) => isTimelineSearchTarget(item, searchTarget))
476
+ : undefined;
477
+ const searchRevealKey =
478
+ searchItem && searchTarget
479
+ ? JSON.stringify([
480
+ searchItem.id,
481
+ searchTarget.query,
482
+ searchTarget.occurrence ?? 0,
483
+ searchTarget.offset ?? null,
484
+ ])
485
+ : null;
486
+ const searchTargetRef = useRef(searchTarget);
487
+ searchTargetRef.current = searchTarget;
488
+ const renderSearchableMessageText = useMemo(
489
+ () =>
490
+ renderMessageText
491
+ ? (text: string, item: AgentMessageItem | UserMessageItem) =>
492
+ renderMessageText(text, item, {
493
+ searchTarget: searchItem?.id === item.id ? (searchTarget ?? null) : null,
494
+ })
495
+ : (text: string, item: AgentMessageItem | UserMessageItem) => {
496
+ const body = (
497
+ <Markdown
498
+ searchTarget={searchItem?.id === item.id ? searchTarget : null}
499
+ streaming={item.kind === "agent-message" && item.streaming}
500
+ >
501
+ {text}
502
+ </Markdown>
503
+ );
504
+ return item.kind === "user-message" ? (
505
+ <UserMessageBody messageId={item.id} text={text}>
506
+ {body}
507
+ </UserMessageBody>
508
+ ) : (
509
+ body
510
+ );
511
+ },
512
+ [renderMessageText, searchItem?.id, searchTarget],
513
+ );
460
514
  // Event-window identity is independent of projected rows (partial messages
461
515
  // can acquire a different first-delta id when older text arrives).
462
516
  const sourceItems = events ?? items;
@@ -1005,7 +1059,7 @@ export function MessageTimeline({
1005
1059
  userMessageDisclosureContext,
1006
1060
  behavior: {
1007
1061
  renderMessageActions,
1008
- renderMessageText,
1062
+ renderMessageText: renderSearchableMessageText,
1009
1063
  onOpenSession,
1010
1064
  onMemoryClick,
1011
1065
  onReconnect,
@@ -1028,7 +1082,7 @@ export function MessageTimeline({
1028
1082
  onReconnect,
1029
1083
  renderAuthNeeded,
1030
1084
  renderMessageActions,
1031
- renderMessageText,
1085
+ renderSearchableMessageText,
1032
1086
  resolveProviderLogo,
1033
1087
  toolRegistry,
1034
1088
  genieLoading,
@@ -1037,6 +1091,19 @@ export function MessageTimeline({
1037
1091
  ],
1038
1092
  );
1039
1093
 
1094
+ const releaseSearchPin = useCallback(() => {
1095
+ applyPinned(false);
1096
+ stopFollow();
1097
+ setRevealed(true);
1098
+ }, [applyPinned, stopFollow]);
1099
+ useTimelineSearchNavigation(
1100
+ scrollRef,
1101
+ searchItem?.id,
1102
+ searchTarget,
1103
+ releaseSearchPin,
1104
+ resolvedItems,
1105
+ );
1106
+
1040
1107
  const requestOlderIfUnderfilled = useCallback(
1041
1108
  (node: HTMLElement, retry?: OlderLoadAttempt) => {
1042
1109
  let currentAttempt = olderLoadAttemptRef.current;
@@ -1707,6 +1774,8 @@ export function MessageTimeline({
1707
1774
  // at what just became the live bottom — paging forward to the tip must not
1708
1775
  // strand them unpinned watching new content grow below.
1709
1776
  useEffect(() => {
1777
+ // Closing find is paint-only; it must not trigger this history-to-tip rule.
1778
+ if (searchTargetRef.current) return;
1710
1779
  if (hasNewer) {
1711
1780
  stopFollow();
1712
1781
  applyPinned(false);
@@ -2100,23 +2169,31 @@ export function MessageTimeline({
2100
2169
  ) : null}
2101
2170
  {groups.map(({ group, key, entranceEnabled }, index) => {
2102
2171
  return (
2103
- <TimelineGroupEntry
2172
+ <TimelineSearchRevealContext.Provider
2104
2173
  key={key}
2105
- groupKey={key}
2106
- group={group}
2107
- nextGroup={groups[index + 1]?.group}
2108
- startupDismissed={
2109
- group.kind === "activity" &&
2110
- group.items.some(
2111
- (item) => item.turnId && turnsWithOutput.has(item.turnId),
2112
- )
2113
- }
2114
- entranceEnabled={entranceEnabled}
2115
- liveEntranceEnabled={
2116
- group.kind === "activity" ? !bulkRender : undefined
2174
+ value={
2175
+ searchItem && timelineGroupItemIds(group).includes(searchItem.id)
2176
+ ? searchRevealKey
2177
+ : null
2117
2178
  }
2118
- context={timelineGroupEntryContext}
2119
- />
2179
+ >
2180
+ <TimelineGroupEntry
2181
+ groupKey={key}
2182
+ group={group}
2183
+ nextGroup={groups[index + 1]?.group}
2184
+ startupDismissed={
2185
+ group.kind === "activity" &&
2186
+ group.items.some(
2187
+ (item) => item.turnId && turnsWithOutput.has(item.turnId),
2188
+ )
2189
+ }
2190
+ entranceEnabled={entranceEnabled}
2191
+ liveEntranceEnabled={
2192
+ group.kind === "activity" ? !bulkRender : undefined
2193
+ }
2194
+ context={timelineGroupEntryContext}
2195
+ />
2196
+ </TimelineSearchRevealContext.Provider>
2120
2197
  );
2121
2198
  })}
2122
2199
  {groups.length > 0 && trailingState ? (
@@ -2455,7 +2532,9 @@ type TimelineGroupEntryContext = {
2455
2532
 
2456
2533
  type TimelineGroupBehaviorProps = {
2457
2534
  renderMessageActions: MessageTimelineProps["renderMessageActions"];
2458
- renderMessageText: MessageTimelineProps["renderMessageText"];
2535
+ renderMessageText:
2536
+ | ((text: string, item: AgentMessageItem | UserMessageItem) => ReactNode)
2537
+ | undefined;
2459
2538
  onOpenSession: MessageTimelineProps["onOpenSession"];
2460
2539
  onMemoryClick: MessageTimelineProps["onMemoryClick"];
2461
2540
  onReconnect: MessageTimelineProps["onReconnect"];
@@ -3260,7 +3339,7 @@ function CompactionRow({ item }: { item: ContextCompactionItem }) {
3260
3339
  item.phase === "compacted"
3261
3340
  ? "Chat history above is unchanged"
3262
3341
  : item.phase === "skipped"
3263
- ? compactionSkipSubtitle(item.skipReason)
3342
+ ? compactionSkipSubtitle(item.skipReason, item.providerRejection)
3264
3343
  : null;
3265
3344
  const pill =
3266
3345
  item.phase === "skipped" && item.skipReason === "summarization_failed"
@@ -3290,21 +3369,6 @@ function CompactionRow({ item }: { item: ContextCompactionItem }) {
3290
3369
  );
3291
3370
  }
3292
3371
 
3293
- function compactionSkipSubtitle(reason: string | null): string {
3294
- switch (reason) {
3295
- case "no_history":
3296
- return "No active history to compact";
3297
- case "replacement_not_smaller":
3298
- return "Checkpoint would not reduce memory size";
3299
- case "replacement_unchanged":
3300
- return "Checkpoint made no progress";
3301
- case "summarization_failed":
3302
- return "Request it again to retry. Chat history is unchanged.";
3303
- default:
3304
- return "Compaction was not needed. Chat history is unchanged.";
3305
- }
3306
- }
3307
-
3308
3372
  /** Hover-reveal clock beside the copy control (sent / finished). */
3309
3373
  function MessageFooterTime({ occurredAt }: { occurredAt: string }) {
3310
3374
  return (
@@ -3355,7 +3419,10 @@ function UserMessageRow({
3355
3419
  >
3356
3420
  <div className={MESSAGE_BUBBLE_CLASS}>
3357
3421
  {item.text ? (
3358
- <div data-og-annotation-source-key={item.annotationSource?.eventId}>
3422
+ <div
3423
+ data-og-search-item={item.id}
3424
+ data-og-annotation-source-key={item.annotationSource?.eventId}
3425
+ >
3359
3426
  {renderMessageText ? (
3360
3427
  renderMessageText(item.text, item)
3361
3428
  ) : (
@@ -3578,6 +3645,7 @@ function AgentMessageRow({
3578
3645
  >
3579
3646
  <div
3580
3647
  data-og-wide-table-message=""
3648
+ data-og-search-item={item.id}
3581
3649
  data-og-annotation-source-key={item.annotationSource?.eventId}
3582
3650
  >
3583
3651
  {body}
@@ -59,7 +59,14 @@ function providerGroups(models: ClientModel[], codexOnly: boolean) {
59
59
  optionDisabled: codexOnly && !isCodexClientModel(model),
60
60
  });
61
61
  }
62
- return [...byProvider.values()];
62
+ const groups = [...byProvider.values()];
63
+ if (codexOnly) {
64
+ const codexIndex = groups.findIndex((group) =>
65
+ group.models.some((model) => !model.optionDisabled && isCodexClientModel(model)),
66
+ );
67
+ if (codexIndex > 0) groups.unshift(...groups.splice(codexIndex, 1));
68
+ }
69
+ return groups;
63
70
  }
64
71
 
65
72
  export function ModelPicker({
@@ -78,8 +85,9 @@ export function ModelPicker({
78
85
  [rows, codexOnly],
79
86
  );
80
87
  const billingGroups = useMemo(
81
- () => (effectiveRows.length > 0 ? groupPickerRowsByBillingClass(effectiveRows) : []),
82
- [effectiveRows],
88
+ () =>
89
+ effectiveRows.length > 0 ? groupPickerRowsByBillingClass(effectiveRows, { codexOnly }) : [],
90
+ [effectiveRows, codexOnly],
83
91
  );
84
92
  const providerGroupList = useMemo(() => providerGroups(models, codexOnly), [models, codexOnly]);
85
93
  const useBillingGroups = Boolean(effectiveRows.length);
@@ -34,7 +34,10 @@ export function ModelPolicyPickerMenu(props: ModelPolicyPickerProps) {
34
34
  `${row.label} ${row.id} ${row.providerLabel} ${row.billingClassLabel} ${payerSummaryForModel(row.catalog)}`.toLowerCase();
35
35
  return words.every((word) => text.includes(word));
36
36
  });
37
- const groups = groupPickerRowsByBillingClass(filtered);
37
+ const matchingIds = new Set(filtered.map((row) => row.id));
38
+ const groups = groupPickerRowsByBillingClass(rows, { codexOnly: props.codexOnly === true })
39
+ .map((group) => ({ ...group, rows: group.rows.filter((row) => matchingIds.has(row.id)) }))
40
+ .filter((group) => group.rows.length > 0);
38
41
  const choose = (row: ClientPickerModelRow) => {
39
42
  if (!row.selectable || props.disabled) return;
40
43
  if (row.id !== props.model) props.onModelChange(row.id);
@@ -139,8 +142,12 @@ export function ModelPolicyPickerMenu(props: ModelPolicyPickerProps) {
139
142
  ) : (
140
143
  <>
141
144
  {groups.map((group) => (
142
- <section key={group.billingClass} aria-label={group.label} className="py-1">
143
- <div className="flex items-center gap-2 px-2.5 py-1.5 text-og-control font-medium text-og-fg-subtle">
145
+ <section
146
+ key={group.billingClass}
147
+ aria-label={group.label}
148
+ className="py-2.5 first:pt-1 [&+section]:border-t [&+section]:border-og-border"
149
+ >
150
+ <div className="flex items-center gap-2 px-2.5 py-1.5 text-og-control font-semibold text-og-fg">
144
151
  <BillingClassMark billingClass={group.billingClass} aria-label="" />
145
152
  {group.label}
146
153
  </div>
@@ -0,0 +1,154 @@
1
+ import { useEffect, useRef } from "react";
2
+
3
+ /** Private loading surface: never receives or executes the unfinished fence. */
4
+ export function PreviewLoading() {
5
+ const ref = useRef<HTMLCanvasElement>(null);
6
+ useEffect(() => {
7
+ const canvas = ref.current;
8
+ if (!canvas) return;
9
+ let ctx: CanvasRenderingContext2D | null;
10
+ try {
11
+ ctx = canvas.getContext("2d");
12
+ } catch {
13
+ return; // The CSS grid and status remain when canvas is unavailable.
14
+ }
15
+ if (!ctx) return;
16
+ const context = ctx;
17
+ const motion = window.matchMedia?.("(prefers-reduced-motion: reduce)");
18
+ let frame: number | undefined;
19
+ let time = 2;
20
+ let last: number | undefined;
21
+ let disposed = false;
22
+ // Without visibility observation, keep a static surface rather than doing
23
+ // unbounded work for previews that may be outside the viewport.
24
+ let visible = false;
25
+ const canAnimate = () => !disposed && visible && !document.hidden && motion?.matches === false;
26
+
27
+ function draw() {
28
+ const w = canvas!.clientWidth;
29
+ const h = canvas!.clientHeight;
30
+ if (!w || !h || document.hidden || disposed) return;
31
+ const d = Math.min(window.devicePixelRatio || 1, 2);
32
+ if (canvas!.width !== Math.round(w * d) || canvas!.height !== Math.round(h * d)) {
33
+ canvas!.width = Math.round(w * d);
34
+ canvas!.height = Math.round(h * d);
35
+ }
36
+ const style = getComputedStyle(canvas!);
37
+ const ink = style.getPropertyValue("--_og-preview-ink").trim() || "#bca3ef";
38
+ const glowColor = style.getPropertyValue("--_og-preview-glow").trim() || "#8f6ccc";
39
+ context.setTransform(d, 0, 0, d, 0, 0);
40
+ context.clearRect(0, 0, w, h);
41
+ const cx = w * (0.5 + 0.13 * Math.sin(time * 0.21));
42
+ const cy = h * (0.5 + 0.12 * Math.cos(time * 0.27));
43
+ const glow = context.createRadialGradient(cx, cy, 0, cx, cy, Math.max(w, h) * 0.65);
44
+ glow.addColorStop(0, glowColor);
45
+ glow.addColorStop(1, "transparent");
46
+ context.globalAlpha = 0.09;
47
+ context.fillStyle = glow;
48
+ context.fillRect(0, 0, w, h);
49
+ const rows = Math.ceil(h / 18) + 7;
50
+ const cols = Math.ceil(w / 18) + 7;
51
+ const points = Array.from({ length: rows }, (_row, r) =>
52
+ Array.from({ length: cols }, (_col, c) => {
53
+ const x = (c - 3) * 18;
54
+ const y = (r - 3) * 18;
55
+ const dist = Math.hypot((x - cx) * 0.8, y - cy);
56
+ const wave = Math.sin(dist * 0.032 - time * 0.95);
57
+ return {
58
+ x: x + ((x - cx) / Math.max(dist, 1)) * wave * 4,
59
+ y: y + ((y - cy) / Math.max(dist, 1)) * wave * 4,
60
+ l: Math.pow((wave + 1) / 2, 5),
61
+ };
62
+ }),
63
+ );
64
+ context.strokeStyle = ink;
65
+ context.fillStyle = ink;
66
+ context.lineWidth = 0.65;
67
+ points.forEach((row, r) =>
68
+ row.forEach((p, c) => {
69
+ context.globalAlpha = 0.035 + p.l * 0.055;
70
+ context.beginPath();
71
+ if (c + 1 < cols) {
72
+ context.moveTo(p.x, p.y);
73
+ context.lineTo(row[c + 1]!.x, row[c + 1]!.y);
74
+ }
75
+ if (r + 1 < rows) {
76
+ context.moveTo(p.x, p.y);
77
+ context.lineTo(points[r + 1]![c]!.x, points[r + 1]![c]!.y);
78
+ }
79
+ context.stroke();
80
+ context.globalAlpha = 0.15 + p.l * 0.55;
81
+ context.beginPath();
82
+ context.arc(p.x, p.y, 0.85 + p.l * 0.65, 0, Math.PI * 2);
83
+ context.fill();
84
+ }),
85
+ );
86
+ context.globalAlpha = 1;
87
+ canvas!.dataset.painted = "true";
88
+ }
89
+
90
+ function stop() {
91
+ if (frame !== undefined) window.cancelAnimationFrame(frame);
92
+ frame = undefined;
93
+ last = undefined;
94
+ }
95
+ function tick(stamp: number) {
96
+ frame = undefined;
97
+ if (!canAnimate()) return;
98
+ if (last !== undefined) time += Math.min((stamp - last) / 1000, 0.05);
99
+ last = stamp;
100
+ draw();
101
+ frame = window.requestAnimationFrame(tick);
102
+ }
103
+ function refresh() {
104
+ stop();
105
+ if (disposed || document.hidden || (!visible && intersection)) return;
106
+ draw();
107
+ if (canAnimate() && typeof window.requestAnimationFrame === "function") {
108
+ frame = window.requestAnimationFrame(tick);
109
+ }
110
+ }
111
+ const intersection =
112
+ typeof IntersectionObserver === "function"
113
+ ? new IntersectionObserver(([entry]) => {
114
+ visible = entry?.isIntersecting === true;
115
+ refresh();
116
+ })
117
+ : undefined;
118
+ intersection?.observe(canvas);
119
+ const resize = typeof ResizeObserver === "function" ? new ResizeObserver(refresh) : undefined;
120
+ resize?.observe(canvas);
121
+ // Observe only ancestors, not the document subtree. This also updates the
122
+ // static reduced-motion canvas when a host changes its local theme.
123
+ const theme =
124
+ typeof MutationObserver === "function" ? new MutationObserver(refresh) : undefined;
125
+ for (let ancestor = canvas.parentElement; ancestor; ancestor = ancestor.parentElement) {
126
+ theme?.observe(ancestor, {
127
+ attributes: true,
128
+ attributeFilter: ["class", "style", "data-og-theme"],
129
+ });
130
+ }
131
+ motion?.addEventListener?.("change", refresh);
132
+ document.addEventListener("visibilitychange", refresh);
133
+ window.addEventListener("resize", refresh);
134
+ // Old hosts get one still frame; observed surfaces wait until onscreen.
135
+ if (!intersection) draw();
136
+ return () => {
137
+ disposed = true;
138
+ stop();
139
+ intersection?.disconnect();
140
+ resize?.disconnect();
141
+ theme?.disconnect();
142
+ motion?.removeEventListener?.("change", refresh);
143
+ document.removeEventListener("visibilitychange", refresh);
144
+ window.removeEventListener("resize", refresh);
145
+ };
146
+ }, []);
147
+
148
+ return (
149
+ <div className="og-preview-loading">
150
+ <canvas ref={ref} aria-hidden="true" />
151
+ <span className="og-preview-loading-label">Preparing preview…</span>
152
+ </div>
153
+ );
154
+ }