@opengeni/react 3.3.2 → 3.7.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 (79) hide show
  1. package/dist/artifacts.d.ts +1 -1
  2. package/dist/artifacts.js +241 -18
  3. package/dist/artifacts.js.map +1 -1
  4. package/dist/{browser-viewer-MKKFRRRV.js → browser-viewer-E5DDEFQ4.js} +3 -3
  5. package/dist/{chunk-2DZVYHVO.js → chunk-4JFUT22W.js} +39 -13
  6. package/dist/chunk-4JFUT22W.js.map +1 -0
  7. package/dist/{chunk-ELIYWBZR.js → chunk-BV5A4OTE.js} +4 -4
  8. package/dist/{chunk-3AGBGTRT.js → chunk-C6WDLHBA.js} +148 -26
  9. package/dist/chunk-C6WDLHBA.js.map +1 -0
  10. package/dist/{chunk-YG4QEQF5.js → chunk-DOK4KZF2.js} +7863 -7736
  11. package/dist/chunk-DOK4KZF2.js.map +1 -0
  12. package/dist/{chunk-JAQ5K7W6.js → chunk-FJBOU2TH.js} +167 -58
  13. package/dist/chunk-FJBOU2TH.js.map +1 -0
  14. package/dist/{chunk-XYRDDSXF.js → chunk-R55HFTT3.js} +80 -34
  15. package/dist/chunk-R55HFTT3.js.map +1 -0
  16. package/dist/{chunk-ZPBTI3TE.js → chunk-W2RGYBCV.js} +4 -4
  17. package/dist/{chunk-WSK7G5LE.js → chunk-W3OUAH6M.js} +80 -80
  18. package/dist/chunk-W3OUAH6M.js.map +1 -0
  19. package/dist/{chunk-X4A66EWZ.js → chunk-WMGF4Z7X.js} +41 -43
  20. package/dist/chunk-WMGF4Z7X.js.map +1 -0
  21. package/dist/client.d.ts +1 -1
  22. package/dist/components/artifacts/index.d.ts +1 -1
  23. package/dist/components/artifacts/published-html-artifact-frame.d.ts +45 -3
  24. package/dist/components/composer.d.ts +2 -0
  25. package/dist/components/session-chrome.d.ts +1 -1
  26. package/dist/composer.js +4 -4
  27. package/dist/{computer-viewer-FWEZISW4.js → computer-viewer-7TEZC6Y2.js} +3 -3
  28. package/dist/{fleet-decision-row-GGCAT4E4.js → fleet-decision-row-YZ3ZKWMM.js} +2 -1
  29. package/dist/fleet-decision-row-YZ3ZKWMM.js.map +1 -0
  30. package/dist/hooks/use-file-attachments.d.ts +8 -0
  31. package/dist/hooks/use-sandbox-files.d.ts +4 -1
  32. package/dist/hooks/use-session-events.d.ts +1 -1
  33. package/dist/hooks/use-workspace-edit.d.ts +4 -0
  34. package/dist/index.js +195 -141
  35. package/dist/index.js.map +1 -1
  36. package/dist/interaction.js +3 -3
  37. package/dist/model-policy.d.ts +2 -1
  38. package/dist/model-policy.js +1 -1
  39. package/dist/{queue-surface-implementation-BSKUWHRI.js → queue-surface-implementation-XL2A6PBM.js} +2 -2
  40. package/dist/realtime.js +2 -2
  41. package/dist/session-ui.js +6 -6
  42. package/dist/session.js +4 -4
  43. package/dist/timeline/registry.d.ts +2 -0
  44. package/dist/timeline/types.d.ts +3 -1
  45. package/package.json +2 -2
  46. package/src/artifacts.ts +3 -0
  47. package/src/client.ts +2 -1
  48. package/src/components/artifacts/index.ts +3 -0
  49. package/src/components/artifacts/published-html-artifact-frame.tsx +284 -4
  50. package/src/components/composer.tsx +103 -29
  51. package/src/components/message-timeline.tsx +48 -10
  52. package/src/components/model-policy-picker.tsx +37 -10
  53. package/src/components/sandbox-workspace.tsx +18 -7
  54. package/src/components/session-chrome.tsx +3 -3
  55. package/src/hooks/use-file-attachments.ts +62 -2
  56. package/src/hooks/use-sandbox-files.ts +68 -20
  57. package/src/hooks/use-session-events.ts +176 -21
  58. package/src/hooks/use-workspace-edit.ts +23 -5
  59. package/src/model-policy.ts +48 -53
  60. package/src/timeline/fleet-decision-projection.ts +2 -1
  61. package/src/timeline/fleet-decision-row.tsx +1 -0
  62. package/src/timeline/projection.ts +157 -8
  63. package/src/timeline/registry.ts +15 -3
  64. package/src/timeline/tool-renderers.tsx +117 -1
  65. package/src/timeline/types.ts +10 -1
  66. package/styles/compiled.css +5 -0
  67. package/dist/chunk-2DZVYHVO.js.map +0 -1
  68. package/dist/chunk-3AGBGTRT.js.map +0 -1
  69. package/dist/chunk-JAQ5K7W6.js.map +0 -1
  70. package/dist/chunk-WSK7G5LE.js.map +0 -1
  71. package/dist/chunk-X4A66EWZ.js.map +0 -1
  72. package/dist/chunk-XYRDDSXF.js.map +0 -1
  73. package/dist/chunk-YG4QEQF5.js.map +0 -1
  74. package/dist/fleet-decision-row-GGCAT4E4.js.map +0 -1
  75. /package/dist/{browser-viewer-MKKFRRRV.js.map → browser-viewer-E5DDEFQ4.js.map} +0 -0
  76. /package/dist/{chunk-ELIYWBZR.js.map → chunk-BV5A4OTE.js.map} +0 -0
  77. /package/dist/{chunk-ZPBTI3TE.js.map → chunk-W2RGYBCV.js.map} +0 -0
  78. /package/dist/{computer-viewer-FWEZISW4.js.map → computer-viewer-7TEZC6Y2.js.map} +0 -0
  79. /package/dist/{queue-surface-implementation-BSKUWHRI.js.map → queue-surface-implementation-XL2A6PBM.js.map} +0 -0
