@opengeni/react 0.57.0 → 2.2.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 (174) hide show
  1. package/README.md +36 -11
  2. package/dist/artifacts-spreadsheet.js +1 -1
  3. package/dist/artifacts.js +1 -1
  4. package/dist/browser-viewer-GXUICIAG.js +14 -0
  5. package/dist/{chunk-R3AZOAB5.js → chunk-4MDLEHXL.js} +9 -115
  6. package/dist/chunk-4MDLEHXL.js.map +1 -0
  7. package/dist/{chunk-Y7OEDUJ5.js → chunk-5RZN3DD5.js} +1014 -420
  8. package/dist/chunk-5RZN3DD5.js.map +1 -0
  9. package/dist/chunk-5YK2ZDIG.js +116 -0
  10. package/dist/chunk-5YK2ZDIG.js.map +1 -0
  11. package/dist/chunk-H5YGYNXS.js +329 -0
  12. package/dist/chunk-H5YGYNXS.js.map +1 -0
  13. package/dist/{chunk-7WCLAEFV.js → chunk-ICA6R6TW.js} +2 -2
  14. package/dist/{chunk-7WCLAEFV.js.map → chunk-ICA6R6TW.js.map} +1 -1
  15. package/dist/{chunk-IGC6A2ZI.js → chunk-JOAVA2TW.js} +589 -217
  16. package/dist/chunk-JOAVA2TW.js.map +1 -0
  17. package/dist/{chunk-5UQPNLIO.js → chunk-LTCHJ4MS.js} +9 -9
  18. package/dist/chunk-LTCHJ4MS.js.map +1 -0
  19. package/dist/{chunk-C5ZPSSS5.js → chunk-NEXLLTP4.js} +2 -1
  20. package/dist/{chunk-C5ZPSSS5.js.map → chunk-NEXLLTP4.js.map} +1 -1
  21. package/dist/chunk-QGBJ6GHG.js +2300 -0
  22. package/dist/chunk-QGBJ6GHG.js.map +1 -0
  23. package/dist/{chunk-B6WPZSTT.js → chunk-UOLJWT5Y.js} +78 -16
  24. package/dist/chunk-UOLJWT5Y.js.map +1 -0
  25. package/dist/{chunk-MKPIAGVL.js → chunk-W6NFJXSA.js} +8 -19
  26. package/dist/chunk-W6NFJXSA.js.map +1 -0
  27. package/dist/{chunk-XBZWOM2K.js → chunk-X4A66EWZ.js} +8 -3
  28. package/dist/chunk-X4A66EWZ.js.map +1 -0
  29. package/dist/chunk-X4JAFD5R.js +3907 -0
  30. package/dist/chunk-X4JAFD5R.js.map +1 -0
  31. package/dist/{chunk-6KLX2RH5.js → chunk-XSA2QAKP.js} +334 -19
  32. package/dist/chunk-XSA2QAKP.js.map +1 -0
  33. package/dist/{chunk-HA46CYQK.js → chunk-XXWH2JLC.js} +792 -206
  34. package/dist/chunk-XXWH2JLC.js.map +1 -0
  35. package/dist/client.d.ts +2 -2
  36. package/dist/components/browser-viewer.d.ts +7 -1
  37. package/dist/components/computer-viewer.d.ts +4 -1
  38. package/dist/components/enrollment-consent.d.ts +6 -3
  39. package/dist/components/file-browser.d.ts +7 -1
  40. package/dist/components/human-input-form.d.ts +1 -1
  41. package/dist/components/human-input-surface.d.ts +1 -1
  42. package/dist/components/machine-card.d.ts +4 -1
  43. package/dist/components/machines/machine-detail.d.ts +4 -1
  44. package/dist/components/machines/machine-operation-policy.d.ts +11 -0
  45. package/dist/components/machines-dashboard.d.ts +3 -1
  46. package/dist/components/markdown.d.ts +20 -1
  47. package/dist/components/message-timeline.d.ts +1 -1
  48. package/dist/components/model-policy-picker.d.ts +2 -0
  49. package/dist/components/pierre-file.d.ts +1 -1
  50. package/dist/components/sandbox-files.d.ts +8 -2
  51. package/dist/components/sandbox-workspace.d.ts +33 -3
  52. package/dist/components/session-chrome.d.ts +15 -3
  53. package/dist/components/workspace-dock.d.ts +6 -2
  54. package/dist/composer.js +5 -5
  55. package/dist/computer-viewer-ZOP4Q7A7.js +16 -0
  56. package/dist/computer-viewer-ZOP4Q7A7.js.map +1 -0
  57. package/dist/hooks/channel-cache.d.ts +23 -0
  58. package/dist/hooks/shared-machines-list.d.ts +14 -0
  59. package/dist/hooks/use-attached-browsers.d.ts +2 -1
  60. package/dist/hooks/use-channels.d.ts +1 -0
  61. package/dist/hooks/use-composer.d.ts +37 -9
  62. package/dist/hooks/use-machine-chip.d.ts +3 -3
  63. package/dist/hooks/use-machines.d.ts +19 -1
  64. package/dist/hooks/use-sandbox-files.d.ts +1 -0
  65. package/dist/hooks/use-session-background-commands.d.ts +14 -0
  66. package/dist/hooks/use-turn-queue.d.ts +12 -0
  67. package/dist/hooks/use-workspace-sessions.d.ts +1 -0
  68. package/dist/index.d.ts +7 -5
  69. package/dist/index.js +1096 -623
  70. package/dist/index.js.map +1 -1
  71. package/dist/interaction.js +29 -181
  72. package/dist/interaction.js.map +1 -1
  73. package/dist/lib/interaction-errors.d.ts +3 -0
  74. package/dist/machines.d.ts +4 -2
  75. package/dist/machines.js +10 -2
  76. package/dist/model-policy.d.ts +1 -1
  77. package/dist/model-policy.js +1 -1
  78. package/dist/platform-activity-row-BJHUHXAT.js +169 -0
  79. package/dist/platform-activity-row-BJHUHXAT.js.map +1 -0
  80. package/dist/{queue-surface-implementation-2KVJTYXH.js → queue-surface-implementation-BSKUWHRI.js} +76 -16
  81. package/dist/queue-surface-implementation-BSKUWHRI.js.map +1 -0
  82. package/dist/realtime/realtime-control.d.ts +10 -3
  83. package/dist/realtime.js +138 -29
  84. package/dist/realtime.js.map +1 -1
  85. package/dist/session-ui.d.ts +1 -1
  86. package/dist/session-ui.js +6 -2
  87. package/dist/session.d.ts +2 -2
  88. package/dist/session.js +4 -4
  89. package/dist/timeline/index.d.ts +1 -1
  90. package/dist/timeline/platform-activity-row.d.ts +15 -0
  91. package/dist/timeline/screenshot-lightbox.d.ts +1 -1
  92. package/dist/timeline/shared.d.ts +4 -2
  93. package/dist/timeline/types.d.ts +48 -5
  94. package/dist/types/machines.d.ts +2 -2
  95. package/package.json +3 -3
  96. package/src/client.ts +12 -1
  97. package/src/components/artifacts/editable-spreadsheet.tsx +1 -1
  98. package/src/components/browser-viewer.tsx +361 -58
  99. package/src/components/chat-composer.tsx +1 -1
  100. package/src/components/composer.tsx +6 -6
  101. package/src/components/computer-viewer.tsx +368 -115
  102. package/src/components/enrollment-consent.tsx +64 -6
  103. package/src/components/file-browser.tsx +117 -2
  104. package/src/components/human-input-form.tsx +91 -55
  105. package/src/components/human-input-surface.tsx +1 -1
  106. package/src/components/machine-card.tsx +111 -0
  107. package/src/components/machine-input-display.ts +15 -0
  108. package/src/components/machines/machine-detail.tsx +47 -0
  109. package/src/components/machines/machine-operation-policy.tsx +179 -0
  110. package/src/components/machines-dashboard.tsx +6 -0
  111. package/src/components/markdown.tsx +174 -20
  112. package/src/components/message-timeline.tsx +162 -17
  113. package/src/components/model-policy-picker.tsx +9 -17
  114. package/src/components/pierre-file.tsx +1 -1
  115. package/src/components/queue-surface-implementation.tsx +82 -16
  116. package/src/components/sandbox-files.tsx +135 -19
  117. package/src/components/sandbox-workspace.tsx +204 -92
  118. package/src/components/session-chrome.tsx +415 -208
  119. package/src/components/session-status.tsx +1 -1
  120. package/src/components/user-message-body.tsx +168 -48
  121. package/src/components/workspace-dock.tsx +141 -175
  122. package/src/hooks/channel-cache.ts +104 -0
  123. package/src/hooks/shared-machines-list.ts +239 -0
  124. package/src/hooks/use-attached-browsers.ts +5 -1
  125. package/src/hooks/use-browser-frame-stream.ts +6 -1
  126. package/src/hooks/use-browser-session.ts +6 -4
  127. package/src/hooks/use-browser-sessions.ts +7 -1
  128. package/src/hooks/use-channels.ts +32 -10
  129. package/src/hooks/use-composer.ts +760 -291
  130. package/src/hooks/use-computer-frame-stream.ts +14 -9
  131. package/src/hooks/use-computer-session.ts +16 -14
  132. package/src/hooks/use-computer-sessions.ts +8 -1
  133. package/src/hooks/use-last-started-turn-policy.ts +1 -1
  134. package/src/hooks/use-machine-chip.ts +23 -6
  135. package/src/hooks/use-machines.ts +104 -6
  136. package/src/hooks/use-sandbox-files.ts +195 -140
  137. package/src/hooks/use-sandbox-git.ts +58 -29
  138. package/src/hooks/use-session-background-commands.ts +56 -0
  139. package/src/hooks/use-session-capabilities.ts +18 -0
  140. package/src/hooks/use-turn-queue.ts +111 -12
  141. package/src/hooks/use-workspace-sessions.ts +5 -1
  142. package/src/index.ts +26 -4
  143. package/src/lib/interaction-errors.ts +18 -0
  144. package/src/machines.ts +9 -1
  145. package/src/model-policy.ts +10 -2
  146. package/src/realtime/realtime-control.tsx +200 -37
  147. package/src/session-context.ts +1 -0
  148. package/src/session-ui.ts +6 -1
  149. package/src/session.ts +9 -1
  150. package/src/timeline/activity-rail.tsx +20 -75
  151. package/src/timeline/index.ts +4 -0
  152. package/src/timeline/platform-activity-row.tsx +201 -0
  153. package/src/timeline/projection.ts +476 -23
  154. package/src/timeline/screenshot-lightbox.tsx +48 -14
  155. package/src/timeline/shared.tsx +8 -2
  156. package/src/timeline/tool-renderers.tsx +104 -6
  157. package/src/timeline/turn-summary.tsx +6 -1
  158. package/src/timeline/types.ts +75 -2
  159. package/src/types/machines.ts +10 -0
  160. package/styles/compiled.css +115 -104
  161. package/dist/chunk-5UQPNLIO.js.map +0 -1
  162. package/dist/chunk-6KLX2RH5.js.map +0 -1
  163. package/dist/chunk-B6WPZSTT.js.map +0 -1
  164. package/dist/chunk-HA46CYQK.js.map +0 -1
  165. package/dist/chunk-IGC6A2ZI.js.map +0 -1
  166. package/dist/chunk-LLOLUHVG.js +0 -5937
  167. package/dist/chunk-LLOLUHVG.js.map +0 -1
  168. package/dist/chunk-MKPIAGVL.js.map +0 -1
  169. package/dist/chunk-R3AZOAB5.js.map +0 -1
  170. package/dist/chunk-XBZWOM2K.js.map +0 -1
  171. package/dist/chunk-Y7OEDUJ5.js.map +0 -1
  172. package/dist/interactive-workbench-viewers-TOHBXJEE.js +0 -15
  173. package/dist/queue-surface-implementation-2KVJTYXH.js.map +0 -1
  174. /package/dist/{interactive-workbench-viewers-TOHBXJEE.js.map → browser-viewer-GXUICIAG.js.map} +0 -0
