@opengeni/react 7.0.0 → 7.1.0-canary.35855175602001

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 (81) hide show
  1. package/README.md +34 -0
  2. package/dist/approvals.d.ts +2 -1
  3. package/dist/artifacts.js +17 -17
  4. package/dist/{browser-viewer-LGH4IP47.js → browser-viewer-MN7MOYQT.js} +5 -5
  5. package/dist/{chunk-YS24CFDI.js → chunk-2O2HXRGP.js} +7 -7
  6. package/dist/{chunk-IP4BUBKH.js → chunk-3LL47F4U.js} +28 -17
  7. package/dist/chunk-3LL47F4U.js.map +1 -0
  8. package/dist/{chunk-UXE5SSNN.js → chunk-4ACTWOOW.js} +8 -8
  9. package/dist/{chunk-IYUPN32E.js → chunk-DBPVUGQT.js} +6 -1
  10. package/dist/{chunk-IYUPN32E.js.map → chunk-DBPVUGQT.js.map} +1 -1
  11. package/dist/{chunk-5GAL23FF.js → chunk-IOLPHOF4.js} +2 -5
  12. package/dist/chunk-IOLPHOF4.js.map +1 -0
  13. package/dist/{chunk-JPHDWKZE.js → chunk-PFDNB3U7.js} +15 -10
  14. package/dist/chunk-PFDNB3U7.js.map +1 -0
  15. package/dist/{chunk-D7GAPJ3V.js → chunk-SHRCATV6.js} +67 -34
  16. package/dist/chunk-SHRCATV6.js.map +1 -0
  17. package/dist/{chunk-H5YGYNXS.js → chunk-TV5PP2PE.js} +11 -11
  18. package/dist/chunk-TV5PP2PE.js.map +1 -0
  19. package/dist/{chunk-PNAVEK2U.js → chunk-UQS7BGNQ.js} +12 -12
  20. package/dist/{chunk-JKLGJQO5.js → chunk-VI7HELBI.js} +40 -20
  21. package/dist/chunk-VI7HELBI.js.map +1 -0
  22. package/dist/{chunk-HJ5VJ4JB.js → chunk-WKFFRFJF.js} +73 -15
  23. package/dist/chunk-WKFFRFJF.js.map +1 -0
  24. package/dist/{chunk-D5NOQPLR.js → chunk-ZC2DK5SC.js} +75 -65
  25. package/dist/chunk-ZC2DK5SC.js.map +1 -0
  26. package/dist/components/markdown.d.ts +2 -0
  27. package/dist/components/model-policy-picker.d.ts +17 -1
  28. package/dist/components/session-conversation.d.ts +3 -0
  29. package/dist/composer.d.ts +1 -1
  30. package/dist/composer.js +7 -7
  31. package/dist/{computer-viewer-CTFCJVS7.js → computer-viewer-FX4WOZMX.js} +4 -4
  32. package/dist/connect.js +5 -5
  33. package/dist/hooks/use-session-events.d.ts +6 -1
  34. package/dist/index.d.ts +1 -1
  35. package/dist/index.js +112 -115
  36. package/dist/index.js.map +1 -1
  37. package/dist/interaction.js +5 -5
  38. package/dist/machines.js +2 -2
  39. package/dist/{model-policy-picker-menu-RVOQ4ZZG.js → model-policy-picker-menu-AGXXJZ2B.js} +4 -4
  40. package/dist/model-policy.js +1 -1
  41. package/dist/realtime.js +5 -5
  42. package/dist/session-ui.js +12 -12
  43. package/dist/session.js +3 -3
  44. package/dist/timeline/tool-display-name.d.ts +2 -1
  45. package/dist/timeline/types.d.ts +2 -1
  46. package/package.json +3 -3
  47. package/src/approvals.ts +8 -1
  48. package/src/components/approval-surface.tsx +12 -2
  49. package/src/components/machine-card.tsx +5 -5
  50. package/src/components/machine-dock-bar.tsx +1 -1
  51. package/src/components/machines/machine-detail.tsx +5 -2
  52. package/src/components/markdown.tsx +101 -55
  53. package/src/components/model-policy-picker-menu.tsx +131 -33
  54. package/src/components/model-policy-picker.tsx +61 -12
  55. package/src/components/session-commands-panel.tsx +8 -3
  56. package/src/components/session-conversation.tsx +5 -1
  57. package/src/composer.ts +1 -0
  58. package/src/hooks/use-last-started-turn-policy.ts +3 -7
  59. package/src/hooks/use-session-events.ts +32 -7
  60. package/src/index.ts +1 -0
  61. package/src/model-policy.ts +1 -4
  62. package/src/timeline/projection.ts +4 -0
  63. package/src/timeline/rolling-activity.tsx +4 -2
  64. package/src/timeline/tool-display-name.ts +8 -6
  65. package/src/timeline/tool-renderers.tsx +5 -3
  66. package/src/timeline/types.ts +2 -0
  67. package/styles/compiled.css +15 -0
  68. package/dist/chunk-5GAL23FF.js.map +0 -1
  69. package/dist/chunk-D5NOQPLR.js.map +0 -1
  70. package/dist/chunk-D7GAPJ3V.js.map +0 -1
  71. package/dist/chunk-H5YGYNXS.js.map +0 -1
  72. package/dist/chunk-HJ5VJ4JB.js.map +0 -1
  73. package/dist/chunk-IP4BUBKH.js.map +0 -1
  74. package/dist/chunk-JKLGJQO5.js.map +0 -1
  75. package/dist/chunk-JPHDWKZE.js.map +0 -1
  76. /package/dist/{browser-viewer-LGH4IP47.js.map → browser-viewer-MN7MOYQT.js.map} +0 -0
  77. /package/dist/{chunk-YS24CFDI.js.map → chunk-2O2HXRGP.js.map} +0 -0
  78. /package/dist/{chunk-UXE5SSNN.js.map → chunk-4ACTWOOW.js.map} +0 -0
  79. /package/dist/{chunk-PNAVEK2U.js.map → chunk-UQS7BGNQ.js.map} +0 -0
  80. /package/dist/{computer-viewer-CTFCJVS7.js.map → computer-viewer-FX4WOZMX.js.map} +0 -0
  81. /package/dist/{model-policy-picker-menu-RVOQ4ZZG.js.map → model-policy-picker-menu-AGXXJZ2B.js.map} +0 -0
