@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.
- package/dist/artifacts.d.ts +1 -1
- package/dist/artifacts.js +241 -18
- package/dist/artifacts.js.map +1 -1
- package/dist/{browser-viewer-MKKFRRRV.js → browser-viewer-E5DDEFQ4.js} +3 -3
- package/dist/{chunk-2DZVYHVO.js → chunk-4JFUT22W.js} +39 -13
- package/dist/chunk-4JFUT22W.js.map +1 -0
- package/dist/{chunk-ELIYWBZR.js → chunk-BV5A4OTE.js} +4 -4
- package/dist/{chunk-3AGBGTRT.js → chunk-C6WDLHBA.js} +148 -26
- package/dist/chunk-C6WDLHBA.js.map +1 -0
- package/dist/{chunk-YG4QEQF5.js → chunk-DOK4KZF2.js} +7863 -7736
- package/dist/chunk-DOK4KZF2.js.map +1 -0
- package/dist/{chunk-JAQ5K7W6.js → chunk-FJBOU2TH.js} +167 -58
- package/dist/chunk-FJBOU2TH.js.map +1 -0
- package/dist/{chunk-XYRDDSXF.js → chunk-R55HFTT3.js} +80 -34
- package/dist/chunk-R55HFTT3.js.map +1 -0
- package/dist/{chunk-ZPBTI3TE.js → chunk-W2RGYBCV.js} +4 -4
- package/dist/{chunk-WSK7G5LE.js → chunk-W3OUAH6M.js} +80 -80
- package/dist/chunk-W3OUAH6M.js.map +1 -0
- package/dist/{chunk-X4A66EWZ.js → chunk-WMGF4Z7X.js} +41 -43
- package/dist/chunk-WMGF4Z7X.js.map +1 -0
- package/dist/client.d.ts +1 -1
- package/dist/components/artifacts/index.d.ts +1 -1
- package/dist/components/artifacts/published-html-artifact-frame.d.ts +45 -3
- package/dist/components/composer.d.ts +2 -0
- package/dist/components/session-chrome.d.ts +1 -1
- package/dist/composer.js +4 -4
- package/dist/{computer-viewer-FWEZISW4.js → computer-viewer-7TEZC6Y2.js} +3 -3
- package/dist/{fleet-decision-row-GGCAT4E4.js → fleet-decision-row-YZ3ZKWMM.js} +2 -1
- package/dist/fleet-decision-row-YZ3ZKWMM.js.map +1 -0
- package/dist/hooks/use-file-attachments.d.ts +8 -0
- package/dist/hooks/use-sandbox-files.d.ts +4 -1
- package/dist/hooks/use-session-events.d.ts +1 -1
- package/dist/hooks/use-workspace-edit.d.ts +4 -0
- package/dist/index.js +195 -141
- package/dist/index.js.map +1 -1
- package/dist/interaction.js +3 -3
- package/dist/model-policy.d.ts +2 -1
- package/dist/model-policy.js +1 -1
- package/dist/{queue-surface-implementation-BSKUWHRI.js → queue-surface-implementation-XL2A6PBM.js} +2 -2
- package/dist/realtime.js +2 -2
- package/dist/session-ui.js +6 -6
- package/dist/session.js +4 -4
- package/dist/timeline/registry.d.ts +2 -0
- package/dist/timeline/types.d.ts +3 -1
- package/package.json +2 -2
- package/src/artifacts.ts +3 -0
- package/src/client.ts +2 -1
- package/src/components/artifacts/index.ts +3 -0
- package/src/components/artifacts/published-html-artifact-frame.tsx +284 -4
- package/src/components/composer.tsx +103 -29
- package/src/components/message-timeline.tsx +48 -10
- package/src/components/model-policy-picker.tsx +37 -10
- package/src/components/sandbox-workspace.tsx +18 -7
- package/src/components/session-chrome.tsx +3 -3
- package/src/hooks/use-file-attachments.ts +62 -2
- package/src/hooks/use-sandbox-files.ts +68 -20
- package/src/hooks/use-session-events.ts +176 -21
- package/src/hooks/use-workspace-edit.ts +23 -5
- package/src/model-policy.ts +48 -53
- package/src/timeline/fleet-decision-projection.ts +2 -1
- package/src/timeline/fleet-decision-row.tsx +1 -0
- package/src/timeline/projection.ts +157 -8
- package/src/timeline/registry.ts +15 -3
- package/src/timeline/tool-renderers.tsx +117 -1
- package/src/timeline/types.ts +10 -1
- package/styles/compiled.css +5 -0
- package/dist/chunk-2DZVYHVO.js.map +0 -1
- package/dist/chunk-3AGBGTRT.js.map +0 -1
- package/dist/chunk-JAQ5K7W6.js.map +0 -1
- package/dist/chunk-WSK7G5LE.js.map +0 -1
- package/dist/chunk-X4A66EWZ.js.map +0 -1
- package/dist/chunk-XYRDDSXF.js.map +0 -1
- package/dist/chunk-YG4QEQF5.js.map +0 -1
- package/dist/fleet-decision-row-GGCAT4E4.js.map +0 -1
- /package/dist/{browser-viewer-MKKFRRRV.js.map → browser-viewer-E5DDEFQ4.js.map} +0 -0
- /package/dist/{chunk-ELIYWBZR.js.map → chunk-BV5A4OTE.js.map} +0 -0
- /package/dist/{chunk-ZPBTI3TE.js.map → chunk-W2RGYBCV.js.map} +0 -0
- /package/dist/{computer-viewer-FWEZISW4.js.map → computer-viewer-7TEZC6Y2.js.map} +0 -0
- /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
|
|
1654
|
-
|
|
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
|
|
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
|
-
?
|
|
1666
|
-
|
|
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
|
|
1740
|
+
{lightbox && (attachment.previewUrl || canLoadPreview) ? (
|
|
1670
1741
|
<button
|
|
1671
1742
|
type="button"
|
|
1672
|
-
className=
|
|
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
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
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
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
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
|
-
{
|
|
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 ||
|
|
1197
|
+
(wasAtLiveTailBeforeCommit || underfillOwned)
|
|
1192
1198
|
) {
|
|
1193
|
-
//
|
|
1194
|
-
//
|
|
1195
|
-
//
|
|
1196
|
-
//
|
|
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
|
|
1663
|
-
//
|
|
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 &&
|
|
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
|
-
{
|
|
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
|
|
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: "
|
|
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 {
|
|
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
|
-
? {
|
|
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 ??
|
|
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(
|
|
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 &&
|
|
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
|
|
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 &&
|
|
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 `
|
|
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}.
|
|
185
|
+
return `Waiting for input${reason ? `: ${reason}` : ""}${until}. Relevant session input—including a child result, background-command result, agent message, schedule, or your prompt—wakes 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
|
|
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 =
|
|
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,
|