@opengeni/react 3.7.0-canary.6 → 3.8.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/README.md +43 -3
- package/dist/artifacts.js +7 -7
- package/dist/{browser-viewer-NTD6UIPR.js → browser-viewer-SS46LUAC.js} +3 -3
- package/dist/chat.d.ts +37 -0
- package/dist/chat.js +315 -0
- package/dist/chat.js.map +1 -0
- package/dist/{chunk-LUYB4SRW.js → chunk-3HIF5IFC.js} +1048 -2439
- package/dist/chunk-3HIF5IFC.js.map +1 -0
- package/dist/{chunk-3IY6UZI6.js → chunk-BQGIXRKV.js} +288 -396
- package/dist/chunk-BQGIXRKV.js.map +1 -0
- package/dist/{chunk-MOWYEBCQ.js → chunk-BXJOQFCD.js} +5 -4
- package/dist/chunk-BXJOQFCD.js.map +1 -0
- package/dist/{chunk-BV5A4OTE.js → chunk-ELIYWBZR.js} +4 -4
- package/dist/chunk-F2CFHN3Y.js +1415 -0
- package/dist/chunk-F2CFHN3Y.js.map +1 -0
- package/dist/chunk-HQV2I5HV.js +124 -0
- package/dist/chunk-HQV2I5HV.js.map +1 -0
- package/dist/{chunk-JT33KZZN.js → chunk-R4KRSFGA.js} +11 -20
- package/dist/chunk-R4KRSFGA.js.map +1 -0
- package/dist/{chunk-KWEZ4MPF.js → chunk-UMECRJRV.js} +63 -60
- package/dist/chunk-UMECRJRV.js.map +1 -0
- package/dist/{chunk-24M4YBCL.js → chunk-VEPNNDNG.js} +4 -4
- package/dist/{chunk-WMGF4Z7X.js → chunk-VRBL3F5E.js} +23 -3
- package/dist/{chunk-WMGF4Z7X.js.map → chunk-VRBL3F5E.js.map} +1 -1
- package/dist/components/model-policy-picker.d.ts +4 -0
- package/dist/components/sandbox-files.d.ts +3 -1
- package/dist/composer.js +6 -5
- package/dist/{computer-viewer-7TEZC6Y2.js → computer-viewer-FWEZISW4.js} +3 -3
- package/dist/hooks/use-session-events.d.ts +2 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +92 -36
- package/dist/index.js.map +1 -1
- package/dist/interaction.js +8 -8
- package/dist/{model-policy-picker-menu-R6WXNWJL.js → model-policy-picker-menu-3R336DP2.js} +4 -4
- package/dist/model-policy.d.ts +2 -0
- package/dist/model-policy.js +3 -1
- package/dist/realtime.js +5 -5
- package/dist/session-ui.js +11 -8
- package/dist/session.js +1 -1
- package/package.json +6 -2
- package/src/chat.tsx +393 -0
- package/src/components/model-policy-picker-menu.tsx +92 -71
- package/src/components/model-policy-picker.tsx +21 -18
- package/src/components/sandbox-files.tsx +90 -14
- package/src/components/sandbox-workspace.tsx +16 -1
- package/src/hooks/use-session-events.ts +3 -0
- package/src/hooks/use-session.ts +10 -3
- package/src/index.ts +1 -0
- package/src/model-policy.ts +26 -2
- package/styles/compiled.css +40 -8
- package/styles/tokens.css +2 -2
- package/dist/chunk-3IY6UZI6.js.map +0 -1
- package/dist/chunk-JT33KZZN.js.map +0 -1
- package/dist/chunk-KWEZ4MPF.js.map +0 -1
- package/dist/chunk-LUYB4SRW.js.map +0 -1
- package/dist/chunk-MOWYEBCQ.js.map +0 -1
- /package/dist/{browser-viewer-NTD6UIPR.js.map → browser-viewer-SS46LUAC.js.map} +0 -0
- /package/dist/{chunk-BV5A4OTE.js.map → chunk-ELIYWBZR.js.map} +0 -0
- /package/dist/{chunk-24M4YBCL.js.map → chunk-VEPNNDNG.js.map} +0 -0
- /package/dist/{computer-viewer-7TEZC6Y2.js.map → computer-viewer-FWEZISW4.js.map} +0 -0
- /package/dist/{model-policy-picker-menu-R6WXNWJL.js.map → model-policy-picker-menu-3R336DP2.js.map} +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { ClientModel, ReasoningEffort } from "@opengeni/sdk";
|
|
2
2
|
import { CheckIcon, SearchIcon, ZapIcon } from "lucide-react";
|
|
3
3
|
import { useRef, useState, type RefObject, type CSSProperties } from "react";
|
|
4
|
-
import { Popover } from "radix-ui";
|
|
4
|
+
import { Popover, RadioGroup } from "radix-ui";
|
|
5
5
|
import { cn } from "../lib/cn";
|
|
6
6
|
import {
|
|
7
7
|
coerceReasoningEffortForModel,
|
|
@@ -34,12 +34,7 @@ export function ModelPolicyPickerMenu(props: ModelPolicyPickerProps) {
|
|
|
34
34
|
`${row.label} ${row.id} ${row.providerLabel} ${row.billingClassLabel} ${payerSummaryForModel(row.catalog)}`.toLowerCase();
|
|
35
35
|
return words.every((word) => text.includes(word));
|
|
36
36
|
});
|
|
37
|
-
const groups = groupPickerRowsByBillingClass(filtered)
|
|
38
|
-
.map((group) => ({
|
|
39
|
-
...group,
|
|
40
|
-
rows: group.rows.filter((row) => words.length > 0 || row.id !== selected?.id),
|
|
41
|
-
}))
|
|
42
|
-
.filter((group) => group.rows.length > 0);
|
|
37
|
+
const groups = groupPickerRowsByBillingClass(filtered);
|
|
43
38
|
const choose = (row: ClientPickerModelRow) => {
|
|
44
39
|
if (!row.selectable || props.disabled) return;
|
|
45
40
|
if (row.id !== props.model) props.onModelChange(row.id);
|
|
@@ -58,14 +53,23 @@ export function ModelPolicyPickerMenu(props: ModelPolicyPickerProps) {
|
|
|
58
53
|
<PickerNavRow
|
|
59
54
|
key={row.id}
|
|
60
55
|
label={row.label}
|
|
61
|
-
hint={
|
|
56
|
+
hint={row.unavailableReason ?? undefined}
|
|
62
57
|
disabled={props.disabled || !row.selectable}
|
|
63
|
-
title={row.unavailableReason
|
|
58
|
+
title={[row.label, row.unavailableReason].filter(Boolean).join(" · ")}
|
|
64
59
|
active={row.id === props.model}
|
|
65
60
|
showChevron={false}
|
|
66
61
|
trailing={
|
|
67
|
-
row.id === props.model ? (
|
|
68
|
-
<
|
|
62
|
+
row.catalog.cost === "free" || row.id === props.model ? (
|
|
63
|
+
<span className="flex items-center gap-2">
|
|
64
|
+
{row.catalog.cost === "free" ? (
|
|
65
|
+
<span className="rounded-og-sm bg-og-surface-2 px-1.5 py-0.5 text-og-control text-og-fg-muted">
|
|
66
|
+
{messages.free}
|
|
67
|
+
</span>
|
|
68
|
+
) : null}
|
|
69
|
+
{row.id === props.model ? (
|
|
70
|
+
<CheckIcon className="size-3.5" aria-label={messages.selected} />
|
|
71
|
+
) : null}
|
|
72
|
+
</span>
|
|
69
73
|
) : null
|
|
70
74
|
}
|
|
71
75
|
testId={`model-picker-choice-${row.id}`}
|
|
@@ -99,25 +103,27 @@ export function ModelPolicyPickerMenu(props: ModelPolicyPickerProps) {
|
|
|
99
103
|
buttons[next]?.focus();
|
|
100
104
|
}}
|
|
101
105
|
>
|
|
102
|
-
<div className="
|
|
103
|
-
<
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
event.
|
|
113
|
-
|
|
114
|
-
.
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
106
|
+
<div className="border-b border-og-border py-1">
|
|
107
|
+
<label className="og-model-policy-search flex items-center gap-2 rounded-og-sm px-2 py-1 focus-within:outline-2 focus-within:-outline-offset-2 focus-within:outline-og-accent/40">
|
|
108
|
+
<SearchIcon className="size-4 shrink-0 text-og-fg-subtle" aria-hidden />
|
|
109
|
+
<input
|
|
110
|
+
aria-label={messages.searchLabel}
|
|
111
|
+
placeholder={messages.searchPlaceholder}
|
|
112
|
+
value={query}
|
|
113
|
+
onChange={(event) => setQuery(event.target.value)}
|
|
114
|
+
onKeyDown={(event) => {
|
|
115
|
+
if (event.key !== "Escape" && event.key !== "Tab") event.stopPropagation();
|
|
116
|
+
if (event.key === "ArrowDown") {
|
|
117
|
+
event.preventDefault();
|
|
118
|
+
event.currentTarget
|
|
119
|
+
.closest('[data-testid="model-picker-menu"]')
|
|
120
|
+
?.querySelector<HTMLButtonElement>("button:not(:disabled)")
|
|
121
|
+
?.focus();
|
|
122
|
+
}
|
|
123
|
+
}}
|
|
124
|
+
className="og-model-policy-search-input h-8 min-w-0 flex-1 bg-transparent text-og-menu text-og-fg outline-hidden placeholder:text-og-fg-subtle"
|
|
125
|
+
/>
|
|
126
|
+
</label>
|
|
121
127
|
</div>
|
|
122
128
|
{props.error ? (
|
|
123
129
|
<p className="px-2 py-2 text-og-control text-og-status-failed" role="alert">
|
|
@@ -132,20 +138,17 @@ export function ModelPolicyPickerMenu(props: ModelPolicyPickerProps) {
|
|
|
132
138
|
<p className="px-2 py-3 text-og-control text-og-fg-subtle">{messages.loading}</p>
|
|
133
139
|
) : (
|
|
134
140
|
<>
|
|
135
|
-
{selected && words.length === 0 ? (
|
|
136
|
-
<div className="border-b border-og-border/60 pb-1 mb-1">
|
|
137
|
-
<p className="px-2.5 py-1.5 text-og-control text-og-fg-subtle">
|
|
138
|
-
{messages.currentModel}
|
|
139
|
-
</p>
|
|
140
|
-
{modelRow(selected)}
|
|
141
|
-
</div>
|
|
142
|
-
) : null}
|
|
143
141
|
{groups.map((group) => (
|
|
144
142
|
<section key={group.billingClass} aria-label={group.label} className="py-1">
|
|
145
143
|
<div className="flex items-center gap-2 px-2.5 py-1.5 text-og-control font-medium text-og-fg-subtle">
|
|
146
144
|
<BillingClassMark billingClass={group.billingClass} aria-label="" />
|
|
147
145
|
{group.label}
|
|
148
146
|
</div>
|
|
147
|
+
{group.billingClass !== "opengeni_credits" ? (
|
|
148
|
+
<p className="px-2.5 pb-2 text-og-control text-og-fg-subtle">
|
|
149
|
+
{messages.billingHints[group.billingClass]}
|
|
150
|
+
</p>
|
|
151
|
+
) : null}
|
|
149
152
|
{group.rows.map(modelRow)}
|
|
150
153
|
</section>
|
|
151
154
|
))}
|
|
@@ -157,9 +160,16 @@ export function ModelPolicyPickerMenu(props: ModelPolicyPickerProps) {
|
|
|
157
160
|
</>
|
|
158
161
|
)}
|
|
159
162
|
</div>
|
|
160
|
-
{selected
|
|
161
|
-
|
|
162
|
-
|
|
163
|
+
{selected &&
|
|
164
|
+
((props.hasImageAttachments &&
|
|
165
|
+
selected.catalog.capabilities?.inputModalities.includes("image") === false) ||
|
|
166
|
+
(effortOptionsForModel(selected.catalog).length > 1 &&
|
|
167
|
+
selected.catalog.capabilities?.reasoning.runnable !== false) ||
|
|
168
|
+
(props.allowLatencyMode !== false &&
|
|
169
|
+
runnableLatencyModesForModel(selected.catalog).includes("fast"))) ? (
|
|
170
|
+
<div className="border-t border-og-border px-2.5 py-2.5">
|
|
171
|
+
{props.hasImageAttachments &&
|
|
172
|
+
selected.catalog.capabilities?.inputModalities.includes("image") === false ? (
|
|
163
173
|
<p className="pb-1.5 text-og-control leading-relaxed text-og-fg-subtle">
|
|
164
174
|
{messages.unsupportedAttachments}
|
|
165
175
|
</p>
|
|
@@ -177,42 +187,53 @@ function ModelThinkingControls(props: ModelPolicyPickerProps) {
|
|
|
177
187
|
const efforts = effortOptionsForModel(selected.catalog);
|
|
178
188
|
const messages = { ...defaultModelPolicyPickerMessages, ...props.messages };
|
|
179
189
|
const supportsFast = runnableLatencyModesForModel(selected.catalog).includes("fast");
|
|
190
|
+
const showThinking =
|
|
191
|
+
efforts.length > 1 && selected.catalog.capabilities?.reasoning.runnable !== false;
|
|
192
|
+
const showFast = supportsFast && props.allowLatencyMode !== false;
|
|
193
|
+
if (!showThinking && !showFast) return null;
|
|
180
194
|
return (
|
|
181
|
-
<div
|
|
182
|
-
className="flex items-center
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
195
|
+
<div className="space-y-2 text-og-control" data-testid="model-picker-reasoning">
|
|
196
|
+
<div className="flex min-h-7 items-center justify-between gap-3">
|
|
197
|
+
{showThinking ? <span className="text-og-fg-subtle">{messages.thinking}</span> : <span />}
|
|
198
|
+
{showFast ? (
|
|
199
|
+
<button
|
|
200
|
+
type="button"
|
|
201
|
+
data-testid="model-picker-fast"
|
|
202
|
+
disabled={props.disabled || !selected.selectable}
|
|
203
|
+
aria-pressed={props.latencyMode === "fast"}
|
|
204
|
+
title={messages.fast + " · " + messages.fastRateHint}
|
|
205
|
+
onClick={() =>
|
|
206
|
+
props.onLatencyModeChange(props.latencyMode === "fast" ? "standard" : "fast")
|
|
207
|
+
}
|
|
208
|
+
className="flex min-h-8 shrink-0 items-center gap-1.5 whitespace-nowrap rounded-og-sm px-2 text-og-fg-muted hover:bg-og-surface-2 focus-visible:ring-2 focus-visible:ring-og-accent/40 disabled:opacity-50"
|
|
209
|
+
>
|
|
210
|
+
<ZapIcon className={cn("size-3.5", props.latencyMode === "fast" && "fill-current")} />
|
|
211
|
+
{messages.fast}
|
|
212
|
+
<span className="text-og-fg-subtle">{messages.fastRateHint}</span>
|
|
213
|
+
</button>
|
|
214
|
+
) : null}
|
|
215
|
+
</div>
|
|
216
|
+
{showThinking ? (
|
|
217
|
+
<RadioGroup.Root
|
|
188
218
|
aria-label={messages.thinkingEffort}
|
|
189
219
|
value={coerceReasoningEffortForModel(selected.catalog, props.effort)}
|
|
190
|
-
disabled={props.disabled || !selected.selectable
|
|
191
|
-
|
|
192
|
-
|
|
220
|
+
disabled={props.disabled || !selected.selectable}
|
|
221
|
+
onValueChange={(value) => props.onEffortChange(value as ReasoningEffort)}
|
|
222
|
+
orientation="horizontal"
|
|
223
|
+
className="flex flex-wrap gap-0.5 rounded-og-md bg-og-surface-2 p-0.5"
|
|
193
224
|
>
|
|
194
225
|
{efforts.map((effort) => (
|
|
195
|
-
<
|
|
196
|
-
{
|
|
197
|
-
|
|
226
|
+
<RadioGroup.Item
|
|
227
|
+
key={effort}
|
|
228
|
+
value={effort}
|
|
229
|
+
aria-label={labelReasoningEffort(effort)}
|
|
230
|
+
title={labelReasoningEffort(effort)}
|
|
231
|
+
className="min-h-8 min-w-10 flex-1 cursor-pointer whitespace-nowrap rounded-og-sm px-1.5 text-og-control text-og-fg-muted outline-hidden transition-colors hover:text-og-fg focus-visible:ring-2 focus-visible:ring-og-accent/40 data-[state=checked]:bg-og-surface-1 data-[state=checked]:text-og-fg data-[state=checked]:shadow-sm disabled:cursor-not-allowed disabled:opacity-50"
|
|
232
|
+
>
|
|
233
|
+
{effort === "xhigh" ? "X-high" : labelReasoningEffort(effort)}
|
|
234
|
+
</RadioGroup.Item>
|
|
198
235
|
))}
|
|
199
|
-
</
|
|
200
|
-
</label>
|
|
201
|
-
{supportsFast && props.allowLatencyMode !== false ? (
|
|
202
|
-
<button
|
|
203
|
-
type="button"
|
|
204
|
-
data-testid="model-picker-fast"
|
|
205
|
-
disabled={props.disabled || !selected.selectable}
|
|
206
|
-
aria-pressed={props.latencyMode === "fast"}
|
|
207
|
-
title={`${messages.fast} · ${messages.fastRateHint}`}
|
|
208
|
-
onClick={() =>
|
|
209
|
-
props.onLatencyModeChange(props.latencyMode === "fast" ? "standard" : "fast")
|
|
210
|
-
}
|
|
211
|
-
className="ml-auto flex min-h-9 items-center gap-1.5 rounded-og-sm px-2 hover:bg-og-surface-2 focus-visible:ring-2 focus-visible:ring-og-accent/40 disabled:opacity-50"
|
|
212
|
-
>
|
|
213
|
-
<ZapIcon className={cn("size-3.5", props.latencyMode === "fast" && "fill-current")} />
|
|
214
|
-
{messages.fast} <span className="text-og-fg-subtle">{messages.fastRateHint}</span>
|
|
215
|
-
</button>
|
|
236
|
+
</RadioGroup.Root>
|
|
216
237
|
) : null}
|
|
217
238
|
</div>
|
|
218
239
|
);
|
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
import { cn } from "../lib/cn";
|
|
22
22
|
import { usePortalTokenSource, usePortalTokenStyle } from "../lib/use-portal-token-style";
|
|
23
23
|
import {
|
|
24
|
+
effortOptionsForModel,
|
|
24
25
|
findPickerRow,
|
|
25
26
|
labelReasoningEffort,
|
|
26
27
|
projectClientModelRows,
|
|
@@ -51,6 +52,7 @@ export type ModelPolicyPickerMessages = {
|
|
|
51
52
|
unsupportedAttachments?: string;
|
|
52
53
|
thinkingEffort?: string;
|
|
53
54
|
selected?: string;
|
|
55
|
+
free?: string;
|
|
54
56
|
|
|
55
57
|
billingHints: Record<PickerBillingClass, string>;
|
|
56
58
|
};
|
|
@@ -67,13 +69,13 @@ export const defaultModelPolicyPickerMessages: ModelPolicyPickerMessages = {
|
|
|
67
69
|
searchPlaceholder: "Search models or providers…",
|
|
68
70
|
currentModel: "Current model",
|
|
69
71
|
noMatches: "No matching models. Try a model or provider name.",
|
|
70
|
-
unsupportedAttachments:
|
|
71
|
-
"Unsupported attachments stay in the session but are hidden from this model.",
|
|
72
|
+
unsupportedAttachments: "This model cannot view the attached images.",
|
|
72
73
|
thinkingEffort: "Thinking effort",
|
|
73
74
|
selected: "Selected",
|
|
75
|
+
free: "Free",
|
|
74
76
|
|
|
75
77
|
billingHints: {
|
|
76
|
-
opengeni_credits: "
|
|
78
|
+
opengeni_credits: "Provided by OpenGeni",
|
|
77
79
|
external: "Provider terms and limits apply",
|
|
78
80
|
codex_subscription: "ChatGPT / Codex plan",
|
|
79
81
|
supergrok_subscription: "SuperGrok / xAI plan",
|
|
@@ -85,6 +87,8 @@ export const defaultModelPolicyPickerMessages: ModelPolicyPickerMessages = {
|
|
|
85
87
|
export type ModelPolicyPickerProps = {
|
|
86
88
|
/** Lightweight deployment models. Catalog rows take precedence when supplied. */
|
|
87
89
|
models?: ClientModel[] | undefined;
|
|
90
|
+
/** Warn only when the draft actually contains images this model cannot view. */
|
|
91
|
+
hasImageAttachments?: boolean | undefined;
|
|
88
92
|
/** Catalog-backed rows with availability and billing-class truth. */
|
|
89
93
|
rows?: PickerModelRow[] | undefined;
|
|
90
94
|
model: string;
|
|
@@ -119,17 +123,10 @@ export type ModelPolicyPickerProps = {
|
|
|
119
123
|
|
|
120
124
|
const SLIDE_EASE = [0.22, 1, 0.36, 1] as const;
|
|
121
125
|
|
|
126
|
+
// Solid facets keep the brand legible at the picker’s 14px icon size.
|
|
122
127
|
function OpenGeniMark(props: SVGProps<SVGSVGElement>) {
|
|
123
128
|
return (
|
|
124
|
-
<svg
|
|
125
|
-
viewBox="0 0 140 133"
|
|
126
|
-
fill="none"
|
|
127
|
-
stroke="currentColor"
|
|
128
|
-
strokeWidth={1.2}
|
|
129
|
-
strokeLinejoin="round"
|
|
130
|
-
aria-hidden="true"
|
|
131
|
-
{...props}
|
|
132
|
-
>
|
|
129
|
+
<svg viewBox="0 0 140 133" fill="currentColor" aria-hidden="true" {...props}>
|
|
133
130
|
<g transform="translate(-17.5,-20.999893188476562) scale(1.75)">
|
|
134
131
|
<g transform="translate(0,-952.36218)">
|
|
135
132
|
<path d="m 60.7828,964.36215 27.1809,0.8834 -27.1809,25.9958 z m -1.9745,1.4513 0,26.7845 -25.2681,0 c 8.6166,-8.7334 16.8796,-17.8103 25.2681,-26.7845 z m 27.7053,3.628 3.4864,1.1989 -12.5877,7.4768 z m -68.1835,2.9656 5.5226,0 12.8654,14.0705 -5.9854,6.1204 -12.4026,0 c 9e-4,-6.7347 0,-13.4597 0,-20.1909 z m -1.9746,1.2304 0,5.8364 -6.3555,0 z m 3.363,20.9796 38.627,0 -10.7675,29.43465 z m 39.0898,4.54286 0,41.20229 -12.5878,-6.8775 c 4.1972,-11.443 8.3886,-22.879 12.5878,-34.32479 z" />
|
|
@@ -249,6 +246,7 @@ export function PickerNavRow(props: {
|
|
|
249
246
|
showChevron?: boolean | undefined;
|
|
250
247
|
disabled?: boolean | undefined;
|
|
251
248
|
title?: string | undefined;
|
|
249
|
+
description?: string | undefined;
|
|
252
250
|
active?: boolean | undefined;
|
|
253
251
|
testId?: string | undefined;
|
|
254
252
|
onClick: () => void;
|
|
@@ -258,6 +256,7 @@ export function PickerNavRow(props: {
|
|
|
258
256
|
type="button"
|
|
259
257
|
disabled={props.disabled}
|
|
260
258
|
title={props.title}
|
|
259
|
+
aria-description={props.description}
|
|
261
260
|
onClick={props.onClick}
|
|
262
261
|
data-testid={props.testId}
|
|
263
262
|
className={cn(
|
|
@@ -387,12 +386,16 @@ export function ModelPolicyPicker(props: ModelPolicyPickerProps) {
|
|
|
387
386
|
<span className="og-model-policy-label-short min-w-0 truncate font-medium text-og-fg sm:hidden @max-[20rem]/model-controls:block">
|
|
388
387
|
{selected?.shortLabel ?? selected?.label ?? props.model}
|
|
389
388
|
</span>
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
389
|
+
{selected &&
|
|
390
|
+
effortOptionsForModel(selected.catalog).length > 1 &&
|
|
391
|
+
selected.catalog.capabilities?.reasoning.runnable !== false ? (
|
|
392
|
+
<span
|
|
393
|
+
className="og-model-policy-effort min-w-0 shrink-[9999] truncate"
|
|
394
|
+
title={labelReasoningEffort(props.effort)}
|
|
395
|
+
>
|
|
396
|
+
{labelReasoningEffort(props.effort)}
|
|
397
|
+
</span>
|
|
398
|
+
) : null}
|
|
396
399
|
{props.latencyMode === "fast" ? (
|
|
397
400
|
<ZapIcon
|
|
398
401
|
className="size-3.5 shrink-0 fill-current stroke-current text-og-fg"
|
|
@@ -57,6 +57,8 @@ export type SandboxFilesProps = {
|
|
|
57
57
|
workspaceWaking?: boolean | undefined;
|
|
58
58
|
/** Whether live reads are currently authoritative. */
|
|
59
59
|
liveWorkspaceReady?: boolean | undefined;
|
|
60
|
+
/** Failed live negotiation; never present a failure as an ongoing wake. */
|
|
61
|
+
workspaceError?: Error | null | undefined;
|
|
60
62
|
/** Deliberately wake the machine to read content absent from the capture. */
|
|
61
63
|
onWakeWorkspace?: (() => void) | undefined;
|
|
62
64
|
themeType?: "dark" | "light" | undefined;
|
|
@@ -87,6 +89,7 @@ export function SandboxFiles({
|
|
|
87
89
|
workspaceResting = false,
|
|
88
90
|
workspaceWaking = false,
|
|
89
91
|
liveWorkspaceReady = true,
|
|
92
|
+
workspaceError = null,
|
|
90
93
|
onWakeWorkspace,
|
|
91
94
|
themeType,
|
|
92
95
|
className,
|
|
@@ -254,10 +257,22 @@ export function SandboxFiles({
|
|
|
254
257
|
const captureFileUnavailable =
|
|
255
258
|
fileView.error instanceof CapturedFileUnavailableError ? fileView.error : null;
|
|
256
259
|
const waitingForSelectedFile =
|
|
260
|
+
!workspaceError &&
|
|
257
261
|
liveRequestedPath === viewPath &&
|
|
258
262
|
(!liveWorkspaceReady || files.loading) &&
|
|
259
263
|
captureFileUnavailable !== null;
|
|
260
264
|
|
|
265
|
+
if (workspaceError && !liveWorkspaceReady && files.source !== "capture") {
|
|
266
|
+
return (
|
|
267
|
+
<Notice className={className} title="Could not open live workspace" announce="alert">
|
|
268
|
+
<p>{workspaceError.message}</p>
|
|
269
|
+
{onWakeWorkspace ? (
|
|
270
|
+
<WakeButton onClick={onWakeWorkspace}>Retry live workspace</WakeButton>
|
|
271
|
+
) : null}
|
|
272
|
+
</Notice>
|
|
273
|
+
);
|
|
274
|
+
}
|
|
275
|
+
|
|
261
276
|
if (workspaceResting || workspaceWaking) {
|
|
262
277
|
return (
|
|
263
278
|
<div className={cn("h-full", className)} data-opengeni-workspace-resting>
|
|
@@ -415,6 +430,15 @@ export function SandboxFiles({
|
|
|
415
430
|
{...(onEditIntent ? { onEditIntent } : {})}
|
|
416
431
|
className="h-full"
|
|
417
432
|
/>
|
|
433
|
+
) : workspaceError &&
|
|
434
|
+
(liveRequestedPath === viewPath || requestedPath === viewPath) &&
|
|
435
|
+
!liveWorkspaceReady ? (
|
|
436
|
+
<Notice title="Could not open live file" announce="alert">
|
|
437
|
+
<p>{workspaceError.message}</p>
|
|
438
|
+
{onWakeWorkspace ? (
|
|
439
|
+
<WakeButton onClick={onWakeWorkspace}>Retry live file</WakeButton>
|
|
440
|
+
) : null}
|
|
441
|
+
</Notice>
|
|
418
442
|
) : waitingForSelectedFile ? (
|
|
419
443
|
<Notice
|
|
420
444
|
icon={
|
|
@@ -455,6 +479,12 @@ export function SandboxFiles({
|
|
|
455
479
|
</Notice>
|
|
456
480
|
) : fileView.loading ? (
|
|
457
481
|
<Notice announce="status">Loading {viewPath}…</Notice>
|
|
482
|
+
) : fileView.imageUrl ? (
|
|
483
|
+
<RasterFilePreview
|
|
484
|
+
key={fileView.imageUrl}
|
|
485
|
+
src={fileView.imageUrl}
|
|
486
|
+
path={viewPath}
|
|
487
|
+
/>
|
|
458
488
|
) : fileView.isBinary ? (
|
|
459
489
|
<Notice>
|
|
460
490
|
{viewPath} is a binary file ({fileView.sizeBytes ?? 0} bytes).
|
|
@@ -497,18 +527,27 @@ export function SandboxFiles({
|
|
|
497
527
|
)
|
|
498
528
|
) : // Nothing selected — the tree shows the whole workspace; pick a file.
|
|
499
529
|
requestedPath && !requestedPathReady ? (
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
<
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
530
|
+
workspaceError ? (
|
|
531
|
+
<Notice title="Could not open live file" announce="alert">
|
|
532
|
+
<p>{workspaceError.message}</p>
|
|
533
|
+
{onWakeWorkspace ? (
|
|
534
|
+
<WakeButton onClick={onWakeWorkspace}>Retry live file</WakeButton>
|
|
535
|
+
) : null}
|
|
536
|
+
</Notice>
|
|
537
|
+
) : (
|
|
538
|
+
<Notice
|
|
539
|
+
icon={
|
|
540
|
+
<LoaderCircleIcon
|
|
541
|
+
className="size-5 animate-spin motion-reduce:animate-none"
|
|
542
|
+
aria-hidden
|
|
543
|
+
/>
|
|
544
|
+
}
|
|
545
|
+
title="Waking sandbox"
|
|
546
|
+
announce="status"
|
|
547
|
+
>
|
|
548
|
+
Opening {requestedPath} when the live workspace is ready…
|
|
549
|
+
</Notice>
|
|
550
|
+
)
|
|
512
551
|
) : (
|
|
513
552
|
<Notice icon={<FileCode2Icon className="size-5" aria-hidden />} title="Choose a file">
|
|
514
553
|
Select a file in the tree to preview it.
|
|
@@ -628,8 +667,10 @@ function GitHeader({ git, loading }: { git: SandboxFilesGitSummary; loading: boo
|
|
|
628
667
|
</span>
|
|
629
668
|
{(git.ahead > 0 || git.behind > 0) && (
|
|
630
669
|
<span className="flex shrink-0 items-center gap-1.5 text-og-xs text-og-fg-subtle">
|
|
631
|
-
{
|
|
632
|
-
|
|
670
|
+
{/* Small arrow glyphs can make axe's pixel overlap heuristic inconclusive.
|
|
671
|
+
Keep both counters in the same computed-contrast audit as the dirty count. */}
|
|
672
|
+
{git.ahead > 0 && <span data-contrast-audited>↑{git.ahead}</span>}
|
|
673
|
+
{git.behind > 0 && <span data-contrast-audited>↓{git.behind}</span>}
|
|
633
674
|
</span>
|
|
634
675
|
)}
|
|
635
676
|
{dirty && (
|
|
@@ -677,6 +718,7 @@ function Segmented({
|
|
|
677
718
|
|
|
678
719
|
type FileViewState = {
|
|
679
720
|
content: string | null;
|
|
721
|
+
imageUrl?: string | null;
|
|
680
722
|
isBinary: boolean;
|
|
681
723
|
/** The backend truncated the read (size cap hit) — content is a PREFIX only.
|
|
682
724
|
* Editing+saving such a file would write the prefix back and corrupt it, so
|
|
@@ -749,6 +791,8 @@ function useFileView(
|
|
|
749
791
|
: res.content;
|
|
750
792
|
setState({
|
|
751
793
|
content,
|
|
794
|
+
imageUrl:
|
|
795
|
+
!res.truncated && res.encoding === "base64" ? rasterImageUrl(path, res.content) : null,
|
|
752
796
|
isBinary: res.isBinary,
|
|
753
797
|
truncated: res.truncated,
|
|
754
798
|
sizeBytes: res.sizeBytes,
|
|
@@ -780,6 +824,38 @@ function useFileView(
|
|
|
780
824
|
return state;
|
|
781
825
|
}
|
|
782
826
|
|
|
827
|
+
/** Only raster formats belong in this preview; never interpret SVG/HTML as a document. */
|
|
828
|
+
function rasterImageUrl(path: string, content: string): string | null {
|
|
829
|
+
const extension = path.split(".").pop()?.toLowerCase();
|
|
830
|
+
const mime =
|
|
831
|
+
extension === "png"
|
|
832
|
+
? "image/png"
|
|
833
|
+
: extension === "jpg" || extension === "jpeg"
|
|
834
|
+
? "image/jpeg"
|
|
835
|
+
: extension === "gif"
|
|
836
|
+
? "image/gif"
|
|
837
|
+
: extension === "webp"
|
|
838
|
+
? "image/webp"
|
|
839
|
+
: null;
|
|
840
|
+
return mime ? `data:${mime};base64,${content}` : null;
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
function RasterFilePreview({ src, path }: { src: string; path: string }) {
|
|
844
|
+
const [failed, setFailed] = useState(false);
|
|
845
|
+
return failed ? (
|
|
846
|
+
<Notice title="Image preview unavailable" announce="alert">
|
|
847
|
+
Could not decode {path} as an image.
|
|
848
|
+
</Notice>
|
|
849
|
+
) : (
|
|
850
|
+
<img
|
|
851
|
+
src={src}
|
|
852
|
+
alt={path}
|
|
853
|
+
onError={() => setFailed(true)}
|
|
854
|
+
className="max-w-full object-contain"
|
|
855
|
+
/>
|
|
856
|
+
);
|
|
857
|
+
}
|
|
858
|
+
|
|
783
859
|
/** Decode a base64 payload to a UTF-8 string (browser `atob` + TextDecoder). */
|
|
784
860
|
function decodeBase64Utf8(b64: string): string {
|
|
785
861
|
try {
|
|
@@ -781,7 +781,20 @@ export function useSandboxWorkspaceTabs(
|
|
|
781
781
|
}
|
|
782
782
|
workspaceWaking={!liveWorkspaceExpected && !captureAvailable && workspaceWaking}
|
|
783
783
|
liveWorkspaceReady={liveWorkspaceExpected}
|
|
784
|
-
|
|
784
|
+
workspaceError={
|
|
785
|
+
caps.error ??
|
|
786
|
+
(caps.viewerCapReached
|
|
787
|
+
? new Error(
|
|
788
|
+
"The live workspace viewer limit has been reached. Close another viewer and retry.",
|
|
789
|
+
)
|
|
790
|
+
: filesWarmRequested && caps.state === "on-demand" && !fileSystemOn
|
|
791
|
+
? new Error("This machine does not currently expose a live file system.")
|
|
792
|
+
: null)
|
|
793
|
+
}
|
|
794
|
+
onWakeWorkspace={() => {
|
|
795
|
+
if (filesWarmRequested) caps.renegotiate();
|
|
796
|
+
else requestWarmIntent("warmFiles");
|
|
797
|
+
}}
|
|
785
798
|
{...(requestedFilePath
|
|
786
799
|
? {
|
|
787
800
|
requestedPath: requestedFilePath,
|
|
@@ -943,6 +956,8 @@ export function useSandboxWorkspaceTabs(
|
|
|
943
956
|
caps.state,
|
|
944
957
|
caps.error,
|
|
945
958
|
caps.viewerCapReached,
|
|
959
|
+
caps.renegotiate,
|
|
960
|
+
filesWarmRequested,
|
|
946
961
|
requestWarmIntent,
|
|
947
962
|
createLinkedComputer,
|
|
948
963
|
]);
|
|
@@ -30,6 +30,8 @@ export type UseSessionEventsResult = {
|
|
|
30
30
|
timeline: TimelineItem[];
|
|
31
31
|
/** Latest session status observed in the event log, if any. */
|
|
32
32
|
sessionStatus: SessionStatus | null;
|
|
33
|
+
/** Sequence of the retained status projection, including events evicted from the window. */
|
|
34
|
+
sessionStatusSequence?: number;
|
|
33
35
|
connectionState: SessionEventsConnectionState;
|
|
34
36
|
/** Highest sequence seen so far (0 before the first event). */
|
|
35
37
|
lastSequence: number;
|
|
@@ -875,6 +877,7 @@ export function useSessionEvents(
|
|
|
875
877
|
events: visibleEvents,
|
|
876
878
|
timeline,
|
|
877
879
|
sessionStatus: identityMatches ? sessionStatusProjection : null,
|
|
880
|
+
sessionStatusSequence: identityMatches ? sessionStatusRef.current.sequence : 0,
|
|
878
881
|
connectionState: identityMatches ? connectionState : "idle",
|
|
879
882
|
lastSequence: identityMatches ? lastSequenceRef.current : after,
|
|
880
883
|
windowBytes: identityMatches ? eventWindow.bytes : 2,
|
package/src/hooks/use-session.ts
CHANGED
|
@@ -39,7 +39,13 @@ export function isTitleEvent(event: Pick<SessionEvent, "type">): boolean {
|
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
function isSessionDetailEvent(event: Pick<SessionEvent, "type">): boolean {
|
|
42
|
-
return
|
|
42
|
+
return (
|
|
43
|
+
isTitleEvent(event) ||
|
|
44
|
+
event.type.startsWith("session.command.") ||
|
|
45
|
+
event.type.startsWith("session.wait.") ||
|
|
46
|
+
event.type === "session.status.changed" ||
|
|
47
|
+
event.type === "system.update.pending"
|
|
48
|
+
);
|
|
43
49
|
}
|
|
44
50
|
|
|
45
51
|
/** Fetch one session (with optional polling), live-patching its title on `session.title_set`. */
|
|
@@ -115,12 +121,13 @@ export function useSession(
|
|
|
115
121
|
sharedEvents?.some(
|
|
116
122
|
(item) =>
|
|
117
123
|
item.sessionId === sessionId &&
|
|
118
|
-
item
|
|
124
|
+
!isTitleEvent(item) &&
|
|
125
|
+
isSessionDetailEvent(item) &&
|
|
119
126
|
item.sequence > (base?.lastSequence ?? -1),
|
|
120
127
|
)
|
|
121
128
|
) {
|
|
122
129
|
void refresh();
|
|
123
|
-
} else if (event
|
|
130
|
+
} else if (!isTitleEvent(event)) {
|
|
124
131
|
if (!base || event.sequence > base.lastSequence) void refresh();
|
|
125
132
|
return;
|
|
126
133
|
}
|
package/src/index.ts
CHANGED
package/src/model-policy.ts
CHANGED
|
@@ -116,6 +116,12 @@ export function labelReasoningEffort(effort: ReasoningEffort): string {
|
|
|
116
116
|
return effort.slice(0, 1).toUpperCase() + effort.slice(1);
|
|
117
117
|
}
|
|
118
118
|
|
|
119
|
+
/** Payment prompts must use cost policy, never the presentation group. */
|
|
120
|
+
export function modelUsesCredits(model: ClientModel | undefined): boolean {
|
|
121
|
+
if (model?.cost !== undefined) return model.cost === "credits";
|
|
122
|
+
return model?.billing?.metering === "opengeni_credits";
|
|
123
|
+
}
|
|
124
|
+
|
|
119
125
|
export function payerSummaryForModel(model: ClientModel): string {
|
|
120
126
|
if (model.cost === "free") {
|
|
121
127
|
return "Free in this deployment";
|
|
@@ -131,6 +137,9 @@ export function payerSummaryForModel(model: ClientModel): string {
|
|
|
131
137
|
if (model.cost === "workspace") {
|
|
132
138
|
return workspaceProviderPayerSummary(model);
|
|
133
139
|
}
|
|
140
|
+
if (model.cost === "organization") {
|
|
141
|
+
return organizationProviderPayerSummary(model);
|
|
142
|
+
}
|
|
134
143
|
|
|
135
144
|
// Older client-config payloads do not carry `cost`; preserve their existing
|
|
136
145
|
// settlement-derived label until every deployment has rolled forward.
|
|
@@ -149,14 +158,19 @@ export function payerSummaryForModel(model: ClientModel): string {
|
|
|
149
158
|
if (billing.upstreamPayer === "workspace") {
|
|
150
159
|
return workspaceProviderPayerSummary(model);
|
|
151
160
|
}
|
|
152
|
-
|
|
161
|
+
if (billing.upstreamPayer === "organization") {
|
|
162
|
+
return organizationProviderPayerSummary(model);
|
|
163
|
+
}
|
|
164
|
+
return billing.upstreamPayer === "deployment"
|
|
165
|
+
? "OpenGeni · no model credits"
|
|
166
|
+
: "External provider · no OpenGeni credits";
|
|
153
167
|
}
|
|
154
168
|
|
|
155
169
|
export function advancedSourceSummary(model: ClientModel): string | null {
|
|
156
170
|
const source = model.credentialSource;
|
|
157
171
|
if (!source) {
|
|
158
172
|
return model.billing?.metering === "external" && model.billing.upstreamPayer === "deployment"
|
|
159
|
-
? "Deployment
|
|
173
|
+
? "Deployment-provided connection"
|
|
160
174
|
: null;
|
|
161
175
|
}
|
|
162
176
|
if (source.kind === "connected_subscription") {
|
|
@@ -191,6 +205,16 @@ function workspaceProviderPayerSummary(model: ClientModel): string {
|
|
|
191
205
|
return "Billed to the workspace provider account";
|
|
192
206
|
}
|
|
193
207
|
|
|
208
|
+
function organizationProviderPayerSummary(model: ClientModel): string {
|
|
209
|
+
if (model.provider === "organization-openrouter") {
|
|
210
|
+
return "Billed to the organization OpenRouter account";
|
|
211
|
+
}
|
|
212
|
+
if (model.provider === "organization-gateway") {
|
|
213
|
+
return "Billed to the organization Vercel account";
|
|
214
|
+
}
|
|
215
|
+
return "Billed to the organization provider account";
|
|
216
|
+
}
|
|
217
|
+
|
|
194
218
|
export function projectPickerRows(models: WorkspaceModelCatalogModel[]): PickerModelRow[] {
|
|
195
219
|
return models
|
|
196
220
|
.filter((catalog) => {
|