@@ -7,29 +7,29 @@ import {
7
7
  useBrowserSession,
8
8
  useBrowserSessions,
9
9
  useSiteAuthConnections
10
- } from "./chunk-PNAVEK2U.js";
10
+ } from "./chunk-UQS7BGNQ.js";
11
11
  import {
12
12
  ComputerViewer,
13
13
  useComputerFrameStream,
14
14
  useComputerSession,
15
15
  useComputerSessions
16
- } from "./chunk-YS24CFDI.js";
16
+ } from "./chunk-2O2HXRGP.js";
17
17
  import {
18
18
  useInteractionInterventions,
19
19
  useInteractionInvalidation
20
- } from "./chunk-H5YGYNXS.js";
20
+ } from "./chunk-TV5PP2PE.js";
21
21
  import "./chunk-4MDLEHXL.js";
22
22
  import "./chunk-5YK2ZDIG.js";
23
- import "./chunk-JVYQRRC4.js";
24
23
  import "./chunk-KZ73RL76.js";
25
- import "./chunk-PLYRL5ER.js";
26
24
  import {
27
25
  useEmbeddedBrowserInteraction,
28
26
  useEmbeddedComputerInteraction,
29
27
  useEmbeddedInteraction,
30
28
  useEmbeddedInterventions
31
29
  } from "./chunk-NEXLLTP4.js";
30
+ import "./chunk-PLYRL5ER.js";
32
31
  import "./chunk-22KP6LR5.js";
32
+ import "./chunk-JVYQRRC4.js";
33
33
 
34
34
  // src/hooks/use-network-routes.ts
35
35
  import { useCallback, useEffect, useRef, useState } from "react";
