@opengeni/react 0.13.0 → 0.20.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 +129 -23
- package/dist/chunk-6BXBGJ3B.js +1956 -0
- package/dist/chunk-6BXBGJ3B.js.map +1 -0
- package/dist/chunk-6UT5OC3P.js +2133 -0
- package/dist/chunk-6UT5OC3P.js.map +1 -0
- package/dist/chunk-EBTT3MYN.js +92 -0
- package/dist/chunk-EBTT3MYN.js.map +1 -0
- package/dist/chunk-HHFA4AFX.js +425 -0
- package/dist/chunk-HHFA4AFX.js.map +1 -0
- package/dist/chunk-LAZ2A743.js +2393 -0
- package/dist/chunk-LAZ2A743.js.map +1 -0
- package/dist/chunk-MRSECXRP.js +563 -0
- package/dist/chunk-MRSECXRP.js.map +1 -0
- package/dist/chunk-TK7G6XLT.js +18 -0
- package/dist/chunk-TK7G6XLT.js.map +1 -0
- package/dist/composer-DvUaa5ki.d.ts +585 -0
- package/dist/composer.d.ts +6 -0
- package/dist/composer.js +56 -0
- package/dist/composer.js.map +1 -0
- package/dist/index.d.ts +642 -1080
- package/dist/index.js +7672 -8005
- package/dist/index.js.map +1 -1
- package/dist/machines.d.ts +412 -3
- package/dist/machines.js +25 -1
- package/dist/noto-sans-arabic-loader-IA2T4X2A.js +21 -0
- package/dist/noto-sans-arabic-loader-IA2T4X2A.js.map +1 -0
- package/dist/noto-sans-jp-loader.generated-HFGLYBR3.js +292 -0
- package/dist/noto-sans-jp-loader.generated-HFGLYBR3.js.map +1 -0
- package/dist/queue-surface-implementation-NQMV2ML3.js +890 -0
- package/dist/queue-surface-implementation-NQMV2ML3.js.map +1 -0
- package/dist/session-Gd4iVPYw.d.ts +425 -0
- package/dist/session-context-DIFFlXKc.d.ts +52 -0
- package/dist/session.d.ts +4 -0
- package/dist/session.js +50 -0
- package/dist/session.js.map +1 -0
- package/dist/use-composer-BCfm4mzX.d.ts +112 -0
- package/package.json +23 -4
- package/src/approvals.ts +5 -3
- package/src/client.ts +50 -1
- package/src/components/approval-surface.tsx +198 -0
- package/src/components/chat-composer.tsx +104 -751
- package/src/components/code-editor.tsx +104 -26
- package/src/components/command-palette.tsx +8 -2
- package/src/components/composer-transcription-control.tsx +211 -0
- package/src/components/composer.tsx +1536 -0
- package/src/components/desktop-viewer.tsx +3 -3
- package/src/components/enrollment-consent.tsx +2 -2
- package/src/components/file-browser.tsx +367 -29
- package/src/components/human-input-form.tsx +417 -0
- package/src/components/machine-card.tsx +82 -16
- package/src/components/machine-health-pill.tsx +68 -0
- package/src/components/machine-metrics.tsx +10 -24
- 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 +16 -4
- package/src/components/message-timeline.tsx +15 -7
- package/src/components/model-picker.tsx +12 -3
- package/src/components/pierre-diff.tsx +32 -6
- package/src/components/queue-surface-implementation.tsx +1041 -0
- package/src/components/queue-surface-state.tsx +94 -0
- package/src/components/queue-surface.tsx +60 -0
- package/src/components/sandbox-files.tsx +230 -21
- package/src/components/sandbox-terminal.tsx +8 -2
- package/src/components/sandbox-workspace.tsx +489 -135
- package/src/components/session-status.tsx +0 -6
- package/src/components/workbench-changes.tsx +145 -50
- package/src/components/workspace-dock.tsx +329 -68
- package/src/composer.ts +60 -0
- package/src/hooks/internal.ts +115 -34
- package/src/hooks/use-composer.ts +635 -74
- package/src/hooks/use-human-input.ts +131 -0
- package/src/hooks/use-machine-chip.ts +2 -2
- package/src/hooks/use-machines.ts +27 -11
- package/src/hooks/use-sandbox-files.ts +310 -62
- package/src/hooks/use-sandbox-git.ts +154 -40
- package/src/hooks/use-sandbox-terminal.ts +28 -6
- package/src/hooks/use-session-capabilities.ts +38 -9
- package/src/hooks/use-session-control.ts +43 -15
- package/src/hooks/use-session-events.ts +523 -48
- package/src/hooks/use-session-lineage.ts +15 -6
- package/src/hooks/use-session.ts +10 -2
- package/src/hooks/use-slash-commands.ts +38 -38
- package/src/hooks/use-transcription.ts +757 -0
- package/src/hooks/use-turn-queue.ts +273 -77
- package/src/hooks/use-windowed-sections.ts +2 -2
- package/src/hooks/use-workspace-capture.ts +146 -40
- package/src/hooks/use-workspace-edit.ts +50 -14
- package/src/hooks/use-workspace-sessions.ts +3 -0
- package/src/human-input.ts +72 -0
- package/src/index.ts +88 -12
- package/src/lib/noto-sans-arabic-loader.ts +21 -0
- package/src/lib/noto-sans-jp-loader.generated.ts +295 -0
- package/src/lib/use-portal-token-style.ts +51 -0
- package/src/lib/use-unicode-fonts.ts +57 -0
- package/src/machines.ts +16 -0
- package/src/provider.tsx +163 -41
- package/src/session-context.ts +135 -0
- package/src/session.ts +79 -0
- package/src/timeline/parsers.ts +78 -6
- package/src/timeline/projection.ts +36 -6
- package/src/timeline/tool-renderers.tsx +41 -0
- package/src/timeline/turn-summary.tsx +2 -2
- package/src/timeline/types.ts +16 -8
- package/styles/index.css +78 -0
- package/styles/index.d.ts +2 -0
- package/styles/tokens.css +11 -6
- package/styles/tokens.d.ts +2 -0
- package/dist/chunk-NFYVQWIB.js +0 -1377
- package/dist/chunk-NFYVQWIB.js.map +0 -1
- package/dist/machines-CnlMb7E-.d.ts +0 -329
|
@@ -12,15 +12,23 @@
|
|
|
12
12
|
// (never an app context), notifications flow through an optional `onNotify` prop
|
|
13
13
|
// (no `sonner` import), and every surface renders with package primitives + og
|
|
14
14
|
// tokens only. `apps/web` consumes this through the exact public surface an
|
|
15
|
-
// external embedder
|
|
15
|
+
// external embedder uses — that is criterion F1.
|
|
16
16
|
import { type ReactNode, useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
17
17
|
import { Popover } from "radix-ui";
|
|
18
18
|
import type { SessionEvent } from "@opengeni/sdk";
|
|
19
|
-
import {
|
|
19
|
+
import {
|
|
20
|
+
CircleCheckIcon,
|
|
21
|
+
CpuIcon,
|
|
22
|
+
LaptopIcon,
|
|
23
|
+
LoaderCircleIcon,
|
|
24
|
+
RefreshCwIcon,
|
|
25
|
+
TriangleAlertIcon,
|
|
26
|
+
} from "lucide-react";
|
|
20
27
|
|
|
21
28
|
import { type ClientOverride, useOpenGeni } from "../provider";
|
|
22
29
|
import { cn } from "../lib/cn";
|
|
23
30
|
import { xtermThemeFromTokens } from "../lib/xterm-theme";
|
|
31
|
+
import { usePortalTokenStyle } from "../lib/use-portal-token-style";
|
|
24
32
|
import { useSessionCapabilities } from "../hooks/use-session-capabilities";
|
|
25
33
|
import { useSandboxFiles } from "../hooks/use-sandbox-files";
|
|
26
34
|
import { useSandboxGit, type UseSandboxGitResult } from "../hooks/use-sandbox-git";
|
|
@@ -44,6 +52,37 @@ export type WorkspaceNotification = { kind: "error" | "info"; message: string };
|
|
|
44
52
|
/** The workbench's canonical tab ids (a host injects extras around these). */
|
|
45
53
|
export const WORKBENCH_TAB_CHANGES = "changes";
|
|
46
54
|
export const WORKBENCH_TAB_FILES = "files";
|
|
55
|
+
export const WORKBENCH_TAB_TERMINAL = "terminal";
|
|
56
|
+
export const WORKBENCH_TAB_DESKTOP = "desktop";
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* The built-in workbench surfaces in their canonical display order. Hosts can
|
|
60
|
+
* pass a subset through `surfaces`; omission preserves the complete standalone
|
|
61
|
+
* workbench.
|
|
62
|
+
*/
|
|
63
|
+
export const WORKBENCH_SURFACES = [
|
|
64
|
+
WORKBENCH_TAB_CHANGES,
|
|
65
|
+
WORKBENCH_TAB_FILES,
|
|
66
|
+
WORKBENCH_TAB_TERMINAL,
|
|
67
|
+
WORKBENCH_TAB_DESKTOP,
|
|
68
|
+
] as const;
|
|
69
|
+
|
|
70
|
+
export type SandboxWorkspaceSurface = (typeof WORKBENCH_SURFACES)[number];
|
|
71
|
+
|
|
72
|
+
function isWorkbenchSurface(value: string): value is SandboxWorkspaceSurface {
|
|
73
|
+
return (WORKBENCH_SURFACES as readonly string[]).includes(value);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function sourceDrivenDefaultTab(
|
|
77
|
+
hasChanges: boolean,
|
|
78
|
+
changesEnabled: boolean,
|
|
79
|
+
filesEnabled: boolean,
|
|
80
|
+
): string | null {
|
|
81
|
+
if (hasChanges && changesEnabled) return WORKBENCH_TAB_CHANGES;
|
|
82
|
+
if (filesEnabled) return WORKBENCH_TAB_FILES;
|
|
83
|
+
if (changesEnabled) return WORKBENCH_TAB_CHANGES;
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
47
86
|
|
|
48
87
|
function captureDegradedMessage(reason: string): string {
|
|
49
88
|
switch (reason) {
|
|
@@ -62,11 +101,9 @@ function captureDegradedMessage(reason: string): string {
|
|
|
62
101
|
* "changes exist → Changes, else Files" needs zero machine round-trips. A host
|
|
63
102
|
* `override` (e.g. a landing "run" tab) wins when supplied.
|
|
64
103
|
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
* 2). This remains exported as a standalone helper for hosts that already hold
|
|
69
|
-
* the event log and want the same decision without the capture fetch.
|
|
104
|
+
* `<SandboxWorkspace>` uses this only as an optional early hint after its own
|
|
105
|
+
* capture GET reports no durable capture. A pure embedder needs no events-at-mount
|
|
106
|
+
* contract: it falls through to authoritative live Git instead.
|
|
70
107
|
*/
|
|
71
108
|
export function initialWorkspaceTab(
|
|
72
109
|
events: SessionEvent[] | undefined,
|
|
@@ -117,7 +154,9 @@ function sandboxProvisionInFlight(events: SessionEvent[]): boolean {
|
|
|
117
154
|
}
|
|
118
155
|
|
|
119
156
|
export type WorkspaceMachine = {
|
|
120
|
-
/**
|
|
157
|
+
/** Whether at least one built-in workbench surface is enabled. */
|
|
158
|
+
enabled: boolean;
|
|
159
|
+
/** The derived live/waking/offline chip model. */
|
|
121
160
|
chip: MachineChip;
|
|
122
161
|
/** The machine these surfaces are bound to (the Modal group box or a
|
|
123
162
|
* self-hosted machine), or null while the fleet is still resolving. */
|
|
@@ -131,22 +170,35 @@ export type UseSandboxWorkspaceTabsOptions = ClientOverride & {
|
|
|
131
170
|
sessionId: string;
|
|
132
171
|
/** Live event log (usually `useSessionEvents().events`). */
|
|
133
172
|
events: SessionEvent[];
|
|
134
|
-
/**
|
|
135
|
-
*
|
|
173
|
+
/**
|
|
174
|
+
* Built-in surfaces this host wants to expose. Omit for all four. A disabled
|
|
175
|
+
* surface is behaviorally dormant: its data hooks, warm intents, stream
|
|
176
|
+
* attachment, and navigation callbacks are not activated.
|
|
177
|
+
*/
|
|
178
|
+
surfaces?: readonly SandboxWorkspaceSurface[] | undefined;
|
|
179
|
+
/** Override the source-driven default tab (e.g. a host landing tab id). When
|
|
180
|
+
* omitted the workbench picks Changes-vs-Files from capture or live Git. */
|
|
136
181
|
initialTab?: string | null | undefined;
|
|
137
182
|
/** Host-routed notifications (mutation errors, desktop-consent failures). The
|
|
138
183
|
* package never imports a toast library — the host decides how to surface. */
|
|
139
184
|
onNotify?: ((notification: WorkspaceNotification) => void) | undefined;
|
|
185
|
+
/** File requested by a Changes guard. The Files surface defers the read until
|
|
186
|
+
* the sandbox is live, then reveals this path. */
|
|
187
|
+
requestedFilePath?: string | null | undefined;
|
|
188
|
+
/** Unique identity for `requestedFilePath`, including repeated requests for the
|
|
189
|
+
* same path. */
|
|
190
|
+
requestedFileRequestId?: string | number | null | undefined;
|
|
191
|
+
/** Route a guarded diff into the host's Files tab. */
|
|
192
|
+
onOpenFile?: ((path: string) => void) | undefined;
|
|
140
193
|
};
|
|
141
194
|
|
|
142
195
|
export type UseSandboxWorkspaceTabsResult = {
|
|
143
196
|
/** Changes | Files | Terminal | Desktop (capability-gated where noted). */
|
|
144
197
|
tabs: WorkspaceTab[];
|
|
145
|
-
/** The
|
|
146
|
-
* Files (a host `initialTab` overrides).
|
|
147
|
-
*
|
|
148
|
-
*
|
|
149
|
-
* never causes a post-render switch. */
|
|
198
|
+
/** The source-driven default tab: Changes when the first authoritative capture
|
|
199
|
+
* or live Git result has changes, else Files (a host `initialTab` overrides).
|
|
200
|
+
* null while that source resolves, or permanently when neither Changes nor
|
|
201
|
+
* Files is enabled; the choice latches once. */
|
|
150
202
|
defaultTab: string | null;
|
|
151
203
|
/** The machine-state model for the dock-header chip. */
|
|
152
204
|
machine: WorkspaceMachine;
|
|
@@ -156,7 +208,7 @@ type SessionWarmIntents = {
|
|
|
156
208
|
sessionId: string;
|
|
157
209
|
watchDesktop: boolean;
|
|
158
210
|
warmTerminal: boolean;
|
|
159
|
-
|
|
211
|
+
warmFiles: boolean;
|
|
160
212
|
};
|
|
161
213
|
|
|
162
214
|
function emptyWarmIntents(sessionId: string): SessionWarmIntents {
|
|
@@ -164,7 +216,7 @@ function emptyWarmIntents(sessionId: string): SessionWarmIntents {
|
|
|
164
216
|
sessionId,
|
|
165
217
|
watchDesktop: false,
|
|
166
218
|
warmTerminal: false,
|
|
167
|
-
|
|
219
|
+
warmFiles: false,
|
|
168
220
|
};
|
|
169
221
|
}
|
|
170
222
|
|
|
@@ -177,20 +229,30 @@ export function useSandboxWorkspaceTabs(
|
|
|
177
229
|
options: UseSandboxWorkspaceTabsOptions,
|
|
178
230
|
): UseSandboxWorkspaceTabsResult {
|
|
179
231
|
const { client, workspaceId } = useOpenGeni(options);
|
|
180
|
-
const { sessionId, events, onNotify } =
|
|
232
|
+
const { sessionId, events, onNotify, requestedFilePath, requestedFileRequestId, onOpenFile } =
|
|
233
|
+
options;
|
|
181
234
|
const initialTab = options.initialTab ?? null;
|
|
235
|
+
const requestedSurfaces = options.surfaces ?? WORKBENCH_SURFACES;
|
|
236
|
+
const surfaceSet = new Set<SandboxWorkspaceSurface>(requestedSurfaces);
|
|
237
|
+
const changesEnabled = surfaceSet.has(WORKBENCH_TAB_CHANGES);
|
|
238
|
+
const filesEnabled = surfaceSet.has(WORKBENCH_TAB_FILES);
|
|
239
|
+
const terminalEnabled = surfaceSet.has(WORKBENCH_TAB_TERMINAL);
|
|
240
|
+
const desktopEnabled = surfaceSet.has(WORKBENCH_TAB_DESKTOP);
|
|
241
|
+
const workspaceDataEnabled = changesEnabled || filesEnabled;
|
|
242
|
+
const anySurfaceEnabled = workspaceDataEnabled || terminalEnabled || desktopEnabled;
|
|
243
|
+
const surfaceIdentity = WORKBENCH_SURFACES.filter((surface) => surfaceSet.has(surface)).join(",");
|
|
182
244
|
|
|
183
|
-
// The three
|
|
245
|
+
// The three box-warming INTENTS, each off by default and each
|
|
184
246
|
// flipped true by a genuine user action (never on mount, never on a passive
|
|
185
247
|
// capture glance): desktop watch consent, terminal engagement (`onActivate`), and
|
|
186
|
-
//
|
|
248
|
+
// a deliberate live-file open/edit in Files. Browsing capture-served
|
|
187
249
|
// Changes/Files warms nothing — that is the whole point of Refinement 1.
|
|
188
250
|
const [storedWarmIntents, setStoredWarmIntents] = useState<SessionWarmIntents>(() =>
|
|
189
251
|
emptyWarmIntents(sessionId),
|
|
190
252
|
);
|
|
191
253
|
const warmIntents =
|
|
192
254
|
storedWarmIntents.sessionId === sessionId ? storedWarmIntents : emptyWarmIntents(sessionId);
|
|
193
|
-
const { watchDesktop, warmTerminal,
|
|
255
|
+
const { watchDesktop, warmTerminal, warmFiles } = warmIntents;
|
|
194
256
|
const requestWarmIntent = useCallback(
|
|
195
257
|
(intent: Exclude<keyof SessionWarmIntents, "sessionId">) => {
|
|
196
258
|
setStoredWarmIntents((previous) => {
|
|
@@ -207,6 +269,7 @@ export function useSandboxWorkspaceTabs(
|
|
|
207
269
|
workspaceId,
|
|
208
270
|
sessionId,
|
|
209
271
|
pollIntervalMs: 8000,
|
|
272
|
+
enabled: anySurfaceEnabled,
|
|
210
273
|
});
|
|
211
274
|
const activeMachine: MachineView | null =
|
|
212
275
|
machines.machines.find((m) => m.sandboxId === machines.activeSandboxId) ??
|
|
@@ -215,11 +278,12 @@ export function useSandboxWorkspaceTabs(
|
|
|
215
278
|
|
|
216
279
|
const caps = useSessionCapabilities(sessionId, {
|
|
217
280
|
events,
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
//
|
|
222
|
-
|
|
281
|
+
enabled: anySurfaceEnabled,
|
|
282
|
+
attachDesktop: desktopEnabled && watchDesktop,
|
|
283
|
+
attachTerminal: terminalEnabled && warmTerminal,
|
|
284
|
+
// Explicit live-file intent only — NOT "the Files tab is open". A cold edit
|
|
285
|
+
// or guarded-file open wakes the box; a glance at the tree/diff does not.
|
|
286
|
+
attachFiles: workspaceDataEnabled && warmFiles,
|
|
223
287
|
});
|
|
224
288
|
const capabilities = caps.capabilities;
|
|
225
289
|
const liveness = capabilities?.liveness;
|
|
@@ -227,13 +291,17 @@ export function useSandboxWorkspaceTabs(
|
|
|
227
291
|
// The FS is writable only when it's live AND not read-only. A self-hosted box
|
|
228
292
|
// that's offline (or any read-only advertisement) or a capture-served cold tree
|
|
229
293
|
// must not offer create/rename/delete/edit affordances — you cannot mutate a
|
|
230
|
-
// machine you can't reach (
|
|
294
|
+
// machine you can't reach (C3). Tree-structure ops need a warm
|
|
231
295
|
// writable box; content editing on a cold CLOUD box is the wake-on-edit path in
|
|
232
296
|
// the editor, not tree mutation.
|
|
233
297
|
const fsReadOnly = capabilities?.FileSystem.readOnly ?? false;
|
|
234
|
-
|
|
298
|
+
// Mutations are live-only. A capture is a review artifact, never a writable
|
|
299
|
+
// surrogate for a sleeping machine; the user must deliberately open the live
|
|
300
|
+
// workspace before create/rename/delete/edit controls appear.
|
|
301
|
+
const filesEditable =
|
|
302
|
+
fileSystemOn && !fsReadOnly && (liveness === "warm" || liveness === "draining");
|
|
235
303
|
const gitOn = capabilities?.Git.available ?? false;
|
|
236
|
-
const terminalOn = (capabilities?.Terminal.transport ?? null) !== null;
|
|
304
|
+
const terminalOn = terminalEnabled && (capabilities?.Terminal.transport ?? null) !== null;
|
|
237
305
|
// The REAL interactive terminal is the ttyd pty-ws stream. When it's live the
|
|
238
306
|
// legacy HTTP PTY must NOT run; `useSandboxTerminal` opens its HTTP PTY only as
|
|
239
307
|
// the firehose-mode fallback (a backend without ttyd).
|
|
@@ -241,13 +309,14 @@ export function useSandboxWorkspaceTabs(
|
|
|
241
309
|
capabilities?.Terminal.transport === "pty-ws" && Boolean(capabilities?.Terminal.url);
|
|
242
310
|
const ptyCapable = (capabilities?.Terminal.ptyCapable ?? false) && !ptyWsLive;
|
|
243
311
|
const terminal = useSandboxTerminal(sessionId, {
|
|
244
|
-
events,
|
|
245
|
-
interactive: ptyCapable,
|
|
312
|
+
events: terminalEnabled ? events : [],
|
|
313
|
+
interactive: terminalEnabled && ptyCapable,
|
|
246
314
|
liveness,
|
|
247
315
|
});
|
|
248
316
|
const desktopAdvertised =
|
|
249
|
-
|
|
250
|
-
capabilities?.DesktopStream.
|
|
317
|
+
desktopEnabled &&
|
|
318
|
+
((capabilities?.DesktopStream.transport ?? null) !== null ||
|
|
319
|
+
capabilities?.DesktopStream.reason === "lease_cold");
|
|
251
320
|
|
|
252
321
|
// Lazy provisioning (#315) creates the box mid-turn on the first sandbox tool
|
|
253
322
|
// call, emitting sandbox.provision started→completed/failed on the live stream.
|
|
@@ -268,9 +337,27 @@ export function useSandboxWorkspaceTabs(
|
|
|
268
337
|
|
|
269
338
|
// The cold-paint data source: the latest turn-end capture, fetched with a single
|
|
270
339
|
// api round-trip on mount (no machine). Feeds the Files tree + the Changes/Git
|
|
271
|
-
// diff
|
|
272
|
-
|
|
340
|
+
// diff immediately; a warm box reconciles live without discarding the capture
|
|
341
|
+
// when that provider read is temporarily unavailable.
|
|
342
|
+
const captureState = useWorkspaceCapture(sessionId, {
|
|
343
|
+
events,
|
|
344
|
+
enabled: workspaceDataEnabled,
|
|
345
|
+
});
|
|
273
346
|
const captureAvailable = captureState.available;
|
|
347
|
+
const liveWorkspaceExpected = liveness === "warm" || liveness === "draining";
|
|
348
|
+
// Capability negotiation and capture resolution are independent requests. A
|
|
349
|
+
// cold capability document can win that race by a few milliseconds; enabling
|
|
350
|
+
// the leaf hooks in that window would make them interpret `capture: null` as a
|
|
351
|
+
// real miss and issue Channel-A reads, waking the very box the capture exists
|
|
352
|
+
// to avoid. Wait until the capture request has conclusively resolved. A warm
|
|
353
|
+
// workspace never waits on this server-side snapshot.
|
|
354
|
+
const capturePending =
|
|
355
|
+
!liveWorkspaceExpected && (captureState.fileCount === null || captureState.loading);
|
|
356
|
+
const repoPaths = useMemo(() => {
|
|
357
|
+
const advertised = capabilities?.Git.repos ?? [];
|
|
358
|
+
if (advertised.length > 0) return advertised;
|
|
359
|
+
return captureState.capture?.repos.map((repo) => repo.root) ?? [];
|
|
360
|
+
}, [capabilities?.Git.repos, captureState.capture]);
|
|
274
361
|
const notifiedCaptureDegradedReason = useRef<{
|
|
275
362
|
sessionId: string;
|
|
276
363
|
reason: string | null;
|
|
@@ -290,7 +377,9 @@ export function useSandboxWorkspaceTabs(
|
|
|
290
377
|
|
|
291
378
|
const files = useSandboxFiles(sessionId, {
|
|
292
379
|
events,
|
|
293
|
-
|
|
380
|
+
// No passive Channel-A reads while cold, even after a conclusive capture
|
|
381
|
+
// miss. A missing capture gets an explicit "Open live workspace" gate.
|
|
382
|
+
enabled: filesEnabled && (captureAvailable || (fileSystemOn && liveWorkspaceExpected)),
|
|
294
383
|
liveness,
|
|
295
384
|
capture: captureState.capture,
|
|
296
385
|
// A reverted optimistic mutation (e.g. a 409 rename collision) surfaces as a
|
|
@@ -303,14 +392,18 @@ export function useSandboxWorkspaceTabs(
|
|
|
303
392
|
});
|
|
304
393
|
const git = useSandboxGit(sessionId, {
|
|
305
394
|
events,
|
|
306
|
-
enabled:
|
|
395
|
+
enabled: workspaceDataEnabled && (captureAvailable || (gitOn && liveWorkspaceExpected)),
|
|
396
|
+
repoPaths,
|
|
307
397
|
liveness,
|
|
308
398
|
capture: captureState.capture,
|
|
309
399
|
});
|
|
310
400
|
const stagedGit = useSandboxGit(sessionId, {
|
|
311
401
|
events,
|
|
312
|
-
|
|
402
|
+
// Captures intentionally expose one combined review diff, not a staged
|
|
403
|
+
// index. Never probe the live index while the lease is cold.
|
|
404
|
+
enabled: filesEnabled && gitOn && liveWorkspaceExpected,
|
|
313
405
|
staged: true,
|
|
406
|
+
repoPaths,
|
|
314
407
|
liveness,
|
|
315
408
|
capture: captureState.capture,
|
|
316
409
|
});
|
|
@@ -319,7 +412,7 @@ export function useSandboxWorkspaceTabs(
|
|
|
319
412
|
// belongs in the package (an embedder's theme toggle drives it too).
|
|
320
413
|
const [xtermTheme, setXtermTheme] = useState<XtermTheme | undefined>(undefined);
|
|
321
414
|
useEffect(() => {
|
|
322
|
-
if (typeof document === "undefined") return;
|
|
415
|
+
if (!terminalEnabled || typeof document === "undefined") return;
|
|
323
416
|
const derive = () => setXtermTheme(xtermThemeFromTokens());
|
|
324
417
|
derive();
|
|
325
418
|
const observer = new MutationObserver(derive);
|
|
@@ -328,7 +421,7 @@ export function useSandboxWorkspaceTabs(
|
|
|
328
421
|
attributeFilter: ["data-og-theme", "class"],
|
|
329
422
|
});
|
|
330
423
|
return () => observer.disconnect();
|
|
331
|
-
}, []);
|
|
424
|
+
}, [terminalEnabled]);
|
|
332
425
|
|
|
333
426
|
async function acknowledgeAndWatch() {
|
|
334
427
|
try {
|
|
@@ -363,31 +456,79 @@ export function useSandboxWorkspaceTabs(
|
|
|
363
456
|
capabilitiesState: caps.state,
|
|
364
457
|
activeMachineState: activeMachine?.state ?? null,
|
|
365
458
|
activeIsSelfhosted: activeMachine?.kind === "selfhosted",
|
|
366
|
-
wantsWarm:
|
|
459
|
+
wantsWarm:
|
|
460
|
+
(terminalEnabled && warmTerminal) ||
|
|
461
|
+
(desktopEnabled && watchDesktop) ||
|
|
462
|
+
(workspaceDataEnabled && warmFiles),
|
|
367
463
|
capturedAt: captureState.capturedAt,
|
|
368
464
|
});
|
|
465
|
+
const workspaceWaking = chip.state === "waking";
|
|
369
466
|
|
|
370
|
-
// The pre-paint default tab
|
|
371
|
-
//
|
|
372
|
-
//
|
|
373
|
-
//
|
|
374
|
-
//
|
|
375
|
-
//
|
|
376
|
-
//
|
|
377
|
-
//
|
|
378
|
-
const
|
|
379
|
-
|
|
467
|
+
// The pre-paint default tab is decided from the first AUTHORITATIVE workspace
|
|
468
|
+
// source. A capture wins immediately on the cold/offline fast path; a warm box
|
|
469
|
+
// waits for live Git so a prior snapshot can never outrank the current tree.
|
|
470
|
+
// When the GET says no capture exists, `fileCount: 0` does NOT itself mean the
|
|
471
|
+
// working tree is clean. A captured-revision announce can
|
|
472
|
+
// resolve Changes earlier, but a pure embedder with no event preload still gets
|
|
473
|
+
// the correct live default. The choice latches once so later edits never steal
|
|
474
|
+
// the user's current tab.
|
|
475
|
+
const defaultIdentity = `${sessionId}\u0000${surfaceIdentity}`;
|
|
476
|
+
const defaultTabRef = useRef<{ identity: string; value: string | null }>({
|
|
477
|
+
identity: defaultIdentity,
|
|
380
478
|
value: null,
|
|
381
479
|
});
|
|
382
|
-
if (defaultTabRef.current.
|
|
383
|
-
defaultTabRef.current = {
|
|
480
|
+
if (defaultTabRef.current.identity !== defaultIdentity) {
|
|
481
|
+
defaultTabRef.current = { identity: defaultIdentity, value: null };
|
|
384
482
|
}
|
|
483
|
+
const captureIsAuthoritative =
|
|
484
|
+
!liveWorkspaceExpected && (liveness !== undefined || caps.error !== null);
|
|
485
|
+
const captureUnavailable = captureState.fileCount === 0 || captureState.error !== null;
|
|
385
486
|
if (defaultTabRef.current.value === null) {
|
|
386
|
-
if (initialTab) {
|
|
487
|
+
if (initialTab && (!isWorkbenchSurface(initialTab) || surfaceSet.has(initialTab))) {
|
|
387
488
|
defaultTabRef.current.value = initialTab;
|
|
388
|
-
} else if (
|
|
389
|
-
defaultTabRef.current.value =
|
|
390
|
-
|
|
489
|
+
} else if (git.source === "live") {
|
|
490
|
+
defaultTabRef.current.value = sourceDrivenDefaultTab(
|
|
491
|
+
git.diff.length > 0,
|
|
492
|
+
changesEnabled,
|
|
493
|
+
filesEnabled,
|
|
494
|
+
);
|
|
495
|
+
} else if (
|
|
496
|
+
captureIsAuthoritative &&
|
|
497
|
+
captureState.fileCount !== null &&
|
|
498
|
+
captureState.available
|
|
499
|
+
) {
|
|
500
|
+
defaultTabRef.current.value = sourceDrivenDefaultTab(
|
|
501
|
+
captureState.fileCount > 0,
|
|
502
|
+
changesEnabled,
|
|
503
|
+
filesEnabled,
|
|
504
|
+
);
|
|
505
|
+
} else if (
|
|
506
|
+
captureIsAuthoritative &&
|
|
507
|
+
captureUnavailable &&
|
|
508
|
+
initialWorkspaceTab(events) === WORKBENCH_TAB_CHANGES
|
|
509
|
+
) {
|
|
510
|
+
defaultTabRef.current.value = sourceDrivenDefaultTab(true, changesEnabled, filesEnabled);
|
|
511
|
+
} else if (captureUnavailable && caps.error) {
|
|
512
|
+
// The Changes surface owns the truthful sandbox-unavailable state + retry.
|
|
513
|
+
// Files would misleadingly describe this as a missing filesystem.
|
|
514
|
+
defaultTabRef.current.value = sourceDrivenDefaultTab(true, changesEnabled, filesEnabled);
|
|
515
|
+
} else if (captureIsAuthoritative && captureState.fileCount !== null && captureUnavailable) {
|
|
516
|
+
// No durable review surface exists and the machine is resting. Files owns
|
|
517
|
+
// the explicit wake gate; do not issue a live Git probe merely to choose a
|
|
518
|
+
// tab.
|
|
519
|
+
defaultTabRef.current.value = sourceDrivenDefaultTab(false, changesEnabled, filesEnabled);
|
|
520
|
+
} else if (captureState.available && git.error && captureState.fileCount !== null) {
|
|
521
|
+
// A warm live read failed but the durable capture is intact: retain an
|
|
522
|
+
// immediate, deterministic review surface instead of hanging unresolved.
|
|
523
|
+
defaultTabRef.current.value = sourceDrivenDefaultTab(
|
|
524
|
+
captureState.fileCount > 0,
|
|
525
|
+
changesEnabled,
|
|
526
|
+
filesEnabled,
|
|
527
|
+
);
|
|
528
|
+
} else if (captureUnavailable && git.error) {
|
|
529
|
+
// No capture and live Git failed: Files is the least surprising fallback
|
|
530
|
+
// and preserves the established no-capture degraded behavior.
|
|
531
|
+
defaultTabRef.current.value = sourceDrivenDefaultTab(false, changesEnabled, filesEnabled);
|
|
391
532
|
}
|
|
392
533
|
}
|
|
393
534
|
// null only during the brief pre-first-resolve window (no host override yet).
|
|
@@ -396,48 +537,82 @@ export function useSandboxWorkspaceTabs(
|
|
|
396
537
|
const tabs = useMemo(() => {
|
|
397
538
|
const list: WorkspaceTab[] = [];
|
|
398
539
|
|
|
399
|
-
// Changes —
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
540
|
+
// Changes — capture-backed and usable cold/offline when the host enables it.
|
|
541
|
+
if (changesEnabled)
|
|
542
|
+
list.push({
|
|
543
|
+
id: WORKBENCH_TAB_CHANGES,
|
|
544
|
+
label: "Changes",
|
|
545
|
+
badge: dirtyCount > 0 ? <DirtyBadge count={dirtyCount} /> : undefined,
|
|
546
|
+
content: (
|
|
547
|
+
<ChangesTabBody
|
|
548
|
+
git={git}
|
|
549
|
+
captureAvailable={captureAvailable}
|
|
550
|
+
captureRevision={captureState.revision}
|
|
551
|
+
capturePending={capturePending}
|
|
552
|
+
liveWorkspaceExpected={liveWorkspaceExpected}
|
|
553
|
+
workspaceWaking={workspaceWaking}
|
|
554
|
+
capabilitiesState={caps.state}
|
|
555
|
+
capabilitiesError={caps.error}
|
|
556
|
+
onRetry={caps.renegotiate}
|
|
557
|
+
onWake={() => requestWarmIntent("warmFiles")}
|
|
558
|
+
{...(filesEnabled && onOpenFile
|
|
559
|
+
? {
|
|
560
|
+
onOpenFile: (path: string) => {
|
|
561
|
+
requestWarmIntent("warmFiles");
|
|
562
|
+
onOpenFile(path);
|
|
563
|
+
},
|
|
564
|
+
}
|
|
565
|
+
: {})}
|
|
566
|
+
/>
|
|
567
|
+
),
|
|
568
|
+
});
|
|
417
569
|
|
|
418
|
-
// Files —
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
570
|
+
// Files — capture-backed cold tree; live warm when the host enables it.
|
|
571
|
+
if (filesEnabled)
|
|
572
|
+
list.push({
|
|
573
|
+
id: WORKBENCH_TAB_FILES,
|
|
574
|
+
label: "Files",
|
|
575
|
+
content: (
|
|
576
|
+
<SandboxFiles
|
|
577
|
+
key={sessionId}
|
|
578
|
+
files={files}
|
|
579
|
+
git={git}
|
|
580
|
+
stagedGit={stagedGit}
|
|
581
|
+
fileSystemAvailable={fileSystemOn || captureAvailable}
|
|
582
|
+
editable={
|
|
583
|
+
filesEditable && files.source === "live" && files.error === null && !files.loading
|
|
584
|
+
}
|
|
585
|
+
workspaceResting={
|
|
586
|
+
!liveWorkspaceExpected &&
|
|
587
|
+
liveness !== undefined &&
|
|
588
|
+
!capturePending &&
|
|
589
|
+
!captureAvailable &&
|
|
590
|
+
!workspaceWaking
|
|
591
|
+
}
|
|
592
|
+
workspaceWaking={!liveWorkspaceExpected && !captureAvailable && workspaceWaking}
|
|
593
|
+
liveWorkspaceReady={liveWorkspaceExpected}
|
|
594
|
+
onWakeWorkspace={() => requestWarmIntent("warmFiles")}
|
|
595
|
+
{...(requestedFilePath
|
|
596
|
+
? {
|
|
597
|
+
requestedPath: requestedFilePath,
|
|
598
|
+
...(requestedFileRequestId !== null && requestedFileRequestId !== undefined
|
|
599
|
+
? { requestedPathRequestId: requestedFileRequestId }
|
|
600
|
+
: {}),
|
|
601
|
+
requestedPathReady: liveness === "warm" || liveness === "draining",
|
|
602
|
+
}
|
|
603
|
+
: {})}
|
|
604
|
+
// Ordinary capture browsing does not warm a box. The first edit — or an
|
|
605
|
+
// explicit guarded-file open from Changes — is deliberate live-file intent.
|
|
606
|
+
onEditIntent={() => requestWarmIntent("warmFiles")}
|
|
607
|
+
className="h-full"
|
|
608
|
+
/>
|
|
609
|
+
),
|
|
610
|
+
});
|
|
436
611
|
|
|
437
612
|
// Terminal — capability-gated (appears after negotiation; never the default).
|
|
438
613
|
if (terminalOn) {
|
|
439
614
|
list.push({
|
|
440
|
-
id:
|
|
615
|
+
id: WORKBENCH_TAB_TERMINAL,
|
|
441
616
|
label: "Terminal",
|
|
442
617
|
content: (
|
|
443
618
|
<div className="h-full bg-og-bg p-1">
|
|
@@ -458,10 +633,10 @@ export function useSandboxWorkspaceTabs(
|
|
|
458
633
|
// Desktop — capability-gated + consent-gated.
|
|
459
634
|
if (desktopAdvertised) {
|
|
460
635
|
list.push({
|
|
461
|
-
id:
|
|
636
|
+
id: WORKBENCH_TAB_DESKTOP,
|
|
462
637
|
label: "Desktop",
|
|
463
638
|
badge: watchDesktop ? (
|
|
464
|
-
<span className="rounded-og-xs bg-og-status-running/20 px-1 text-
|
|
639
|
+
<span className="rounded-og-xs bg-og-status-running/20 px-1 text-og-xs text-og-status-running">
|
|
465
640
|
Live
|
|
466
641
|
</span>
|
|
467
642
|
) : undefined,
|
|
@@ -482,18 +657,26 @@ export function useSandboxWorkspaceTabs(
|
|
|
482
657
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
483
658
|
}, [
|
|
484
659
|
fileSystemOn,
|
|
660
|
+
changesEnabled,
|
|
661
|
+
filesEnabled,
|
|
485
662
|
terminalOn,
|
|
486
663
|
desktopAdvertised,
|
|
487
664
|
captureAvailable,
|
|
488
665
|
dirtyCount,
|
|
489
666
|
watchDesktop,
|
|
490
667
|
warmTerminal,
|
|
668
|
+
requestedFilePath,
|
|
669
|
+
requestedFileRequestId,
|
|
670
|
+
onOpenFile,
|
|
671
|
+
liveness,
|
|
672
|
+
sessionId,
|
|
491
673
|
files,
|
|
492
674
|
git,
|
|
493
675
|
stagedGit,
|
|
494
676
|
terminal,
|
|
495
677
|
xtermTheme,
|
|
496
678
|
capabilities,
|
|
679
|
+
workspaceWaking,
|
|
497
680
|
caps.state,
|
|
498
681
|
caps.error,
|
|
499
682
|
caps.viewerCapReached,
|
|
@@ -504,6 +687,7 @@ export function useSandboxWorkspaceTabs(
|
|
|
504
687
|
tabs,
|
|
505
688
|
defaultTab,
|
|
506
689
|
machine: {
|
|
690
|
+
enabled: anySurfaceEnabled,
|
|
507
691
|
chip,
|
|
508
692
|
activeMachine,
|
|
509
693
|
loading: machines.loading,
|
|
@@ -519,12 +703,17 @@ export type SandboxWorkspaceProps = ClientOverride & {
|
|
|
519
703
|
events: SessionEvent[];
|
|
520
704
|
/** The chat / primary pane shown beside the dock. */
|
|
521
705
|
primary: ReactNode;
|
|
706
|
+
/**
|
|
707
|
+
* Built-in surfaces this host wants to expose. Omit for Changes, Files,
|
|
708
|
+
* Terminal, and Desktop. Disabled surfaces remain behaviorally dormant.
|
|
709
|
+
*/
|
|
710
|
+
surfaces?: readonly SandboxWorkspaceSurface[] | undefined;
|
|
522
711
|
/** Host tabs injected BEFORE the workbench tabs (e.g. a "Run" landing tab). */
|
|
523
712
|
leadingTabs?: WorkspaceTab[] | undefined;
|
|
524
713
|
/** Host tabs injected AFTER the workbench tabs (e.g. a "Debug" tab). */
|
|
525
714
|
trailingTabs?: WorkspaceTab[] | undefined;
|
|
526
715
|
/** Override the pre-paint default tab (e.g. a host landing tab id). When
|
|
527
|
-
* omitted the workbench decides
|
|
716
|
+
* omitted the workbench decides from capture, then live Git when no capture exists. */
|
|
528
717
|
initialTab?: string | undefined;
|
|
529
718
|
/** Host-routed notifications (no toast dependency in the package). */
|
|
530
719
|
onNotify?: ((notification: WorkspaceNotification) => void) | undefined;
|
|
@@ -552,6 +741,7 @@ export function SandboxWorkspace(props: SandboxWorkspaceProps): ReactNode {
|
|
|
552
741
|
sessionId,
|
|
553
742
|
events,
|
|
554
743
|
primary,
|
|
744
|
+
surfaces,
|
|
555
745
|
leadingTabs,
|
|
556
746
|
trailingTabs,
|
|
557
747
|
initialTab,
|
|
@@ -566,6 +756,25 @@ export function SandboxWorkspace(props: SandboxWorkspaceProps): ReactNode {
|
|
|
566
756
|
className,
|
|
567
757
|
} = props;
|
|
568
758
|
|
|
759
|
+
const [storedSelection, setStoredSelection] = useState<{
|
|
760
|
+
sessionId: string;
|
|
761
|
+
tab: string;
|
|
762
|
+
} | null>(null);
|
|
763
|
+
const [requestedFile, setRequestedFile] = useState<{
|
|
764
|
+
sessionId: string;
|
|
765
|
+
path: string;
|
|
766
|
+
requestId: number;
|
|
767
|
+
} | null>(null);
|
|
768
|
+
const nextFileRequestId = useRef(0);
|
|
769
|
+
const openFile = useCallback(
|
|
770
|
+
(path: string) => {
|
|
771
|
+
nextFileRequestId.current += 1;
|
|
772
|
+
setRequestedFile({ sessionId, path, requestId: nextFileRequestId.current });
|
|
773
|
+
setStoredSelection({ sessionId, tab: WORKBENCH_TAB_FILES });
|
|
774
|
+
},
|
|
775
|
+
[sessionId],
|
|
776
|
+
);
|
|
777
|
+
|
|
569
778
|
const {
|
|
570
779
|
tabs: workbenchTabs,
|
|
571
780
|
machine,
|
|
@@ -575,22 +784,24 @@ export function SandboxWorkspace(props: SandboxWorkspaceProps): ReactNode {
|
|
|
575
784
|
...(props.workspaceId ? { workspaceId: props.workspaceId } : {}),
|
|
576
785
|
sessionId,
|
|
577
786
|
events,
|
|
787
|
+
...(surfaces ? { surfaces } : {}),
|
|
578
788
|
...(initialTab ? { initialTab } : {}),
|
|
579
789
|
...(onNotify ? { onNotify } : {}),
|
|
790
|
+
requestedFilePath: requestedFile?.sessionId === sessionId ? requestedFile.path : null,
|
|
791
|
+
requestedFileRequestId: requestedFile?.sessionId === sessionId ? requestedFile.requestId : null,
|
|
792
|
+
onOpenFile: openFile,
|
|
580
793
|
});
|
|
581
794
|
|
|
582
|
-
// A user's tab click wins forever; before that we follow the
|
|
795
|
+
// A user's tab click wins forever; before that we follow the source-driven
|
|
583
796
|
// default. While it is still resolving (null, pure-embedder pre-first-resolve)
|
|
584
797
|
// we pass no controlled tab, so the dock renders its own first-tab fallback
|
|
585
798
|
// (Changes) — whose body is a connecting/loading state until the capture lands,
|
|
586
799
|
// so committing the real default at first-resolve produces no CONTENT switch.
|
|
587
800
|
const tabs: WorkspaceTab[] = [...(leadingTabs ?? []), ...workbenchTabs, ...(trailingTabs ?? [])];
|
|
588
|
-
const [storedSelection, setStoredSelection] = useState<{
|
|
589
|
-
sessionId: string;
|
|
590
|
-
tab: string;
|
|
591
|
-
} | null>(null);
|
|
592
801
|
const selectedTab = storedSelection?.sessionId === sessionId ? storedSelection.tab : null;
|
|
593
|
-
const
|
|
802
|
+
const preferredTab = selectedTab ?? defaultTab;
|
|
803
|
+
const activeTab =
|
|
804
|
+
preferredTab && tabs.some((tab) => tab.id === preferredTab) ? preferredTab : tabs[0]?.id;
|
|
594
805
|
const selectTab = useCallback(
|
|
595
806
|
(tab: string) => setStoredSelection({ sessionId, tab }),
|
|
596
807
|
[sessionId],
|
|
@@ -602,14 +813,18 @@ export function SandboxWorkspace(props: SandboxWorkspaceProps): ReactNode {
|
|
|
602
813
|
tabs={tabs}
|
|
603
814
|
{...(activeTab !== undefined ? { activeTab } : {})}
|
|
604
815
|
onActiveTabChange={selectTab}
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
816
|
+
{...(machine.enabled
|
|
817
|
+
? {
|
|
818
|
+
headerAccessory: (
|
|
819
|
+
<MachineStateChip
|
|
820
|
+
chip={machine.chip}
|
|
821
|
+
activeMachine={machine.activeMachine}
|
|
822
|
+
error={machine.error}
|
|
823
|
+
onRetry={() => void machine.refresh()}
|
|
824
|
+
/>
|
|
825
|
+
),
|
|
826
|
+
}
|
|
827
|
+
: {})}
|
|
613
828
|
{...(mobileLeadingControl !== undefined ? { mobileLeadingControl } : {})}
|
|
614
829
|
{...(collapsed !== undefined ? { collapsed } : {})}
|
|
615
830
|
{...(onCollapsedChange ? { onCollapsedChange } : {})}
|
|
@@ -627,7 +842,9 @@ export type { WorkspaceDockProps };
|
|
|
627
842
|
|
|
628
843
|
function DirtyBadge({ count }: { count: number }) {
|
|
629
844
|
return (
|
|
630
|
-
<span className="rounded-og-xs bg-og-accent-soft px-1 text-
|
|
845
|
+
<span className="rounded-og-xs bg-og-accent-soft px-1 text-og-xs text-og-fg-muted">
|
|
846
|
+
{count}
|
|
847
|
+
</span>
|
|
631
848
|
);
|
|
632
849
|
}
|
|
633
850
|
|
|
@@ -645,7 +862,7 @@ function MachineKindIcon({
|
|
|
645
862
|
|
|
646
863
|
function chipDotClass(state: MachineChip["state"]): string {
|
|
647
864
|
if (state === "live") return "bg-og-status-running";
|
|
648
|
-
if (state === "waking") return "bg-og-status-idle animate-pulse";
|
|
865
|
+
if (state === "waking") return "bg-og-status-idle animate-pulse motion-reduce:animate-none";
|
|
649
866
|
return "bg-og-fg-subtle";
|
|
650
867
|
}
|
|
651
868
|
|
|
@@ -654,7 +871,7 @@ function chipDotClass(state: MachineChip["state"]): string {
|
|
|
654
871
|
* with a popover carrying the machine identity, connection state, the "shown as
|
|
655
872
|
* of <time>" staleness note, the shared-session disclosure, and a retry when the
|
|
656
873
|
* fleet failed to resolve (the old per-surface machine bar + Sandbox info tab,
|
|
657
|
-
* folded into one header affordance —
|
|
874
|
+
* folded into one header affordance — recommendation).
|
|
658
875
|
*/
|
|
659
876
|
function MachineStateChip({
|
|
660
877
|
chip,
|
|
@@ -667,13 +884,16 @@ function MachineStateChip({
|
|
|
667
884
|
error: Error | null;
|
|
668
885
|
onRetry: () => void;
|
|
669
886
|
}) {
|
|
887
|
+
const triggerRef = useRef<HTMLButtonElement | null>(null);
|
|
888
|
+
const portalStyle = usePortalTokenStyle(triggerRef);
|
|
670
889
|
return (
|
|
671
890
|
<Popover.Root>
|
|
672
891
|
<Popover.Trigger asChild>
|
|
673
892
|
<button
|
|
893
|
+
ref={triggerRef}
|
|
674
894
|
type="button"
|
|
675
895
|
aria-label={`Machine: ${chip.label}`}
|
|
676
|
-
className="inline-flex min-h-7 items-center gap-1.5 rounded-og-sm px-2 py-1 text-og-xs font-medium text-og-fg-muted transition-colors hover:bg-og-surface-2 hover:text-og-fg max-[1023px]:min-h-11 pointer-coarse:min-h-11"
|
|
896
|
+
className="inline-flex min-h-7 items-center gap-1.5 rounded-og-sm px-2 py-1 text-og-xs font-medium text-og-fg-muted 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]:min-h-11 pointer-coarse:min-h-11"
|
|
677
897
|
>
|
|
678
898
|
<span
|
|
679
899
|
className={cn("size-1.5 shrink-0 rounded-full", chipDotClass(chip.state))}
|
|
@@ -685,9 +905,12 @@ function MachineStateChip({
|
|
|
685
905
|
<Popover.Portal forceMount>
|
|
686
906
|
<Popover.Content
|
|
687
907
|
forceMount
|
|
908
|
+
data-machine-state-popover
|
|
688
909
|
align="end"
|
|
689
910
|
sideOffset={6}
|
|
690
|
-
|
|
911
|
+
collisionPadding={8}
|
|
912
|
+
className="og-root z-50 w-64 max-w-[calc(100vw-1rem)] rounded-og-md border border-og-border bg-og-surface-1 p-3 text-og-sm text-og-fg shadow-lg outline-none"
|
|
913
|
+
style={portalStyle}
|
|
691
914
|
>
|
|
692
915
|
<div className="flex min-w-0 items-center gap-1.5">
|
|
693
916
|
<MachineKindIcon kind={activeMachine?.kind} />
|
|
@@ -719,7 +942,7 @@ function MachineStateChip({
|
|
|
719
942
|
) : null}
|
|
720
943
|
{error ? (
|
|
721
944
|
<div className="mt-2.5 space-y-1.5 border-t border-og-border pt-2.5">
|
|
722
|
-
<p className="text-og-xs leading-4 text-og-
|
|
945
|
+
<p className="text-og-xs leading-4 text-og-danger">
|
|
723
946
|
Couldn't reach the sandbox for this session.
|
|
724
947
|
</p>
|
|
725
948
|
<DockActionButton onClick={onRetry}>
|
|
@@ -740,7 +963,7 @@ function DockActionButton({ onClick, children }: { onClick: () => void; children
|
|
|
740
963
|
<button
|
|
741
964
|
type="button"
|
|
742
965
|
onClick={onClick}
|
|
743
|
-
className="inline-flex items-center gap-1.5 rounded-og-sm border border-og-border px-2 py-1 text-og-xs font-medium text-og-fg-muted transition-colors hover:border-og-border-strong hover:text-og-fg pointer-coarse:min-h-
|
|
966
|
+
className="inline-flex items-center gap-1.5 rounded-og-sm border border-og-border px-2 py-1 text-og-xs font-medium text-og-fg-muted transition-colors hover:border-og-border-strong hover:text-og-fg focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-og-accent max-[1023px]:min-h-11 pointer-coarse:min-h-11"
|
|
744
967
|
>
|
|
745
968
|
{children}
|
|
746
969
|
</button>
|
|
@@ -757,35 +980,77 @@ function ChangesTabBody({
|
|
|
757
980
|
git,
|
|
758
981
|
captureAvailable,
|
|
759
982
|
captureRevision,
|
|
983
|
+
capturePending,
|
|
984
|
+
liveWorkspaceExpected,
|
|
985
|
+
workspaceWaking,
|
|
760
986
|
capabilitiesState,
|
|
761
987
|
capabilitiesError,
|
|
762
988
|
onRetry,
|
|
989
|
+
onWake,
|
|
990
|
+
onOpenFile,
|
|
763
991
|
}: {
|
|
764
992
|
git: UseSandboxGitResult;
|
|
765
993
|
captureAvailable: boolean;
|
|
766
994
|
captureRevision: number | null;
|
|
995
|
+
capturePending: boolean;
|
|
996
|
+
liveWorkspaceExpected: boolean;
|
|
997
|
+
workspaceWaking: boolean;
|
|
767
998
|
capabilitiesState: string;
|
|
768
999
|
capabilitiesError: Error | null;
|
|
769
1000
|
onRetry: () => void;
|
|
1001
|
+
onWake: () => void;
|
|
1002
|
+
onOpenFile?: ((path: string) => void) | undefined;
|
|
770
1003
|
}) {
|
|
771
1004
|
const diff = git.diff;
|
|
772
1005
|
|
|
773
1006
|
if (diff.length > 0) {
|
|
774
1007
|
return (
|
|
775
|
-
<
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
1008
|
+
<div className="flex h-full min-h-0 flex-col">
|
|
1009
|
+
{git.error ? (
|
|
1010
|
+
<div
|
|
1011
|
+
role="status"
|
|
1012
|
+
aria-live="polite"
|
|
1013
|
+
data-opengeni-changes-degraded
|
|
1014
|
+
className="flex shrink-0 items-center gap-2 border-b border-og-status-running/30 bg-og-status-running/10 px-2 py-1.5 text-og-xs text-og-fg-muted"
|
|
1015
|
+
>
|
|
1016
|
+
<TriangleAlertIcon className="size-3.5 shrink-0 text-og-status-running" aria-hidden />
|
|
1017
|
+
<span className="min-w-0 flex-1">
|
|
1018
|
+
{git.source === "capture"
|
|
1019
|
+
? "Live changes are temporarily unavailable. Showing the latest captured revision."
|
|
1020
|
+
: "Live refresh failed. Showing the last loaded changes."}
|
|
1021
|
+
</span>
|
|
1022
|
+
<button
|
|
1023
|
+
type="button"
|
|
1024
|
+
onClick={() => void git.refresh()}
|
|
1025
|
+
disabled={git.loading}
|
|
1026
|
+
className="inline-flex min-h-7 shrink-0 items-center gap-1 rounded-og-sm border border-og-border bg-og-surface-1 px-2 font-medium text-og-fg transition-colors hover:border-og-border-strong focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-og-accent disabled:cursor-not-allowed disabled:opacity-50 pointer-coarse:min-h-11"
|
|
1027
|
+
>
|
|
1028
|
+
<RefreshCwIcon
|
|
1029
|
+
className={cn("size-3", git.loading && "animate-spin motion-reduce:animate-none")}
|
|
1030
|
+
aria-hidden
|
|
1031
|
+
/>
|
|
1032
|
+
Retry
|
|
1033
|
+
</button>
|
|
1034
|
+
</div>
|
|
1035
|
+
) : null}
|
|
1036
|
+
<div className="min-h-0 flex-1">
|
|
1037
|
+
<WorkbenchChanges
|
|
1038
|
+
diff={diff}
|
|
1039
|
+
source={git.source}
|
|
1040
|
+
capturedAt={git.capturedAt}
|
|
1041
|
+
captureRevision={captureRevision}
|
|
1042
|
+
{...(onOpenFile ? { onOpenFile } : {})}
|
|
1043
|
+
/>
|
|
1044
|
+
</div>
|
|
1045
|
+
</div>
|
|
781
1046
|
);
|
|
782
1047
|
}
|
|
783
1048
|
|
|
784
1049
|
if (capabilitiesError && !captureAvailable) {
|
|
785
1050
|
return (
|
|
786
|
-
<CenteredState>
|
|
1051
|
+
<CenteredState icon={<TriangleAlertIcon className="size-5" aria-hidden />} tone="danger">
|
|
787
1052
|
<p className="text-og-sm font-medium text-og-fg">Sandbox unavailable</p>
|
|
788
|
-
<p className="text-og-sm leading-5 text-og-fg-muted">
|
|
1053
|
+
<p data-contrast-audited className="text-og-sm leading-5 text-og-fg-muted">
|
|
789
1054
|
{capabilitiesError.message || "Couldn't reach the sandbox for this session."}
|
|
790
1055
|
</p>
|
|
791
1056
|
<DockActionButton onClick={onRetry}>
|
|
@@ -796,28 +1061,117 @@ function ChangesTabBody({
|
|
|
796
1061
|
);
|
|
797
1062
|
}
|
|
798
1063
|
|
|
799
|
-
if (
|
|
1064
|
+
if (
|
|
1065
|
+
(capturePending || capabilitiesState === "negotiating") &&
|
|
1066
|
+
!captureAvailable &&
|
|
1067
|
+
!workspaceWaking
|
|
1068
|
+
) {
|
|
800
1069
|
return (
|
|
801
|
-
<CenteredState
|
|
802
|
-
|
|
1070
|
+
<CenteredState
|
|
1071
|
+
icon={
|
|
1072
|
+
<LoaderCircleIcon
|
|
1073
|
+
className="size-5 animate-spin motion-reduce:animate-none"
|
|
1074
|
+
aria-hidden
|
|
1075
|
+
/>
|
|
1076
|
+
}
|
|
1077
|
+
>
|
|
1078
|
+
<p className="text-og-sm font-medium text-og-fg">Connecting workspace</p>
|
|
1079
|
+
<p className="text-og-sm leading-5 text-og-fg-subtle">
|
|
1080
|
+
Looking for the latest files and changes…
|
|
1081
|
+
</p>
|
|
1082
|
+
</CenteredState>
|
|
1083
|
+
);
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
if (!liveWorkspaceExpected && !captureAvailable) {
|
|
1087
|
+
return (
|
|
1088
|
+
<CenteredState
|
|
1089
|
+
icon={
|
|
1090
|
+
workspaceWaking ? (
|
|
1091
|
+
<LoaderCircleIcon
|
|
1092
|
+
className="size-5 animate-spin motion-reduce:animate-none"
|
|
1093
|
+
aria-hidden
|
|
1094
|
+
/>
|
|
1095
|
+
) : (
|
|
1096
|
+
<CpuIcon className="size-5" aria-hidden />
|
|
1097
|
+
)
|
|
1098
|
+
}
|
|
1099
|
+
>
|
|
1100
|
+
<p className="text-og-sm font-medium text-og-fg">
|
|
1101
|
+
{workspaceWaking ? "Waking workspace" : "Workspace is resting"}
|
|
1102
|
+
</p>
|
|
1103
|
+
<p className="text-og-sm leading-5 text-og-fg-subtle">
|
|
1104
|
+
{workspaceWaking
|
|
1105
|
+
? "Connecting to the live working tree…"
|
|
1106
|
+
: "No captured revision is available yet. Wake the sandbox to inspect current changes."}
|
|
1107
|
+
</p>
|
|
1108
|
+
{!workspaceWaking ? (
|
|
1109
|
+
<DockActionButton onClick={onWake}>
|
|
1110
|
+
<CpuIcon className="size-3" />
|
|
1111
|
+
Open live workspace
|
|
1112
|
+
</DockActionButton>
|
|
1113
|
+
) : null}
|
|
1114
|
+
</CenteredState>
|
|
1115
|
+
);
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
if (git.loading && git.source === null) {
|
|
1119
|
+
return (
|
|
1120
|
+
<CenteredState
|
|
1121
|
+
icon={
|
|
1122
|
+
<LoaderCircleIcon
|
|
1123
|
+
className="size-5 animate-spin motion-reduce:animate-none"
|
|
1124
|
+
aria-hidden
|
|
1125
|
+
/>
|
|
1126
|
+
}
|
|
1127
|
+
>
|
|
1128
|
+
<p className="text-og-sm font-medium text-og-fg">Loading workspace</p>
|
|
1129
|
+
<p className="text-og-sm leading-5 text-og-fg-subtle">Reading the current working tree…</p>
|
|
803
1130
|
</CenteredState>
|
|
804
1131
|
);
|
|
805
1132
|
}
|
|
806
1133
|
|
|
807
1134
|
return (
|
|
808
|
-
<CenteredState>
|
|
809
|
-
<p className="text-og-sm font-medium text-og-fg">
|
|
1135
|
+
<CenteredState icon={<CircleCheckIcon className="size-5" aria-hidden />} tone="success">
|
|
1136
|
+
<p className="text-og-sm font-medium text-og-fg">Working tree is clean</p>
|
|
810
1137
|
<p className="text-og-sm leading-5 text-og-fg-subtle">
|
|
811
|
-
File edits from
|
|
1138
|
+
File edits from future turns will appear here.
|
|
812
1139
|
</p>
|
|
813
1140
|
</CenteredState>
|
|
814
1141
|
);
|
|
815
1142
|
}
|
|
816
1143
|
|
|
817
|
-
function CenteredState({
|
|
1144
|
+
function CenteredState({
|
|
1145
|
+
children,
|
|
1146
|
+
icon,
|
|
1147
|
+
tone = "neutral",
|
|
1148
|
+
}: {
|
|
1149
|
+
children: ReactNode;
|
|
1150
|
+
icon?: ReactNode | undefined;
|
|
1151
|
+
tone?: "neutral" | "success" | "danger";
|
|
1152
|
+
}) {
|
|
818
1153
|
return (
|
|
819
|
-
<div
|
|
820
|
-
|
|
1154
|
+
<div
|
|
1155
|
+
role={tone === "danger" ? "alert" : "status"}
|
|
1156
|
+
aria-atomic="true"
|
|
1157
|
+
aria-live={tone === "danger" ? "assertive" : "polite"}
|
|
1158
|
+
className="grid h-full place-items-center p-6 text-center"
|
|
1159
|
+
>
|
|
1160
|
+
<div className="flex max-w-sm flex-col items-center gap-2.5">
|
|
1161
|
+
{icon ? (
|
|
1162
|
+
<span
|
|
1163
|
+
className={cn(
|
|
1164
|
+
"grid size-10 place-items-center rounded-og-lg border bg-og-surface-1 shadow-sm",
|
|
1165
|
+
tone === "success" && "border-og-status-idle/30 text-og-status-idle",
|
|
1166
|
+
tone === "danger" && "border-og-status-failed/30 text-og-status-failed",
|
|
1167
|
+
tone === "neutral" && "border-og-border text-og-fg-muted",
|
|
1168
|
+
)}
|
|
1169
|
+
>
|
|
1170
|
+
{icon}
|
|
1171
|
+
</span>
|
|
1172
|
+
) : null}
|
|
1173
|
+
{children}
|
|
1174
|
+
</div>
|
|
821
1175
|
</div>
|
|
822
1176
|
);
|
|
823
1177
|
}
|