@opengeni/react 6.1.0 → 7.0.0-canary.35625901699001

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 (214) hide show
  1. package/README.md +83 -2
  2. package/dist/approvals.d.ts +2 -1
  3. package/dist/artifacts-document.js +1 -1
  4. package/dist/artifacts-presentation.js +1 -1
  5. package/dist/artifacts-spreadsheet.js +1 -1
  6. package/dist/artifacts.js +3 -3
  7. package/dist/{browser-viewer-NTD6UIPR.js → browser-viewer-LGH4IP47.js} +3 -3
  8. package/dist/capability-catalog-row.d.ts +20 -0
  9. package/dist/capability-logo-fallback.d.ts +3 -0
  10. package/dist/{chunk-NOSYBS2D.js → chunk-2VHARTBL.js} +2 -1
  11. package/dist/{chunk-VRBL3F5E.js → chunk-5GAL23FF.js} +10 -2
  12. package/dist/{chunk-VRBL3F5E.js.map → chunk-5GAL23FF.js.map} +1 -1
  13. package/dist/chunk-5UONOQ3Y.js +635 -0
  14. package/dist/chunk-5UONOQ3Y.js.map +1 -0
  15. package/dist/{chunk-NYESIADM.js → chunk-AKOMC3VO.js} +3 -3
  16. package/dist/chunk-AKOMC3VO.js.map +1 -0
  17. package/dist/chunk-BRM6FVZA.js +302 -0
  18. package/dist/chunk-BRM6FVZA.js.map +1 -0
  19. package/dist/{chunk-KANFU34B.js → chunk-DBPVUGQT.js} +11 -2
  20. package/dist/chunk-DBPVUGQT.js.map +1 -0
  21. package/dist/chunk-FHIGTPME.js +1589 -0
  22. package/dist/chunk-FHIGTPME.js.map +1 -0
  23. package/dist/{chunk-5M6VTFJ5.js → chunk-GBPCZH3Y.js} +2 -2
  24. package/dist/{chunk-YBM6CQRU.js → chunk-GEN54DU5.js} +4 -4
  25. package/dist/{chunk-UMECRJRV.js → chunk-HJ5VJ4JB.js} +21 -12
  26. package/dist/chunk-HJ5VJ4JB.js.map +1 -0
  27. package/dist/{chunk-R4KRSFGA.js → chunk-IP4BUBKH.js} +3 -3
  28. package/dist/{chunk-NLTS7JHI.js → chunk-JPHDWKZE.js} +5 -5
  29. package/dist/{chunk-TDSEILUH.js → chunk-MI6ALLLD.js} +1297 -1535
  30. package/dist/chunk-MI6ALLLD.js.map +1 -0
  31. package/dist/{chunk-ICA6R6TW.js → chunk-OWHPKAOV.js} +28 -20
  32. package/dist/chunk-OWHPKAOV.js.map +1 -0
  33. package/dist/{chunk-24M4YBCL.js → chunk-PNAVEK2U.js} +4 -4
  34. package/dist/{chunk-L2NBTKRX.js → chunk-TXCBE32L.js} +203 -19
  35. package/dist/chunk-TXCBE32L.js.map +1 -0
  36. package/dist/{chunk-45XEUVTK.js → chunk-UXE5SSNN.js} +293 -463
  37. package/dist/chunk-UXE5SSNN.js.map +1 -0
  38. package/dist/{chunk-PEI6RANI.js → chunk-VR4Z73EU.js} +203 -9
  39. package/dist/chunk-VR4Z73EU.js.map +1 -0
  40. package/dist/{chunk-HUIKIDM5.js → chunk-XMXRI5JK.js} +65 -18
  41. package/dist/chunk-XMXRI5JK.js.map +1 -0
  42. package/dist/client.d.ts +1 -1
  43. package/dist/components/artifacts/spreadsheet-general-display.d.ts +24 -0
  44. package/dist/components/chat-composer.d.ts +13 -3
  45. package/dist/components/markdown.d.ts +6 -3
  46. package/dist/components/message-timeline.d.ts +7 -2
  47. package/dist/components/preview-loading.d.ts +2 -0
  48. package/dist/components/session-capability-frame.d.ts +24 -0
  49. package/dist/components/session-chrome.d.ts +1 -2
  50. package/dist/components/session-commands.d.ts +6 -0
  51. package/dist/components/session-connection-request.d.ts +8 -0
  52. package/dist/components/session-conversation.d.ts +3 -0
  53. package/dist/components/session-mcp-capability-card.d.ts +22 -0
  54. package/dist/components/timeline-search.d.ts +34 -0
  55. package/dist/composer.js +7 -7
  56. package/dist/connect-account-identity.d.ts +8 -0
  57. package/dist/connect-accounts.d.ts +5 -0
  58. package/dist/connect-chooser.d.ts +6 -0
  59. package/dist/connect-panel.d.ts +9 -2
  60. package/dist/connect-setup.d.ts +5 -1
  61. package/dist/connect.d.ts +5 -21
  62. package/dist/connect.js +1268 -584
  63. package/dist/connect.js.map +1 -1
  64. package/dist/connection-catalog.d.ts +6 -1
  65. package/dist/connection-discovery.d.ts +12 -0
  66. package/dist/connection-installed.d.ts +1 -0
  67. package/dist/connection-logo.d.ts +5 -2
  68. package/dist/connection-service-logo.d.ts +8 -0
  69. package/dist/connection-service-presentation.d.ts +13 -0
  70. package/dist/connection-skeleton.d.ts +5 -0
  71. package/dist/discovery-cache.d.ts +5 -0
  72. package/dist/hooks/use-codex-accounts.d.ts +11 -5
  73. package/dist/hooks/use-connect.d.ts +22 -0
  74. package/dist/hooks/use-session-events.d.ts +5 -0
  75. package/dist/hooks/use-session-mcp-approval-policy.d.ts +2 -2
  76. package/dist/hooks/use-workspace-sessions.d.ts +2 -0
  77. package/dist/index.d.ts +8 -2
  78. package/dist/index.js +771 -793
  79. package/dist/index.js.map +1 -1
  80. package/dist/interaction.js +2 -2
  81. package/dist/machines.js +2 -2
  82. package/dist/mcp-connection-status.d.ts +14 -0
  83. package/dist/{model-policy-picker-menu-3R336DP2.js → model-policy-picker-menu-RVOQ4ZZG.js} +4 -4
  84. package/dist/model-policy.d.ts +6 -2
  85. package/dist/model-policy.js +1 -1
  86. package/dist/{platform-activity-row-DUE2W74J.js → platform-activity-row-HLOH7FW4.js} +9 -4
  87. package/dist/platform-activity-row-HLOH7FW4.js.map +1 -0
  88. package/dist/plugin-discovery.d.ts +6 -0
  89. package/dist/{queue-surface-implementation-LK3S4BIW.js → queue-surface-implementation-3RIFK2BS.js} +4 -4
  90. package/dist/realtime.js +22 -22
  91. package/dist/realtime.js.map +1 -1
  92. package/dist/session-auth-recommendation.d.ts +5 -0
  93. package/dist/session-capability-policy.d.ts +12 -0
  94. package/dist/session-ui.d.ts +2 -0
  95. package/dist/session-ui.js +14 -12
  96. package/dist/session.d.ts +1 -0
  97. package/dist/session.js +6 -4
  98. package/dist/skill-discovery.d.ts +11 -0
  99. package/dist/timeline/compaction-copy.d.ts +10 -0
  100. package/dist/timeline/projection.d.ts +3 -1
  101. package/dist/timeline/tool-display-name.d.ts +2 -1
  102. package/dist/timeline/turn-summary.d.ts +2 -2
  103. package/dist/timeline/types.d.ts +24 -2
  104. package/dist/{timeline-annotation-markers-DRSTLEM6.js → timeline-annotation-markers-OY6OWR3V.js} +2 -2
  105. package/dist/{timeline-annotation-selection-IHLUGVND.js → timeline-annotation-selection-BOXA2TIN.js} +2 -2
  106. package/dist/{timeline-annotations-dialog-6Q53N5FG.js → timeline-annotations-dialog-RGY3MHL3.js} +3 -3
  107. package/package.json +3 -3
  108. package/src/approvals.ts +8 -1
  109. package/src/assets/README.md +8 -0
  110. package/src/assets/gmail.ico +0 -0
  111. package/src/assets/outlook.ico +0 -0
  112. package/src/capability-catalog-row.tsx +94 -0
  113. package/src/capability-logo-fallback.ts +22 -0
  114. package/src/client.ts +0 -9
  115. package/src/components/approval-surface.tsx +12 -2
  116. package/src/components/artifacts/document-editor.tsx +63 -6
  117. package/src/components/artifacts/presentation-editor.tsx +214 -9
  118. package/src/components/artifacts/spreadsheet-canvas.ts +3 -8
  119. package/src/components/artifacts/spreadsheet-general-display.ts +43 -0
  120. package/src/components/artifacts/spreadsheet-grid.tsx +10 -11
  121. package/src/components/chat-composer.tsx +26 -3
  122. package/src/components/composer.tsx +10 -4
  123. package/src/components/human-input-form.tsx +12 -2
  124. package/src/components/markdown.tsx +133 -9
  125. package/src/components/message-timeline.tsx +104 -36
  126. package/src/components/model-picker.tsx +11 -3
  127. package/src/components/model-policy-picker-menu.tsx +10 -3
  128. package/src/components/preview-loading.tsx +154 -0
  129. package/src/components/sandbox-workspace.tsx +20 -36
  130. package/src/components/session-capability-frame.tsx +163 -0
  131. package/src/components/session-chrome.tsx +46 -77
  132. package/src/components/session-commands.tsx +18 -0
  133. package/src/components/session-connection-request.tsx +97 -0
  134. package/src/components/session-conversation.tsx +10 -5
  135. package/src/components/session-mcp-capability-card.tsx +532 -0
  136. package/src/components/session-status.tsx +2 -2
  137. package/src/components/timeline-anchor.tsx +9 -8
  138. package/src/components/timeline-search.tsx +168 -0
  139. package/src/components/user-message-body.tsx +7 -0
  140. package/src/connect-account-identity.tsx +34 -0
  141. package/src/connect-accounts.tsx +114 -52
  142. package/src/connect-chooser.tsx +171 -39
  143. package/src/connect-panel.tsx +73 -9
  144. package/src/connect-setup.tsx +72 -25
  145. package/src/connect.ts +13 -22
  146. package/src/connection-assets.d.ts +4 -0
  147. package/src/connection-catalog.tsx +61 -39
  148. package/src/connection-discovery.tsx +235 -0
  149. package/src/connection-installed.tsx +2 -0
  150. package/src/connection-logo.tsx +18 -10
  151. package/src/connection-service-logo.tsx +55 -0
  152. package/src/connection-service-presentation.ts +46 -0
  153. package/src/connection-skeleton.tsx +16 -0
  154. package/src/conversation-timeline.ts +1 -0
  155. package/src/discovery-cache.ts +42 -0
  156. package/src/hooks/use-codex-accounts.ts +33 -14
  157. package/src/hooks/use-composer.ts +4 -5
  158. package/src/hooks/use-connect.ts +23 -0
  159. package/src/hooks/use-session-events.ts +114 -1
  160. package/src/hooks/use-session-mcp-approval-policy.ts +13 -5
  161. package/src/hooks/use-workspace-sessions.ts +10 -0
  162. package/src/index.ts +22 -2
  163. package/src/mcp-connection-status.ts +35 -0
  164. package/src/model-policy.ts +21 -0
  165. package/src/plugin-details.tsx +8 -5
  166. package/src/plugin-discovery.tsx +92 -72
  167. package/src/realtime/realtime-control.tsx +6 -8
  168. package/src/session-auth-recommendation.ts +48 -0
  169. package/src/session-capability-policy.ts +83 -0
  170. package/src/session-ui.ts +2 -0
  171. package/src/session.ts +1 -0
  172. package/src/skill-discovery.tsx +101 -56
  173. package/src/timeline/compaction-copy.ts +40 -0
  174. package/src/timeline/platform-activity-row.tsx +8 -3
  175. package/src/timeline/projection.ts +78 -2
  176. package/src/timeline/rolling-activity.tsx +4 -2
  177. package/src/timeline/shared.tsx +6 -1
  178. package/src/timeline/startup-timings.tsx +1 -1
  179. package/src/timeline/tool-display-name.ts +8 -6
  180. package/src/timeline/tool-renderers.tsx +5 -3
  181. package/src/timeline/turn-summary.tsx +17 -3
  182. package/src/timeline/types.ts +24 -2
  183. package/styles/compiled.css +325 -27
  184. package/styles/connect.css +90 -15
  185. package/styles/index.css +46 -0
  186. package/styles/session-capability.css +42 -0
  187. package/dist/chunk-45XEUVTK.js.map +0 -1
  188. package/dist/chunk-HQV2I5HV.js +0 -124
  189. package/dist/chunk-HQV2I5HV.js.map +0 -1
  190. package/dist/chunk-HUIKIDM5.js.map +0 -1
  191. package/dist/chunk-ICA6R6TW.js.map +0 -1
  192. package/dist/chunk-IHQII5WO.js +0 -886
  193. package/dist/chunk-IHQII5WO.js.map +0 -1
  194. package/dist/chunk-KANFU34B.js.map +0 -1
  195. package/dist/chunk-L2NBTKRX.js.map +0 -1
  196. package/dist/chunk-NYESIADM.js.map +0 -1
  197. package/dist/chunk-PEI6RANI.js.map +0 -1
  198. package/dist/chunk-TDSEILUH.js.map +0 -1
  199. package/dist/chunk-UMECRJRV.js.map +0 -1
  200. package/dist/hooks/use-packs.d.ts +0 -32
  201. package/dist/platform-activity-row-DUE2W74J.js.map +0 -1
  202. package/src/hooks/use-packs.ts +0 -157
  203. /package/dist/{browser-viewer-NTD6UIPR.js.map → browser-viewer-LGH4IP47.js.map} +0 -0
  204. /package/dist/{chunk-NOSYBS2D.js.map → chunk-2VHARTBL.js.map} +0 -0
  205. /package/dist/{chunk-5M6VTFJ5.js.map → chunk-GBPCZH3Y.js.map} +0 -0
  206. /package/dist/{chunk-YBM6CQRU.js.map → chunk-GEN54DU5.js.map} +0 -0
  207. /package/dist/{chunk-R4KRSFGA.js.map → chunk-IP4BUBKH.js.map} +0 -0
  208. /package/dist/{chunk-NLTS7JHI.js.map → chunk-JPHDWKZE.js.map} +0 -0
  209. /package/dist/{chunk-24M4YBCL.js.map → chunk-PNAVEK2U.js.map} +0 -0
  210. /package/dist/{model-policy-picker-menu-3R336DP2.js.map → model-policy-picker-menu-RVOQ4ZZG.js.map} +0 -0
  211. /package/dist/{queue-surface-implementation-LK3S4BIW.js.map → queue-surface-implementation-3RIFK2BS.js.map} +0 -0
  212. /package/dist/{timeline-annotation-markers-DRSTLEM6.js.map → timeline-annotation-markers-OY6OWR3V.js.map} +0 -0
  213. /package/dist/{timeline-annotation-selection-IHLUGVND.js.map → timeline-annotation-selection-BOXA2TIN.js.map} +0 -0
  214. /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,16 @@ 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