@@ -6,6 +6,7 @@ import {
6
6
  type SessionRealtimeController,
7
7
  type SessionRealtimeControllerSnapshot,
8
8
  type CreateSessionRealtimeControllerOptions,
9
+ type SessionRealtimeLifecycleProjection,
9
10
  type SessionRealtimeModel,
10
11
  type WorkspaceRealtimeModelCatalogItem,
11
12
  } from "@opengeni/sdk/realtime";
@@ -98,9 +99,108 @@ const REALTIME_PROVIDER_META: Record<
98
99
  "Your Gateway": { billingClass: "byok", hint: "Billed to your AI Gateway" },
99
100
  };
100
101
  const REALTIME_MODEL_STORAGE_PREFIX = "opengeni:realtime-model";
102
+ const REALTIME_MODEL_CATALOG_CACHE_TTL_MS = 60_000;
103
+ const REALTIME_MODEL_CATALOG_CACHE_MAX_WORKSPACES = 64;
101
104
 
102
105
  type RealtimeControllerClient = EmbeddedRealtimeSessionClientLike & SessionRealtimeClientLike;
103
106
 
107
+ type RealtimeModelCatalogCacheEntry =
108
+ | { state: "loading"; promise: Promise<RealtimeModelOption[]> }
109
+ | { state: "ready"; models: RealtimeModelOption[]; expiresAt: number };
110
+
111
+ // Scope advisory availability to the exact client object and workspace. The
112
+ // server still authorizes every realtime operation; changing principals should
113
+ // replace the client just as it does for the rest of the SDK state.
114
+ const realtimeModelCatalogCache = new WeakMap<
115
+ RealtimeControllerClient,
116
+ Map<string, RealtimeModelCatalogCacheEntry>
117
+ >();
118
+
119
+ function realtimeModelFallback(codexConnected: boolean): RealtimeModelOption[] {
120
+ return [
121
+ {
122
+ ...CODEX_LIVE_MODEL,
123
+ available: codexConnected,
124
+ unavailableReason: codexConnected ? null : CODEX_LIVE_MODEL.unavailableReason,
125
+ },
126
+ ];
127
+ }
128
+
129
+ function realtimeModelCatalogCacheFor(
130
+ client: RealtimeControllerClient,
131
+ ): Map<string, RealtimeModelCatalogCacheEntry> {
132
+ const existing = realtimeModelCatalogCache.get(client);
133
+ if (existing) return existing;
134
+ const created = new Map<string, RealtimeModelCatalogCacheEntry>();
135
+ realtimeModelCatalogCache.set(client, created);
136
+ return created;
137
+ }
138
+
139
+ function readCachedRealtimeModelCatalog(
140
+ client: RealtimeControllerClient,
141
+ workspaceId: string,
142
+ now = Date.now(),
143
+ ): RealtimeModelOption[] | null {
144
+ const cache = realtimeModelCatalogCache.get(client);
145
+ const entry = cache?.get(workspaceId);
146
+ if (!entry || entry.state === "loading") return null;
147
+ if (entry.expiresAt > now) return entry.models;
148
+ cache?.delete(workspaceId);
149
+ return null;
150
+ }
151
+
152
+ function pruneRealtimeModelCatalogCache(
153
+ cache: Map<string, RealtimeModelCatalogCacheEntry>,
154
+ now: number,
155
+ ): void {
156
+ for (const [workspaceId, entry] of cache) {
157
+ if (entry.state === "ready" && entry.expiresAt <= now) cache.delete(workspaceId);
158
+ }
159
+ while (cache.size >= REALTIME_MODEL_CATALOG_CACHE_MAX_WORKSPACES) {
160
+ const oldestWorkspaceId = cache.keys().next().value;
161
+ if (oldestWorkspaceId === undefined) return;
162
+ cache.delete(oldestWorkspaceId);
163
+ }
164
+ }
165
+
166
+ function loadRealtimeModelCatalog(
167
+ client: RealtimeControllerClient,
168
+ workspaceId: string,
169
+ ): Promise<RealtimeModelOption[]> | null {
170
+ const load = client.getWorkspaceRealtimeModelCatalog;
171
+ if (!load) return null;
172
+ const now = Date.now();
173
+ const cache = realtimeModelCatalogCacheFor(client);
174
+ const entry = cache.get(workspaceId);
175
+ if (entry?.state === "loading") return entry.promise;
176
+ if (entry?.state === "ready" && entry.expiresAt > now) return Promise.resolve(entry.models);
177
+ if (entry) cache.delete(workspaceId);
178
+ pruneRealtimeModelCatalogCache(cache, now);
179
+
180
+ const promise = load
181
+ .call(client, workspaceId)
182
+ .then((response) => response.models.map(toRealtimeModelOption))
183
+ .then((models) => {
184
+ const current = cache.get(workspaceId);
185
+ if (current?.state === "loading" && current.promise === promise) {
186
+ cache.delete(workspaceId);
187
+ cache.set(workspaceId, {
188
+ state: "ready",
189
+ models,
190
+ expiresAt: Date.now() + REALTIME_MODEL_CATALOG_CACHE_TTL_MS,
191
+ });
192
+ }
193
+ return models;
194
+ })
195
+ .catch((error: unknown) => {
196
+ const current = cache.get(workspaceId);
197
+ if (current?.state === "loading" && current.promise === promise) cache.delete(workspaceId);
198
+ throw error;
199
+ });
200
+ cache.set(workspaceId, { state: "loading", promise });
201
+ return promise;
202
+ }
203
+
104
204
  export type SessionRealtimeControllerFactory = (
105
205
  options: CreateSessionRealtimeControllerOptions,
106
206
  ) => SessionRealtimeController;
