@opengeni/react 0.12.0 → 0.15.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 +20 -14
- package/dist/chunk-TOJR776I.js +2280 -0
- package/dist/chunk-TOJR776I.js.map +1 -0
- package/dist/index.d.ts +1040 -363
- package/dist/index.js +10938 -6281
- package/dist/index.js.map +1 -1
- package/dist/{machines-BeZ3bD3t.d.ts → machines-BpdwuQcD.d.ts} +134 -14
- package/dist/machines.d.ts +1 -1
- package/dist/machines.js +23 -1
- package/package.json +20 -12
- package/src/approvals.ts +16 -4
- package/src/client.ts +41 -5
- package/src/commands/index.ts +1 -7
- package/src/commands/registry.ts +46 -12
- package/src/components/chat-composer.tsx +417 -69
- package/src/components/code-editor.tsx +28 -36
- package/src/components/command-palette.tsx +26 -5
- package/src/components/desktop-viewer.tsx +29 -16
- package/src/components/diff-view.tsx +27 -189
- package/src/components/enrollment-consent.tsx +28 -10
- package/src/components/enrollment-device-flow.tsx +8 -5
- package/src/components/file-browser.tsx +190 -56
- package/src/components/fleet-tile.tsx +13 -4
- package/src/components/machine-card.tsx +93 -15
- package/src/components/machine-dock-bar.tsx +20 -8
- package/src/components/machine-health-pill.tsx +68 -0
- package/src/components/machine-metrics.tsx +28 -19
- package/src/components/machine-status-pill.tsx +26 -5
- package/src/components/machines/health.ts +146 -0
- package/src/components/machines/machine-detail.tsx +220 -0
- package/src/components/machines/metric-history-chart.tsx +298 -0
- package/src/components/machines/metric-sparkline.tsx +76 -0
- package/src/components/machines/series.ts +113 -0
- package/src/components/machines-dashboard.tsx +21 -3
- package/src/components/markdown.tsx +39 -9
- package/src/components/message-timeline.tsx +633 -109
- package/src/components/pierre-diff.tsx +83 -15
- package/src/components/pierre-file.tsx +10 -9
- package/src/components/queue-surface.tsx +578 -0
- package/src/components/sandbox-files.tsx +193 -225
- package/src/components/sandbox-terminal.tsx +256 -88
- package/src/components/sandbox-workspace.tsx +957 -0
- package/src/components/session-status.tsx +22 -2
- package/src/components/workbench-changes.tsx +585 -0
- package/src/components/workspace-dock.tsx +213 -59
- package/src/hooks/internal.ts +5 -2
- package/src/hooks/use-available-models.ts +7 -2
- package/src/hooks/use-billing-usage.ts +4 -1
- package/src/hooks/use-codex-accounts.ts +74 -44
- package/src/hooks/use-composer.ts +414 -63
- package/src/hooks/use-environments.ts +92 -47
- package/src/hooks/use-file-attachments.ts +101 -55
- package/src/hooks/use-goal.ts +52 -16
- package/src/hooks/use-machine-chip.ts +134 -0
- package/src/hooks/use-machines.ts +34 -16
- package/src/hooks/use-packs.ts +24 -19
- package/src/hooks/use-relay-frame-stream.ts +5 -2
- package/src/hooks/use-rigs.ts +335 -0
- package/src/hooks/use-sandbox-files.ts +213 -39
- package/src/hooks/use-sandbox-git.ts +188 -11
- package/src/hooks/use-sandbox-terminal.ts +18 -14
- package/src/hooks/use-scheduled-tasks.ts +10 -2
- package/src/hooks/use-session-capabilities.ts +77 -20
- package/src/hooks/use-session-control.ts +50 -23
- package/src/hooks/use-session-events.ts +50 -21
- package/src/hooks/use-session-lineage.ts +128 -0
- package/src/hooks/use-session.ts +53 -30
- package/src/hooks/use-slash-commands.ts +6 -4
- package/src/hooks/use-turn-queue.ts +202 -147
- package/src/hooks/use-windowed-sections.ts +204 -0
- package/src/hooks/use-workspace-capture.ts +233 -0
- package/src/hooks/use-workspace-edit.ts +231 -0
- package/src/hooks/use-workspace-sessions.ts +48 -5
- package/src/hooks/use-workspaces.ts +18 -15
- package/src/index.ts +133 -23
- package/src/lib/format.ts +3 -3
- package/src/lib/xterm-renderer.ts +65 -0
- package/src/lib/xterm-theme.ts +224 -18
- package/src/machines.ts +29 -3
- package/src/provider.tsx +195 -6
- package/src/timeline/activity-rail.tsx +213 -54
- package/src/timeline/disclosure-context.tsx +12 -2
- package/src/timeline/index.ts +21 -2
- package/src/timeline/parsers.ts +87 -16
- package/src/timeline/projection.ts +412 -87
- package/src/timeline/shared.tsx +111 -24
- package/src/timeline/tool-diff.tsx +24 -20
- package/src/timeline/tool-renderers.tsx +98 -21
- package/src/timeline/turn-summary.tsx +76 -23
- package/src/timeline/types.ts +96 -12
- package/styles/index.css +22 -0
- package/styles/tokens.css +2 -2
- package/dist/chunk-5TIXRCSI.js +0 -1291
- package/dist/chunk-5TIXRCSI.js.map +0 -1
|
@@ -1,4 +1,13 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
type KeyboardEvent as ReactKeyboardEvent,
|
|
3
|
+
type ReactNode,
|
|
4
|
+
useCallback,
|
|
5
|
+
useEffect,
|
|
6
|
+
useId,
|
|
7
|
+
useLayoutEffect,
|
|
8
|
+
useRef,
|
|
9
|
+
useState,
|
|
10
|
+
} from "react";
|
|
2
11
|
import {
|
|
3
12
|
ChevronsLeftRightIcon,
|
|
4
13
|
Maximize2Icon,
|
|
@@ -6,13 +15,7 @@ import {
|
|
|
6
15
|
PanelRightCloseIcon,
|
|
7
16
|
XIcon,
|
|
8
17
|
} from "lucide-react";
|
|
9
|
-
import {
|
|
10
|
-
Group,
|
|
11
|
-
Panel,
|
|
12
|
-
Separator,
|
|
13
|
-
useDefaultLayout,
|
|
14
|
-
usePanelRef,
|
|
15
|
-
} from "react-resizable-panels";
|
|
18
|
+
import { Group, Panel, Separator, useDefaultLayout, usePanelRef } from "react-resizable-panels";
|
|
16
19
|
import { cn } from "../lib/cn";
|
|
17
20
|
|
|
18
21
|
export type WorkspaceTab = {
|
|
@@ -34,6 +37,13 @@ export type WorkspaceDockProps = {
|
|
|
34
37
|
/** Controlled collapsed state for hosts that expose their own dock toggle. */
|
|
35
38
|
collapsed?: boolean | undefined;
|
|
36
39
|
onCollapsedChange?: ((collapsed: boolean) => void) | undefined;
|
|
40
|
+
/** A status accessory pinned to the right of the tab strip, left of the
|
|
41
|
+
* maximize/collapse controls (e.g. the machine-state chip). Renders in both
|
|
42
|
+
* the docked header and the full-screen overlay header. */
|
|
43
|
+
headerAccessory?: ReactNode | undefined;
|
|
44
|
+
/** Optional host navigation shown at the start of the phone overlay header.
|
|
45
|
+
* It is intentionally absent from desktop dock chrome. */
|
|
46
|
+
mobileLeadingControl?: ReactNode | undefined;
|
|
37
47
|
/** Persisted layout id (localStorage key) for react-resizable-panels. */
|
|
38
48
|
autoSaveId?: string | undefined;
|
|
39
49
|
/** Default dock width as a percent of the session area. */
|
|
@@ -94,6 +104,8 @@ export function WorkspaceDock({
|
|
|
94
104
|
onActiveTabChange,
|
|
95
105
|
collapsed: collapsedProp,
|
|
96
106
|
onCollapsedChange,
|
|
107
|
+
headerAccessory,
|
|
108
|
+
mobileLeadingControl,
|
|
97
109
|
autoSaveId = "og.session.dock",
|
|
98
110
|
defaultSize = 34,
|
|
99
111
|
minSize = 22,
|
|
@@ -101,6 +113,9 @@ export function WorkspaceDock({
|
|
|
101
113
|
className,
|
|
102
114
|
}: WorkspaceDockProps) {
|
|
103
115
|
const narrow = useIsNarrow(DOCK_OVERLAY_BREAKPOINT);
|
|
116
|
+
const rootRef = useRef<HTMLDivElement | null>(null);
|
|
117
|
+
const reopenRef = useRef<HTMLButtonElement | null>(null);
|
|
118
|
+
const returnFocusRef = useRef<HTMLElement | null>(null);
|
|
104
119
|
const dockPanelRef = usePanelRef();
|
|
105
120
|
const [internalCollapsed, setInternalCollapsed] = useState(false);
|
|
106
121
|
const [maximized, setMaximized] = useState(false);
|
|
@@ -112,6 +127,7 @@ export function WorkspaceDock({
|
|
|
112
127
|
// bug). Standalone (uncontrolled) usage keeps the built-in collapse button
|
|
113
128
|
// and the thin re-open rail as its only affordances.
|
|
114
129
|
const hostControlled = collapsedProp !== undefined;
|
|
130
|
+
const previousCollapsedRef = useRef(collapsed);
|
|
115
131
|
|
|
116
132
|
// Persisted layout (width split) keyed by autoSaveId.
|
|
117
133
|
const { defaultLayout, onLayoutChanged } = useDefaultLayout({
|
|
@@ -120,9 +136,11 @@ export function WorkspaceDock({
|
|
|
120
136
|
id: autoSaveId,
|
|
121
137
|
} as Parameters<typeof useDefaultLayout>[0]);
|
|
122
138
|
|
|
123
|
-
const
|
|
124
|
-
const tabIds = tabs.map((tab) => tab.id)
|
|
139
|
+
const requestedTab = activeTab ?? internalTab;
|
|
140
|
+
const tabIds = tabs.map((tab) => tab.id);
|
|
125
141
|
const firstTabId = tabs[0]?.id ?? "";
|
|
142
|
+
const requestedTabIsValid = tabIds.includes(requestedTab);
|
|
143
|
+
const current = requestedTabIsValid ? requestedTab : firstTabId;
|
|
126
144
|
const setTab = useCallback(
|
|
127
145
|
(id: string) => {
|
|
128
146
|
setInternalTab(id);
|
|
@@ -150,14 +168,52 @@ export function WorkspaceDock({
|
|
|
150
168
|
}
|
|
151
169
|
}, [collapsedProp, dockPanelRef]);
|
|
152
170
|
|
|
171
|
+
// Treat the narrow overlay like a real modal: remember the external opener,
|
|
172
|
+
// move focus into the selected tab, and restore it on close. The standalone
|
|
173
|
+
// desktop dock instead focuses its newly-visible reopen rail after collapse.
|
|
174
|
+
useEffect(() => {
|
|
175
|
+
const previous = previousCollapsedRef.current;
|
|
176
|
+
previousCollapsedRef.current = collapsed;
|
|
177
|
+
if (previous === collapsed) return;
|
|
178
|
+
|
|
179
|
+
if (!collapsed) {
|
|
180
|
+
const active = document.activeElement;
|
|
181
|
+
const workspaceSurface = rootRef.current?.querySelector<HTMLElement>(
|
|
182
|
+
narrow ? '[role="dialog"]' : "[data-workspace-surface]",
|
|
183
|
+
);
|
|
184
|
+
if (active instanceof HTMLElement && !workspaceSurface?.contains(active)) {
|
|
185
|
+
returnFocusRef.current = active;
|
|
186
|
+
}
|
|
187
|
+
if (narrow) {
|
|
188
|
+
const frame = requestAnimationFrame(() => {
|
|
189
|
+
rootRef.current
|
|
190
|
+
?.querySelector<HTMLElement>('[role="dialog"] [role="tab"][aria-selected="true"]')
|
|
191
|
+
?.focus();
|
|
192
|
+
});
|
|
193
|
+
return () => cancelAnimationFrame(frame);
|
|
194
|
+
}
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const frame = requestAnimationFrame(() => {
|
|
199
|
+
if (!hostControlled) {
|
|
200
|
+
reopenRef.current?.focus();
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
const target = returnFocusRef.current;
|
|
204
|
+
if (target?.isConnected) target.focus();
|
|
205
|
+
});
|
|
206
|
+
return () => cancelAnimationFrame(frame);
|
|
207
|
+
}, [collapsed, hostControlled, narrow]);
|
|
208
|
+
|
|
153
209
|
// Keep the active tab valid if the available tabs change.
|
|
154
210
|
useEffect(() => {
|
|
155
|
-
if (firstTabId && !
|
|
211
|
+
if (firstTabId && !requestedTabIsValid) {
|
|
156
212
|
setTab(firstTabId);
|
|
157
213
|
}
|
|
158
214
|
// Depend on tab identity, not the tab content objects. Session live events
|
|
159
215
|
// rebuild tab JSX frequently; only id changes can invalidate the active tab.
|
|
160
|
-
}, [
|
|
216
|
+
}, [firstTabId, requestedTabIsValid, setTab]);
|
|
161
217
|
|
|
162
218
|
const collapse = useCallback(() => {
|
|
163
219
|
dockPanelRef.current?.collapse();
|
|
@@ -186,31 +242,45 @@ export function WorkspaceDock({
|
|
|
186
242
|
// driven by the same `collapsed` contract (collapsed → hidden).
|
|
187
243
|
if (narrow) {
|
|
188
244
|
return (
|
|
189
|
-
<div className={cn("relative flex h-full min-h-0 w-full min-w-0", className)}>
|
|
245
|
+
<div ref={rootRef} className={cn("relative flex h-full min-h-0 w-full min-w-0", className)}>
|
|
190
246
|
<div className="min-h-0 min-w-0 flex-1">{primary}</div>
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
247
|
+
<div
|
|
248
|
+
role="dialog"
|
|
249
|
+
aria-modal="true"
|
|
250
|
+
aria-label="Workspace"
|
|
251
|
+
className="fixed inset-0 z-40 flex flex-col bg-og-bg"
|
|
252
|
+
hidden={collapsed}
|
|
253
|
+
style={{
|
|
254
|
+
paddingTop: "env(safe-area-inset-top)",
|
|
255
|
+
paddingBottom: "env(safe-area-inset-bottom)",
|
|
256
|
+
}}
|
|
257
|
+
>
|
|
258
|
+
<DockChrome
|
|
259
|
+
tabs={tabs}
|
|
260
|
+
current={current}
|
|
261
|
+
onTab={setTab}
|
|
262
|
+
leading={mobileLeadingControl}
|
|
263
|
+
accessory={headerAccessory}
|
|
264
|
+
controls={
|
|
265
|
+
<ChromeButton onClick={collapse} title="Close workspace" label="Close workspace">
|
|
266
|
+
<XIcon className="size-4" />
|
|
267
|
+
</ChromeButton>
|
|
268
|
+
}
|
|
269
|
+
/>
|
|
270
|
+
</div>
|
|
271
|
+
{collapsed && !hostControlled ? (
|
|
272
|
+
<button
|
|
273
|
+
ref={reopenRef}
|
|
274
|
+
type="button"
|
|
275
|
+
onClick={expand}
|
|
276
|
+
title="Open workspace"
|
|
277
|
+
aria-label="Open workspace"
|
|
278
|
+
className="absolute right-3 top-3 z-30 inline-flex size-11 items-center justify-center rounded-og-md border border-og-border bg-og-surface-1 text-og-fg-muted shadow-lg transition-colors hover:border-og-border-strong hover:text-og-fg focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-og-accent"
|
|
279
|
+
style={{ marginTop: "env(safe-area-inset-top)" }}
|
|
201
280
|
>
|
|
202
|
-
<
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
onTab={setTab}
|
|
206
|
-
controls={
|
|
207
|
-
<ChromeButton onClick={collapse} title="Close workspace" label="Close workspace">
|
|
208
|
-
<XIcon className="size-4" />
|
|
209
|
-
</ChromeButton>
|
|
210
|
-
}
|
|
211
|
-
/>
|
|
212
|
-
</div>
|
|
213
|
-
)}
|
|
281
|
+
<ChevronsLeftRightIcon className="size-4" aria-hidden />
|
|
282
|
+
</button>
|
|
283
|
+
) : null}
|
|
214
284
|
</div>
|
|
215
285
|
);
|
|
216
286
|
}
|
|
@@ -220,6 +290,7 @@ export function WorkspaceDock({
|
|
|
220
290
|
tabs={tabs}
|
|
221
291
|
current={current}
|
|
222
292
|
onTab={setTab}
|
|
293
|
+
accessory={headerAccessory}
|
|
223
294
|
controls={
|
|
224
295
|
<>
|
|
225
296
|
<ChromeButton
|
|
@@ -227,7 +298,11 @@ export function WorkspaceDock({
|
|
|
227
298
|
title={maximized ? "Restore (Esc)" : "Maximize"}
|
|
228
299
|
label={maximized ? "Restore dock" : "Maximize dock"}
|
|
229
300
|
>
|
|
230
|
-
{maximized ?
|
|
301
|
+
{maximized ? (
|
|
302
|
+
<Minimize2Icon className="size-3.5" />
|
|
303
|
+
) : (
|
|
304
|
+
<Maximize2Icon className="size-3.5" />
|
|
305
|
+
)}
|
|
231
306
|
</ChromeButton>
|
|
232
307
|
{hostControlled ? null : (
|
|
233
308
|
<ChromeButton onClick={collapse} title="Collapse" label="Collapse dock">
|
|
@@ -240,7 +315,7 @@ export function WorkspaceDock({
|
|
|
240
315
|
);
|
|
241
316
|
|
|
242
317
|
return (
|
|
243
|
-
<div className={cn("relative flex h-full min-h-0 w-full min-w-0", className)}>
|
|
318
|
+
<div ref={rootRef} className={cn("relative flex h-full min-h-0 w-full min-w-0", className)}>
|
|
244
319
|
<Group
|
|
245
320
|
orientation="horizontal"
|
|
246
321
|
className="min-h-0 flex-1"
|
|
@@ -275,13 +350,19 @@ export function WorkspaceDock({
|
|
|
275
350
|
}}
|
|
276
351
|
className="min-h-0 min-w-0"
|
|
277
352
|
>
|
|
278
|
-
{/*
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
353
|
+
{/* One persistent mount across normal, collapsed, and maximized modes:
|
|
354
|
+
layout changes must never destroy an editor buffer or terminal view. */}
|
|
355
|
+
<div
|
|
356
|
+
data-workspace-surface
|
|
357
|
+
aria-hidden={collapsed ? true : undefined}
|
|
358
|
+
className={cn(
|
|
359
|
+
"flex h-full min-h-0 min-w-0 flex-col bg-og-bg",
|
|
360
|
+
maximized ? "fixed inset-0 z-40" : "border-l border-og-border",
|
|
361
|
+
collapsed && "invisible pointer-events-none",
|
|
362
|
+
)}
|
|
363
|
+
>
|
|
364
|
+
{dockChrome}
|
|
365
|
+
</div>
|
|
285
366
|
</Panel>
|
|
286
367
|
</Group>
|
|
287
368
|
|
|
@@ -289,6 +370,7 @@ export function WorkspaceDock({
|
|
|
289
370
|
when the host controls collapse — its own toggle is the one way in. */}
|
|
290
371
|
{collapsed && !maximized && !hostControlled && (
|
|
291
372
|
<button
|
|
373
|
+
ref={reopenRef}
|
|
292
374
|
type="button"
|
|
293
375
|
onClick={expand}
|
|
294
376
|
title="Open workspace"
|
|
@@ -297,13 +379,6 @@ export function WorkspaceDock({
|
|
|
297
379
|
<ChevronsLeftRightIcon className="size-3.5" />
|
|
298
380
|
</button>
|
|
299
381
|
)}
|
|
300
|
-
|
|
301
|
-
{/* Maximize overlay: full-workspace surface above everything. */}
|
|
302
|
-
{maximized && (
|
|
303
|
-
<div className="fixed inset-0 z-40 flex flex-col bg-og-bg">
|
|
304
|
-
{dockChrome}
|
|
305
|
-
</div>
|
|
306
|
-
)}
|
|
307
382
|
</div>
|
|
308
383
|
);
|
|
309
384
|
}
|
|
@@ -326,7 +401,7 @@ function ChromeButton({
|
|
|
326
401
|
onClick={onClick}
|
|
327
402
|
title={title}
|
|
328
403
|
aria-label={label}
|
|
329
|
-
className="inline-flex items-center justify-center rounded-og-sm p-1 transition-colors hover:bg-og-surface-2 hover:text-og-fg pointer-coarse:size-
|
|
404
|
+
className="inline-flex size-7 items-center justify-center rounded-og-sm p-1 transition-colors hover:bg-og-surface-2 hover:text-og-fg focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-og-accent max-[1023px]:size-11 pointer-coarse:size-11"
|
|
330
405
|
>
|
|
331
406
|
{children}
|
|
332
407
|
</button>
|
|
@@ -337,34 +412,93 @@ function DockChrome({
|
|
|
337
412
|
tabs,
|
|
338
413
|
current,
|
|
339
414
|
onTab,
|
|
415
|
+
leading,
|
|
416
|
+
accessory,
|
|
340
417
|
controls,
|
|
341
418
|
}: {
|
|
342
419
|
tabs: WorkspaceTab[];
|
|
343
420
|
current: string;
|
|
344
421
|
onTab: (id: string) => void;
|
|
422
|
+
/** Host navigation at the start of mobile overlay chrome. */
|
|
423
|
+
leading?: ReactNode | undefined;
|
|
424
|
+
/** A status accessory (machine chip) between the tab strip and the controls. */
|
|
425
|
+
accessory?: ReactNode | undefined;
|
|
345
426
|
/** Right-aligned chrome controls (maximize / collapse, or the overlay close). */
|
|
346
427
|
controls: ReactNode;
|
|
347
428
|
}) {
|
|
348
429
|
const active = tabs.find((t) => t.id === current) ?? tabs[0];
|
|
430
|
+
const tabsetId = useId();
|
|
431
|
+
const tabRefs = useRef<Array<HTMLButtonElement | null>>([]);
|
|
432
|
+
const [visitedTabs, setVisitedTabs] = useState<Set<string>>(() => new Set());
|
|
433
|
+
const activeId = active?.id ?? "";
|
|
434
|
+
|
|
435
|
+
useEffect(() => {
|
|
436
|
+
if (!activeId) return;
|
|
437
|
+
setVisitedTabs((previous) => {
|
|
438
|
+
if (previous.has(activeId)) return previous;
|
|
439
|
+
const next = new Set(previous);
|
|
440
|
+
next.add(activeId);
|
|
441
|
+
return next;
|
|
442
|
+
});
|
|
443
|
+
}, [activeId]);
|
|
444
|
+
|
|
445
|
+
const activateTab = (index: number) => {
|
|
446
|
+
const tab = tabs[index];
|
|
447
|
+
if (!tab) {
|
|
448
|
+
return;
|
|
449
|
+
}
|
|
450
|
+
onTab(tab.id);
|
|
451
|
+
tabRefs.current[index]?.focus();
|
|
452
|
+
};
|
|
453
|
+
|
|
454
|
+
const onTabKeyDown = (event: ReactKeyboardEvent<HTMLButtonElement>, index: number) => {
|
|
455
|
+
if (tabs.length === 0) return;
|
|
456
|
+
let nextIndex: number | null = null;
|
|
457
|
+
if (event.key === "ArrowRight") nextIndex = (index + 1) % tabs.length;
|
|
458
|
+
else if (event.key === "ArrowLeft") nextIndex = (index - 1 + tabs.length) % tabs.length;
|
|
459
|
+
else if (event.key === "Home") nextIndex = 0;
|
|
460
|
+
else if (event.key === "End") nextIndex = tabs.length - 1;
|
|
461
|
+
if (nextIndex === null) return;
|
|
462
|
+
event.preventDefault();
|
|
463
|
+
activateTab(nextIndex);
|
|
464
|
+
};
|
|
465
|
+
|
|
349
466
|
return (
|
|
350
467
|
<>
|
|
351
|
-
<div className="
|
|
468
|
+
<div className="grid shrink-0 grid-cols-[auto_minmax(0,1fr)_auto_auto] items-center gap-x-2 border-b border-og-border px-1.5 py-1 max-[1023px]:grid-cols-[auto_minmax(0,1fr)_auto] max-[1023px]:gap-y-0 max-[1023px]:px-2 max-[1023px]:pb-1 max-[1023px]:pt-0">
|
|
469
|
+
<div className="flex min-w-0 shrink-0 items-center max-[1023px]:min-h-11">
|
|
470
|
+
{leading ?? (
|
|
471
|
+
<span className="hidden truncate px-1 text-og-sm font-semibold text-og-fg max-[1023px]:inline">
|
|
472
|
+
Workspace
|
|
473
|
+
</span>
|
|
474
|
+
)}
|
|
475
|
+
</div>
|
|
352
476
|
{/* The tab list scrolls horizontally when it can't fit — it must never
|
|
353
477
|
grow into or overlap the chrome controls (they stay shrink-0). The
|
|
354
|
-
scrollbar is hidden to keep the strip calm.
|
|
478
|
+
scrollbar is hidden to keep the strip calm. On the narrow overlay it
|
|
479
|
+
owns a full second row, so status/close chrome can never squeeze the
|
|
480
|
+
canonical workspace navigation out of view. */}
|
|
355
481
|
<div
|
|
356
|
-
className="flex min-w-0
|
|
482
|
+
className="flex min-w-0 items-center gap-0.5 overflow-x-auto [scrollbar-width:none] [&::-webkit-scrollbar]:hidden max-[1023px]:col-span-3 max-[1023px]:row-start-2 max-[1023px]:w-full"
|
|
357
483
|
role="tablist"
|
|
484
|
+
aria-orientation="horizontal"
|
|
358
485
|
>
|
|
359
|
-
{tabs.map((tab) => (
|
|
486
|
+
{tabs.map((tab, index) => (
|
|
360
487
|
<button
|
|
361
488
|
key={tab.id}
|
|
489
|
+
ref={(node) => {
|
|
490
|
+
tabRefs.current[index] = node;
|
|
491
|
+
}}
|
|
492
|
+
id={`${tabsetId}-tab-${index}`}
|
|
362
493
|
type="button"
|
|
363
494
|
role="tab"
|
|
364
495
|
aria-selected={tab.id === current}
|
|
496
|
+
aria-controls={`${tabsetId}-panel-${index}`}
|
|
497
|
+
tabIndex={tab.id === current ? 0 : -1}
|
|
365
498
|
onClick={() => onTab(tab.id)}
|
|
499
|
+
onKeyDown={(event) => onTabKeyDown(event, index)}
|
|
366
500
|
className={cn(
|
|
367
|
-
"flex shrink-0 items-center gap-1 rounded-og-sm px-2 py-1 text-og-xs font-medium transition-colors pointer-coarse:min-h-
|
|
501
|
+
"flex min-h-7 shrink-0 items-center justify-center gap-1 rounded-og-sm px-2 py-1 text-og-xs font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-og-accent max-[1023px]:min-h-11 max-[1023px]:min-w-11 pointer-coarse:min-h-11 pointer-coarse:min-w-11",
|
|
368
502
|
tab.id === current
|
|
369
503
|
? "bg-og-accent-soft text-og-fg"
|
|
370
504
|
: "text-og-fg-subtle hover:text-og-fg",
|
|
@@ -375,10 +509,30 @@ function DockChrome({
|
|
|
375
509
|
</button>
|
|
376
510
|
))}
|
|
377
511
|
</div>
|
|
512
|
+
<div className="flex min-w-0 shrink-0 items-center justify-self-end">{accessory}</div>
|
|
378
513
|
<div className="flex shrink-0 items-center gap-0.5 text-og-fg-subtle">{controls}</div>
|
|
379
514
|
</div>
|
|
380
|
-
<div className="min-h-0 min-w-0 flex-1 overflow-hidden"
|
|
381
|
-
{
|
|
515
|
+
<div className="min-h-0 min-w-0 flex-1 overflow-hidden">
|
|
516
|
+
{tabs.length > 0 ? (
|
|
517
|
+
tabs.map((tab, index) => {
|
|
518
|
+
const selected = tab.id === activeId;
|
|
519
|
+
const shouldMount = selected || visitedTabs.has(tab.id);
|
|
520
|
+
return (
|
|
521
|
+
<div
|
|
522
|
+
key={tab.id}
|
|
523
|
+
id={`${tabsetId}-panel-${index}`}
|
|
524
|
+
aria-labelledby={`${tabsetId}-tab-${index}`}
|
|
525
|
+
className="h-full min-h-0 min-w-0 overflow-hidden"
|
|
526
|
+
hidden={!selected}
|
|
527
|
+
role="tabpanel"
|
|
528
|
+
>
|
|
529
|
+
{shouldMount ? tab.content : null}
|
|
530
|
+
</div>
|
|
531
|
+
);
|
|
532
|
+
})
|
|
533
|
+
) : (
|
|
534
|
+
<div className="h-full min-h-0 min-w-0" aria-label="Workspace" role="tabpanel" />
|
|
535
|
+
)}
|
|
382
536
|
</div>
|
|
383
537
|
</>
|
|
384
538
|
);
|
package/src/hooks/internal.ts
CHANGED
|
@@ -13,7 +13,10 @@ export type AsyncListState<T> = {
|
|
|
13
13
|
* Shared fetch + optional polling loop for the list/read hooks. Stale
|
|
14
14
|
* responses (superseded by a newer load or an unmount) are dropped.
|
|
15
15
|
*/
|
|
16
|
-
export function usePolledValue<T>(
|
|
16
|
+
export function usePolledValue<T>(
|
|
17
|
+
load: () => Promise<T>,
|
|
18
|
+
options: { pollIntervalMs?: number | undefined; enabled?: boolean | undefined } = {},
|
|
19
|
+
): AsyncListState<T> {
|
|
17
20
|
const enabled = options.enabled ?? true;
|
|
18
21
|
const pollIntervalMs = options.pollIntervalMs;
|
|
19
22
|
const [data, setData] = useState<T | null>(null);
|
|
@@ -95,7 +98,7 @@ export function useMutationRunner(): MutationState & {
|
|
|
95
98
|
mounted.current = false;
|
|
96
99
|
};
|
|
97
100
|
}, []);
|
|
98
|
-
const run = useCallback(async <T
|
|
101
|
+
const run = useCallback(async <T>(operation: () => Promise<T>): Promise<T | null> => {
|
|
99
102
|
inFlight.current += 1;
|
|
100
103
|
if (mounted.current) {
|
|
101
104
|
setMutating(true);
|
|
@@ -25,10 +25,15 @@ export type UseAvailableModelsResult = {
|
|
|
25
25
|
* provider-grouped `models` plus the `defaultModel` the picker should preselect.
|
|
26
26
|
* Deployment-scoped, so it only needs the client (no workspace).
|
|
27
27
|
*/
|
|
28
|
-
export function useAvailableModels(
|
|
28
|
+
export function useAvailableModels(
|
|
29
|
+
options: UseAvailableModelsOptions = {},
|
|
30
|
+
): UseAvailableModelsResult {
|
|
29
31
|
const client = useOpenGeniClient(options);
|
|
30
32
|
const load = useCallback(async () => await client.getClientConfig(), [client]);
|
|
31
|
-
const state = usePolledValue(load, {
|
|
33
|
+
const state = usePolledValue(load, {
|
|
34
|
+
pollIntervalMs: options.pollIntervalMs,
|
|
35
|
+
enabled: options.enabled,
|
|
36
|
+
});
|
|
32
37
|
return {
|
|
33
38
|
models: state.data?.models ?? [],
|
|
34
39
|
defaultModel: state.data?.defaultModel ?? null,
|
|
@@ -40,7 +40,10 @@ export function useBillingUsage(options: UseBillingUsageOptions = {}): UseBillin
|
|
|
40
40
|
}),
|
|
41
41
|
[client, accountId, workspaceId],
|
|
42
42
|
);
|
|
43
|
-
const state = usePolledValue(load, {
|
|
43
|
+
const state = usePolledValue(load, {
|
|
44
|
+
pollIntervalMs: options.pollIntervalMs,
|
|
45
|
+
enabled: options.enabled,
|
|
46
|
+
});
|
|
44
47
|
return {
|
|
45
48
|
balance: state.data?.balance ?? null,
|
|
46
49
|
usage: state.data?.usage ?? [],
|
|
@@ -1,7 +1,17 @@
|
|
|
1
1
|
import { useCallback, useState } from "react";
|
|
2
|
-
import type {
|
|
2
|
+
import type {
|
|
3
|
+
CodexAccount,
|
|
4
|
+
CodexAccountsResponse,
|
|
5
|
+
CodexRotationSettings,
|
|
6
|
+
SessionEvent,
|
|
7
|
+
} from "@opengeni/sdk";
|
|
3
8
|
import { useOpenGeni, type ClientOverride } from "../provider";
|
|
4
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
useMutationRunner,
|
|
11
|
+
usePolledValue,
|
|
12
|
+
useSessionEventTrigger,
|
|
13
|
+
type SessionEventFeedOptions,
|
|
14
|
+
} from "./internal";
|
|
5
15
|
|
|
6
16
|
/** Events that change which Codex account a session runs on (or just ran). */
|
|
7
17
|
export function isCodexAccountEvent(event: Pick<SessionEvent, "type">): boolean {
|
|
@@ -17,19 +27,27 @@ export function isCodexAccountEvent(event: Pick<SessionEvent, "type">): boolean
|
|
|
17
27
|
*/
|
|
18
28
|
export type CodexAccountsClientLike = {
|
|
19
29
|
listCodexAccounts: (workspaceId: string) => Promise<CodexAccountsResponse>;
|
|
20
|
-
getSession?: (
|
|
21
|
-
|
|
30
|
+
getSession?: (
|
|
31
|
+
workspaceId: string,
|
|
32
|
+
sessionId: string,
|
|
33
|
+
) => Promise<{ codexPinnedCredentialId?: string | null; codexLastCredentialId?: string | null }>;
|
|
34
|
+
pinSessionCodexAccount?: (
|
|
35
|
+
workspaceId: string,
|
|
36
|
+
sessionId: string,
|
|
37
|
+
target: string,
|
|
38
|
+
) => Promise<{ pinned: string }>;
|
|
22
39
|
/** Optional (absent ⇒ the card hides live refresh): batched live /wham/usage refresh. */
|
|
23
40
|
refreshCodexUsage?: (workspaceId: string) => Promise<{ usage: Record<string, unknown> }>;
|
|
24
41
|
};
|
|
25
42
|
|
|
26
|
-
export type UseCodexAccountsOptions = ClientOverride &
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
43
|
+
export type UseCodexAccountsOptions = ClientOverride &
|
|
44
|
+
SessionEventFeedOptions & {
|
|
45
|
+
pollIntervalMs?: number | undefined;
|
|
46
|
+
/** Scope to a session so the hook resolves the pin + the effective account. */
|
|
47
|
+
sessionId?: string | undefined;
|
|
48
|
+
/** Override the client with one implementing `CodexAccountsClientLike`. */
|
|
49
|
+
codexClient?: CodexAccountsClientLike | undefined;
|
|
50
|
+
};
|
|
33
51
|
|
|
34
52
|
export type UseCodexAccountsResult = {
|
|
35
53
|
accounts: CodexAccount[];
|
|
@@ -60,7 +78,11 @@ export type UseCodexAccountsResult = {
|
|
|
60
78
|
mutationError: Error | null;
|
|
61
79
|
};
|
|
62
80
|
|
|
63
|
-
const EMPTY_SETTINGS: CodexRotationSettings = {
|
|
81
|
+
const EMPTY_SETTINGS: CodexRotationSettings = {
|
|
82
|
+
rotationEnabled: false,
|
|
83
|
+
rotationStrategy: "most_remaining",
|
|
84
|
+
activeCredentialId: null,
|
|
85
|
+
};
|
|
64
86
|
|
|
65
87
|
type CodexAccountsState = {
|
|
66
88
|
accounts: CodexAccount[];
|
|
@@ -87,15 +109,16 @@ const EMPTY_STATE: CodexAccountsState = {
|
|
|
87
109
|
*/
|
|
88
110
|
export function useCodexAccounts(options: UseCodexAccountsOptions = {}): UseCodexAccountsResult {
|
|
89
111
|
const { client, workspaceId } = useOpenGeni(options);
|
|
90
|
-
const codexClient =
|
|
112
|
+
const codexClient = options.codexClient ?? (client as unknown as CodexAccountsClientLike);
|
|
91
113
|
const sessionId = options.sessionId;
|
|
92
114
|
const sharedEvents = options.events;
|
|
93
115
|
|
|
94
116
|
const load = useCallback(async (): Promise<CodexAccountsState> => {
|
|
95
117
|
const accountsP = codexClient.listCodexAccounts(workspaceId);
|
|
96
|
-
const sessionP =
|
|
97
|
-
|
|
98
|
-
|
|
118
|
+
const sessionP =
|
|
119
|
+
sessionId && codexClient.getSession
|
|
120
|
+
? codexClient.getSession(workspaceId, sessionId).catch(() => null)
|
|
121
|
+
: Promise.resolve(null);
|
|
99
122
|
const [acc, session] = await Promise.all([accountsP, sessionP]);
|
|
100
123
|
return {
|
|
101
124
|
accounts: acc.accounts,
|
|
@@ -106,9 +129,16 @@ export function useCodexAccounts(options: UseCodexAccountsOptions = {}): UseCode
|
|
|
106
129
|
};
|
|
107
130
|
}, [codexClient, workspaceId, sessionId]);
|
|
108
131
|
|
|
109
|
-
const
|
|
110
|
-
|
|
111
|
-
|
|
132
|
+
const {
|
|
133
|
+
data: loadedData,
|
|
134
|
+
loading,
|
|
135
|
+
refresh,
|
|
136
|
+
} = usePolledValue(load, {
|
|
137
|
+
pollIntervalMs: options.pollIntervalMs,
|
|
138
|
+
enabled: options.enabled,
|
|
139
|
+
});
|
|
140
|
+
const { run: runMutation, mutating: pinning, mutationError } = useMutationRunner();
|
|
141
|
+
const { run: runUsageMutation, mutating: refreshingUsage } = useMutationRunner();
|
|
112
142
|
const [pinningTarget, setPinningTarget] = useState<string | null>(null);
|
|
113
143
|
|
|
114
144
|
// Live flip: a manual switch (P1) or a failover (P3) emits codex.account.switched;
|
|
@@ -118,8 +148,11 @@ export function useCodexAccounts(options: UseCodexAccountsOptions = {}): UseCode
|
|
|
118
148
|
workspaceId,
|
|
119
149
|
sessionId,
|
|
120
150
|
isCodexAccountEvent,
|
|
121
|
-
() => void
|
|
122
|
-
{
|
|
151
|
+
() => void refresh(),
|
|
152
|
+
{
|
|
153
|
+
enabled: options.enabled ?? true,
|
|
154
|
+
...(sharedEvents !== undefined ? { events: sharedEvents } : {}),
|
|
155
|
+
},
|
|
123
156
|
);
|
|
124
157
|
|
|
125
158
|
const pin = useCallback(
|
|
@@ -128,36 +161,33 @@ export function useCodexAccounts(options: UseCodexAccountsOptions = {}): UseCode
|
|
|
128
161
|
return false;
|
|
129
162
|
}
|
|
130
163
|
setPinningTarget(target);
|
|
131
|
-
const result = await
|
|
164
|
+
const result = await runMutation(async () => {
|
|
132
165
|
await codexClient.pinSessionCodexAccount!(workspaceId, sessionId, target);
|
|
133
166
|
return true;
|
|
134
167
|
});
|
|
135
168
|
setPinningTarget(null);
|
|
136
|
-
if (result) await
|
|
169
|
+
if (result) await refresh();
|
|
137
170
|
return result === true;
|
|
138
171
|
},
|
|
139
|
-
[codexClient, workspaceId, sessionId,
|
|
172
|
+
[codexClient, workspaceId, sessionId, runMutation, refresh],
|
|
140
173
|
);
|
|
141
174
|
|
|
142
175
|
// Live usage refresh: hit the batched provider read, then re-read cached
|
|
143
176
|
// metadata so the fresh windows land on `accounts`. The provider read writes the
|
|
144
177
|
// cache columns server-side; state.refresh() pulls them back.
|
|
145
|
-
const refreshUsage = useCallback(
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
},
|
|
157
|
-
[codexClient, workspaceId, usageMutation.run, state.refresh],
|
|
158
|
-
);
|
|
178
|
+
const refreshUsage = useCallback(async (): Promise<boolean> => {
|
|
179
|
+
if (!codexClient.refreshCodexUsage) {
|
|
180
|
+
return false;
|
|
181
|
+
}
|
|
182
|
+
const result = await runUsageMutation(async () => {
|
|
183
|
+
await codexClient.refreshCodexUsage!(workspaceId);
|
|
184
|
+
return true;
|
|
185
|
+
});
|
|
186
|
+
if (result) await refresh();
|
|
187
|
+
return result === true;
|
|
188
|
+
}, [codexClient, workspaceId, runUsageMutation, refresh]);
|
|
159
189
|
|
|
160
|
-
const data =
|
|
190
|
+
const data = loadedData ?? EMPTY_STATE;
|
|
161
191
|
const effectiveAccountId = data.pinnedAccountId ?? data.activeAccountId;
|
|
162
192
|
|
|
163
193
|
return {
|
|
@@ -167,13 +197,13 @@ export function useCodexAccounts(options: UseCodexAccountsOptions = {}): UseCode
|
|
|
167
197
|
effectiveAccountId,
|
|
168
198
|
lastAccountId: data.lastAccountId,
|
|
169
199
|
settings: data.settings,
|
|
170
|
-
loading
|
|
171
|
-
refresh
|
|
200
|
+
loading,
|
|
201
|
+
refresh,
|
|
172
202
|
refreshUsage,
|
|
173
|
-
refreshingUsage
|
|
203
|
+
refreshingUsage,
|
|
174
204
|
pin,
|
|
175
|
-
pinning
|
|
205
|
+
pinning,
|
|
176
206
|
pinningTarget,
|
|
177
|
-
mutationError
|
|
207
|
+
mutationError,
|
|
178
208
|
};
|
|
179
209
|
}
|