package/dist/machines.js CHANGED
@@ -28,11 +28,11 @@ import {
28
28
  healthPulses,
29
29
  pointsFor,
30
30
  useMachines
31
- } from "./chunk-JPHDWKZE.js";
31
+ } from "./chunk-PFDNB3U7.js";
32
32
  import "./chunk-PAJU5KKL.js";
33
33
  import "./chunk-KZ73RL76.js";
34
- import "./chunk-PLYRL5ER.js";
35
34
  import "./chunk-NEXLLTP4.js";
35
+ import "./chunk-PLYRL5ER.js";
36
36
  import "./chunk-22KP6LR5.js";
37
37
  export {
38
38
  CONNECTION_STATUS_META,
@@ -1,13 +1,13 @@
1
1
  import {
2
2
  ModelPolicyPickerMenu,
3
3
  ModelPolicyPickerPopover
4
- } from "./chunk-HJ5VJ4JB.js";
5
- import "./chunk-IP4BUBKH.js";
4
+ } from "./chunk-WKFFRFJF.js";
5
+ import "./chunk-3LL47F4U.js";
6
+ import "./chunk-IOLPHOF4.js";
6
7
  import "./chunk-VZTQ73XT.js";
7
- import "./chunk-5GAL23FF.js";
8
8
  import "./chunk-22KP6LR5.js";
9
9
  export {
10
10
  ModelPolicyPickerMenu,
11
11
  ModelPolicyPickerPopover
12
12
  };
13
- //# sourceMappingURL=model-policy-picker-menu-RVOQ4ZZG.js.map
13
+ //# sourceMappingURL=model-policy-picker-menu-AGXXJZ2B.js.map
@@ -16,7 +16,7 @@ import {
16
16
  projectPickerRows,
17
17
  runnableLatencyModesForModel,
18
18
  sortPickerRows
19
- } from "./chunk-5GAL23FF.js";
19
+ } from "./chunk-IOLPHOF4.js";
20
20
  export {
21
21
  advancedSourceSummary,
22
22
  availabilityReasonLabel,
package/dist/realtime.js CHANGED
@@ -3,15 +3,15 @@ import {
3
3
  PickerAnimatedPage,
4
4
  PickerBackHeader,
5
5
  PickerNavRow
6
- } from "./chunk-IP4BUBKH.js";
6
+ } from "./chunk-3LL47F4U.js";
7
+ import "./chunk-IOLPHOF4.js";
8
+ import {
9
+ useEmbeddedRealtimeSession
10
+ } from "./chunk-NEXLLTP4.js";
7
11
  import {
8
12
  usePortalTokenSource,
9
13
  usePortalTokenStyle
10
14
  } from "./chunk-VZTQ73XT.js";
11
- import {
12
- useEmbeddedRealtimeSession
13
- } from "./chunk-NEXLLTP4.js";
14
- import "./chunk-5GAL23FF.js";
15
15
  import {
16
16
  cn
17
17
  } from "./chunk-22KP6LR5.js";
@@ -19,28 +19,28 @@ import {
19
19
  setStartupDetails,
20
20
  useStartupDetails,
21
21
  userMessageLikelyNeedsDisclosure
22
- } from "./chunk-D5NOQPLR.js";
22
+ } from "./chunk-ZC2DK5SC.js";
23
23
  import "./chunk-GEN54DU5.js";
24
24
  import "./chunk-GBPCZH3Y.js";
25
25
  import "./chunk-ZYRGEKWS.js";
26
- import "./chunk-PAJU5KKL.js";
27
- import "./chunk-D7GAPJ3V.js";
28
- import "./chunk-2VHARTBL.js";
29
- import "./chunk-JVYQRRC4.js";
30
- import "./chunk-UXE5SSNN.js";
31
- import "./chunk-BRM6FVZA.js";
32
26
  import "./chunk-WYWRYUNQ.js";
33
- import "./chunk-IP4BUBKH.js";
34
- import "./chunk-VZTQ73XT.js";
35
27
  import {
36
28
  createOlderHistoryLoadReceipt
37
- } from "./chunk-JKLGJQO5.js";
29
+ } from "./chunk-VI7HELBI.js";
30
+ import "./chunk-PAJU5KKL.js";
31
+ import "./chunk-4ACTWOOW.js";
32
+ import "./chunk-3LL47F4U.js";
38
33
  import "./chunk-AKOMC3VO.js";
