@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
@@ -1,5 +1,5 @@
1
1
  import type { ClientModel, ReasoningEffort } from "@opengeni/sdk";
2
- import { CheckIcon, SearchIcon, ZapIcon } from "lucide-react";
2
+ import { ArrowRightIcon, CheckIcon, SearchIcon, ZapIcon } from "lucide-react";
3
3
  import { useRef, useState, type RefObject, type CSSProperties } from "react";
4
4
  import { Popover, RadioGroup } from "radix-ui";
5
5
  import { cn } from "../lib/cn";
@@ -30,13 +30,26 @@ export function ModelPolicyPickerMenu(props: ModelPolicyPickerProps) {
30
30
  const selected = findPickerRow(rows, props.model);
31
31
  const words = query.toLowerCase().trim().split(/\s+/).filter(Boolean);
32
32
  const filtered = rows.filter((row) => {
33
+ const description = props.groupPresentation?.[row.billingClass]?.description;
33
34
  const text =
34
- `${row.label} ${row.id} ${row.providerLabel} ${row.billingClassLabel} ${payerSummaryForModel(row.catalog)}`.toLowerCase();
35
+ `${row.label} ${row.id} ${row.providerLabel} ${row.billingClassLabel} ${description === undefined ? payerSummaryForModel(row.catalog) : (description ?? "")}`.toLowerCase();
35
36
  return words.every((word) => text.includes(word));
36
37
  });
37
38
  const matchingIds = new Set(filtered.map((row) => row.id));
38
39
  const groups = groupPickerRowsByBillingClass(rows, { codexOnly: props.codexOnly === true })
39
- .map((group) => ({ ...group, rows: group.rows.filter((row) => matchingIds.has(row.id)) }))
40
+ .map((group) => {
41
+ const override = props.groupPresentation?.[group.billingClass]?.description;
42
+ return {
43
+ ...group,
44
+ rows: group.rows.filter((row) => matchingIds.has(row.id)),
45
+ description:
46
+ override === undefined
47
+ ? group.billingClass === "opengeni_credits"
48
+ ? null
49
+ : messages.billingHints[group.billingClass]
50
+ : override,
51
+ };
52
+ })
40
53
  .filter((group) => group.rows.length > 0);
41
54
  const choose = (row: ClientPickerModelRow) => {
42
55
  if (!row.selectable || props.disabled) return;
@@ -59,7 +72,7 @@ export function ModelPolicyPickerMenu(props: ModelPolicyPickerProps) {
59
72
  hint={row.unavailableReason ?? undefined}
60
73
  disabled={props.disabled || !row.selectable}
61
74
  title={[row.label, row.unavailableReason].filter(Boolean).join(" · ")}
62
- active={row.id === props.model}
75
+ active={row.selectable && row.id === props.model}
63
76
  showChevron={false}
64
77
  trailing={
65
78
  row.catalog.cost === "free" || row.id === props.model ? (
@@ -106,28 +119,30 @@ export function ModelPolicyPickerMenu(props: ModelPolicyPickerProps) {
106
119
  buttons[next]?.focus();
107
120
  }}
108
121
  >
109
- <div className="border-b border-og-border py-1">
110
- <label className="og-model-policy-search flex items-center gap-2 rounded-og-sm px-2 py-1 focus-within:outline-2 focus-within:-outline-offset-2 focus-within:outline-og-accent/40">
111
- <SearchIcon className="size-4 shrink-0 text-og-fg-subtle" aria-hidden />
112
- <input
113
- aria-label={messages.searchLabel}
114
- placeholder={messages.searchPlaceholder}
115
- value={query}
116
- onChange={(event) => setQuery(event.target.value)}
117
- onKeyDown={(event) => {
118
- if (event.key !== "Escape" && event.key !== "Tab") event.stopPropagation();
119
- if (event.key === "ArrowDown") {
120
- event.preventDefault();
121
- event.currentTarget
122
- .closest('[data-testid="model-picker-menu"]')
123
- ?.querySelector<HTMLButtonElement>("button:not(:disabled)")
124
- ?.focus();
125
- }
126
- }}
127
- className="og-model-policy-search-input h-8 min-w-0 flex-1 bg-transparent text-og-menu text-og-fg outline-hidden placeholder:text-og-fg-subtle"
128
- />
129
- </label>
130
- </div>
122
+ {rows.some((row) => row.selectable) ? (
123
+ <div className="border-b border-og-border py-1">
124
+ <label className="og-model-policy-search flex items-center gap-2 rounded-og-sm px-2 py-1 focus-within:outline-2 focus-within:-outline-offset-2 focus-within:outline-og-accent/40">
125
+ <SearchIcon className="size-4 shrink-0 text-og-fg-subtle" aria-hidden />
126
+ <input
127
+ aria-label={messages.searchLabel}
128
+ placeholder={messages.searchPlaceholder}
129
+ value={query}
130
+ onChange={(event) => setQuery(event.target.value)}
131
+ onKeyDown={(event) => {
132
+ if (event.key !== "Escape" && event.key !== "Tab") event.stopPropagation();
133
+ if (event.key === "ArrowDown") {
134
+ event.preventDefault();
135
+ event.currentTarget
136
+ .closest('[data-testid="model-picker-menu"]')
137
+ ?.querySelector<HTMLButtonElement>("button:not(:disabled)")
138
+ ?.focus();
139
+ }
140
+ }}
141
+ className="og-model-policy-search-input h-8 min-w-0 flex-1 bg-transparent text-og-menu text-og-fg outline-hidden placeholder:text-og-fg-subtle"
142
+ />
143
+ </label>
144
+ </div>
145
+ ) : null}
131
146
  {props.error ? (
132
147
  <p className="px-2 py-2 text-og-control text-og-status-failed" role="alert">
133
148
  {props.error}
@@ -139,7 +154,7 @@ export function ModelPolicyPickerMenu(props: ModelPolicyPickerProps) {
139
154
  >
140
155
  {props.loading ? (
141
156
  <p className="px-2 py-3 text-og-control text-og-fg-subtle">{messages.loading}</p>
142
- ) : (
157
+ ) : rows.some((row) => row.selectable) ? (
143
158
  <>
144
159
  {groups.map((group) => (
145
160
  <section
@@ -148,12 +163,16 @@ export function ModelPolicyPickerMenu(props: ModelPolicyPickerProps) {
148
163
  className="py-2.5 first:pt-1 [&+section]:border-t [&+section]:border-og-border"
149
164
  >
150
165
  <div className="flex items-center gap-2 px-2.5 py-1.5 text-og-control font-semibold text-og-fg">
151
- <BillingClassMark billingClass={group.billingClass} aria-label="" />
152
- {group.label}
166
+ <BillingClassMark
167
+ billingClass={group.billingClass}
168
+ presentation={props.groupPresentation?.[group.billingClass]}
169
+ aria-label=""
170
+ />
171
+ <span className="min-w-0 break-words">{group.label}</span>
153
172
  </div>
154
- {group.billingClass !== "opengeni_credits" ? (
155
- <p className="px-2.5 pb-2 text-og-control text-og-fg-subtle">
156
- {messages.billingHints[group.billingClass]}
173
+ {group.description ? (
174
+ <p className="break-words px-2.5 pb-2 text-og-control text-og-fg-subtle">
175
+ {group.description}
157
176
  </p>
158
177
  ) : null}
159
178
  {group.rows.map(modelRow)}
@@ -165,9 +184,14 @@ export function ModelPolicyPickerMenu(props: ModelPolicyPickerProps) {
165
184
  </p>
166
185
  ) : null}
167
186
  </>
187
+ ) : props.connectModelsHref || rows.length > 0 ? (
188
+ <ConnectModelsPanel href={props.connectModelsHref} messages={messages} />
189
+ ) : (
190
+ <p className="px-2 py-4 text-og-control text-og-fg-subtle">{messages.noModels}</p>
168
191
  )}
169
192
  </div>
170
- {selected &&
193
+ {rows.some((row) => row.selectable) &&
194
+ selected &&
171
195
  ((props.hasImageAttachments &&
172
196
  selected.catalog.capabilities?.inputModalities.includes("image") === false) ||
173
197
  (effortOptionsForModel(selected.catalog).length > 1 &&
@@ -188,6 +212,80 @@ export function ModelPolicyPickerMenu(props: ModelPolicyPickerProps) {
188
212
  );
189
213
  }
190
214
 
215
+ const CONNECT_MODEL_OPTIONS: Array<{
216
+ billingClass: "codex_subscription" | "supergrok_subscription" | "byok" | "external";
217
+ title: string;
218
+ detail: string;
219
+ }> = [
220
+ { billingClass: "codex_subscription", title: "Codex", detail: "ChatGPT" },
221
+ { billingClass: "supergrok_subscription", title: "SuperGrok", detail: "xAI" },
222
+ { billingClass: "byok", title: "AI Gateway", detail: "Vercel" },
223
+ { billingClass: "external", title: "OpenRouter", detail: "API key" },
224
+ ];
225
+
226
+ function ConnectModelsPanel(props: {
227
+ href: string | undefined;
228
+ messages: typeof defaultModelPolicyPickerMessages;
229
+ }) {
230
+ return (
231
+ <div className="px-1.5 pb-1.5 pt-2" data-testid="model-picker-connect">
232
+ <div className="px-1.5 pb-3">
233
+ <p className="text-og-menu font-medium tracking-tight text-og-fg">
234
+ {props.messages.connectTitle}
235
+ </p>
236
+ <p className="mt-1 text-og-control leading-snug text-og-fg-subtle">
237
+ {props.messages.connectBody}
238
+ </p>
239
+ </div>
240
+ <div className="grid grid-cols-2 gap-1">
241
+ {CONNECT_MODEL_OPTIONS.map((option) => {
242
+ const content = (
243
+ <>
244
+ <span className="flex size-8 shrink-0 items-center justify-center rounded-og-md bg-og-surface-2 text-og-fg">
245
+ <BillingClassMark
246
+ billingClass={option.billingClass}
247
+ aria-label=""
248
+ className="size-4 text-og-fg"
249
+ />
250
+ </span>
251
+ <span className="min-w-0">
252
+ <span className="block truncate text-og-control font-medium text-og-fg">
253
+ {option.title}
254
+ </span>
255
+ <span className="block truncate text-og-control text-og-fg-subtle">
256
+ {option.detail}
257
+ </span>
258
+ </span>
259
+ </>
260
+ );
261
+ const className =
262
+ "flex min-w-0 items-center gap-2 rounded-og-md px-1.5 py-1.5 outline-hidden transition-colors hover:bg-og-surface-2 focus-visible:ring-2 focus-visible:ring-og-accent/40";
263
+ return props.href ? (
264
+ <a key={option.title} href={props.href} className={className}>
265
+ {content}
266
+ </a>
267
+ ) : (
268
+ <div key={option.title} className={className}>
269
+ {content}
270
+ </div>
271
+ );
272
+ })}
273
+ </div>
274
+ {props.href ? (
275
+ <a
276
+ href={props.href}
277
+ className="mt-2 flex h-9 w-full items-center justify-center gap-1.5 rounded-og-md bg-og-fg text-og-control font-medium text-og-surface-1 outline-hidden transition-opacity hover:opacity-90 focus-visible:ring-2 focus-visible:ring-og-accent/40"
278
+ >
279
+ {props.messages.connectAction}
280
+ <ArrowRightIcon className="size-3.5" aria-hidden />
281
+ </a>
282
+ ) : (
283
+ <p className="px-1.5 pt-3 text-og-control text-og-fg-subtle">{props.messages.noModels}</p>
284
+ )}
285
+ </div>
286
+ );
287
+ }
288
+
191
289
  function ModelThinkingControls(props: ModelPolicyPickerProps) {
192
290
  const selected = findPickerRow(effectiveRows(props), props.model);
193
291
  if (!selected) return null;
@@ -5,6 +5,7 @@ import {
5
5
  ChevronRightIcon,
6
6
  Globe2Icon,
7
7
  KeyRoundIcon,
8
+ SparklesIcon,
8
9
  ZapIcon,
9
10
  } from "lucide-react";
10
11
  import { motion, useReducedMotion } from "motion/react";
@@ -37,10 +38,27 @@ const LazyModelPolicyPickerMenu = lazy(() =>
37
38
 
38
39
  type ClientPickerModelRow = PickerModelRow<ClientModel>;
39
40
 
41
+ /** Presentation only; never changes model identity, billing or availability. */
42
+ export type ModelPolicyPickerGroupPresentation = Partial<
43
+ Record<
44
+ PickerBillingClass,
45
+ {
46
+ label?: string | undefined;
47
+ /** Omit to preserve the default; null hides the supporting text. */
48
+ description?: string | null | undefined;
49
+ /** Decorative, non-interactive content. Omit for the default; null hides it. */
50
+ icon?: ReactNode | undefined;
51
+ }
52
+ >
53
+ >;
54
+
40
55
  export type ModelPolicyPickerMessages = {
41
56
  label: string;
42
57
  loading: string;
43
58
  noModels: string;
59
+ connectTitle: string;
60
+ connectBody: string;
61
+ connectAction: string;
44
62
  thinking: string;
45
63
  fast: string;
46
64
  fastRateHint: string;
@@ -61,6 +79,9 @@ export const defaultModelPolicyPickerMessages: ModelPolicyPickerMessages = {
61
79
  label: "Model and effort",
62
80
  loading: "Loading model catalog…",
63
81
  noModels: "No models available.",
82
+ connectTitle: "Connect a model",
83
+ connectBody: "Use a subscription or a provider key you already have.",
84
+ connectAction: "Open Models",
64
85
  thinking: "Thinking",
65
86
  fast: "Fast",
66
87
  fastRateHint: "2× rate",
@@ -85,6 +106,8 @@ export const defaultModelPolicyPickerMessages: ModelPolicyPickerMessages = {
85
106
  };
86
107
 
87
108
  export type ModelPolicyPickerProps = {
109
+ /** Host branding for payment-source groups, shared by menu and trigger. */
110
+ groupPresentation?: ModelPolicyPickerGroupPresentation | undefined;
88
111
  /** Lightweight deployment models. Catalog rows take precedence when supplied. */
89
112
  models?: ClientModel[] | undefined;
90
113
  /** Warn only when the draft actually contains images this model cannot view. */
@@ -110,6 +133,8 @@ export type ModelPolicyPickerProps = {
110
133
  menuSide?: "top" | "bottom" | undefined;
111
134
  /** Hide latency controls on surfaces whose saved policy does not include latency. */
112
135
  allowLatencyMode?: boolean | undefined;
136
+ /** Settings → Models. Shown when the catalog has no model that can run. */
137
+ connectModelsHref?: string | undefined;
113
138
  /** Classes for the portalled menu surface. Prefer --og-* tokens for theming. */
114
139
  contentClassName?: string | undefined;
115
140
  /** Inline styles for the portalled menu, applied after inherited --og-* tokens. */
@@ -157,6 +182,7 @@ function XaiMark(props: SVGProps<SVGSVGElement>) {
157
182
 
158
183
  export function BillingClassMark(props: {
159
184
  billingClass: PickerBillingClass;
185
+ presentation?: ModelPolicyPickerGroupPresentation[PickerBillingClass] | undefined;
160
186
  className?: string | undefined;
161
187
  "aria-label"?: string | undefined;
162
188
  }) {
@@ -168,13 +194,14 @@ export function BillingClassMark(props: {
168
194
  byok: "Workspace provider account",
169
195
  organization_byok: "Organization provider account",
170
196
  };
171
- const label = props["aria-label"] ?? labels[props.billingClass];
197
+ if (props.presentation?.icon === null) return null;
198
+ const label = props["aria-label"] ?? props.presentation?.label ?? labels[props.billingClass];
172
199
  const accessibility =
173
200
  label.length === 0
174
201
  ? { "aria-hidden": true as const }
175
202
  : { role: "img" as const, "aria-label": label };
176
203
  const shell = cn(
177
- "inline-flex size-3.5 shrink-0 items-center justify-center overflow-hidden text-og-fg-subtle",
204
+ "inline-flex size-3.5 shrink-0 items-center justify-center overflow-hidden text-og-fg-subtle [&>svg]:size-full [&>img]:size-full",
178
205
  props.className,
179
206
  );
180
207
  const mark = "size-3.5";
@@ -184,7 +211,9 @@ export function BillingClassMark(props: {
184
211
  data-testid={`billing-class-icon-${props.billingClass}`}
185
212
  {...accessibility}
186
213
  >
187
- {props.billingClass === "opengeni_credits" ? (
214
+ {props.presentation?.icon !== undefined ? (
215
+ props.presentation.icon
216
+ ) : props.billingClass === "opengeni_credits" ? (
188
217
  <OpenGeniMark className={mark} />
189
218
  ) : props.billingClass === "external" ? (
190
219
  <Globe2Icon className={mark} aria-hidden />
@@ -235,7 +264,10 @@ function applyCodexOnly(
235
264
  export function effectiveRows(props: ModelPolicyPickerProps): ClientPickerModelRow[] {
236
265
  const rows = props.rows !== undefined ? props.rows : projectClientModelRows(props.models ?? []);
237
266
  const messages = { ...defaultModelPolicyPickerMessages, ...props.messages };
238
- return applyCodexOnly(rows, props.codexOnly === true, messages.codexOnly);
267
+ return applyCodexOnly(rows, props.codexOnly === true, messages.codexOnly).map((row) => {
268
+ const label = props.groupPresentation?.[row.billingClass]?.label;
269
+ return label === undefined ? row : { ...row, billingClassLabel: label };
270
+ });
239
271
  }
240
272
 
241
273
  export function PickerNavRow(props: {
@@ -346,6 +378,8 @@ export function ModelPolicyPicker(props: ModelPolicyPickerProps) {
346
378
  );
347
379
  const { open, setOpen, rows } = useModelPolicyPickerState(props);
348
380
  const selected = findPickerRow(rows, props.model);
381
+ const needsModel =
382
+ !rows.some((row) => row.selectable) && (rows.length > 0 || Boolean(props.connectModelsHref));
349
383
 
350
384
  if (props.loading) {
351
385
  return (
@@ -372,21 +406,36 @@ export function ModelPolicyPicker(props: ModelPolicyPickerProps) {
372
406
  disabled={props.disabled}
373
407
  aria-label={messages.label}
374
408
  className={cn(
375
- "og-root og-model-policy-trigger inline-flex h-[var(--og-model-picker-trigger-height)] min-w-0 max-w-64 items-center gap-1 rounded-full border border-transparent px-2.5 text-og-control text-og-fg-muted outline-hidden transition-colors hover:border-og-border hover:bg-og-surface-2 hover:text-og-fg focus-visible:ring-2 focus-visible:ring-og-accent/40 disabled:cursor-not-allowed disabled:opacity-50 max-sm:h-11 max-sm:max-w-[7.5rem] max-sm:px-2",
409
+ "og-root og-model-policy-trigger inline-flex h-[var(--og-model-picker-trigger-height)] min-w-0 max-w-64 items-center gap-1 rounded-full border px-2.5 text-og-control outline-hidden transition-colors focus-visible:ring-2 focus-visible:ring-og-accent/40 disabled:cursor-not-allowed disabled:opacity-50 max-sm:h-11 max-sm:max-w-[7.5rem] max-sm:px-2",
410
+ needsModel
411
+ ? "border-og-border bg-og-surface-2 text-og-fg hover:bg-og-surface-3"
412
+ : "border-transparent text-og-fg-muted hover:border-og-border hover:bg-og-surface-2 hover:text-og-fg",
376
413
  props.className,
377
414
  )}
378
415
  >
379
- <BillingClassMark
380
- billingClass={selected?.billingClass ?? billingClassForMissingSelection(props.model)}
381
- className="text-og-fg"
382
- />
416
+ {needsModel ? (
417
+ <SparklesIcon className="size-3.5 shrink-0" aria-hidden />
418
+ ) : (
419
+ <BillingClassMark
420
+ billingClass={selected?.billingClass ?? billingClassForMissingSelection(props.model)}
421
+ presentation={
422
+ props.groupPresentation?.[
423
+ selected?.billingClass ?? billingClassForMissingSelection(props.model)
424
+ ]
425
+ }
426
+ className="text-og-fg"
427
+ />
428
+ )}
383
429
  <span className="og-model-policy-label-full min-w-0 truncate font-medium text-og-fg max-sm:hidden @max-[20rem]/model-controls:hidden">
384
- {selected?.label ?? props.model}
430
+ {needsModel ? messages.connectTitle : (selected?.label ?? props.model)}
385
431
  </span>
386
432
  <span className="og-model-policy-label-short min-w-0 truncate font-medium text-og-fg sm:hidden @max-[20rem]/model-controls:block">
387
- {selected?.shortLabel ?? selected?.label ?? props.model}
433
+ {needsModel
434
+ ? messages.connectTitle
435
+ : (selected?.shortLabel ?? selected?.label ?? props.model)}
388
436
  </span>
389
437
  {selected &&
438
+ !needsModel &&
390
439
  effortOptionsForModel(selected.catalog).length > 1 &&
391
440
  selected.catalog.capabilities?.reasoning.runnable !== false ? (
392
441
  <span
@@ -396,7 +445,7 @@ export function ModelPolicyPicker(props: ModelPolicyPickerProps) {
396
445
  {labelReasoningEffort(props.effort)}
397
446
  </span>
398
447
  ) : null}
399
- {props.latencyMode === "fast" ? (
448
+ {props.latencyMode === "fast" && !needsModel ? (
400
449
  <ZapIcon
401
450
  className="size-3.5 shrink-0 fill-current stroke-current text-og-fg"
402
451
  aria-label={messages.fast}
@@ -62,13 +62,18 @@ export function SessionCommandsPanel({
62
62
  title="Expand command"
63
63
  >
64
64
  <ChevronRightIcon className="size-3 shrink-0 text-og-fg-subtle transition-transform group-open/command:rotate-90" />
65
- <span className="truncate">
66
- {command.commandPreview || "Background command"}
65
+ <span
66
+ className="min-w-0 truncate"
67
+ title={
68
+ command.commandText ?? (command.commandPreview || "Background command")
69
+ }
70
+ >
71
+ {command.commandText ?? (command.commandPreview || "Background command")}
67
72
  </span>
68
73
  </summary>
69
74
  <div className="mt-2 space-y-1">
70
75
  <p className="whitespace-pre-wrap break-all font-mono text-og-fg-muted">
71
- {command.commandPreview || "Background command"}
76
+ {command.commandText ?? (command.commandPreview || "Background command")}
72
77
  </p>
73
78
  <time
74
79
  className="block text-og-fg-subtle"
@@ -10,13 +10,15 @@ import { useHumanInputRequests } from "../hooks/use-human-input";
10
10
  import { ChatComposer, type ChatComposerProps } from "./chat-composer";
11
11
  import { SessionChrome } from "./session-chrome";
12
12
  import { HumanInputSurface, type HumanInputSurfaceProps } from "./human-input-surface";
13
- import { MessageTimeline } from "./message-timeline";
13
+ import { MessageTimeline, type MessageTimelineProps } from "./message-timeline";
14
14
  import type { UserMessageDisclosureLabels } from "./user-message-body";
15
15
  import { conversationTimeline } from "../conversation-timeline";
16
16
  import { cn } from "../lib/cn";
17
17
 
18
18
  export type SessionConversationProps = ClientOverride & {
19
19
  sessionId: string;
20
+ /** Host-owned artifact links, previews and other message presentation. */
21
+ renderMessageText?: MessageTimelineProps["renderMessageText"];
20
22
  /** Localized actions for already-sent user-message disclosure. */
21
23
  userMessageDisclosureLabels?: UserMessageDisclosureLabels | undefined;
22
24
  loadSkillReview?: HumanInputSurfaceProps["loadSkillReview"];
@@ -35,6 +37,7 @@ export function SessionConversation(props: SessionConversationProps) {
35
37
 
36
38
  function Conversation({
37
39
  sessionId,
40
+ renderMessageText,
38
41
  userMessageDisclosureLabels,
39
42
  loadSkillReview,
40
43
  client,
@@ -75,6 +78,7 @@ function Conversation({
75
78
  >
76
79
  {error && <p role="alert">{error.message}</p>}
77
80
  <MessageTimeline
81
+ renderMessageText={renderMessageText}
78
82
  userMessageDisclosureLabels={userMessageDisclosureLabels}
79
83
  className="min-h-0 flex-1"
80
84
  items={conversationTimeline(feed.timeline, queue, composer)}
package/src/composer.ts CHANGED
@@ -15,6 +15,7 @@ export {
15
15
  export type {
16
16
  ModelPolicyPickerMessages,
17
17
  ModelPolicyPickerProps,
18
+ ModelPolicyPickerGroupPresentation,
18
19
  } from "./components/model-policy-picker";
19
20
  export {
20
21
  Actions,
@@ -47,8 +47,7 @@ export function useLastStartedTurnPolicy(
47
47
  sessionId: string | null | undefined,
48
48
  options: UseLastStartedTurnPolicyOptions = {},
49
49
  ): UseLastStartedTurnPolicyResult {
50
- const { client, workspaceId, workspaceControlEvent, registerSessionReconciler } =
51
- useOpenGeni(options);
50
+ const { client, workspaceId, registerSessionReconciler } = useOpenGeni(options);
52
51
  const enabled = (options.enabled ?? true) && Boolean(sessionId);
53
52
 
54
53
  const load = useCallback(
@@ -68,10 +67,6 @@ export function useLastStartedTurnPolicy(
68
67
  enabled,
69
68
  });
70
69
 
71
- useEffect(() => {
72
- if (enabled && workspaceControlEvent) void refresh();
73
- }, [enabled, refresh, workspaceControlEvent]);
74
-
75
70
  useEffect(() => {
76
71
  if (!sessionId || !enabled) return;
77
72
  return registerSessionReconciler(sessionId, "last-started-turn-policy", refresh);
@@ -79,7 +74,8 @@ export function useLastStartedTurnPolicy(
79
74
 
80
75
  // Shared-feed only: the session header lives outside the route event log and
81
76
  // must not open a second SSE. Without `events`, rely on the initial fetch +
82
- // optional poll / workspace-control / reconciler.
77
+ // optional poll / reconciler. Pause/resume controls do not change the last
78
+ // admitted turn; its own turn.started event supplies that invalidation.
83
79
  useSessionEventTrigger(
84
80
  client,
85
81
  workspaceId,
@@ -105,8 +105,8 @@ const FOREGROUND_COMPACT_CATCHUP_MAX_GROUPS = 16;
105
105
  const FOREGROUND_COMPACT_CATCHUP_MAX_BYTES = 512 * 1024;
106
106
  const EMPTY_EVENTS: SessionEvent[] = [];
107
107
  const encoder = new TextEncoder();
108
- export const SESSION_EVENT_BROWSER_MAX_BYTES = 8 * 1024 * 1024;
109
- export const SESSION_EVENT_BROWSER_MAX_COUNT = 10_000;
108
+ export const SESSION_EVENT_BROWSER_MAX_BYTES = 160 * 1024 * 1024;
109
+ export const SESSION_EVENT_BROWSER_MAX_COUNT = 200_000;
110
110
  export const SESSION_EVENT_BROWSER_PENDING_MAX_BYTES = 1024 * 1024;
111
111
  export const SESSION_EVENT_BROWSER_PENDING_MAX_COUNT = 256;
112
112
 
@@ -325,7 +325,7 @@ export function useSessionEvents(
325
325
  setSessionStatusProjection(status);
326
326
  }
327
327
  const current = eventWindowRef.current;
328
- const next = boundBrowserSessionEventWindow([...current.events, ...batch]);
328
+ const next = appendBrowserSessionEventWindow(current, batch);
329
329
  const retained = {
330
330
  ...next,
331
331
  truncated: current.truncated || next.truncated,
@@ -366,7 +366,7 @@ export function useSessionEvents(
366
366
  const current = eventWindowRef.current;
367
367
  assertAppendOrder(current.events, plan.events);
368
368
  const status = observeSessionStatus(plan.events, sessionStatusRef);
369
- const next = boundBrowserSessionEventWindow([...current.events, ...plan.events]);
369
+ const next = appendBrowserSessionEventWindow(current, plan.events);
370
370
  const retained = {
371
371
  ...next,
372
372
  truncated: current.truncated || next.truncated,
@@ -764,9 +764,7 @@ export function useSessionEvents(
764
764
  const status = observeSessionStatus(window.events, sessionStatusRef);
765
765
  const previousOldest = current.events[0]?.sequence ?? null;
766
766
  // Moving forward through history: keep the newest suffix of the merge.
767
- const next = boundBrowserSessionEventWindow([...current.events, ...window.events], {
768
- direction: "newest",
769
- });
767
+ const next = appendBrowserSessionEventWindow(current, window.events);
770
768
  const retained = {
771
769
  ...next,
772
770
  truncated: current.truncated || next.truncated || window.hasNewer,
@@ -1049,6 +1047,33 @@ export function boundBrowserSessionEventWindow(
1049
1047
  };
1050
1048
  }
1051
1049
 
1050
+ /** @internal Append immutable events without serializing the retained history again. */
1051
+ export function appendBrowserSessionEventWindow(
1052
+ current: BrowserSessionEventWindow,
1053
+ batch: readonly SessionEvent[],
1054
+ options: { maxBytes?: number; maxCount?: number } = {},
1055
+ ): BrowserSessionEventWindow {
1056
+ const maxBytes = Math.max(1024, options.maxBytes ?? SESSION_EVENT_BROWSER_MAX_BYTES);
1057
+ const maxCount = Math.max(1, Math.floor(options.maxCount ?? SESSION_EVENT_BROWSER_MAX_COUNT));
1058
+ const events = [...current.events, ...batch];
1059
+ let bytes = current.bytes;
1060
+ for (let index = 0; index < batch.length; index += 1) {
1061
+ bytes += browserJsonBytes(batch[index]) + (current.events.length + index > 0 ? 1 : 0);
1062
+ }
1063
+ let start = 0;
1064
+ // Preserve the newest event even when it alone exceeds the byte target,
1065
+ // matching the full-window reducer and retaining exact cursor progress.
1066
+ while (events.length - start > 1 && (bytes > maxBytes || events.length - start > maxCount)) {
1067
+ bytes -= browserJsonBytes(events[start]) + 1;
1068
+ start += 1;
1069
+ }
1070
+ return {
1071
+ events: start === 0 ? events : events.slice(start),
1072
+ bytes,
1073
+ truncated: current.truncated || start > 0,
1074
+ };
1075
+ }
1076
+
1052
1077
  function browserJsonBytes(value: unknown): number {
1053
1078
  return encoder.encode(JSON.stringify(value)).byteLength;
1054
1079
  }
package/src/index.ts CHANGED
@@ -504,6 +504,7 @@ export { ModelPolicyPickerMenu } from "./components/model-policy-picker-menu";
504
504
  export type {
505
505
  ModelPolicyPickerMessages,
506
506
  ModelPolicyPickerProps,
507
+ ModelPolicyPickerGroupPresentation,
507
508
  } from "./components/model-policy-picker";
508
509
  export {
509
510
  advancedSourceSummary,
@@ -217,10 +217,7 @@ function organizationProviderPayerSummary(model: ClientModel): string {
217
217
 
218
218
  export function projectPickerRows(models: WorkspaceModelCatalogModel[]): PickerModelRow[] {
219
219
  return models
220
- .filter((catalog) => {
221
- const billingClass = billingClassForModel(catalog);
222
- return billingClass === "opengeni_credits" || catalog.credentialReadiness.status === "ready";
223
- })
220
+ .filter((catalog) => catalog.credentialReadiness.status === "ready")
224
221
  .map((catalog) => {
225
222
  const billingClass = billingClassForModel(catalog);
226
223
  return {
@@ -1,5 +1,6 @@
1
1
  import {
2
2
  parseMediaGenerationResult,
3
+ parseToolDisplayMetadata,
3
4
  type HumanInputAnswer,
4
5
  type HumanInputQuestion,
5
6
  type HumanInputResponse,
@@ -610,6 +611,7 @@ export function buildTimeline(
610
611
 
611
612
  case "agent.toolCall.created": {
612
613
  const name = typeof payload.name === "string" ? payload.name : "tool";
614
+ const display = parseToolDisplayMetadata(payload.display);
613
615
  const callId = typeof payload.id === "string" ? payload.id : null;
614
616
  const args = payload.arguments ?? null;
615
617
  closeStreamingTail();
@@ -651,6 +653,7 @@ export function buildTimeline(
651
653
  (item): item is ToolCallItem => item.kind === "tool-call" && item.callId === callId,
652
654
  );
653
655
  if (existing) {
656
+ if (display) existing.display = display;
654
657
  if (args != null) {
655
658
  existing.arguments = args;
656
659
  }
@@ -667,6 +670,7 @@ export function buildTimeline(
667
670
  turnId,
668
671
  callId,
669
672
  name,
673
+ ...(display ? { display } : {}),
670
674
  arguments: args,
671
675
  output: undefined,
672
676
  truncation: null,
@@ -36,7 +36,9 @@ export function RollingActivity({
36
36
  const fallback = (
37
37
  <span className="og-rolling-label">
38
38
  <WrenchIcon className="size-3.5" />
39
- <span>{item.kind === "tool-call" ? toolDisplayName(item.name) : "Working"}</span>
39
+ <span>
40
+ {item.kind === "tool-call" ? toolDisplayName(item.name, item.display) : "Working"}
41
+ </span>
40
42
  </span>
41
43
  );
42
44
  return (
@@ -46,7 +48,7 @@ export function RollingActivity({
46
48
  >
47
49
  <span className="sr-only">
48
50
  {item.kind === "tool-call"
49
- ? toolDisplayName(item.name)
51
+ ? toolDisplayName(item.name, item.display)
50
52
  : item.kind === "reasoning"
51
53
  ? "Thinking"
52
54
  : "Working"}