@opengeni/react 6.0.0-canary.0 → 6.1.0-canary.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/README.md +17 -0
  2. package/dist/capability-catalog-row.d.ts +20 -0
  3. package/dist/{chunk-VRBL3F5E.js → chunk-5GAL23FF.js} +10 -2
  4. package/dist/{chunk-VRBL3F5E.js.map → chunk-5GAL23FF.js.map} +1 -1
  5. package/dist/{chunk-IHQII5WO.js → chunk-G5JFM6ZE.js} +29 -4
  6. package/dist/chunk-G5JFM6ZE.js.map +1 -0
  7. package/dist/{chunk-UMECRJRV.js → chunk-HJ5VJ4JB.js} +21 -12
  8. package/dist/chunk-HJ5VJ4JB.js.map +1 -0
  9. package/dist/{chunk-R4KRSFGA.js → chunk-IP4BUBKH.js} +3 -3
  10. package/dist/{chunk-CC7AVRIJ.js → chunk-KZ4QDBGM.js} +87 -14
  11. package/dist/chunk-KZ4QDBGM.js.map +1 -0
  12. package/dist/{chunk-PZ27MCZP.js → chunk-L5VJYZIW.js} +34 -4
  13. package/dist/chunk-L5VJYZIW.js.map +1 -0
  14. package/dist/{chunk-45XEUVTK.js → chunk-UWVY3OXF.js} +12 -5
  15. package/dist/chunk-UWVY3OXF.js.map +1 -0
  16. package/dist/components/message-timeline.d.ts +2 -2
  17. package/dist/composer.js +4 -4
  18. package/dist/connect-panel.d.ts +2 -1
  19. package/dist/connect-setup.d.ts +3 -1
  20. package/dist/connect.d.ts +1 -0
  21. package/dist/connect.js +620 -424
  22. package/dist/connect.js.map +1 -1
  23. package/dist/connection-catalog.d.ts +6 -1
  24. package/dist/connection-installed.d.ts +1 -0
  25. package/dist/connection-logo.d.ts +4 -2
  26. package/dist/discovery-cache.d.ts +5 -0
  27. package/dist/index.js +7 -7
  28. package/dist/{model-policy-picker-menu-3R336DP2.js → model-policy-picker-menu-RVOQ4ZZG.js} +4 -4
  29. package/dist/model-policy.d.ts +6 -2
  30. package/dist/model-policy.js +1 -1
  31. package/dist/plugin-discovery.d.ts +4 -0
  32. package/dist/realtime.js +2 -2
  33. package/dist/session-ui.js +6 -6
  34. package/dist/session.js +1 -1
  35. package/dist/skill-discovery.d.ts +11 -0
  36. package/dist/timeline/projection.d.ts +3 -1
  37. package/dist/timeline/turn-summary.d.ts +2 -2
  38. package/package.json +3 -3
  39. package/src/capability-catalog-row.tsx +94 -0
  40. package/src/components/markdown.tsx +32 -3
  41. package/src/components/message-timeline.tsx +88 -4
  42. package/src/components/model-picker.tsx +11 -3
  43. package/src/components/model-policy-picker-menu.tsx +10 -3
  44. package/src/components/session-conversation.tsx +1 -3
  45. package/src/components/session-status.tsx +2 -2
  46. package/src/connect-panel.tsx +19 -8
  47. package/src/connect-setup.tsx +49 -14
  48. package/src/connect.ts +6 -0
  49. package/src/connection-catalog.tsx +61 -39
  50. package/src/connection-installed.tsx +2 -0
  51. package/src/connection-logo.tsx +14 -10
  52. package/src/discovery-cache.ts +42 -0
  53. package/src/hooks/use-session-events.ts +24 -2
  54. package/src/model-policy.ts +21 -0
  55. package/src/plugin-details.tsx +8 -5
  56. package/src/plugin-discovery.tsx +87 -71
  57. package/src/skill-discovery.tsx +101 -56
  58. package/src/timeline/projection.ts +31 -1
  59. package/src/timeline/turn-summary.tsx +3 -3
  60. package/styles/compiled.css +27 -0
  61. package/styles/connect.css +56 -15
  62. package/dist/chunk-45XEUVTK.js.map +0 -1
  63. package/dist/chunk-CC7AVRIJ.js.map +0 -1
  64. package/dist/chunk-IHQII5WO.js.map +0 -1
  65. package/dist/chunk-PZ27MCZP.js.map +0 -1
  66. package/dist/chunk-UMECRJRV.js.map +0 -1
  67. /package/dist/{chunk-R4KRSFGA.js.map → chunk-IP4BUBKH.js.map} +0 -0
  68. /package/dist/{model-policy-picker-menu-3R336DP2.js.map → model-policy-picker-menu-RVOQ4ZZG.js.map} +0 -0