34
+ import "./chunk-IOLPHOF4.js";
39
35
  import "./chunk-KZ73RL76.js";
40
- import "./chunk-PLYRL5ER.js";
41
36
  import "./chunk-NEXLLTP4.js";
42
- import "./chunk-5GAL23FF.js";
37
+ import "./chunk-SHRCATV6.js";
38
+ import "./chunk-2VHARTBL.js";
39
+ import "./chunk-BRM6FVZA.js";
40
+ import "./chunk-VZTQ73XT.js";
41
+ import "./chunk-PLYRL5ER.js";
43
42
  import "./chunk-22KP6LR5.js";
43
+ import "./chunk-JVYQRRC4.js";
44
44
  export {
45
45
  ApprovalSurface,
46
46
  BUILT_IN_TURN_SUMMARY_FACET_IDS,
package/dist/session.js CHANGED
@@ -10,7 +10,7 @@ import {
10
10
  useSessionControl,
11
11
  useSessionLineage,
12
12
  useSessionMcpApprovalPolicy
13
- } from "./chunk-IYUPN32E.js";
13
+ } from "./chunk-DBPVUGQT.js";
14
14
  import {
15
15
  buildTimeline,
16
16
  conversationTimeline,
@@ -29,7 +29,7 @@ import {
29
29
  useSession,
30
30
  useSessionEvents,
31
31
  useTurnQueue
32
- } from "./chunk-JKLGJQO5.js";
32
+ } from "./chunk-VI7HELBI.js";
33
33
  import {
34
34
  FILE_ONLY_MESSAGE_TEXT,
35
35
  composeSendInput,
@@ -38,8 +38,8 @@ import {
38
38
  useComposer
39
39
  } from "./chunk-AKOMC3VO.js";
40
40
  import "./chunk-KZ73RL76.js";
41
- import "./chunk-PLYRL5ER.js";
42
41
  import "./chunk-NEXLLTP4.js";
42
+ import "./chunk-PLYRL5ER.js";
43
43
  export {
44
44
  FILE_ONLY_MESSAGE_TEXT,
45
45
  approvalsFromRequiresAction,
@@ -6,6 +6,7 @@
6
6
  * and bare `session_create` resolve the same UI — without inventing previews
7
7
  * from argument JSON.
8
8
  */
9
+ import type { ToolDisplayMetadata } from "@opengeni/sdk";
9
10
  /** Leaf tool name after the first `__` server boundary (or the whole name). */
10
11
  export declare function mcpToolLeaf(name: string): string;
11
12
  /**
@@ -18,4 +19,4 @@ export declare function toolMatchesLeaf(wireName: string, leaf: string): boolean
18
19
  * Readable label for a tool call ("session_create" / "opengeni__session_create"
19
20
  * → "Session create"). Title-cases the first character of the leaf phrase.
20
21
  */
21
- export declare function toolDisplayName(name: string): string;
22
+ export declare function toolDisplayName(name: string, display?: ToolDisplayMetadata): string;
@@ -1,4 +1,4 @@
1
- import type { HumanInputQuestion, HumanInputResponse, MediaGenerationResult, ResourceRef, SessionStatus, TimelineAnnotation, TimelineAnnotationSource, ToolAuthNeededPayload, ToolRef } from "@opengeni/sdk";
1
+ import type { HumanInputQuestion, HumanInputResponse, MediaGenerationResult, ResourceRef, SessionStatus, TimelineAnnotation, TimelineAnnotationSource, ToolAuthNeededPayload, ToolRef, ToolDisplayMetadata } from "@opengeni/sdk";
2
2
  export type TimelineAnnotationSourceDescriptor = Omit<TimelineAnnotationSource, "startOffset" | "endOffset" | "contextBefore" | "contextAfter"> & {
3
3
  text: string;
4
4
  };
@@ -97,6 +97,7 @@ export type ToolCallItem = {
97
97
  turnId: string | null;
98
98
  callId: string | null;
99
99
  name: string;
100
+ display?: ToolDisplayMetadata;
100
101
  arguments: unknown;
101
102
  output: unknown;
102
103
  truncation?: ToolCallTruncation | null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opengeni/react",
3
- "version": "7.0.0",
3
+ "version": "7.1.0-canary.35855175602001",
4
4
  "description": "React hooks and styled components for OpenGeni: live session streaming, chat composer, message timeline, session status, and fleet views — token-themed (CSS variables), dark-first, built on Tailwind v4 + Radix + Motion.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -133,8 +133,8 @@
133
133
  "@dnd-kit/utilities": "3.2.2",
134
134
  "@fontsource-variable/noto-sans-arabic": "5.2.10",
135
135
  "@fontsource-variable/noto-sans-jp": "5.2.10",
136
- "@opengeni/connect": "^0.3.0",
137
- "@opengeni/sdk": "^7.0.0",
136
+ "@opengeni/connect": "^0.3.0-canary.35855175602001",
137
+ "@opengeni/sdk": "^7.1.0-canary.35855175602001",
138
138
  "clsx": "^2.1.1",
139
139
  "dequal": "2.0.3",
140
140
  "lucide-react": "^1.8.0",
package/src/approvals.ts CHANGED
@@ -1,4 +1,8 @@
1
- import type { SessionEvent } from "@opengeni/sdk";
1
+ import {
2
+ parseToolDisplayMetadata,
3
+ type ToolDisplayMetadata,
4
+ type SessionEvent,
5
+ } from "@opengeni/sdk";
2
6
 
3
7
  /* ----------------------------------------------------------------------------
4
8
  Pending-approvals projection
@@ -26,6 +30,7 @@ export type PendingApproval = {
26
30
  id: string;
27
31
  /** Tool/function name awaiting the decision. */
28
32
  name: string;
33
+ display?: ToolDisplayMetadata;
29
34
  arguments?: unknown;
30
35
  /** The raw approval entry from the `session.requiresAction` payload. */
31
36
  raw?: unknown;
@@ -49,11 +54,13 @@ export function approvalsFromRequiresAction(payload: unknown): PendingApproval[]
49
54
  raw.rawItem && typeof raw.rawItem === "object"
50
55
  ? (raw.rawItem as Record<string, unknown>)
51
56
  : {};
57
+ const display = parseToolDisplayMetadata(raw.display);
52
58
  return {
53
59
  // Mirror the runtime's canonical interruption identity before retaining
54
60
  // the legacy top-level callId fallback for already-normalized payloads.
55
61
  id: String(rawItem.callId ?? rawItem.id ?? raw.id ?? raw.callId ?? raw.name ?? index),
56
62
  name: String(raw.name ?? raw.toolName ?? rawItem.name ?? "approval"),
63
+ ...(display ? { display } : {}),
57
64
  arguments: raw.arguments ?? rawItem.arguments,
58
65
  raw: approval,
59
66
  };
@@ -2,6 +2,7 @@ import { CheckIcon, ShieldCheckIcon, XIcon } from "lucide-react";
2
2
  import { useEffect, useId, useRef, useState, type ReactNode } from "react";
3
3
  import type { PendingApproval } from "../approvals";
4
4
  import { cn } from "../lib/cn";
5
+ import { toolDisplayName } from "../timeline/tool-display-name";
5
6
 
6
7
  export type ApprovalSurfaceMessages = {
7
8
  title: string;
@@ -20,7 +21,10 @@ export const defaultApprovalSurfaceMessages: ApprovalSurfaceMessages = {
20
21
  reject: "Reject",
21
22
  approving: "Approving…",
22
23
  rejecting: "Rejecting…",
23
- formatToolName: (name) => name.replaceAll("_", " ").replaceAll(".", " › "),
24
+ formatToolName: (name) =>
25
+ name.includes("__") || /^[a-f0-9]{64}$/.test(name)
26
+ ? toolDisplayName(name)
27
+ : name.replaceAll("_", " ").replaceAll(".", " › "),
24
28
  };
25
29
 
26
30
  export type ApprovalSurfaceProps = {
@@ -154,8 +158,14 @@ export function ApprovalSurface({
154
158
  ) : (
155
159
  <>
156
160
  <p className="text-og-sm font-medium text-og-fg">
157
- {messages.formatToolName(approval.name)}
161
+ {approval.display?.title ??
162
+ messages.formatToolName(approval.display?.toolName ?? approval.name)}
158
163
  </p>
164
+ {approval.display?.accountLabel ? (
165
+ <p className="mt-1 break-words text-og-xs text-og-fg-muted">
166
+ {approval.display.accountLabel}
167
+ </p>
168
+ ) : null}
159
169
  {argumentsPreview !== null ? (
160
170
  <pre className="mt-2 max-h-48 overflow-auto whitespace-pre-wrap break-words rounded-og-sm bg-og-surface-2 p-2 font-mono text-og-xs text-og-fg-muted">
161
171
  {argumentsPreview}
@@ -36,10 +36,12 @@ export type MachineCardProps = {
36
36
  };
37
37
 
38
38
  function KindIcon({ machine }: { machine: MachineView }) {
39
- if (machine.isSessionGroup)
39
+ if (machine.kind === "local" || machine.kind === "selfhosted")
40
+ return <LaptopIcon className="size-4 text-og-fg-subtle" aria-hidden />;
41
+ if (machine.kind === "docker" || machine.isSessionGroup)
40
42
  return <ServerIcon className="size-4 text-og-fg-subtle" aria-hidden />;
41
43
  if (machine.kind === "modal") return <CpuIcon className="size-4 text-og-fg-subtle" aria-hidden />;
42
- return <LaptopIcon className="size-4 text-og-fg-subtle" aria-hidden />;
44
+ return <ServerIcon className="size-4 text-og-fg-subtle" aria-hidden />;
43
45
  }
44
46
 
45
47
  /**
@@ -158,9 +160,7 @@ export function MachineCard({
158
160
  {machine.scope === "user" ? "personal" : machine.scope}
159
161
  </span>
160
162
  <span aria-hidden>·</span>
161
- <span className="capitalize">
162
- {machine.isSessionGroup ? "session sandbox" : machine.kind}
163
- </span>
163
+ <span className="capitalize">{machine.kind}</span>
164
164
  <span aria-hidden>·</span>
165
165
  <span className="font-og-mono">
166
166
  {machine.os}/{machine.arch}
@@ -23,7 +23,7 @@ export type MachineDockBarProps = {
23
23
  * chrome (the dock-parity contract: selfhosted == Modal for the surfaces).
24
24
  */
25
25
  export function MachineDockBar({ name, kind, state, className }: MachineDockBarProps) {
26
- const Icon = kind === "selfhosted" ? LaptopIcon : CpuIcon;
26
+ const Icon = kind === "selfhosted" || kind === "local" ? LaptopIcon : CpuIcon;
27
27
  const stateBadge = MACHINE_STATE_BADGE_META[state];
28
28
  return (
29
29
  <div
@@ -44,9 +44,12 @@ export type MachineDetailProps = {
44
44
  };
45
45
 
46
46
  function KindIcon({ kind, session }: { kind: string; session: boolean }) {
47
- if (session) return <ServerIcon className="size-4 text-og-fg-subtle" aria-hidden />;
47
+ if (kind === "local" || kind === "selfhosted")
48
+ return <LaptopIcon className="size-4 text-og-fg-subtle" aria-hidden />;
49
+ if (kind === "docker" || session)
50
+ return <ServerIcon className="size-4 text-og-fg-subtle" aria-hidden />;
48
51
  if (kind === "modal") return <CpuIcon className="size-4 text-og-fg-subtle" aria-hidden />;
49
- return <LaptopIcon className="size-4 text-og-fg-subtle" aria-hidden />;
52
+ return <ServerIcon className="size-4 text-og-fg-subtle" aria-hidden />;
50
53
  }
51
54
 
52
55
  export function MachineDetail({
@@ -56,6 +56,8 @@ const InteractiveContext = createContext<{
56
56
  }>({ source: "" });
57
57
 
58
58
  export type MarkdownProps = {
59
+ /** Resolve a retained-file reference to the host's authenticated artifact page. */
60
+ artifactHref?: ((artifactId: string) => string) | undefined;
59
61
  /** Reveals a bounded source excerpt; closing Find retains it until explicit restore. */
60
62
  searchTarget?: TimelineSearchTarget | null | undefined;
61
63
  renderImage?: ((image: { src: string; alt: string }) => ReactNode) | undefined;
@@ -217,47 +219,85 @@ const baseComponents: Components = {
217
219
  const MARKDOWN_LINK_CLASS =
218
220
  "break-words font-medium text-og-accent-strong underline-offset-2 hover:underline";
219
221
 
220
- function markdownComponents(onSandboxFile: MarkdownProps["onSandboxFile"]): Components {
221
- return {
222
- ...baseComponents,
223
- a: ({ children, href, ...props }) => {
224
- const location = sandboxFileLocationFromHref(href);
225
- if (location !== null) {
226
- return (
227
- <SandboxMarkdownLink location={location} onSandboxFile={onSandboxFile}>
228
- {children}
229
- </SandboxMarkdownLink>
230
- );
231
- }
232
- if (isSandboxHref(href) || !href) {
233
- return (
234
- <span
235
- className="break-words font-medium text-og-fg-subtle underline decoration-dotted underline-offset-2"
236
- aria-disabled="true"
237
- title={
238
- isSandboxHref(href)
239
- ? "This sandbox file reference is invalid"
240
- : "This link is unavailable"
241
- }
242
- >
243
- {children}
244
- </span>
245
- );
246
- }
247
- return (
222
+ // Keep renderer component types stable: rebuilding them remounts paragraphs,
223
+ // destroys native selections, and resets embedded media on host updates.
224
+ const MarkdownLinkContext = createContext<Pick<MarkdownProps, "onSandboxFile" | "artifactHref">>(
225
+ {},
226
+ );
227
+
228
+ const markdownComponents: Components = {
229
+ ...baseComponents,
230
+ p: ({ children, node, ...props }) => {
231
+ const containsArtifact = node?.children.some(
232
+ (child) =>
233
+ child.type === "element" &&
234
+ child.tagName === "img" &&
235
+ typeof child.properties.src === "string" &&
236
+ retainedImageId(child.properties.src),
237
+ );
238
+ const Tag = containsArtifact ? "div" : "p";
239
+ return (
240
+ <Tag className="my-2.5 leading-7 first:mt-0 last:mb-0" {...props}>
241
+ {children}
242
+ </Tag>
243
+ );
244
+ },
245
+ a: ({ children, href, ...props }) => {
246
+ const { onSandboxFile, artifactHref } = useContext(MarkdownLinkContext);
247
+ const artifactId = href ? retainedImageId(href) : null;
248
+ if (artifactId) {
249
+ const destination = artifactHref?.(artifactId);
250
+ return destination ? (
248
251
  <a
249
252
  className={MARKDOWN_LINK_CLASS}
253
+ href={defaultUrlTransform(destination)}
250
254
  target="_blank"
251
255
  rel="noreferrer noopener"
252
- href={href}
253
- {...props}
254
256
  >
255
257
  {children}
256
258
  </a>
259
+ ) : (
260
+ <span title="This artifact requires a workspace-aware host">
261
+ {children} (artifact unavailable)
262
+ </span>
257
263
  );
258
- },
259
- };
260
- }
264
+ }
265
+ const location = sandboxFileLocationFromHref(href);
266
+ if (location !== null) {
267
+ return (
268
+ <SandboxMarkdownLink location={location} onSandboxFile={onSandboxFile}>
269
+ {children}
270
+ </SandboxMarkdownLink>
271
+ );
272
+ }
273
+ if (isSandboxHref(href) || !href) {
274
+ return (
275
+ <span
276
+ className="break-words text-og-fg-subtle"
277
+ aria-disabled="true"
278
+ title={
279
+ isSandboxHref(href)
280
+ ? "This sandbox file reference is invalid"
281
+ : "This link is unavailable"
282
+ }
283
+ >
284
+ {children} (link unavailable)
285
+ </span>
286
+ );
287
+ }
288
+ return (
289
+ <a
290
+ className={MARKDOWN_LINK_CLASS}
291
+ target="_blank"
292
+ rel="noreferrer noopener"
293
+ href={href}
294
+ {...props}
295
+ >
296
+ {children}
297
+ </a>
298
+ );
299
+ },
300
+ };
261
301
 
262
302
  function SandboxMarkdownLink({
263
303
  location,
@@ -371,7 +411,7 @@ function MarkdownImage({ src, alt }: ComponentPropsWithoutRef<"img">) {
371
411
  }
372
412
 
373
413
  const markdownUrlTransform: UrlTransform = (url, key, node) =>
374
- (key === "href" && node.tagName === "a" && isSandboxHref(url)) ||
414
+ (key === "href" && node.tagName === "a" && (isSandboxHref(url) || retainedImageId(url))) ||
375
415
  (key === "src" && node.tagName === "img" && retainedImageId(url))
376
416
  ? url
377
417
  : defaultUrlTransform(url);
@@ -546,6 +586,7 @@ function MarkdownTable({ children, className, ...props }: ComponentPropsWithoutR
546
586
 
547
587
  function MarkdownImpl({
548
588
  children,
589
+ artifactHref,
549
590
  className,
550
591
  streaming = false,
551
592
  onSandboxFile,
@@ -640,7 +681,10 @@ function MarkdownImpl({
640
681
  // fences don't snap to final GFM one commit before the crystallize morph.
641
682
  // Reveal identity still tracks the true source (`children`).
642
683
  const parseText = streaming || revealActive ? softenStreamingMarkdown(children) : children;
643
- const components = useMemo(() => markdownComponents(onSandboxFile), [onSandboxFile]);
684
+ const linkContext = useMemo(
685
+ () => ({ onSandboxFile, artifactHref }),
686
+ [onSandboxFile, artifactHref],
687
+ );
644
688
 
645
689
  const interactiveContext = useMemo(
646
690
  () => ({
@@ -655,27 +699,29 @@ function MarkdownImpl({
655
699
  // `min-w-0` lets the prose shrink inside flex parents (message bubbles) so
656
700
  // long links and code blocks wrap/scroll instead of forcing overflow.
657
701
  return (
658
- <InteractiveContext.Provider value={interactiveContext}>
659
- <TooltipProvider delayDuration={400}>
660
- <div
661
- ref={bodyRef}
662
- className={cn(
663
- "og-markdown-body min-w-0 break-words",
664
- settling && "og-markdown-settle",
665
- className,
666
- )}
667
- >
668
- <ReactMarkdown
669
- remarkPlugins={[remarkGfm]}
670
- rehypePlugins={rehypePlugins}
671
- components={components}
672
- urlTransform={markdownUrlTransform}
702
+ <MarkdownLinkContext.Provider value={linkContext}>
703
+ <InteractiveContext.Provider value={interactiveContext}>
704
+ <TooltipProvider delayDuration={400}>
705
+ <div
706
+ ref={bodyRef}
707
+ className={cn(
708
+ "og-markdown-body min-w-0 break-words",
709
+ settling && "og-markdown-settle",
710
+ className,
711
+ )}
673
712
  >
674
- {parseText}
675
- </ReactMarkdown>
676
- </div>
677
- </TooltipProvider>
678
- </InteractiveContext.Provider>
713
+ <ReactMarkdown
714
+ remarkPlugins={[remarkGfm]}
715
+ rehypePlugins={rehypePlugins}
716
+ components={markdownComponents}
717
+ urlTransform={markdownUrlTransform}
718
+ >
719
+ {parseText}
720
+ </ReactMarkdown>
721
+ </div>
722
+ </TooltipProvider>
723
+ </InteractiveContext.Provider>
724
+ </MarkdownLinkContext.Provider>
679
725
  );
680
726
  }
681
727