+ /** Resolve a retained-file reference to the host's authenticated artifact page. */
60
+ artifactHref?: ((artifactId: string) => string) | undefined;
61
+ /** Reveals a bounded source excerpt; closing Find retains it until explicit restore. */
62
+ searchTarget?: TimelineSearchTarget | null | undefined;
54
63
  renderImage?: ((image: { src: string; alt: string }) => ReactNode) | undefined;
55
64
  /** Host opt-in for assistant-authored interactive fences. */
56
65
  renderInteractiveBlock?: ((block: MarkdownInteractiveBlock) => ReactNode) | undefined;
@@ -210,10 +219,46 @@ const baseComponents: Components = {
210
219
  const MARKDOWN_LINK_CLASS =
211
220
  "break-words font-medium text-og-accent-strong underline-offset-2 hover:underline";
212
221
 
213
- function markdownComponents(onSandboxFile: MarkdownProps["onSandboxFile"]): Components {
222
+ function markdownComponents(
223
+ onSandboxFile: MarkdownProps["onSandboxFile"],
224
+ artifactHref: MarkdownProps["artifactHref"],
225
+ ): Components {
214
226
  return {
215
227
  ...baseComponents,
228
+ p: ({ children, node, ...props }) => {
229
+ const containsArtifact = node?.children.some(
230
+ (child) =>
231
+ child.type === "element" &&
232
+ child.tagName === "img" &&
233
+ typeof child.properties.src === "string" &&
234
+ retainedImageId(child.properties.src),
235
+ );
236
+ const Tag = containsArtifact ? "div" : "p";
237
+ return (
238
+ <Tag className="my-2.5 leading-7 first:mt-0 last:mb-0" {...props}>
239
+ {children}
240
+ </Tag>
241
+ );
242
+ },
216
243
  a: ({ children, href, ...props }) => {
244
+ const artifactId = href ? retainedImageId(href) : null;
245
+ if (artifactId) {
246
+ const destination = artifactHref?.(artifactId);
247
+ return destination ? (
248
+ <a
249
+ className={MARKDOWN_LINK_CLASS}
250
+ href={defaultUrlTransform(destination)}
251
+ target="_blank"
252
+ rel="noreferrer noopener"
253
+ >
254
+ {children}
255
+ </a>
256
+ ) : (
257
+ <span title="This artifact requires a workspace-aware host">
258
+ {children} (artifact unavailable)
259
+ </span>
260
+ );
261
+ }
217
262
  const location = sandboxFileLocationFromHref(href);
218
263
  if (location !== null) {
219
264
  return (
@@ -225,7 +270,7 @@ function markdownComponents(onSandboxFile: MarkdownProps["onSandboxFile"]): Comp
225
270
  if (isSandboxHref(href) || !href) {
226
271
  return (
227
272
  <span
228
- className="break-words font-medium text-og-fg-subtle underline decoration-dotted underline-offset-2"
273
+ className="break-words text-og-fg-subtle"
229
274
  aria-disabled="true"
230
275
  title={
231
276
  isSandboxHref(href)
@@ -233,7 +278,7 @@ function markdownComponents(onSandboxFile: MarkdownProps["onSandboxFile"]): Comp
233
278
  : "This link is unavailable"
234
279
  }
235
280
  >
236
- {children}
281
+ {children} (link unavailable)
237
282
  </span>
238
283
  );
239
284
  }
@@ -364,7 +409,7 @@ function MarkdownImage({ src, alt }: ComponentPropsWithoutRef<"img">) {
364
409
  }
365
410
 
366
411
  const markdownUrlTransform: UrlTransform = (url, key, node) =>
367
- (key === "href" && node.tagName === "a" && isSandboxHref(url)) ||
412
+ (key === "href" && node.tagName === "a" && (isSandboxHref(url) || retainedImageId(url))) ||
368
413
  (key === "src" && node.tagName === "img" && retainedImageId(url))
369
414
  ? url
370
415
  : defaultUrlTransform(url);
@@ -406,7 +451,7 @@ function fenceLanguage(children: ReactNode): string | null {
406
451
  }
407
452
 
408
453
  function InteractiveCodeBlock({ children, node }: ComponentPropsWithoutRef<"pre"> & ExtraProps) {
409
- const { source, render } = useContext(InteractiveContext);
454
+ const { source, render, streaming } = useContext(InteractiveContext);
410
455
  const language = fenceLanguage(children);
411
456
  if (render && (language === "opengeni-html" || language === "opengeni-site")) {
412
457
  const start = node?.position?.start.offset;
@@ -428,7 +473,23 @@ function InteractiveCodeBlock({ children, node }: ComponentPropsWithoutRef<"pre"
428
473
  closing &&
429
474
  closing.length >= opening[1]!.length &&
430
475
  [...closing].every((c) => c === opening[1]![0]);
431
- if (!complete) return <p role="status">Preparing preview…</p>;
476
+ if (!complete) {
477
+ return (
478
+ <div role="status" aria-live="polite" aria-busy={streaming === true} className="my-3">
479
+ {streaming ? (
480
+ <PreviewLoading />
481
+ ) : (
482
+ <ActivityDisclosure
483
+ icon={<PanelsTopLeftIcon aria-hidden className="size-3.5" />}
484
+ title="Preview incomplete"
485
+ running={false}
486
+ expandable={false}
487
+ preview="Generation stopped before the preview was ready."
488
+ />
489
+ )}
490
+ </div>
491
+ );
492
+ }
432
493
  return (
433
494
  <>
434
495
  {render({
@@ -523,6 +584,7 @@ function MarkdownTable({ children, className, ...props }: ComponentPropsWithoutR
523
584
 
524
585
  function MarkdownImpl({
525
586
  children,
587
+ artifactHref,
526
588
  className,
527
589
  streaming = false,
528
590
  onSandboxFile,
@@ -617,15 +679,19 @@ function MarkdownImpl({
617
679
  // fences don't snap to final GFM one commit before the crystallize morph.
618
680
  // Reveal identity still tracks the true source (`children`).
619
681
  const parseText = streaming || revealActive ? softenStreamingMarkdown(children) : children;
620
- const components = useMemo(() => markdownComponents(onSandboxFile), [onSandboxFile]);
682
+ const components = useMemo(
683
+ () => markdownComponents(onSandboxFile, artifactHref),
684
+ [onSandboxFile, artifactHref],
685
+ );
621
686
 
622
687
  const interactiveContext = useMemo(
623
688
  () => ({
624
689
  source: children,
690
+ streaming,
625
691
  ...(renderInteractiveBlock ? { render: renderInteractiveBlock } : {}),
626
692
  ...(renderImage ? { renderImage } : {}),
627
693
  }),
628
- [children, renderInteractiveBlock, renderImage],
694
+ [children, streaming, renderInteractiveBlock, renderImage],
629
695
  );
630
696
 
631
697
  // `min-w-0` lets the prose shrink inside flex parents (message bubbles) so
@@ -656,4 +722,62 @@ function MarkdownImpl({
656
722
  }
657
723
 
658
724
  /** Memoized so streaming re-renders of the parent don't re-parse settled bodies. */
659
- export const Markdown = memo(MarkdownImpl);
725
+ export const Markdown = memo(function SearchableMarkdown(props: MarkdownProps) {
726
+ const [retained, setRetained] = useState<{ text: string; target: TimelineSearchTarget } | null>(
727
+ null,
728
+ );
729
+ if (
730
+ props.searchTarget &&
731
+ (retained?.target !== props.searchTarget || retained.text !== props.children)
732
+ ) {
733
+ setRetained({ text: props.children, target: props.searchTarget });
734
+ }
735
+ // Closing Find only removes the highlight. Retain the source window so a huge
736
+ // formatted message cannot replace it and unexpectedly move the reading point.
737
+ const target = props.searchTarget ?? (retained?.text === props.children ? retained.target : null);
738
+ if (!target) return <MarkdownImpl {...props} />;
739
+ const offset = searchMatchOffset(props.children, target);
740
+ if (offset < 0)
741
+ return props.searchTarget ? (
742
+ <div role="status">This match is no longer in the message source.</div>
743
+ ) : (
744
+ <MarkdownImpl {...props} />
745
+ );
746
+ // Bound mounted text even for multi-megabyte messages. Do not split a UTF-16 pair
747
+ // at excerpt edges; the selected range itself is validated against the source.
748
+ let start = Math.max(0, offset - 240);
749
+ let end = Math.min(props.children.length, offset + target.query.length + 240);
750
+ if (start > 0 && /[\uDC00-\uDFFF]/.test(props.children[start]!)) start--;
751
+ if (end < props.children.length && /[\uDC00-\uDFFF]/.test(props.children[end]!)) end++;
752
+ const MatchTag = props.searchTarget ? "mark" : "span";
753
+ return (
754
+ <div className={cn("og-markdown-body min-w-0 break-words", props.className)}>
755
+ <div data-og-annotation-chrome="" className="text-xs opacity-70">
756
+ Match in message source · Excerpt stays in place when Find closes
757
+ <button
758
+ type="button"
759
+ disabled={!!props.searchTarget}
760
+ title={props.searchTarget ? "Close Find to show the formatted message" : undefined}
761
+ 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"
762
+ onClick={() => setRetained(null)}
763
+ >
764
+ Show formatted message
765
+ </button>
766
+ </div>
767
+ <div style={{ whiteSpace: "pre-wrap", overflowWrap: "anywhere" }}>
768
+ {start > 0 ? "…" : ""}
769
+ {props.children.slice(start, offset)}
770
+ <MatchTag
771
+ data-og-search-occurrence={target.occurrence ?? 0}
772
+ data-og-search-sequence={target.sequence}
773
+ data-og-search-query={target.query}
774
+ data-og-search-offset={offset}
775
+ >
776
+ {props.children.slice(offset, offset + target.query.length)}
777
+ </MatchTag>
778
+ {props.children.slice(offset + target.query.length, end)}
779
+ {end < props.children.length ? "…" : ""}
780
+ </div>
781
+ </div>
782
+ );
783
+ });
@@ -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>