@opengeni/react 0.11.0 → 0.13.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 +1 -1
- package/dist/{chunk-5C7RGAWA.js → chunk-NFYVQWIB.js} +170 -64
- package/dist/chunk-NFYVQWIB.js.map +1 -0
- package/dist/index.d.ts +791 -145
- package/dist/index.js +5447 -2213
- package/dist/index.js.map +1 -1
- package/dist/{machines-BeZ3bD3t.d.ts → machines-CnlMb7E-.d.ts} +5 -5
- package/dist/machines.d.ts +1 -1
- package/dist/machines.js +1 -1
- package/package.json +17 -12
- package/src/approvals.ts +16 -4
- package/src/client.ts +32 -5
- package/src/commands/index.ts +1 -7
- package/src/commands/registry.ts +46 -12
- package/src/components/chat-composer.tsx +135 -39
- 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 +16 -4
- package/src/components/machine-dock-bar.tsx +20 -8
- package/src/components/machine-metrics.tsx +31 -8
- package/src/components/machine-status-pill.tsx +26 -5
- package/src/components/machines-dashboard.tsx +8 -2
- 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/sandbox-files.tsx +110 -227
- package/src/components/sandbox-terminal.tsx +256 -88
- package/src/components/sandbox-workspace.tsx +823 -0
- package/src/components/session-status.tsx +28 -2
- package/src/components/workbench-changes.tsx +541 -0
- package/src/components/workspace-dock.tsx +85 -22
- 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 +57 -36
- 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 +49 -21
- package/src/hooks/use-session-events.ts +48 -20
- package/src/hooks/use-session-lineage.ts +119 -0
- package/src/hooks/use-session.ts +43 -28
- package/src/hooks/use-slash-commands.ts +6 -4
- package/src/hooks/use-turn-queue.ts +74 -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 +128 -23
- package/src/lib/format.ts +44 -6
- package/src/lib/xterm-renderer.ts +65 -0
- package/src/lib/xterm-theme.ts +224 -18
- package/src/machines.ts +13 -3
- package/src/provider.tsx +3 -1
- 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 +44 -10
- package/src/timeline/projection.ts +435 -84
- 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/tokens.css +2 -2
- package/dist/chunk-5C7RGAWA.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,
|
|
@@ -120,9 +132,11 @@ export function WorkspaceDock({
|
|
|
120
132
|
id: autoSaveId,
|
|
121
133
|
} as Parameters<typeof useDefaultLayout>[0]);
|
|
122
134
|
|
|
123
|
-
const
|
|
124
|
-
const tabIds = tabs.map((tab) => tab.id)
|
|
135
|
+
const requestedTab = activeTab ?? internalTab;
|
|
136
|
+
const tabIds = tabs.map((tab) => tab.id);
|
|
125
137
|
const firstTabId = tabs[0]?.id ?? "";
|
|
138
|
+
const requestedTabIsValid = tabIds.includes(requestedTab);
|
|
139
|
+
const current = requestedTabIsValid ? requestedTab : firstTabId;
|
|
126
140
|
const setTab = useCallback(
|
|
127
141
|
(id: string) => {
|
|
128
142
|
setInternalTab(id);
|
|
@@ -152,12 +166,12 @@ export function WorkspaceDock({
|
|
|
152
166
|
|
|
153
167
|
// Keep the active tab valid if the available tabs change.
|
|
154
168
|
useEffect(() => {
|
|
155
|
-
if (firstTabId && !
|
|
169
|
+
if (firstTabId && !requestedTabIsValid) {
|
|
156
170
|
setTab(firstTabId);
|
|
157
171
|
}
|
|
158
172
|
// Depend on tab identity, not the tab content objects. Session live events
|
|
159
173
|
// rebuild tab JSX frequently; only id changes can invalidate the active tab.
|
|
160
|
-
}, [
|
|
174
|
+
}, [firstTabId, requestedTabIsValid, setTab]);
|
|
161
175
|
|
|
162
176
|
const collapse = useCallback(() => {
|
|
163
177
|
dockPanelRef.current?.collapse();
|
|
@@ -203,6 +217,8 @@ export function WorkspaceDock({
|
|
|
203
217
|
tabs={tabs}
|
|
204
218
|
current={current}
|
|
205
219
|
onTab={setTab}
|
|
220
|
+
leading={mobileLeadingControl}
|
|
221
|
+
accessory={headerAccessory}
|
|
206
222
|
controls={
|
|
207
223
|
<ChromeButton onClick={collapse} title="Close workspace" label="Close workspace">
|
|
208
224
|
<XIcon className="size-4" />
|
|
@@ -220,6 +236,7 @@ export function WorkspaceDock({
|
|
|
220
236
|
tabs={tabs}
|
|
221
237
|
current={current}
|
|
222
238
|
onTab={setTab}
|
|
239
|
+
accessory={headerAccessory}
|
|
223
240
|
controls={
|
|
224
241
|
<>
|
|
225
242
|
<ChromeButton
|
|
@@ -227,7 +244,11 @@ export function WorkspaceDock({
|
|
|
227
244
|
title={maximized ? "Restore (Esc)" : "Maximize"}
|
|
228
245
|
label={maximized ? "Restore dock" : "Maximize dock"}
|
|
229
246
|
>
|
|
230
|
-
{maximized ?
|
|
247
|
+
{maximized ? (
|
|
248
|
+
<Minimize2Icon className="size-3.5" />
|
|
249
|
+
) : (
|
|
250
|
+
<Maximize2Icon className="size-3.5" />
|
|
251
|
+
)}
|
|
231
252
|
</ChromeButton>
|
|
232
253
|
{hostControlled ? null : (
|
|
233
254
|
<ChromeButton onClick={collapse} title="Collapse" label="Collapse dock">
|
|
@@ -299,11 +320,7 @@ export function WorkspaceDock({
|
|
|
299
320
|
)}
|
|
300
321
|
|
|
301
322
|
{/* 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
|
-
)}
|
|
323
|
+
{maximized && <div className="fixed inset-0 z-40 flex flex-col bg-og-bg">{dockChrome}</div>}
|
|
307
324
|
</div>
|
|
308
325
|
);
|
|
309
326
|
}
|
|
@@ -326,7 +343,7 @@ function ChromeButton({
|
|
|
326
343
|
onClick={onClick}
|
|
327
344
|
title={title}
|
|
328
345
|
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-
|
|
346
|
+
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 max-[1023px]:size-11 pointer-coarse:size-11"
|
|
330
347
|
>
|
|
331
348
|
{children}
|
|
332
349
|
</button>
|
|
@@ -337,34 +354,73 @@ function DockChrome({
|
|
|
337
354
|
tabs,
|
|
338
355
|
current,
|
|
339
356
|
onTab,
|
|
357
|
+
leading,
|
|
358
|
+
accessory,
|
|
340
359
|
controls,
|
|
341
360
|
}: {
|
|
342
361
|
tabs: WorkspaceTab[];
|
|
343
362
|
current: string;
|
|
344
363
|
onTab: (id: string) => void;
|
|
364
|
+
/** Host navigation at the start of mobile overlay chrome. */
|
|
365
|
+
leading?: ReactNode | undefined;
|
|
366
|
+
/** A status accessory (machine chip) between the tab strip and the controls. */
|
|
367
|
+
accessory?: ReactNode | undefined;
|
|
345
368
|
/** Right-aligned chrome controls (maximize / collapse, or the overlay close). */
|
|
346
369
|
controls: ReactNode;
|
|
347
370
|
}) {
|
|
348
371
|
const active = tabs.find((t) => t.id === current) ?? tabs[0];
|
|
372
|
+
const activeIndex = tabs.findIndex((tab) => tab.id === active?.id);
|
|
373
|
+
const tabsetId = useId();
|
|
374
|
+
const tabRefs = useRef<Array<HTMLButtonElement | null>>([]);
|
|
375
|
+
const panelId = `${tabsetId}-panel`;
|
|
376
|
+
|
|
377
|
+
const activateTab = (index: number) => {
|
|
378
|
+
const tab = tabs[index];
|
|
379
|
+
if (!tab) return;
|
|
380
|
+
onTab(tab.id);
|
|
381
|
+
tabRefs.current[index]?.focus();
|
|
382
|
+
};
|
|
383
|
+
|
|
384
|
+
const onTabKeyDown = (event: ReactKeyboardEvent<HTMLButtonElement>, index: number) => {
|
|
385
|
+
if (tabs.length === 0) return;
|
|
386
|
+
let nextIndex: number | null = null;
|
|
387
|
+
if (event.key === "ArrowRight") nextIndex = (index + 1) % tabs.length;
|
|
388
|
+
else if (event.key === "ArrowLeft") nextIndex = (index - 1 + tabs.length) % tabs.length;
|
|
389
|
+
else if (event.key === "Home") nextIndex = 0;
|
|
390
|
+
else if (event.key === "End") nextIndex = tabs.length - 1;
|
|
391
|
+
if (nextIndex === null) return;
|
|
392
|
+
event.preventDefault();
|
|
393
|
+
activateTab(nextIndex);
|
|
394
|
+
};
|
|
395
|
+
|
|
349
396
|
return (
|
|
350
397
|
<>
|
|
351
398
|
<div className="flex shrink-0 items-center gap-2 border-b border-og-border px-1.5 py-1">
|
|
399
|
+
{leading ? <div className="flex shrink-0 items-center">{leading}</div> : null}
|
|
352
400
|
{/* The tab list scrolls horizontally when it can't fit — it must never
|
|
353
401
|
grow into or overlap the chrome controls (they stay shrink-0). The
|
|
354
402
|
scrollbar is hidden to keep the strip calm. */}
|
|
355
403
|
<div
|
|
356
404
|
className="flex min-w-0 flex-1 items-center gap-0.5 overflow-x-auto [scrollbar-width:none] [&::-webkit-scrollbar]:hidden"
|
|
357
405
|
role="tablist"
|
|
406
|
+
aria-orientation="horizontal"
|
|
358
407
|
>
|
|
359
|
-
{tabs.map((tab) => (
|
|
408
|
+
{tabs.map((tab, index) => (
|
|
360
409
|
<button
|
|
361
410
|
key={tab.id}
|
|
411
|
+
ref={(node) => {
|
|
412
|
+
tabRefs.current[index] = node;
|
|
413
|
+
}}
|
|
414
|
+
id={`${tabsetId}-tab-${index}`}
|
|
362
415
|
type="button"
|
|
363
416
|
role="tab"
|
|
364
417
|
aria-selected={tab.id === current}
|
|
418
|
+
aria-controls={panelId}
|
|
419
|
+
tabIndex={tab.id === current ? 0 : -1}
|
|
365
420
|
onClick={() => onTab(tab.id)}
|
|
421
|
+
onKeyDown={(event) => onTabKeyDown(event, index)}
|
|
366
422
|
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-
|
|
423
|
+
"flex min-h-7 shrink-0 items-center gap-1 rounded-og-sm px-2 py-1 text-og-xs font-medium transition-colors max-[1023px]:min-h-11 max-[1023px]:min-w-11 pointer-coarse:min-h-11 pointer-coarse:min-w-11",
|
|
368
424
|
tab.id === current
|
|
369
425
|
? "bg-og-accent-soft text-og-fg"
|
|
370
426
|
: "text-og-fg-subtle hover:text-og-fg",
|
|
@@ -375,9 +431,16 @@ function DockChrome({
|
|
|
375
431
|
</button>
|
|
376
432
|
))}
|
|
377
433
|
</div>
|
|
434
|
+
{accessory ? <div className="flex shrink-0 items-center">{accessory}</div> : null}
|
|
378
435
|
<div className="flex shrink-0 items-center gap-0.5 text-og-fg-subtle">{controls}</div>
|
|
379
436
|
</div>
|
|
380
|
-
<div
|
|
437
|
+
<div
|
|
438
|
+
id={panelId}
|
|
439
|
+
aria-label={activeIndex < 0 ? "Workspace" : undefined}
|
|
440
|
+
aria-labelledby={activeIndex >= 0 ? `${tabsetId}-tab-${activeIndex}` : undefined}
|
|
441
|
+
className="min-h-0 min-w-0 flex-1 overflow-hidden"
|
|
442
|
+
role="tabpanel"
|
|
443
|
+
>
|
|
381
444
|
{active?.content}
|
|
382
445
|
</div>
|
|
383
446
|
</>
|
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
|
}
|
|
@@ -5,9 +5,8 @@ import { useOpenGeni, type ClientOverride } from "../provider";
|
|
|
5
5
|
export type ComposerSendExtras = Omit<SendMessageInput, "text" | "clientEventId">;
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* `steer` interrupts the running turn and injects the message now.
|
|
8
|
+
* Enter appends a prompt; Cmd/Ctrl+Enter steers it to the head and supersedes
|
|
9
|
+
* the current inference.
|
|
11
10
|
*/
|
|
12
11
|
export type ComposerMode = "queue" | "steer";
|
|
13
12
|
|
|
@@ -20,46 +19,42 @@ export type UseComposerOptions = ClientOverride & {
|
|
|
20
19
|
* surrounding UI state (attachment pickers, model selectors, ...).
|
|
21
20
|
*/
|
|
22
21
|
sendExtras?: ComposerSendExtras | (() => ComposerSendExtras) | undefined;
|
|
23
|
-
/** Initial delivery mode. Defaults to `"queue"`. */
|
|
24
|
-
defaultMode?: ComposerMode | undefined;
|
|
25
22
|
};
|
|
26
23
|
|
|
27
24
|
export type ComposerState = {
|
|
28
25
|
value: string;
|
|
29
26
|
setValue: (value: string) => void;
|
|
30
|
-
/** Send the draft (or
|
|
31
|
-
send: (text?: string) => Promise<boolean>;
|
|
27
|
+
/** Send the draft (or explicit text). Queue is the default; steer is explicit. */
|
|
28
|
+
send: (text?: string, mode?: ComposerMode) => Promise<boolean>;
|
|
32
29
|
sending: boolean;
|
|
33
30
|
canSend: boolean;
|
|
34
|
-
/**
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
interrupting: boolean;
|
|
31
|
+
/** Pause the session without deleting its prompt queue. */
|
|
32
|
+
pause: (reason?: string) => Promise<void>;
|
|
33
|
+
pausing: boolean;
|
|
34
|
+
resume: (reason?: string) => Promise<void>;
|
|
35
|
+
resuming: boolean;
|
|
40
36
|
error: Error | null;
|
|
41
37
|
clearError: () => void;
|
|
42
38
|
};
|
|
43
39
|
|
|
44
40
|
/**
|
|
45
|
-
* Draft + send +
|
|
41
|
+
* Draft + send + Pause/Resume state for the chat composer — the only
|
|
46
42
|
* human-to-agent input surface. The draft survives a failed send (nothing is
|
|
47
43
|
* more hostile than losing a typed message); each send carries a generated
|
|
48
44
|
* `clientEventId` so retries stay idempotent server-side.
|
|
49
45
|
*/
|
|
50
|
-
export function useComposer(
|
|
46
|
+
export function useComposer(
|
|
47
|
+
sessionId: string | null | undefined,
|
|
48
|
+
options: UseComposerOptions = {},
|
|
49
|
+
): ComposerState {
|
|
51
50
|
const { client, workspaceId } = useOpenGeni(options);
|
|
52
|
-
const defaultMode = options.defaultMode ?? "queue";
|
|
53
51
|
const [value, setValue] = useState("");
|
|
54
|
-
const [mode, setMode] = useState<ComposerMode>(defaultMode);
|
|
55
52
|
const [sending, setSending] = useState(false);
|
|
56
|
-
const [
|
|
53
|
+
const [pausing, setPausing] = useState(false);
|
|
54
|
+
const [resuming, setResuming] = useState(false);
|
|
57
55
|
const [error, setError] = useState<Error | null>(null);
|
|
58
56
|
const pendingClientEventId = useRef<string | null>(null);
|
|
59
57
|
const onSent = options.onSent;
|
|
60
|
-
// Read at send time so an in-flight send uses the mode chosen at submit.
|
|
61
|
-
const modeRef = useRef(mode);
|
|
62
|
-
modeRef.current = mode;
|
|
63
58
|
// Read through a ref so a new extras closure (created every render by
|
|
64
59
|
// callers passing inline functions) does not invalidate `send`.
|
|
65
60
|
const sendExtrasRef = useRef(options.sendExtras);
|
|
@@ -75,12 +70,11 @@ export function useComposer(sessionId: string | null | undefined, options: UseCo
|
|
|
75
70
|
pendingClientEventId.current = null;
|
|
76
71
|
setValue("");
|
|
77
72
|
setError(null);
|
|
78
|
-
setMode(defaultMode);
|
|
79
73
|
}
|
|
80
|
-
}, [targetKey
|
|
74
|
+
}, [targetKey]);
|
|
81
75
|
|
|
82
76
|
const send = useCallback(
|
|
83
|
-
async (explicit?: string): Promise<boolean> => {
|
|
77
|
+
async (explicit?: string, delivery: ComposerMode = "queue"): Promise<boolean> => {
|
|
84
78
|
const draftAtSend = value;
|
|
85
79
|
const text = (explicit ?? draftAtSend).trim();
|
|
86
80
|
// Resolve the extras once: a file-only message (empty text + ≥1 ready
|
|
@@ -101,7 +95,7 @@ export function useComposer(sessionId: string | null | undefined, options: UseCo
|
|
|
101
95
|
// carries a minimal default so the attachments still get delivered.
|
|
102
96
|
const sendText = text || FILE_ONLY_MESSAGE_TEXT;
|
|
103
97
|
const input = composeSendInput(sendText, pendingClientEventId.current, extras);
|
|
104
|
-
if (
|
|
98
|
+
if (delivery === "steer") {
|
|
105
99
|
await client.steerMessage(workspaceId, sessionId, input);
|
|
106
100
|
} else {
|
|
107
101
|
await client.sendMessage(workspaceId, sessionId, input);
|
|
@@ -131,22 +125,38 @@ export function useComposer(sessionId: string | null | undefined, options: UseCo
|
|
|
131
125
|
// on its `attachments.uploading` flag so a message never departs mid-upload.
|
|
132
126
|
const hasReadyResources = (resolveSendExtras(sendExtrasRef.current).resources?.length ?? 0) > 0;
|
|
133
127
|
|
|
134
|
-
const
|
|
128
|
+
const pause = useCallback(
|
|
135
129
|
async (reason?: string): Promise<void> => {
|
|
136
|
-
if (!sessionId ||
|
|
130
|
+
if (!sessionId || pausing) {
|
|
137
131
|
return;
|
|
138
132
|
}
|
|
139
|
-
|
|
133
|
+
setPausing(true);
|
|
140
134
|
setError(null);
|
|
141
135
|
try {
|
|
142
|
-
await client.
|
|
136
|
+
await client.pauseSession(workspaceId, sessionId, reason !== undefined ? { reason } : {});
|
|
143
137
|
} catch (cause) {
|
|
144
138
|
setError(cause instanceof Error ? cause : new Error(String(cause)));
|
|
145
139
|
} finally {
|
|
146
|
-
|
|
140
|
+
setPausing(false);
|
|
147
141
|
}
|
|
148
142
|
},
|
|
149
|
-
[client, workspaceId, sessionId,
|
|
143
|
+
[client, workspaceId, sessionId, pausing],
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
const resume = useCallback(
|
|
147
|
+
async (reason?: string): Promise<void> => {
|
|
148
|
+
if (!sessionId || resuming) return;
|
|
149
|
+
setResuming(true);
|
|
150
|
+
setError(null);
|
|
151
|
+
try {
|
|
152
|
+
await client.resumeSession(workspaceId, sessionId, reason !== undefined ? { reason } : {});
|
|
153
|
+
} catch (cause) {
|
|
154
|
+
setError(cause instanceof Error ? cause : new Error(String(cause)));
|
|
155
|
+
} finally {
|
|
156
|
+
setResuming(false);
|
|
157
|
+
}
|
|
158
|
+
},
|
|
159
|
+
[client, workspaceId, sessionId, resuming],
|
|
150
160
|
);
|
|
151
161
|
|
|
152
162
|
const updateValue = useCallback((next: string) => {
|
|
@@ -160,10 +170,10 @@ export function useComposer(sessionId: string | null | undefined, options: UseCo
|
|
|
160
170
|
send,
|
|
161
171
|
sending,
|
|
162
172
|
canSend: Boolean(sessionId) && !sending && (value.trim().length > 0 || hasReadyResources),
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
173
|
+
pause,
|
|
174
|
+
pausing,
|
|
175
|
+
resume,
|
|
176
|
+
resuming,
|
|
167
177
|
error,
|
|
168
178
|
clearError: useCallback(() => setError(null), []),
|
|
169
179
|
};
|
|
@@ -197,13 +207,24 @@ export function composeSendInput(
|
|
|
197
207
|
}
|
|
198
208
|
|
|
199
209
|
/** Submit on plain Enter; Shift+Enter inserts a newline. Exported for tests. */
|
|
200
|
-
export function shouldSubmitOnKey(event: {
|
|
210
|
+
export function shouldSubmitOnKey(event: {
|
|
211
|
+
key: string;
|
|
212
|
+
shiftKey: boolean;
|
|
213
|
+
metaKey?: boolean;
|
|
214
|
+
ctrlKey?: boolean;
|
|
215
|
+
nativeEvent?: { isComposing?: boolean };
|
|
216
|
+
}): boolean {
|
|
201
217
|
if (event.key !== "Enter" || event.shiftKey) {
|
|
202
218
|
return false;
|
|
203
219
|
}
|
|
204
220
|
return event.nativeEvent?.isComposing !== true;
|
|
205
221
|
}
|
|
206
222
|
|
|
223
|
+
/** Cmd/Ctrl+Enter steers; ordinary Enter appends to the queue. */
|
|
224
|
+
export function composerModeForKey(event: { metaKey?: boolean; ctrlKey?: boolean }): ComposerMode {
|
|
225
|
+
return event.metaKey || event.ctrlKey ? "steer" : "queue";
|
|
226
|
+
}
|
|
227
|
+
|
|
207
228
|
function generateClientEventId(): string {
|
|
208
229
|
const cryptoApi = globalThis.crypto;
|
|
209
230
|
if (cryptoApi && "randomUUID" in cryptoApi) {
|