@@ -138,6 +238,8 @@ export function useSessionRealtime(options: {
138
238
  model?: SessionRealtimeModel | undefined;
139
239
  modelAvailable?: boolean | undefined;
140
240
  modelUnavailableReason?: string | null | undefined;
241
+ /** Model-visible application context captured with each durable realtime message. */
242
+ getModelContext?: (() => string | undefined) | undefined;
141
243
  /** Deterministic browser-test/demo seam. Production hosts should use the SDK default. */
142
244
  controllerFactory?: SessionRealtimeControllerFactory | undefined;
143
245
  }) {
@@ -149,6 +251,8 @@ export function useSessionRealtime(options: {
149
251
  const audioRef = useRef<HTMLAudioElement>(null);
150
252
  const controllerRef = useRef<SessionRealtimeController | null>(null);
151
253
  const controllerModelRef = useRef<SessionRealtimeModel | null>(null);
254
+ const modelContextProviderRef = useRef(options.getModelContext);
255
+ modelContextProviderRef.current = options.getModelContext;
152
256
  const [snapshot, setSnapshot] = useState<SessionRealtimeControllerSnapshot>(() => ({
153
257
  status: hasStoredSessionRealtimeOwnerProof({
154
258
  workspaceId,
@@ -177,7 +281,7 @@ export function useSessionRealtime(options: {
177
281
  const eventsReadyRef = useRef(options.eventsReady);
178
282
  lifecycleRef.current = lifecycle;
179
283
  eventsReadyRef.current = options.eventsReady;
180
- const lifecycleActive = lifecycle?.state === "active";
284
+ const lifecycleActive = sessionRealtimeLifecycleIsActive(lifecycle);
181
285
 
182
286
  useEffect(() => {
183
287
  let disposed = false;
@@ -193,6 +297,7 @@ export function useSessionRealtime(options: {
193
297
  sessionId: options.sessionId,
194
298
  ...(audioRef.current ? { remoteAudio: audioRef.current } : {}),
195
299
  model,
300
+ getModelContext: () => modelContextProviderRef.current?.(),
196
301
  });
197
302
  controllerRef.current = controller;
198
303
  controllerModelRef.current = model;
@@ -297,6 +402,14 @@ export function useSessionRealtime(options: {
297
402
  };
298
403
  }
299
404
 
405
+ function sessionRealtimeLifecycleIsActive(
406
+ lifecycle: SessionRealtimeLifecycleProjection | null,
407
+ ): boolean {
408
+ if (lifecycle?.state !== "active") return false;
409
+ const leaseExpiresAt = Date.parse(lifecycle.leaseExpiresAt);
410
+ return !Number.isFinite(leaseExpiresAt) || leaseExpiresAt > Date.now();
411
+ }
412
+
300
413
  export function SessionRealtimeControl(props: {
301
414
  client?: RealtimeControllerClient | undefined;
302
415
  workspaceId?: string | undefined;
@@ -306,6 +419,8 @@ export function SessionRealtimeControl(props: {
306
419
  events: SessionEvent[];
307
420
  eventsReady: boolean;
308
421
  codexConnected: boolean;
422
+ /** Model-visible application context captured with each durable realtime message. */
423
+ getModelContext?: (() => string | undefined) | undefined;
309
424
  realtimeAutostartModel?: SessionRealtimeModel | undefined;
310
425
  onRealtimeAutostartConsumed?: (() => void) | undefined;
311
426
  /** Host hides dictate (and can choreograph layout) while realtime voice is live. */
@@ -317,15 +432,18 @@ export function SessionRealtimeControl(props: {
317
432
  * `none` never attaches a model menu to the bar button.
318
433
  */
319
434
  modelMenu?: "split" | "split-desktop" | "none" | undefined;
435
+ /** Explicitly expose low-level controller diagnostics; disabled by default. */
436
+ showDiagnostics?: boolean | undefined;
320
437
  /** Deterministic browser-test/demo seam. Production hosts should use the SDK default. */
321
438
  controllerFactory?: SessionRealtimeControllerFactory | undefined;
322
439
  }) {
323
440
  const lifecycle = useMemo(() => projectSessionRealtimeLifecycle(props.events), [props.events]);
441
+ const lifecycleActive = sessionRealtimeLifecycleIsActive(lifecycle);
324
442
  const selection = useRealtimeModelSelection({
325
443
  client: props.client,
326
444
  workspaceId: props.workspaceId,
327
445
  codexConnected: props.codexConnected,
328
- activeModel: lifecycle?.state === "active" ? lifecycle.model : null,
446
+ activeModel: lifecycleActive && lifecycle?.state === "active" ? lifecycle.model : null,
329
447
  });
330
448
  const selectedModel = selection.selectedModel;
331
449
  const realtime = useSessionRealtime({
@@ -349,7 +467,7 @@ export function SessionRealtimeControl(props: {
349
467
 
350
468
  useEffect(() => {
351
469
  const pending = autostartModelRef.current;
352
- if (!pending || autostartStartedRef.current || lifecycle?.state === "active") return;
470
+ if (!pending || autostartStartedRef.current || lifecycleActive) return;
353
471
  const available = models.some((model) => model.id === pending && model.available);
354
472
  if (!available) return;
355
473
  if (selectedRealtimeModel.id !== pending) {
@@ -368,7 +486,7 @@ export function SessionRealtimeControl(props: {
368
486
  });
369
487
  }, [
370
488
  canStart,
371
- lifecycle?.state,
489
+ lifecycleActive,
372
490
  models,
373
491
  props.sessionId,
374
492
  props.workspaceId,
@@ -386,6 +504,7 @@ export function SessionRealtimeControl(props: {
386
504
  modelAvailable={selectedModel.available}
387
505
  menuSide="top"
388
506
  modelMenu={props.modelMenu ?? "split"}
507
+ showDiagnostics={props.showDiagnostics}
389
508
  audioRef={realtime.audioRef}
390
509
  selectedModel={selectedModel}
391
510
  models={selection.models}
@@ -411,31 +530,52 @@ export function useRealtimeModelSelection(options: {
411
530
  workspaceId: options.workspaceId,
412
531
  });
413
532
  const activeModel = options.activeModel;
414
- const [catalog, setCatalog] = useState<RealtimeModelOption[]>(() => [
415
- {
416
- ...CODEX_LIVE_MODEL,
417
- available: options.codexConnected,
418
- unavailableReason: options.codexConnected ? null : CODEX_LIVE_MODEL.unavailableReason,
419
- },
420
- ]);
421
- const [selectedModelId, setSelectedModelId] = useState<SessionRealtimeModel>(
422
- () => readRealtimeModelPreference(workspaceId) ?? CODEX_LIVE_MODEL.id,
533
+ const initialCachedCatalog = readCachedRealtimeModelCatalog(client, workspaceId);
534
+ const [catalog, setCatalog] = useState<RealtimeModelOption[]>(
535
+ () => initialCachedCatalog ?? realtimeModelFallback(options.codexConnected),
423
536
  );
537
+ const catalogScopeRef = useRef({
538
+ client,
539
+ workspaceId,
540
+ source: initialCachedCatalog ? ("cache" as const) : ("fallback" as const),
541
+ });
542
+ const [selectedModelId, setSelectedModelId] = useState<SessionRealtimeModel>(() => {
543
+ const preferred = readRealtimeModelPreference(workspaceId) ?? CODEX_LIVE_MODEL.id;
544
+ if (!initialCachedCatalog) return preferred;
545
+ if (initialCachedCatalog.some((model) => model.id === preferred && model.available)) {
546
+ return preferred;
547
+ }
548
+ return initialCachedCatalog.find((model) => model.available)?.id ?? CODEX_LIVE_MODEL.id;
549
+ });
424
550
 
425
551
  useEffect(() => {
426
552
  let disposed = false;
427
- const load = client.getWorkspaceRealtimeModelCatalog;
428
- if (!load) return;
429
- void load
430
- .call(client, workspaceId)
431
- .then((response) => {
553
+ const applyCatalog = (models: RealtimeModelOption[]) => {
554
+ catalogScopeRef.current = { client, workspaceId, source: "cache" };
555
+ setCatalog(models);
556
+ setSelectedModelId((current) => {
557
+ if (models.some((model) => model.id === current && model.available)) return current;
558
+ return models.find((model) => model.available)?.id ?? CODEX_LIVE_MODEL.id;
559
+ });
560
+ };
561
+ const cached = readCachedRealtimeModelCatalog(client, workspaceId);
562
+ if (cached) {
563
+ const scope = catalogScopeRef.current;
564
+ if (
565
+ scope.client !== client ||
566
+ scope.workspaceId !== workspaceId ||
567
+ scope.source !== "cache"
568
+ ) {
569
+ applyCatalog(cached);
570
+ }
571
+ return;
572
+ }
573
+ const loading = loadRealtimeModelCatalog(client, workspaceId);
574
+ if (!loading) return;
575
+ void loading
576
+ .then((models) => {
432
577
  if (disposed) return;
433
- const models = response.models.map(toRealtimeModelOption);
434
- setCatalog(models);
435
- setSelectedModelId((current) => {
436
- if (models.some((model) => model.id === current && model.available)) return current;
437
- return models.find((model) => model.available)?.id ?? CODEX_LIVE_MODEL.id;
438
- });
578
+ applyCatalog(models);
439
579
  })
440
580
  .catch(() => undefined);
441
581
  return () => {
@@ -482,7 +622,7 @@ const IDLE_REALTIME_SNAPSHOT: SessionRealtimeControllerSnapshot = {
482
622
  error: null,
483
623
  };
484
624
 
485
- export function NewSessionRealtimeControl(props: {
625
+ type NewSessionRealtimeControlProps = {
486
626
  client?: RealtimeControllerClient | undefined;
487
627
  workspaceId?: string | undefined;
488
628
  codexConnected: boolean;
@@ -500,20 +640,43 @@ export function NewSessionRealtimeControl(props: {
500
640
  * `none` never attaches a model menu to the bar button.
501
641
  */
502
642
  modelMenu?: "split" | "split-desktop" | "none" | undefined;
503
- }) {
504
- const internalSelection = useRealtimeModelSelection({
643
+ };
644
+
645
+ type NewSessionRealtimeSelection = {
646
+ models: readonly RealtimeModelOption[];
647
+ selectedModel: RealtimeModelOption;
648
+ selectModel: (modelId: string) => void;
649
+ };
650
+
651
+ export function NewSessionRealtimeControl(props: NewSessionRealtimeControlProps) {
652
+ if (props.models && props.selectedModel && props.onSelectModel) {
653
+ return (
654
+ <NewSessionRealtimeControlView
655
+ {...props}
656
+ selection={{
657
+ models: props.models,
658
+ selectedModel: props.selectedModel,
659
+ selectModel: props.onSelectModel,
660
+ }}
661
+ />
662
+ );
663
+ }
664
+ return <NewSessionRealtimeControlWithInternalSelection {...props} />;
665
+ }
666
+
667
+ function NewSessionRealtimeControlWithInternalSelection(props: NewSessionRealtimeControlProps) {
668
+ const selection = useRealtimeModelSelection({
505
669
  client: props.client,
506
670
  workspaceId: props.workspaceId,
507
671
  codexConnected: props.codexConnected,
508
672
  });
509
- const selection =
510
- props.models && props.selectedModel && props.onSelectModel
511
- ? {
512
- models: props.models,
513
- selectedModel: props.selectedModel,
514
- selectModel: props.onSelectModel,
515
- }
516
- : internalSelection;
673
+ return <NewSessionRealtimeControlView {...props} selection={selection} />;
674
+ }
675
+
676
+ function NewSessionRealtimeControlView(
677
+ props: NewSessionRealtimeControlProps & { selection: NewSessionRealtimeSelection },
678
+ ) {
679
+ const selection = props.selection;
517
680
  const audioRef = useRef<HTMLAudioElement>(null);
518
681
  const [starting, setStarting] = useState(false);
519
682
  const [error, setError] = useState<string | null>(null);
@@ -582,7 +745,7 @@ export function RealtimeVoiceControl(props: {
582
745
  * `none` = start button only.
583
746
  */
584
747
  modelMenu?: "split" | "split-desktop" | "none" | undefined;
585
- showDiagnostics?: boolean;
748
+ showDiagnostics?: boolean | undefined;
586
749
  /** Extra classes on the in-bar mute cluster (e.g. `max-sm:hidden` when mutes live in “+”). */
587
750
  muteControlsClassName?: string | undefined;
588
751
  audioRef: RefObject<HTMLAudioElement | null>;
@@ -636,7 +799,7 @@ export function RealtimeVoiceControl(props: {
636
799
  : modeOwned
637
800
  ? props.onStop
638
801
  : props.onStart;
639
- const diagnosticsVisible = props.showDiagnostics ?? import.meta.env.DEV;
802
+ const diagnosticsVisible = props.showDiagnostics ?? false;
640
803
  const modelMenu = props.modelMenu ?? "split";
641
804
  const showAttachedModelMenu = modelMenu === "split" || modelMenu === "split-desktop";
642
805
  const desktopOnlyModelMenu = modelMenu === "split-desktop";
@@ -316,6 +316,7 @@ export function useEmbeddedSession(
316
316
  "streamEvents",
317
317
  "getComposerDraft",
318
318
  "saveComposerDraft",
319
+ "submitComposerDraft",
319
320
  "sendMessage",
320
321
  "steerMessage",
321
322
  "getQueue",
package/src/session-ui.ts CHANGED
@@ -24,7 +24,12 @@ export type {
24
24
  } from "./timeline/turn-summary";
25
25
  export { QueueSurface } from "./components/queue-surface";
26
26
  export type { QueueSurfaceProps } from "./components/queue-surface";
27
- export { SessionChrome, sessionChromeGoalPillState } from "./components/session-chrome";
27
+ export {
28
+ SessionChrome,
29
+ sessionChromeGoalPillExplanation,
30
+ sessionChromeGoalPillLabel,
31
+ sessionChromeGoalPillState,
32
+ } from "./components/session-chrome";
28
33
  export type {
29
34
  SessionChromeAgentsSignal,
30
35
  SessionChromeProps,
package/src/session.ts CHANGED
@@ -38,7 +38,13 @@ export {
38
38
  shouldSubmitOnKey,
39
39
  FILE_ONLY_MESSAGE_TEXT,
40
40
  } from "./hooks/use-composer";
41
- export type { ComposerSendExtras, ComposerState, UseComposerOptions } from "./hooks/use-composer";
41
+ export type {
42
+ ComposerControllerState,
43
+ ComposerPolicy,
44
+ ComposerSendExtras,
45
+ ComposerState,
46
+ UseComposerOptions,
47
+ } from "./hooks/use-composer";
42
48
  export { useFileAttachments } from "./hooks/use-file-attachments";
43
49
  export type {
44
50
  FileAttachment,
@@ -96,6 +102,8 @@ export type {
96
102
  AgentMessageItem,
97
103
  AuthNeededItem,
98
104
  GoalItem,
105
+ HumanInputAnswerSummary,
106
+ HumanInputItem,
99
107
  MachineInputBatchItem,
100
108
  MachineInputMember,
101
109
  MemoryItem,
@@ -1,10 +1,4 @@
1
- import {
2
- ArrowRightIcon,
3
- BotIcon,
4
- BrainCircuitIcon,
5
- BrainIcon,
6
- SquareTerminalIcon,
7
- } from "lucide-react";
1
+ import { ArrowRightIcon, BotIcon, BrainCircuitIcon } from "lucide-react";
8
2
  import { lazy, Suspense, useLayoutEffect, useRef } from "react";
9
3
  import { jsx as rowJsx, jsxs as rowJsxs } from "react/jsx-runtime";
10
4
  import { Markdown } from "../components/markdown";
@@ -16,9 +10,10 @@ import type { RetainedArtifactLoader, RetainedScreenshotLoader, ToolRegistry } f
16
10
  import { useSeenActivityIds } from "./seen-activity-ids";
17
11
  import { BodyNote, PayloadBlock, ActivityDisclosure, ToolCallTruncationProvider } from "./shared";
18
12
  import { toolDisplayName } from "./tool-display-name";
19
- import type { ActivityItem, MemoryItem, ReasoningItem, SandboxItem, WorkerItem } from "./types";
13
+ import type { ActivityItem, MemoryItem, WorkerItem } from "./types";
20
14
 
21
15
  const LazyFleetDecisionRow = lazy(() => import("./fleet-decision-row"));
16
+ const LazyPlatformActivityRow = lazy(() => import("./platform-activity-row"));
22
17
 
23
18
  /* ----------------------------------------------------------------------------
24
19
  Activity rail
@@ -157,7 +152,23 @@ function renderActivity(
157
152
  ) {
158
153
  switch (item.kind) {
159
154
  case "reasoning":
160
- return <ReasoningRow item={item} />;
155
+ case "sandbox":
156
+ case "startup-phase":
157
+ return (
158
+ <Suspense fallback={null}>
159
+ <LazyPlatformActivityRow
160
+ item={item}
161
+ d={ActivityDisclosure}
162
+ p={PayloadBlock}
163
+ t={toolDisplayName}
164
+ b={BotIcon}
165
+ m={Markdown}
166
+ r={truncate}
167
+ j={rowJsx}
168
+ s={rowJsxs}
169
+ />
170
+ </Suspense>
171
+ );
161
172
  case "tool-call": {
162
173
  const Renderer = toolRegistry.resolve(item);
163
174
  return (
@@ -172,8 +183,6 @@ function renderActivity(
172
183
  }
173
184
  case "worker":
174
185
  return <WorkerRow item={item} onOpenSession={onOpenSession} />;
175
- case "sandbox":
176
- return <SandboxRow item={item} />;
177
186
  case "memory":
178
187
  return <MemoryRow item={item} onMemoryClick={onMemoryClick} />;
179
188
  case "fleet-decision":
@@ -193,32 +202,6 @@ function renderActivity(
193
202
  }
194
203
  }
195
204
 
196
- function ReasoningRow({ item }: { item: ReasoningItem }) {
197
- // Reasoning recedes: a dimmer, lighter-weight title so action rows lead and
198
- // thought rows sit a half-step back in the hierarchy. Body uses the same
199
- // GFM markdown path as agent messages — Codex reasoning often carries
200
- // headings/lists/bold that were previously shown as raw plaintext.
201
- return (
202
- <ActivityDisclosure
203
- icon={<BrainIcon className="size-3.5" />}
204
- iconTone="muted"
205
- title={
206
- item.streaming ? (
207
- "Thinking"
208
- ) : (
209
- <span className="font-normal italic text-og-fg-subtle">Thought</span>
210
- )
211
- }
212
- running={item.streaming}
213
- preview={truncate(item.text, 110)}
214
- >
215
- <div className="text-og-base leading-6 text-og-fg-muted [&_strong]:text-og-fg-muted">
216
- <Markdown streaming={item.streaming}>{item.text}</Markdown>
217
- </div>
218
- </ActivityDisclosure>
219
- );
220
- }
221
-
222
205
  /**
223
206
  * Human labels for the memory kinds, translated at the SDK boundary so a raw
224
207
  * enum slug never renders as UI. Kept local to the library (the app has its own
@@ -317,44 +300,6 @@ function MemoryRow({
317
300
  );
318
301
  }
319
302
 
320
- /**
321
- * A human title for a sandbox operation row. Named platform operations (the lazy
322
- * `sandbox.provision` first-establish that now runs mid-turn) read as calm,
323
- * status-aware English instead of a raw op id — the box coming up should look
324
- * like "Starting sandbox", never an unexplained long-running command. Unnamed /
325
- * unknown ops fall back to the generic id-to-words {@link toolDisplayName}.
326
- */
327
- function sandboxRowTitle(item: SandboxItem): string {
328
- if (item.name === "sandbox.provision") {
329
- if (item.status === "failed") return "Sandbox didn’t start";
330
- if (item.status === "running") return "Starting sandbox";
331
- if (item.origin === "resumed") return "Sandbox reattached";
332
- if (item.origin === "restored") return "Sandbox restored";
333
- if (item.origin === "created") return "Sandbox created";
334
- return "Sandbox ready";
335
- }
336
- return toolDisplayName(item.name);
337
- }
338
-
339
- function SandboxRow({ item }: { item: SandboxItem }) {
340
- return (
341
- <ActivityDisclosure
342
- icon={<SquareTerminalIcon className="size-3.5" />}
343
- iconTone={
344
- item.status === "failed" ? "failed" : item.status === "running" ? "running" : "muted"
345
- }
346
- title={sandboxRowTitle(item)}
347
- running={item.status === "running"}
348
- failed={item.status === "failed"}
349
- cancelled={item.status === "cancelled"}
350
- preview={item.command ?? undefined}
351
- >
352
- {item.command ? <PayloadBlock label="Command" value={item.command} /> : null}
353
- {item.output ? <PayloadBlock label="Output" value={item.output} /> : null}
354
- </ActivityDisclosure>
355
- );
356
- }
357
-
358
303
  /** Spawned/messaged worker sessions get a first-class card, not a tool row. */
359
304
  function WorkerRow({
360
305
  item,
@@ -36,12 +36,16 @@ export type {
36
36
  FleetDecisionItem,
37
37
  FleetDecisionScoreItem,
38
38
  GoalItem,
39
+ HumanInputAnswerSummary,
40
+ HumanInputItem,
39
41
  MachineInputBatchItem,
40
42
  MachineInputMember,
41
43
  MemoryItem,
42
44
  NoticeItem,
43
45
  ReasoningItem,
44
46
  SandboxItem,
47
+ StartupPhase,
48
+ StartupPhaseItem,
45
49
  SessionStatusItem,
46
50
  TimelineGroup,
47
51
  TimelineItem,