@@ -137,6 +137,7 @@ export type ChatComposerMessages = {
137
137
  uploading: string;
138
138
  uploadFailed: string;
139
139
  previewAttachment: (name: string) => string;
140
+ previewUnavailable?: string | undefined;
140
141
  attachmentPreviewLabel: string;
141
142
  downloadAttachment: (name: string) => string;
142
143
  closeAttachmentPreview: string;
@@ -204,6 +205,7 @@ export const defaultChatComposerMessages: ChatComposerMessages = {
204
205
  uploading: "Uploading",
205
206
  uploadFailed: "Upload failed",
206
207
  previewAttachment: (name) => `Preview ${name}`,
208
+ previewUnavailable: "Preview unavailable",
207
209
  attachmentPreviewLabel: "Attachment preview",
208
210
  downloadAttachment: (name) => `Download ${name}`,
209
211
  closeAttachmentPreview: "Close",
@@ -947,6 +949,7 @@ export function Attachments() {
947
949
  onRemove={controller.attachments.remove}
948
950
  onRetry={controller.attachments.retry}
949
951
  onRetainPreview={controller.attachments.retainPreview}
952
+ onLoadPreview={controller.attachments.loadPreview}
950
953
  />
951
954
  );
952
955
  }
@@ -1638,20 +1641,83 @@ function AttachmentChips({
1638
1641
  onRemove,
1639
1642
  onRetry,
1640
1643
  onRetainPreview,
1644
+ onLoadPreview,
1641
1645
  }: {
1642
1646
  attachments: UseFileAttachmentsResult["attachments"];
1643
1647
  messages: ChatComposerMessages;
1644
1648
  onRemove: (id: string) => void;
1645
1649
  onRetry?: ((id: string) => void) | undefined;
1646
1650
  onRetainPreview: UseFileAttachmentsResult["retainPreview"];
1651
+ onLoadPreview: UseFileAttachmentsResult["loadPreview"];
1647
1652
  }) {
1648
1653
  const lightbox = useLightboxOptional();
1654
+ const previewRequest = useRef<AbortController | null>(null);
1655
+ const [previewLoadingId, setPreviewLoadingId] = useState<string | null>(null);
1656
+ const [previewErrorId, setPreviewErrorId] = useState<string | null>(null);
1657
+
1658
+ useEffect(() => () => previewRequest.current?.abort(), [onLoadPreview]);
1659
+
1660
+ async function openPreview(
1661
+ attachment: UseFileAttachmentsResult["attachments"][number],
1662
+ source: HTMLButtonElement,
1663
+ canLoadPreview: boolean,
1664
+ ) {
1665
+ previewRequest.current?.abort();
1666
+ const attachmentId = attachment.id;
1667
+ const releaseSource = onRetainPreview(attachmentId);
1668
+ let src = attachment.previewUrl;
1669
+ if (!releaseSource && canLoadPreview) {
1670
+ const controller = new AbortController();
1671
+ previewRequest.current = controller;
1672
+ setPreviewLoadingId(attachmentId);
1673
+ setPreviewErrorId(null);
1674
+ try {
1675
+ src = await onLoadPreview!(attachmentId, controller.signal);
1676
+ if (controller.signal.aborted) return;
1677
+ if (!src) {
1678
+ setPreviewErrorId(attachmentId);
1679
+ return;
1680
+ }
1681
+ } catch {
1682
+ if (!controller.signal.aborted) setPreviewErrorId(attachmentId);
1683
+ return;
1684
+ } finally {
1685
+ if (previewRequest.current === controller) {
1686
+ setPreviewLoadingId(null);
1687
+ }
1688
+ }
1689
+ }
1690
+ lightbox!.open(
1691
+ src!,
1692
+ attachment.name,
1693
+ source,
1694
+ messages.attachmentPreviewLabel,
1695
+ attachment.name,
1696
+ {
1697
+ download: messages.downloadAttachment(attachment.name),
1698
+ close: messages.closeAttachmentPreview,
1699
+ },
1700
+ releaseSource,
1701
+ );
1702
+ }
1703
+
1649
1704
  return (
1650
1705
  <div className="flex flex-wrap gap-2 px-3 py-2">
1651
1706
  {attachments.map((attachment) => {
1652
1707
  const failed = attachment.status === "failed";
1653
- const statusText =
1654
- attachment.status === "uploading"
1708
+ const secureContextRequired = attachment.errorCode === "secure_context_required";
1709
+ const previewFailed = previewErrorId === attachment.id;
1710
+ const previewLoading = previewLoadingId === attachment.id;
1711
+ const canLoadPreview = Boolean(
1712
+ lightbox &&
1713
+ onLoadPreview &&
1714
+ attachment.status === "ready" &&
1715
+ attachment.file &&
1716
+ attachment.contentType.startsWith("image/"),
1717
+ );
1718
+ const statusText = previewFailed
1719
+ ? (messages.previewUnavailable ?? "Preview unavailable")
1720
+ : attachment.status === "uploading"
1655
1721
  ? messages.uploading
1656
1722
  : failed
1657
1723
  ? attachment.error || messages.uploadFailed
@@ -1660,38 +1726,42 @@ function AttachmentChips({
1660
1726
  <div
1661
1727
  key={attachment.id}
1662
1728
  className={cn(
1663
- "flex min-w-0 max-w-[240px] items-center gap-2 rounded-og-md border px-2 py-1.5 text-og-sm",
1729
+ "flex min-w-0 items-center gap-2 rounded-og-md border px-2 py-1.5 text-og-sm",
1664
1730
  failed
1665
- ? "border-og-status-failed/40 bg-og-status-failed/10"
1666
- : "border-og-border bg-og-surface-2",
1731
+ ? cn(
1732
+ "border-og-status-failed/40 bg-og-status-failed/10",
1733
+ secureContextRequired
1734
+ ? "max-w-full items-start sm:max-w-[32rem]"
1735
+ : "max-w-[240px]",
1736
+ )
1737
+ : "max-w-[240px] border-og-border bg-og-surface-2",
1667
1738
  )}
1668
1739
  >
1669
- {attachment.previewUrl && lightbox ? (
1740
+ {lightbox && (attachment.previewUrl || canLoadPreview) ? (
1670
1741
  <button
1671
1742
  type="button"
1672
- className="size-8 shrink-0 overflow-hidden rounded outline-hidden focus-visible:ring-2 focus-visible:ring-og-accent"
1743
+ className={cn(
1744
+ "size-8 shrink-0 overflow-hidden rounded outline-hidden focus-visible:ring-2 focus-visible:ring-og-accent",
1745
+ !attachment.previewUrl && "flex items-center justify-center disabled:cursor-wait",
1746
+ )}
1673
1747
  aria-label={messages.previewAttachment(attachment.name)}
1674
- onClick={(event) => {
1675
- const releasePreview = onRetainPreview(attachment.id);
1676
- lightbox.open(
1677
- attachment.previewUrl!,
1678
- attachment.name,
1679
- event.currentTarget,
1680
- messages.attachmentPreviewLabel,
1681
- attachment.name,
1682
- {
1683
- download: messages.downloadAttachment(attachment.name),
1684
- close: messages.closeAttachmentPreview,
1685
- },
1686
- releasePreview,
1687
- );
1688
- }}
1748
+ aria-busy={previewLoading || undefined}
1749
+ disabled={previewLoading}
1750
+ onClick={(event) =>
1751
+ void openPreview(attachment, event.currentTarget, canLoadPreview)
1752
+ }
1689
1753
  >
1690
- <img
1691
- src={attachment.previewUrl}
1692
- alt=""
1693
- className="h-full w-full object-cover transition-opacity hover:opacity-80"
1694
- />
1754
+ {attachment.previewUrl ? (
1755
+ <img
1756
+ src={attachment.previewUrl}
1757
+ alt=""
1758
+ className="h-full w-full object-cover transition-opacity hover:opacity-80"
1759
+ />
1760
+ ) : previewLoading ? (
1761
+ <LoaderCircleIcon className="size-4 animate-og-spin text-og-fg-muted" />
1762
+ ) : (
1763
+ <ImageIcon className="size-4 text-og-fg-muted" />
1764
+ )}
1695
1765
  </button>
1696
1766
  ) : attachment.previewUrl ? (
1697
1767
  <img
@@ -1706,7 +1776,11 @@ function AttachmentChips({
1706
1776
  )}
1707
1777
  <div className="min-w-0 flex-1">
1708
1778
  <div className="truncate font-medium text-og-fg">{attachment.name}</div>
1709
- {failed ? (
1779
+ {secureContextRequired ? (
1780
+ <div className="break-words text-og-xs leading-4 text-og-status-failed">
1781
+ {statusText}
1782
+ </div>
1783
+ ) : failed || previewFailed ? (
1710
1784
  <ComposerTip tip={statusText}>
1711
1785
  <div className="truncate text-og-xs text-og-status-failed">{statusText}</div>
1712
1786
  </ComposerTip>
@@ -1717,7 +1791,7 @@ function AttachmentChips({
1717
1791
  {attachment.status === "uploading" ? (
1718
1792
  <LoaderCircleIcon className="size-3.5 shrink-0 animate-og-spin" />
1719
1793
  ) : null}
1720
- {failed && onRetry ? (
1794
+ {failed && !secureContextRequired && onRetry ? (
1721
1795
  <ComposerTip tip={messages.retryUpload}>
1722
1796
  <button
1723
1797
  type="button"
@@ -1183,17 +1183,23 @@ export function MessageTimeline({
1183
1183
  applyPinned(false);
1184
1184
  }
1185
1185
  } else if (prepended) {
1186
+ const olderAttemptState = olderLoadAttemptRef.current?.[1];
1187
+ const underfillOwned =
1188
+ !!olderLoadAttemptRef.current &&
1189
+ olderAttemptState !== 1 &&
1190
+ olderAttemptState !== 2 &&
1191
+ olderAttemptState !== 3;
1186
1192
  if (
1187
1193
  autoFollow &&
1188
1194
  pinnedRef.current &&
1189
1195
  !hasNewer &&
1190
1196
  !pendingReaderLeaveRef.current &&
1191
- (wasAtLiveTailBeforeCommit || olderLoadAttemptRef.current)
1197
+ (wasAtLiveTailBeforeCommit || underfillOwned)
1192
1198
  ) {
1193
- // A pending short-window load owns the prepend even when live growth
1194
- // has left its still-pinned camera with raw geometry debt. Unrelated
1195
- // prepends retain the prior-tip fallback so a stale pin after an
1196
- // extension/programmatic history jump still restores its row anchor.
1199
+ // Still following the live tip: underfill, or a prefetch the reader
1200
+ // started and then returned from. Park at the new tip. A stale pin
1201
+ // while they are actually up in a short window (gap is not inside
1202
+ // PIN_THRESHOLD of maxScroll, so wasAtLiveTail is false) restores.
1197
1203
  clearPendingReaderLeave();
1198
1204
  snapToBottom(node);
1199
1205
  } else {
@@ -1598,6 +1604,19 @@ export function MessageTimeline({
1598
1604
  return;
1599
1605
  }
1600
1606
 
1607
+ if (programmatic && !pinnedRef.current) {
1608
+ // Restore/camera writes while reading history are not a return to the tip.
1609
+ // Still expire a pending Jump-to-latest latch: the in-window jump itself
1610
+ // is a programmatic snap, and a later reader scroll-away can arrive
1611
+ // before that echo is consumed. Skipping this left the latch armed and
1612
+ // snapped the reader when hasNewer later flipped false.
1613
+ syncScrollBaseline(node);
1614
+ if (wantPinRef.current && !isNearBottom(node)) {
1615
+ wantPinRef.current = false;
1616
+ }
1617
+ return;
1618
+ }
1619
+
1601
1620
  if (autoFollow && pinnedRef.current && !hasNewer) {
1602
1621
  // Fold / composer / SessionChrome: viewport shrink raises maxScroll without
1603
1622
  // growing content. Must hit tipFollow before we adopt the new clientHeight
@@ -1659,10 +1678,15 @@ export function MessageTimeline({
1659
1678
  syncScrollBaseline(node);
1660
1679
  const nearBottom = isNearBottom(node);
1661
1680
 
1662
- // Re-pin only when the reader moved toward/at the tip not when a fold
1663
- // clamp dragged scrollTop down onto nearBottom.
1681
+ // Re-pin only when the reader moved toward/at the tip without a content
1682
+ // insertion. Prepend restore and overflow-anchor raise scrollTop by
1683
+ // roughly the same amount as maxScroll; treating that as a scroll-down
1684
+ // re-pinned a compact-tail history reader (their preserved gap falls
1685
+ // inside PIN_THRESHOLD once the window is tall) and snapped them back.
1686
+ const inserted = Math.max(0, nextMaxScroll - previousMaxScroll);
1687
+ const towardTip = nextTop - previousTop - inserted;
1664
1688
  const nextPinned =
1665
- !hasNewer && nearBottom && nextTop >= previousTop - TIP_FOLLOW_READER_UP_EPS_PX;
1689
+ !hasNewer && nearBottom && towardTip >= -TIP_FOLLOW_READER_UP_EPS_PX && inserted <= 1;
1666
1690
  if (!nextPinned) {
1667
1691
  stopFollow();
1668
1692
  }
@@ -3581,6 +3605,7 @@ function AuthNeededRow({
3581
3605
  ? `${provider} tools unavailable`
3582
3606
  : `${actionLabel} ${provider}`;
3583
3607
  const reasonLine = recommendation?.rationale ?? authReasonLine(item.reason);
3608
+ const hostAuthorizationUrl = item.authoritySource === "host" ? item.authorizationUrl : null;
3584
3609
 
3585
3610
  const start = async () => {
3586
3611
  if (!onReconnect || busy) {
@@ -3623,7 +3648,20 @@ function AuthNeededRow({
3623
3648
  ) : null}
3624
3649
  </div>
3625
3650
  </div>
3626
- {!unavailable && onReconnect ? (
3651
+ {hostAuthorizationUrl ? (
3652
+ <a
3653
+ href={hostAuthorizationUrl}
3654
+ rel="noreferrer"
3655
+ target="_blank"
3656
+ className={cn(
3657
+ PRIMARY_ACTION_CLASS,
3658
+ "transition-colors hover:bg-og-accent-strong pointer-coarse:min-h-9",
3659
+ )}
3660
+ >
3661
+ <RefreshCwIcon className="size-3.5" aria-hidden />
3662
+ {actionLabel}
3663
+ </a>
3664
+ ) : item.authoritySource !== "host" && !unavailable && onReconnect ? (
3627
3665
  <button
3628
3666
  type="button"
3629
3667
  onClick={() => void start()}
@@ -3636,7 +3674,7 @@ function AuthNeededRow({
3636
3674
  <RefreshCwIcon className={cn("size-3.5", busy && "animate-og-spin")} aria-hidden />
3637
3675
  {busy ? "Opening…" : actionLabel}
3638
3676
  </button>
3639
- ) : !unavailable && item.authorizationUrl ? (
3677
+ ) : item.authoritySource !== "host" && !unavailable && item.authorizationUrl ? (
3640
3678
  <a
3641
3679
  href={item.authorizationUrl}
3642
3680
  rel="noreferrer"
@@ -26,6 +26,7 @@ import {
26
26
  findPickerRow,
27
27
  groupPickerRowsByBillingClass,
28
28
  labelReasoningEffort,
29
+ payerSummaryForModel,
29
30
  projectClientModelRows,
30
31
  runnableLatencyModesForModel,
31
32
  type PickerBillingClass,
@@ -68,7 +69,8 @@ export const defaultModelPolicyPickerMessages: ModelPolicyPickerMessages = {
68
69
  external: "Provider terms and limits apply",
69
70
  codex_subscription: "ChatGPT / Codex plan",
70
71
  supergrok_subscription: "SuperGrok / xAI plan",
71
- byok: "Billed to your AI Gateway",
72
+ byok: "Billed to the workspace provider account",
73
+ organization_byok: "Billed to the organization provider account",
72
74
  },
73
75
  };
74
76
 
@@ -158,7 +160,8 @@ export function BillingClassMark(props: {
158
160
  external: "External provider",
159
161
  codex_subscription: "Codex",
160
162
  supergrok_subscription: "SuperGrok",
161
- byok: "Bring your own key",
163
+ byok: "Workspace provider account",
164
+ organization_byok: "Organization provider account",
162
165
  };
163
166
  const label = props["aria-label"] ?? labels[props.billingClass];
164
167
  const accessibility =
@@ -195,6 +198,18 @@ function isCodexModel(model: ClientModel): boolean {
195
198
  return model.id.startsWith("codex/") || model.source === "codex";
196
199
  }
197
200
 
201
+ function billingClassForMissingSelection(modelId: string): PickerBillingClass {
202
+ if (modelId.startsWith("workspace-gateway/")) return "byok";
203
+ if (modelId.startsWith("workspace-openrouter/")) return "byok";
204
+ // A deployment OpenRouter ID does not encode its workspace-facing cost.
205
+ // Missing rows therefore use the credits-safe rail instead of falsely
206
+ // claiming that an unknown former selection was externally funded.
207
+ if (modelId.startsWith("openrouter/")) return "opengeni_credits";
208
+ if (modelId.startsWith("codex/")) return "codex_subscription";
209
+ if (modelId.startsWith("supergrok/")) return "supergrok_subscription";
210
+ return "opengeni_credits";
211
+ }
212
+
198
213
  function applyCodexOnly(
199
214
  rows: ClientPickerModelRow[],
200
215
  codexOnly: boolean,
@@ -227,7 +242,11 @@ function defaultNavState(rows: ClientPickerModelRow[], modelId: string): PickerN
227
242
  ? { level: "models", rail: onlyRail, modelId: null }
228
243
  : { level: "providers", rail: null, modelId: null };
229
244
  }
230
- return { level: "thinking", rail: selected.billingClass, modelId: selected.id };
245
+ return {
246
+ level: "thinking",
247
+ rail: selected.billingClass,
248
+ modelId: selected.id,
249
+ };
231
250
  }
232
251
 
233
252
  function usePickerNavState(
@@ -352,7 +371,11 @@ export function ModelPolicyPickerMenu(
352
371
 
353
372
  const effectiveNav =
354
373
  nav.level === "providers" && groups.length === 1
355
- ? { level: "models" as const, rail: groups[0]!.billingClass, modelId: null }
374
+ ? {
375
+ level: "models" as const,
376
+ rail: groups[0]!.billingClass,
377
+ modelId: null,
378
+ }
356
379
  : nav;
357
380
  const activeGroup =
358
381
  effectiveNav.rail === null
@@ -416,14 +439,21 @@ export function ModelPolicyPickerMenu(
416
439
  <PickerNavRow
417
440
  key={`${row.billingClass}:${row.id}`}
418
441
  label={row.label}
419
- hint={row.unavailableReason ?? undefined}
442
+ hint={row.unavailableReason ?? payerSummaryForModel(row.catalog)}
420
443
  disabled={!row.selectable}
421
444
  title={row.unavailableReason ?? undefined}
422
445
  active={row.id === props.model}
423
446
  testId={`model-picker-choice-${row.id}`}
424
447
  onClick={() => {
425
448
  if (row.selectable) {
426
- go({ level: "thinking", rail: row.billingClass, modelId: row.id }, 1);
449
+ go(
450
+ {
451
+ level: "thinking",
452
+ rail: row.billingClass,
453
+ modelId: row.id,
454
+ },
455
+ 1,
456
+ );
427
457
  }
428
458
  }}
429
459
  />
@@ -605,10 +635,7 @@ export function ModelPolicyPicker(props: ModelPolicyPickerProps) {
605
635
  )}
606
636
  >
607
637
  <BillingClassMark
608
- billingClass={
609
- selected?.billingClass ??
610
- (props.model.startsWith("codex/") ? "codex_subscription" : "opengeni_credits")
611
- }
638
+ billingClass={selected?.billingClass ?? billingClassForMissingSelection(props.model)}
612
639
  className="text-og-fg"
613
640
  />
614
641
  <span className="og-model-policy-label-full min-w-0 truncate font-medium text-og-fg max-sm:hidden">
@@ -102,6 +102,12 @@ function isWorkbenchSurface(value: string): value is SandboxWorkspaceSurface {
102
102
  return (WORKBENCH_SURFACES as readonly string[]).includes(value);
103
103
  }
104
104
 
105
+ function isCanonicalAbsoluteSandboxPath(value: string | null | undefined): boolean {
106
+ if (!value) return false;
107
+ const portable = value.replaceAll("\\", "/");
108
+ return portable.startsWith("/") || /^[A-Za-z]:\//u.test(portable);
109
+ }
110
+
105
111
  function sourceDrivenDefaultTab(
106
112
  hasChanges: boolean,
107
113
  changesEnabled: boolean,
@@ -424,6 +430,11 @@ export function useSandboxWorkspaceTabs(
424
430
  const warmIntents =
425
431
  storedWarmIntents.sessionId === sessionId ? storedWarmIntents : emptyWarmIntents(sessionId);
426
432
  const { warmTerminal, warmFiles } = warmIntents;
433
+ // A host-requested file is itself explicit live-file intent. Derive the wake
434
+ // directly from the request instead of relying on a follow-up effect to copy
435
+ // it into local state; otherwise a missing or delayed effect leaves the file
436
+ // waiting for a sandbox that this hook never asks to start.
437
+ const filesWarmRequested = warmFiles || Boolean(requestedFilePath);
427
438
  const requestWarmIntent = useCallback(
428
439
  (intent: Exclude<keyof SessionWarmIntents, "sessionId">) => {
429
440
  setStoredWarmIntents((previous) => {
@@ -433,10 +444,6 @@ export function useSandboxWorkspaceTabs(
433
444
  },
434
445
  [sessionId],
435
446
  );
436
- useEffect(() => {
437
- if (requestedFilePath) requestWarmIntent("warmFiles");
438
- }, [requestWarmIntent, requestedFilePath, requestedFileRequestId]);
439
-
440
447
  // The session's machine fleet + the active-sandbox pointer. Drives the header
441
448
  // chip (which machine + its connection state). Shares the session list poll.
442
449
  const machines = useMachines({
@@ -457,7 +464,7 @@ export function useSandboxWorkspaceTabs(
457
464
  attachTerminal: terminalEnabled && warmTerminal,
458
465
  // Explicit live-file intent only — NOT "the Files tab is open". A cold edit
459
466
  // or guarded-file open wakes the box; a glance at the tree/diff does not.
460
- attachFiles: workspaceDataEnabled && warmFiles,
467
+ attachFiles: workspaceDataEnabled && filesWarmRequested,
461
468
  });
462
469
  const capabilities = caps.capabilities;
463
470
  const liveness = capabilities?.liveness;
@@ -467,6 +474,9 @@ export function useSandboxWorkspaceTabs(
467
474
  // is still pending.
468
475
  const liveIoLiveness = liveness ?? null;
469
476
  const fileSystemOn = capabilities?.FileSystem.available ?? false;
477
+ const fileSystemRoute = capabilities
478
+ ? { epoch: capabilities.leaseEpoch, root: capabilities.FileSystem.root }
479
+ : undefined;
470
480
  // The FS is writable only when it's live AND not read-only. A self-hosted box
471
481
  // that's offline (or any read-only advertisement) or a capture-served cold tree
472
482
  // must not offer create/rename/delete/edit affordances — you cannot mutate a
@@ -574,9 +584,10 @@ export function useSandboxWorkspaceTabs(
574
584
  active: filesActive && !turnInFlight,
575
585
  // A deliberate canonical absolute-path open browses in the selected target's
576
586
  // advertised namespace, so the authoritative tree and link share exact paths.
577
- ...(requestedFilePath?.startsWith("/") && capabilities?.FileSystem.root
587
+ ...(isCanonicalAbsoluteSandboxPath(requestedFilePath) && capabilities?.FileSystem.root
578
588
  ? { rootPath: capabilities.FileSystem.root }
579
589
  : {}),
590
+ ...(fileSystemRoute ? { route: fileSystemRoute } : {}),
580
591
  repoPaths,
581
592
  liveness: liveIoLiveness,
582
593
  capture: captureState.capture,
@@ -665,7 +676,7 @@ export function useSandboxWorkspaceTabs(
665
676
  capabilitiesState: caps.state,
666
677
  activeMachineState: activeMachine?.state ?? null,
667
678
  activeIsSelfhosted: activeMachine?.kind === "selfhosted",
668
- wantsWarm: (terminalEnabled && warmTerminal) || (workspaceDataEnabled && warmFiles),
679
+ wantsWarm: (terminalEnabled && warmTerminal) || (workspaceDataEnabled && filesWarmRequested),
669
680
  capturedAt: captureState.capturedAt,
670
681
  });
671
682
  const workspaceWaking = chip.state === "waking";
@@ -159,7 +159,7 @@ export function sessionChromeGoalPillLabel(
159
159
 
160
160
  /**
161
161
  * One human sentence explaining WHY the goal is not pursuing right now: the
162
- * pause reason, the agent's own `goal_wait` hold (reason + deadline), or the
162
+ * pause reason, the agent's own `wait_for_input` hold (reason + deadline), or the
163
163
  * next idle-backoff check time. Null when the state needs no explanation.
164
164
  */
165
165
  export function sessionChromeGoalPillExplanation(
@@ -182,7 +182,7 @@ export function sessionChromeGoalPillExplanation(
182
182
  const until = continuation.nextAttemptAt
183
183
  ? ` until ${formatClockTime(continuation.nextAttemptAt)}`
184
184
  : "";
185
- return `Waiting for input${reason ? `: ${reason}` : ""}${until}. A child result, an agent message, or your prompt wakes it sooner.`;
185
+ return `Waiting for input${reason ? `: ${reason}` : ""}${until}. Relevant session input—including a child result, background-command result, agent message, schedule, or your promptwakes it sooner.`;
186
186
  }
187
187
  return null;
188
188
  }
@@ -228,7 +228,7 @@ export function sessionChromeGoalPillState(
228
228
  // next evaluation at `nextAttemptAt`) is an ordinary scheduled state.
229
229
  if (continuation.state === "scheduled") return "scheduled";
230
230
  if (continuation.state === "blocked") {
231
- // `held_for_input` is the agent's own goal_wait hold (waiting for child
231
+ // `held_for_input` is the agent's own wait_for_input hold (waiting for child
232
232
  // results / external input until a deadline); it shares the Held pill.
233
233
  return continuation.reason === "workstream_paused" || continuation.reason === "held_for_input"
234
234
  ? "held"
@@ -26,6 +26,8 @@ export type FileAttachment = {
26
26
  file?: FileAsset | undefined;
27
27
  /** Object-URL for an inline preview; minted for `image/*` files only. */
28
28
  previewUrl?: string | undefined;
29
+ /** Stable SDK failure code for UI behavior that must not parse error copy. */
30
+ errorCode?: "secure_context_required" | undefined;
29
31
  error?: string | undefined;
30
32
  };
31
33
 
@@ -50,6 +52,12 @@ export type UseFileAttachmentsResult = {
50
52
  addFromPaste: (event: { clipboardData: DataTransfer | null }) => void;
51
53
  /** Restore already-ready server assets without recreating browser-local bytes. */
52
54
  restoreReadyFiles: (files: Iterable<FileAsset>) => void;
55
+ /**
56
+ * Resolve a ready image's short-lived server preview URL on demand. Optional
57
+ * for upload-only embedded clients; local object-URL previews remain usable
58
+ * without it.
59
+ */
60
+ loadPreview?: ((id: string, signal?: AbortSignal) => Promise<string | undefined>) | undefined;
53
61
  /**
54
62
  * Re-run the upload for a `failed` attachment, in place (same id, same
55
63
  * source file). No-op for an id that isn't a known failed upload.
@@ -76,6 +84,27 @@ export type UseFileAttachmentsResult = {
76
84
 
77
85
  const isImage = (file: File): boolean => file.type.startsWith("image/");
78
86
 
87
+ let fallbackAttachmentId = 0;
88
+
89
+ function createAttachmentId(): string {
90
+ const cryptoSource = globalThis.crypto;
91
+ if (typeof cryptoSource?.randomUUID === "function") return cryptoSource.randomUUID();
92
+ fallbackAttachmentId += 1;
93
+ // This id is only a browser-local React key and retry lookup, never durable
94
+ // authority. Keep attachment tracking usable when HTTP withholds randomUUID
95
+ // or Web Crypto is unavailable so the SDK's typed failure reaches the card.
96
+ return `attachment:${Date.now().toString(36)}:${fallbackAttachmentId.toString(36)}`;
97
+ }
98
+
99
+ function secureContextRequiredErrorCode(error: unknown): "secure_context_required" | undefined {
100
+ return typeof error === "object" &&
101
+ error !== null &&
102
+ "code" in error &&
103
+ error.code === "secure_context_required"
104
+ ? error.code
105
+ : undefined;
106
+ }
107
+
79
108
  /**
80
109
  * Upload-and-track state for files attached to the next message. Owns the
81
110
  * full client-side upload layer: a per-file `uploading | ready | failed`
@@ -91,6 +120,8 @@ export function useFileAttachments(
91
120
  const { client, workspaceId } = useEmbeddedFileAttachments(options);
92
121
  const pasteFilter = options.pasteFilter ?? isImage;
93
122
  const [attachments, setAttachments] = useState<FileAttachment[]>([]);
123
+ const attachmentsRef = useRef(attachments);
124
+ attachmentsRef.current = attachments;
94
125
  // Keep the source File per attachment id so a failed upload can be retried
95
126
  // in place. Cleared on remove/clear so it never outlives its attachment.
96
127
  const sources = useRef<Map<string, File>>(new Map());
@@ -198,6 +229,7 @@ export function useFileAttachments(
198
229
  name: asset.filename,
199
230
  contentType: asset.contentType,
200
231
  sizeBytes: asset.sizeBytes,
232
+ errorCode: undefined,
201
233
  error: undefined,
202
234
  }
203
235
  : attachment,
@@ -212,6 +244,7 @@ export function useFileAttachments(
212
244
  ? {
213
245
  ...attachment,
214
246
  status: "failed",
247
+ errorCode: secureContextRequiredErrorCode(error),
215
248
  error: error instanceof Error ? error.message : String(error),
216
249
  }
217
250
  : attachment,
@@ -225,7 +258,7 @@ export function useFileAttachments(
225
258
  const addFiles = useCallback(
226
259
  (files: Iterable<File>) => {
227
260
  for (const file of files) {
228
- const id = crypto.randomUUID();
261
+ const id = createAttachmentId();
229
262
  sources.current.set(id, file);
230
263
  const previewUrl = isImage(file) ? URL.createObjectURL(file) : undefined;
231
264
  if (previewUrl) previewUrls.current.set(id, previewUrl);
@@ -255,7 +288,7 @@ export function useFileAttachments(
255
288
  setAttachments((current) =>
256
289
  current.map((attachment) =>
257
290
  attachment.id === id
258
- ? { ...attachment, status: "uploading", error: undefined }
291
+ ? { ...attachment, status: "uploading", errorCode: undefined, error: undefined }
259
292
  : attachment,
260
293
  ),
261
294
  );
@@ -305,6 +338,7 @@ export function useFileAttachments(
305
338
  sizeBytes: file.sizeBytes,
306
339
  status: "ready",
307
340
  file,
341
+ errorCode: undefined,
308
342
  error: undefined,
309
343
  }
310
344
  : {
@@ -327,6 +361,31 @@ export function useFileAttachments(
327
361
  [workspaceId],
328
362
  );
329
363
 
364
+ const loadPreview = useCallback(
365
+ async (id: string, signal?: AbortSignal): Promise<string | undefined> => {
366
+ const generation = scopeGeneration.current;
367
+ const attachment = attachmentsRef.current.find((candidate) => candidate.id === id);
368
+ const createDownloadUrl = client.createFileDownloadUrl;
369
+ if (
370
+ !attachment ||
371
+ attachment.status !== "ready" ||
372
+ !attachment.file ||
373
+ !attachment.contentType.startsWith("image/") ||
374
+ typeof createDownloadUrl !== "function" ||
375
+ signal?.aborted
376
+ ) {
377
+ return undefined;
378
+ }
379
+ const fileId = attachment.file.id;
380
+ const signed = await createDownloadUrl.call(client, workspaceId, fileId, { signal });
381
+ if (scopeGeneration.current !== generation || signal?.aborted) return undefined;
382
+ const current = attachmentsRef.current.find((candidate) => candidate.id === id);
383
+ if (current?.status !== "ready" || current.file?.id !== fileId) return undefined;
384
+ return signed.url;
385
+ },
386
+ [client, workspaceId],
387
+ );
388
+
330
389
  const remove = useCallback(
331
390
  (id: string) => {
332
391
  sources.current.delete(id);
@@ -368,6 +427,7 @@ export function useFileAttachments(
368
427
  addFiles,
369
428
  addFromPaste,
370
429
  restoreReadyFiles,
430
+ ...(typeof client.createFileDownloadUrl === "function" ? { loadPreview } : {}),
371
431
  retry,
372
432
  retainPreview,
373
433
  remove,