@@ -1,4 +1,5 @@
1
1
  import type { ReactNode } from "react";
2
+ import { type CapabilityCatalogStatus } from "./capability-catalog-row.js";
2
3
  /** Status comes from the host/controller, never inferred from a display label. */
3
4
  export type ConnectionCatalogOption = {
4
5
  id: string;
@@ -6,6 +7,8 @@ export type ConnectionCatalogOption = {
6
7
  description?: string | undefined;
7
8
  status: string;
8
9
  connected: boolean;
10
+ /** Explicit health/availability. Omit to retain visible legacy status text. */
11
+ state?: CapabilityCatalogStatus;
9
12
  onOpen: () => void;
10
13
  action?: ReactNode;
11
14
  };
@@ -19,6 +22,8 @@ export type ConnectionCatalogProps = {
19
22
  services: ConnectionCatalogService[];
20
23
  query?: string;
21
24
  columns?: 1 | 2;
25
+ resultLimit?: number;
26
+ onShowMore?: () => void;
22
27
  /** Group capabilities by service, or show every capability independently. */
23
28
  grouped?: boolean;
24
29
  className?: string;
@@ -30,4 +35,4 @@ export declare function ConnectionOptionRow({ option }: {
30
35
  export declare function ConnectionServiceRow({ service }: {
31
36
  service: ConnectionCatalogService;
32
37
  }): import("react").JSX.Element | null;
33
- export declare function ConnectionCatalog({ services, query, columns, grouped, className, emptyMessage, }: ConnectionCatalogProps): import("react").JSX.Element;
38
+ export declare function ConnectionCatalog({ services, query, columns, grouped, resultLimit, onShowMore, className, emptyMessage, }: ConnectionCatalogProps): import("react").JSX.Element;
@@ -5,6 +5,7 @@ export type ConnectionInstalledItem = {
5
5
  status: string;
6
6
  icon?: ReactNode;
7
7
  needsAttention?: boolean;
8
+ disabled?: boolean;
8
9
  onOpen: () => void;
9
10
  };
10
11
  /** Compact account/service shortcuts. The host supplies authoritative status. */
@@ -1,6 +1,8 @@
1
- /** Brand identifier; failed/missing assets use a stable text fallback. */
2
- export declare function ConnectionLogo({ src, name, size, }: {
1
+ import { type ReactNode } from "react";
2
+ /** Brand identifier; failed/missing assets use the supplied fallback or stable initials. */
3
+ export declare function ConnectionLogo({ src, name, size, fallback, }: {
3
4
  src: string | null;
4
5
  name: string;
5
6
  size?: number;
7
+ fallback?: ReactNode;
6
8
  }): import("react").JSX.Element;
@@ -0,0 +1,5 @@
1
+ /** Short-lived presentation data only. Never cache installation or authorization state. */
2
+ export declare function createDiscoveryCache<T>(ttlMs?: number, capacity?: number): {
3
+ peek(client: object, key: string): T | undefined;
4
+ read(client: object, key: string, fetch: () => Promise<T>): Promise<T>;
5
+ };
package/dist/index.js CHANGED
@@ -44,7 +44,7 @@ import {
44
44
  } from "./chunk-5YK2ZDIG.js";
45
45
  import {
46
46
  ModelPolicyPickerMenu
47
- } from "./chunk-UMECRJRV.js";
47
+ } from "./chunk-HJ5VJ4JB.js";
48
48
  import {
49
49
  ActivityDisclosure,
50
50
  ActivityRail,
@@ -108,7 +108,7 @@ import {
108
108
  useWorkspaceModelCatalog,
109
109
  userMessageLikelyNeedsDisclosure,
110
110
  v4aToGitFileDiff
111
- } from "./chunk-CC7AVRIJ.js";
111
+ } from "./chunk-KZ4QDBGM.js";
112
112
  import "./chunk-YBM6CQRU.js";
113
113
  import "./chunk-5M6VTFJ5.js";
114
114
  import "./chunk-ZYRGEKWS.js";
@@ -124,7 +124,7 @@ import {
124
124
  retainedImageId,
125
125
  sandboxFileLocationFromHref,
126
126
  sandboxFilePathFromHref
127
- } from "./chunk-IHQII5WO.js";
127
+ } from "./chunk-G5JFM6ZE.js";
128
128
  import {
129
129
  copyTextToClipboard,
130
130
  tableElementToTsv
@@ -164,7 +164,7 @@ import {
164
164
  useSlashCommands,
165
165
  useTranscription,
166
166
  useVoiceInput
167
- } from "./chunk-45XEUVTK.js";
167
+ } from "./chunk-UWVY3OXF.js";
168
168
  import {
169
169
  Tooltip,
170
170
  TooltipContent,
@@ -179,7 +179,7 @@ import {
179
179
  PickerBackHeader,
180
180
  PickerNavRow,
181
181
  defaultModelPolicyPickerMessages
182
- } from "./chunk-R4KRSFGA.js";
182
+ } from "./chunk-IP4BUBKH.js";
183
183
  import {
184
184
  usePortalTokenSource,
185
185
  usePortalTokenStyle
@@ -219,7 +219,7 @@ import {
219
219
  useSession,
220
220
  useSessionEvents,
221
221
  useTurnQueue
222
- } from "./chunk-PZ27MCZP.js";
222
+ } from "./chunk-L5VJYZIW.js";
223
223
  import {
224
224
  FILE_ONLY_MESSAGE_TEXT,
225
225
  composeSendInput,
@@ -269,7 +269,7 @@ import {
269
269
  projectPickerRows,
270
270
  runnableLatencyModesForModel,
271
271
  sortPickerRows
272
- } from "./chunk-VRBL3F5E.js";
272
+ } from "./chunk-5GAL23FF.js";
273
273
  import {
274
274
  cn
275
275
  } from "./chunk-22KP6LR5.js";
@@ -1,13 +1,13 @@
1
1
  import {
2
2
  ModelPolicyPickerMenu,
3
3
  ModelPolicyPickerPopover
4
- } from "./chunk-UMECRJRV.js";
5
- import "./chunk-R4KRSFGA.js";
4
+ } from "./chunk-HJ5VJ4JB.js";
5
+ import "./chunk-IP4BUBKH.js";
6
6
  import "./chunk-VZTQ73XT.js";
7
- import "./chunk-VRBL3F5E.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-3R336DP2.js.map
13
+ //# sourceMappingURL=model-policy-picker-menu-RVOQ4ZZG.js.map
@@ -33,12 +33,16 @@ export declare function projectPickerRows(models: WorkspaceModelCatalogModel[]):
33
33
  export declare function projectClientModelRows(models: ClientModel[]): PickerModelRow<ClientModel>[];
34
34
  export declare function sortPickerRows<TCatalog extends ClientModel>(rows: PickerModelRow<TCatalog>[]): PickerModelRow<TCatalog>[];
35
35
  export declare function findPickerRow<TCatalog extends ClientModel>(rows: PickerModelRow<TCatalog>[], modelId: string): PickerModelRow<TCatalog> | null;
36
- export declare function groupPickerRowsByBillingClass(rows: PickerModelRow[]): Array<{
36
+ export declare function groupPickerRowsByBillingClass(rows: PickerModelRow[], options?: {
37
+ codexOnly?: boolean;
38
+ }): Array<{
37
39
  billingClass: PickerBillingClass;
38
40
  label: string;
39
41
  rows: PickerModelRow[];
40
42
  }>;
41
- export declare function groupPickerRowsByBillingClass<TCatalog extends ClientModel>(rows: PickerModelRow<TCatalog>[]): Array<{
43
+ export declare function groupPickerRowsByBillingClass<TCatalog extends ClientModel>(rows: PickerModelRow<TCatalog>[], options?: {
44
+ codexOnly?: boolean;
45
+ }): Array<{
42
46
  billingClass: PickerBillingClass;
43
47
  label: string;
44
48
  rows: PickerModelRow<TCatalog>[];
@@ -16,7 +16,7 @@ import {
16
16
  projectPickerRows,
17
17
  runnableLatencyModesForModel,
18
18
  sortPickerRows
19
- } from "./chunk-VRBL3F5E.js";
19
+ } from "./chunk-5GAL23FF.js";
20
20
  export {
21
21
  advancedSourceSummary,
22
22
  availabilityReasonLabel,
@@ -9,6 +9,10 @@ export type PluginDiscoveryProps = {
9
9
  };
10
10
  workspaceId: string;
11
11
  query: string;
12
+ /** Discovery identities already installed in the current workspace. */
13
+ installedIds?: ReadonlySet<string>;
14
+ resultLimit?: number;
15
+ onShowMore?: () => void;
12
16
  onOpen: (item: PluginDiscoveryItem) => void;
13
17
  };
14
18
  export declare function PluginDiscovery(props: PluginDiscoveryProps): import("react").JSX.Element;
package/dist/realtime.js CHANGED
@@ -3,7 +3,7 @@ import {
3
3
  PickerAnimatedPage,
4
4
  PickerBackHeader,
5
5
  PickerNavRow
6
- } from "./chunk-R4KRSFGA.js";
6
+ } from "./chunk-IP4BUBKH.js";
7
7
  import {
8
8
  usePortalTokenSource,
9
9
  usePortalTokenStyle
@@ -11,7 +11,7 @@ import {
11
11
  import {
12
12
  useEmbeddedRealtimeSession
13
13
  } from "./chunk-NEXLLTP4.js";
14
- import "./chunk-VRBL3F5E.js";
14
+ import "./chunk-5GAL23FF.js";
15
15
  import {
16
16
  cn
17
17
  } from "./chunk-22KP6LR5.js";
@@ -18,27 +18,27 @@ import {
18
18
  setStartupDetails,
19
19
  useStartupDetails,
20
20
  userMessageLikelyNeedsDisclosure
21
- } from "./chunk-CC7AVRIJ.js";
21
+ } from "./chunk-KZ4QDBGM.js";
22
22
  import "./chunk-YBM6CQRU.js";
23
23
  import "./chunk-5M6VTFJ5.js";
24
24
  import "./chunk-ZYRGEKWS.js";
25
25
  import "./chunk-NOSYBS2D.js";
26
26
  import "./chunk-PAJU5KKL.js";
27
- import "./chunk-IHQII5WO.js";
27
+ import "./chunk-G5JFM6ZE.js";
28
28
  import "./chunk-JVYQRRC4.js";
29
- import "./chunk-45XEUVTK.js";
29
+ import "./chunk-UWVY3OXF.js";
30
30
  import "./chunk-HQV2I5HV.js";
31
31
  import "./chunk-WYWRYUNQ.js";
32
- import "./chunk-R4KRSFGA.js";
32
+ import "./chunk-IP4BUBKH.js";
33
33
  import "./chunk-VZTQ73XT.js";
34
34
  import {
35
35
  createOlderHistoryLoadReceipt
36
- } from "./chunk-PZ27MCZP.js";
36
+ } from "./chunk-L5VJYZIW.js";
37
37
  import "./chunk-NYESIADM.js";
38
38
  import "./chunk-PLYRL5ER.js";
39
39
  import "./chunk-KZ73RL76.js";
40
40
  import "./chunk-NEXLLTP4.js";
41
- import "./chunk-VRBL3F5E.js";
41
+ import "./chunk-5GAL23FF.js";
42
42
  import "./chunk-22KP6LR5.js";
43
43
  export {
44
44
  ApprovalSurface,
package/dist/session.js CHANGED
@@ -28,7 +28,7 @@ import {
28
28
  useSession,
29
29
  useSessionEvents,
30
30
  useTurnQueue
31
- } from "./chunk-PZ27MCZP.js";
31
+ } from "./chunk-L5VJYZIW.js";
32
32
  import {
33
33
  FILE_ONLY_MESSAGE_TEXT,
34
34
  composeSendInput,
@@ -1,3 +1,4 @@
1
+ import { type CapabilityCatalogStatus } from "./capability-catalog-row.js";
1
2
  export type SkillDiscoveryItem = {
2
3
  id: string;
3
4
  name: string;
@@ -23,6 +24,16 @@ export type SkillDiscoveryProps = {
23
24
  query: string;
24
25
  onImport: (url: string) => void;
25
26
  canManage: boolean;
27
+ localSkills?: readonly {
28
+ id: string;
29
+ name: string;
30
+ description?: string;
31
+ status?: CapabilityCatalogStatus;
32
+ statusLabel?: string;
33
+ onOpen: () => void;
34
+ }[];
35
+ resultLimit?: number;
36
+ onShowMore?: () => void;
26
37
  onSearch?: (() => void) | undefined;
27
38
  };
28
39
  /** Host-owned query and import flow; browse presentation is reusable independently. */
@@ -1,7 +1,9 @@
1
1
  import { type SessionEvent, type SessionStatus } from "@opengeni/sdk";
2
2
  import type { TimelineGroup, TimelineItem } from "./types.js";
3
3
  export { toolDisplayName, mcpToolLeaf, toolMatchesLeaf } from "./tool-display-name.js";
4
- export declare function buildTimeline(events: SessionEvent[]): TimelineItem[];
4
+ export declare function buildTimeline(events: SessionEvent[], options?: {
5
+ partialStart?: boolean;
6
+ }): TimelineItem[];
5
7
  /**
6
8
  * Codex subscription web search can return private citation handles without
7
9
  * the URL annotation table that would make them resolvable. Keep the canonical
@@ -65,8 +65,8 @@ export type TurnSummaryProps = {
65
65
  items: ActivityItem[];
66
66
  /**
67
67
  * The settled verdict — or absent for a completed CLUSTER of a still-running
68
- * turn, which folds neutrally: no verdict glyph (the turn has none yet), a
69
- * quiet pulse dot in its place so alignment and the running feel both hold.
68
+ * turn, which folds neutrally. Only unfinished activity inside this summary
69
+ * animates; the parent turn may still be composing a response elsewhere.
70
70
  */
71
71
  outcome?: TurnOutcome | undefined;
72
72
  /** A short failure reason shown inline on a failed chip (never hidden). */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opengeni/react",
3
- "version": "6.0.0-canary.0",
3
+ "version": "6.1.0-canary.0",
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.2.0-canary.9",
137
- "@opengeni/sdk": "^6.0.0-canary.0",
136
+ "@opengeni/connect": "^0.2.0-canary.11",
137
+ "@opengeni/sdk": "^6.1.0-canary.0",
138
138
  "clsx": "^2.1.1",
139
139
  "dequal": "2.0.3",
140
140
  "lucide-react": "^1.8.0",
@@ -0,0 +1,94 @@
1
+ import type { ComponentPropsWithoutRef, ReactNode } from "react";
2
+ import { AlertTriangleIcon, CheckIcon, Loader2Icon, PlusIcon } from "lucide-react";
3
+
4
+ export type CapabilityCatalogStatus =
5
+ | "available"
6
+ | "added"
7
+ | "attention"
8
+ | "unavailable"
9
+ | "loading";
10
+
11
+ export type CapabilityCatalogRowProps = Omit<
12
+ ComponentPropsWithoutRef<"button">,
13
+ "children" | "onClick" | "name"
14
+ > & {
15
+ name: string;
16
+ description?: string | undefined;
17
+ icon?: ReactNode;
18
+ status?: CapabilityCatalogStatus;
19
+ statusLabel?: string | undefined;
20
+ /** Preserve opaque host status text when adopting this row without typed health. */
21
+ showStatusLabel?: boolean;
22
+ onOpen: () => void;
23
+ };
24
+
25
+ const labels: Record<CapabilityCatalogStatus, string> = {
26
+ available: "Available to add",
27
+ added: "Added",
28
+ attention: "Needs attention",
29
+ unavailable: "Unavailable",
30
+ loading: "Working",
31
+ };
32
+
33
+ /** A display-only state. The entire catalog row is the action, never this glyph. */
34
+ export function CapabilityCatalogIndicator({
35
+ status = "available",
36
+ label,
37
+ showLabel = false,
38
+ }: {
39
+ status?: CapabilityCatalogStatus;
40
+ label?: string | undefined;
41
+ showLabel?: boolean;
42
+ }) {
43
+ const Icon =
44
+ status === "added"
45
+ ? CheckIcon
46
+ : status === "attention" || status === "unavailable"
47
+ ? AlertTriangleIcon
48
+ : status === "loading"
49
+ ? Loader2Icon
50
+ : PlusIcon;
51
+ return (
52
+ <span className="og-capability-catalog-indicator" data-status={status}>
53
+ <Icon aria-hidden="true" />
54
+ <span
55
+ className={
56
+ showLabel || status === "attention" || status === "unavailable"
57
+ ? "og-capability-catalog-notice"
58
+ : "og-capability-catalog-sr-only"
59
+ }
60
+ >
61
+ {label ?? labels[status]}
62
+ </span>
63
+ </span>
64
+ );
65
+ }
66
+
67
+ /** Shared by connections, skills, and plugins: one row, one focus target, one action. */
68
+ export function CapabilityCatalogRow({
69
+ name,
70
+ description,
71
+ icon,
72
+ status = "available",
73
+ statusLabel,
74
+ showStatusLabel = false,
75
+ onOpen,
76
+ className = "",
77
+ ...props
78
+ }: CapabilityCatalogRowProps) {
79
+ return (
80
+ <button
81
+ {...props}
82
+ type="button"
83
+ className={`og-capability-catalog-row ${className}`}
84
+ onClick={onOpen}
85
+ >
86
+ {icon ? <span className="og-capability-catalog-icon">{icon}</span> : null}
87
+ <span className="og-capability-catalog-copy">
88
+ <strong>{name}</strong>
89
+ {description ? <span>{description}</span> : null}
90
+ </span>
91
+ <CapabilityCatalogIndicator status={status} label={statusLabel} showLabel={showStatusLabel} />
92
+ </button>
93
+ );
94
+ }
@@ -19,6 +19,7 @@ import ReactMarkdown, {
19
19
  type UrlTransform,
20
20
  } from "react-markdown";
21
21
  import remarkGfm from "remark-gfm";
22
+ import { LoaderCircleIcon } from "lucide-react";
22
23
  import { cn } from "../lib/cn";
23
24
  import { tableElementToTsv } from "../lib/clipboard";
24
25
  import { prefersReducedMotion } from "../lib/motion";
@@ -46,6 +47,7 @@ import { TooltipProvider } from "./tooltip";
46
47
  export type MarkdownInteractiveBlock = { kind: "html" | "site"; content: string };
47
48
  const InteractiveContext = createContext<{
48
49
  source: string;
50
+ streaming?: boolean | undefined;
49
51
  render?: (block: MarkdownInteractiveBlock) => ReactNode;
50
52
  renderImage?: (image: { src: string; alt: string }) => ReactNode;
51
53
  }>({ source: "" });
@@ -406,7 +408,7 @@ function fenceLanguage(children: ReactNode): string | null {
406
408
  }
407
409
 
408
410
  function InteractiveCodeBlock({ children, node }: ComponentPropsWithoutRef<"pre"> & ExtraProps) {
409
- const { source, render } = useContext(InteractiveContext);
411
+ const { source, render, streaming } = useContext(InteractiveContext);
410
412
  const language = fenceLanguage(children);
411
413
  if (render && (language === "opengeni-html" || language === "opengeni-site")) {
412
414
  const start = node?.position?.start.offset;
@@ -428,7 +430,33 @@ function InteractiveCodeBlock({ children, node }: ComponentPropsWithoutRef<"pre"
428
430
  closing &&
429
431
  closing.length >= opening[1]!.length &&
430
432
  [...closing].every((c) => c === opening[1]![0]);
431
- if (!complete) return <p role="status">Preparing preview…</p>;
433
+ if (!complete) {
434
+ return (
435
+ <div
436
+ role="status"
437
+ aria-live="polite"
438
+ aria-busy={streaming === true}
439
+ className="my-3 flex min-h-32 items-center gap-3 rounded-og-md bg-og-surface-1/70 px-4 py-5"
440
+ >
441
+ {streaming ? (
442
+ <LoaderCircleIcon
443
+ aria-hidden
444
+ className="size-5 shrink-0 text-og-fg-muted motion-safe:animate-spin"
445
+ />
446
+ ) : null}
447
+ <div>
448
+ <div className="text-og-sm font-medium text-og-fg">
449
+ {streaming ? "Preparing preview…" : "Preview incomplete"}
450
+ </div>
451
+ <div className="mt-1 text-og-sm text-og-fg-muted">
452
+ {streaming
453
+ ? "Generating the interactive content. It will appear here when ready."
454
+ : "Generation stopped before the preview was ready."}
455
+ </div>
456
+ </div>
457
+ </div>
458
+ );
459
+ }
432
460
  return (
433
461
  <>
434
462
  {render({
@@ -622,10 +650,11 @@ function MarkdownImpl({
622
650
  const interactiveContext = useMemo(
623
651
  () => ({
624
652
  source: children,
653
+ streaming,
625
654
  ...(renderInteractiveBlock ? { render: renderInteractiveBlock } : {}),
626
655
  ...(renderImage ? { renderImage } : {}),
627
656
  }),
628
- [children, renderInteractiveBlock, renderImage],
657
+ [children, streaming, renderInteractiveBlock, renderImage],
629
658
  );
630
659
 
631
660
  // `min-w-0` lets the prose shrink inside flex parents (message bubbles) so
@@ -229,8 +229,8 @@ export type MessageTimelineProps = {
229
229
  hasNewer?: boolean | undefined;
230
230
  /** A newer history page is being fetched. */
231
231
  loadingNewer?: boolean | undefined;
232
- /** Page forward through history without loading the whole gap to the tip. */
233
- onLoadNewer?: (() => void) | undefined;
232
+ /** Page forward through history. Return the request promise to enable inline error/retry. */
233
+ onLoadNewer?: (() => unknown) | undefined;
234
234
  /**
235
235
  * Reload the live tip window. When omitted, Jump to latest only re-pins and
236
236
  * scrolls the in-memory window.
@@ -461,6 +461,67 @@ export function MessageTimeline({
461
461
  // can acquire a different first-delta id when older text arrives).
462
462
  const sourceItems = events ?? items;
463
463
  const olderBoundaryKey = sourceItems?.[0]?.id;
464
+ const newerBoundaryKey = `${events?.[0]?.sessionId ?? ""}:${olderBoundaryKey ?? ""}`;
465
+ const newerScopeRef = useRef(newerBoundaryKey);
466
+ newerScopeRef.current = newerBoundaryKey;
467
+ const newerAttemptRef = useRef<{ pending: boolean } | null>(null);
468
+ const newerRetryButtonRef = useRef<HTMLButtonElement | null>(null);
469
+ const [newerRetryPending, setNewerRetryPending] = useState(false);
470
+ const [newerFailure, setNewerFailure] = useState<{ key: string; message: string } | null>(null);
471
+ useEffect(() => {
472
+ newerScopeRef.current = newerBoundaryKey;
473
+ newerAttemptRef.current = null;
474
+ setNewerFailure(null);
475
+ setNewerRetryPending(false);
476
+ return () => {
477
+ newerScopeRef.current = "";
478
+ newerAttemptRef.current = null;
479
+ };
480
+ }, [newerBoundaryKey]);
481
+ const requestNewer = useCallback(
482
+ (explicitRetry = false) => {
483
+ if (
484
+ newerScopeRef.current !== newerBoundaryKey ||
485
+ !onLoadNewer ||
486
+ loadingNewer ||
487
+ (newerAttemptRef.current && (!explicitRetry || newerAttemptRef.current.pending))
488
+ ) {
489
+ return;
490
+ }
491
+ const attempt = { pending: true };
492
+ newerAttemptRef.current = attempt;
493
+ if (explicitRetry) setNewerRetryPending(true);
494
+ const isCurrent = () =>
495
+ newerAttemptRef.current === attempt && newerScopeRef.current === newerBoundaryKey;
496
+ // Both synchronous host errors and rejected promises belong to this
497
+ // boundary. Retain the failed attempt so observers cannot hot-retry it.
498
+ void Promise.resolve()
499
+ .then(() => (isCurrent() ? onLoadNewer() : undefined))
500
+ .then(
501
+ () => {
502
+ if (!isCurrent()) return;
503
+ newerAttemptRef.current = null;
504
+ // The successful page removes the recovery control. Return focus
505
+ // to the reading surface without moving the reader's viewport.
506
+ if (document.activeElement === newerRetryButtonRef.current) {
507
+ scrollRef.current?.focus({ preventScroll: true });
508
+ }
509
+ setNewerFailure(null);
510
+ setNewerRetryPending(false);
511
+ },
512
+ (reason: unknown) => {
513
+ if (!isCurrent()) return;
514
+ attempt.pending = false;
515
+ setNewerRetryPending(false);
516
+ setNewerFailure({
517
+ key: newerBoundaryKey,
518
+ message: reason instanceof Error ? reason.message : String(reason),
519
+ });
520
+ },
521
+ );
522
+ },
523
+ [onLoadNewer, loadingNewer, newerBoundaryKey],
524
+ );
464
525
  const previousSourceIdsRef = useRef(new Set<string>());
465
526
  const previousSourceBoundaryRef = useRef<string | undefined>(undefined);
466
527
  const readingAnchorRef = useRef<TimelineAnchor | null>(null);
@@ -1586,14 +1647,14 @@ export function MessageTimeline({
1586
1647
  // An underfilled history window has both sentinels visible. Advancing
1587
1648
  // it automatically would undo an explicit older-page navigation.
1588
1649
  if (maxScrollOf(root) > 1 && entries.some((entry) => entry.isIntersecting)) {
1589
- onLoadNewer();
1650
+ requestNewer();
1590
1651
  }
1591
1652
  },
1592
1653
  { root, rootMargin: "0px 0px 1200px 0px" },
1593
1654
  );
1594
1655
  observer.observe(target);
1595
1656
  return () => observer.disconnect();
1596
- }, [hasNewer, loadingNewer, onLoadNewer, firstGroupKey]);
1657
+ }, [hasNewer, loadingNewer, onLoadNewer, requestNewer, firstGroupKey]);
1597
1658
 
1598
1659
  // Late layout that React commits cannot see (images decoding, fonts, code
1599
1660
  // blocks) grows content without a commit. While pinned, soft-follow the tip;
@@ -2061,6 +2122,29 @@ export function MessageTimeline({
2061
2122
  {groups.length > 0 && trailingState ? (
2062
2123
  <div data-og-timeline-trailing-state="">{trailingState}</div>
2063
2124
  ) : null}
2125
+ {hasNewer && newerFailure?.key === newerBoundaryKey ? (
2126
+ <div
2127
+ data-og-newer-error=""
2128
+ className="flex flex-col items-center gap-2 px-4 py-3 text-center text-og-menu text-og-fg-muted"
2129
+ >
2130
+ <p role="status" className="max-w-prose [overflow-wrap:anywhere]">
2131
+ Couldn’t load later activity. {newerFailure.message}
2132
+ </p>
2133
+ <button
2134
+ ref={newerRetryButtonRef}
2135
+ type="button"
2136
+ data-og-retry-newer=""
2137
+ className="min-h-11 rounded-og-md border border-og-border px-3 py-2 text-og-fg hover:bg-og-surface-2 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-og-accent"
2138
+ aria-disabled={loadingNewer || newerRetryPending}
2139
+ aria-busy={newerRetryPending}
2140
+ onClick={() => requestNewer(true)}
2141
+ >
2142
+ {newerRetryPending
2143
+ ? "Retrying later activity…"
2144
+ : "Retry later activity"}
2145
+ </button>
2146
+ </div>
2147
+ ) : null}
2064
2148
  {hasNewer ? (
2065
2149
  <div
2066
2150
  ref={bottomSentinelRef}
@@ -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>