@opengeni/react 0.15.0 → 0.23.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 +110 -10
- package/dist/{chunk-TOJR776I.js → chunk-6XUS5VFM.js} +45 -369
- package/dist/chunk-6XUS5VFM.js.map +1 -0
- package/dist/chunk-CATATY77.js +2393 -0
- package/dist/chunk-CATATY77.js.map +1 -0
- package/dist/chunk-I3BJZIG5.js +439 -0
- package/dist/chunk-I3BJZIG5.js.map +1 -0
- package/dist/chunk-TK7G6XLT.js +18 -0
- package/dist/chunk-TK7G6XLT.js.map +1 -0
- package/dist/chunk-UAHPKLB6.js +2237 -0
- package/dist/chunk-UAHPKLB6.js.map +1 -0
- package/dist/chunk-UQS7OQDE.js +591 -0
- package/dist/chunk-UQS7OQDE.js.map +1 -0
- package/dist/chunk-YDNWMKXO.js +102 -0
- package/dist/chunk-YDNWMKXO.js.map +1 -0
- package/dist/composer-CVm5uoUI.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 +1288 -1785
- package/dist/index.js +7140 -8817
- package/dist/index.js.map +1 -1
- package/dist/machines.d.ts +412 -3
- package/dist/machines.js +3 -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-3UW3MIIR.js +890 -0
- package/dist/queue-surface-implementation-3UW3MIIR.js.map +1 -0
- package/dist/session-Db00TRwl.d.ts +446 -0
- package/dist/session-context-rgrfElMl.d.ts +58 -0
- package/dist/session.d.ts +4 -0
- package/dist/session.js +54 -0
- package/dist/session.js.map +1 -0
- package/dist/use-composer-By1wn3MM.d.ts +99 -0
- package/package.json +20 -4
- package/src/approvals.ts +5 -3
- package/src/client.ts +44 -0
- package/src/components/approval-surface.tsx +198 -0
- package/src/components/chat-composer.tsx +94 -993
- 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 +1 -1
- package/src/components/machines-dashboard.tsx +3 -3
- package/src/components/message-timeline.tsx +205 -25
- package/src/components/model-picker.tsx +12 -3
- package/src/components/pierre-diff.tsx +32 -6
- package/src/components/queue-draft-policy.ts +18 -0
- package/src/components/queue-surface-implementation.tsx +1037 -0
- package/src/components/queue-surface-state.tsx +94 -0
- package/src/components/queue-surface.tsx +42 -559
- package/src/components/sandbox-files.tsx +138 -14
- package/src/components/sandbox-terminal.tsx +8 -2
- package/src/components/sandbox-workspace.tsx +339 -119
- package/src/components/workbench-changes.tsx +84 -33
- package/src/components/workspace-dock.tsx +188 -18
- package/src/composer.ts +60 -0
- package/src/hooks/internal.ts +115 -34
- package/src/hooks/use-composer.ts +385 -104
- 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 +37 -8
- package/src/hooks/use-session-events.ts +522 -48
- package/src/hooks/use-session-mcp-approval-policy.ts +161 -0
- package/src/hooks/use-slash-commands.ts +38 -38
- package/src/hooks/use-transcription.ts +757 -0
- package/src/hooks/use-turn-queue.ts +134 -66
- 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 +85 -6
- 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/provider.tsx +4 -69
- package/src/session-context.ts +160 -0
- package/src/session.ts +89 -0
- package/src/timeline/parsers.ts +35 -0
- package/src/timeline/projection.ts +12 -4
- package/src/timeline/tool-renderers.tsx +41 -0
- package/src/timeline/turn-summary.tsx +2 -2
- package/src/timeline/types.ts +10 -9
- package/styles/index.css +56 -0
- package/styles/index.d.ts +2 -0
- package/styles/tokens.css +11 -6
- package/styles/tokens.d.ts +2 -0
- package/dist/chunk-TOJR776I.js.map +0 -1
- package/dist/machines-BpdwuQcD.d.ts +0 -449
|
@@ -12,7 +12,7 @@
|
|
|
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";
|
|
@@ -28,6 +28,7 @@ import {
|
|
|
28
28
|
import { type ClientOverride, useOpenGeni } from "../provider";
|
|
29
29
|
import { cn } from "../lib/cn";
|
|
30
30
|
import { xtermThemeFromTokens } from "../lib/xterm-theme";
|
|
31
|
+
import { usePortalTokenStyle } from "../lib/use-portal-token-style";
|
|
31
32
|
import { useSessionCapabilities } from "../hooks/use-session-capabilities";
|
|
32
33
|
import { useSandboxFiles } from "../hooks/use-sandbox-files";
|
|
33
34
|
import { useSandboxGit, type UseSandboxGitResult } from "../hooks/use-sandbox-git";
|
|
@@ -51,6 +52,37 @@ export type WorkspaceNotification = { kind: "error" | "info"; message: string };
|
|
|
51
52
|
/** The workbench's canonical tab ids (a host injects extras around these). */
|
|
52
53
|
export const WORKBENCH_TAB_CHANGES = "changes";
|
|
53
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
|
+
}
|
|
54
86
|
|
|
55
87
|
function captureDegradedMessage(reason: string): string {
|
|
56
88
|
switch (reason) {
|
|
@@ -122,7 +154,9 @@ function sandboxProvisionInFlight(events: SessionEvent[]): boolean {
|
|
|
122
154
|
}
|
|
123
155
|
|
|
124
156
|
export type WorkspaceMachine = {
|
|
125
|
-
/**
|
|
157
|
+
/** Whether at least one built-in workbench surface is enabled. */
|
|
158
|
+
enabled: boolean;
|
|
159
|
+
/** The derived live/waking/offline chip model. */
|
|
126
160
|
chip: MachineChip;
|
|
127
161
|
/** The machine these surfaces are bound to (the Modal group box or a
|
|
128
162
|
* self-hosted machine), or null while the fleet is still resolving. */
|
|
@@ -136,6 +170,12 @@ export type UseSandboxWorkspaceTabsOptions = ClientOverride & {
|
|
|
136
170
|
sessionId: string;
|
|
137
171
|
/** Live event log (usually `useSessionEvents().events`). */
|
|
138
172
|
events: SessionEvent[];
|
|
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;
|
|
139
179
|
/** Override the source-driven default tab (e.g. a host landing tab id). When
|
|
140
180
|
* omitted the workbench picks Changes-vs-Files from capture or live Git. */
|
|
141
181
|
initialTab?: string | null | undefined;
|
|
@@ -157,7 +197,8 @@ export type UseSandboxWorkspaceTabsResult = {
|
|
|
157
197
|
tabs: WorkspaceTab[];
|
|
158
198
|
/** The source-driven default tab: Changes when the first authoritative capture
|
|
159
199
|
* or live Git result has changes, else Files (a host `initialTab` overrides).
|
|
160
|
-
* null while that source resolves
|
|
200
|
+
* null while that source resolves, or permanently when neither Changes nor
|
|
201
|
+
* Files is enabled; the choice latches once. */
|
|
161
202
|
defaultTab: string | null;
|
|
162
203
|
/** The machine-state model for the dock-header chip. */
|
|
163
204
|
machine: WorkspaceMachine;
|
|
@@ -191,6 +232,15 @@ export function useSandboxWorkspaceTabs(
|
|
|
191
232
|
const { sessionId, events, onNotify, requestedFilePath, requestedFileRequestId, onOpenFile } =
|
|
192
233
|
options;
|
|
193
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(",");
|
|
194
244
|
|
|
195
245
|
// The three box-warming INTENTS, each off by default and each
|
|
196
246
|
// flipped true by a genuine user action (never on mount, never on a passive
|
|
@@ -219,6 +269,7 @@ export function useSandboxWorkspaceTabs(
|
|
|
219
269
|
workspaceId,
|
|
220
270
|
sessionId,
|
|
221
271
|
pollIntervalMs: 8000,
|
|
272
|
+
enabled: anySurfaceEnabled,
|
|
222
273
|
});
|
|
223
274
|
const activeMachine: MachineView | null =
|
|
224
275
|
machines.machines.find((m) => m.sandboxId === machines.activeSandboxId) ??
|
|
@@ -227,11 +278,12 @@ export function useSandboxWorkspaceTabs(
|
|
|
227
278
|
|
|
228
279
|
const caps = useSessionCapabilities(sessionId, {
|
|
229
280
|
events,
|
|
230
|
-
|
|
231
|
-
|
|
281
|
+
enabled: anySurfaceEnabled,
|
|
282
|
+
attachDesktop: desktopEnabled && watchDesktop,
|
|
283
|
+
attachTerminal: terminalEnabled && warmTerminal,
|
|
232
284
|
// Explicit live-file intent only — NOT "the Files tab is open". A cold edit
|
|
233
285
|
// or guarded-file open wakes the box; a glance at the tree/diff does not.
|
|
234
|
-
attachFiles: warmFiles,
|
|
286
|
+
attachFiles: workspaceDataEnabled && warmFiles,
|
|
235
287
|
});
|
|
236
288
|
const capabilities = caps.capabilities;
|
|
237
289
|
const liveness = capabilities?.liveness;
|
|
@@ -239,13 +291,17 @@ export function useSandboxWorkspaceTabs(
|
|
|
239
291
|
// The FS is writable only when it's live AND not read-only. A self-hosted box
|
|
240
292
|
// that's offline (or any read-only advertisement) or a capture-served cold tree
|
|
241
293
|
// must not offer create/rename/delete/edit affordances — you cannot mutate a
|
|
242
|
-
// machine you can't reach (
|
|
294
|
+
// machine you can't reach (C3). Tree-structure ops need a warm
|
|
243
295
|
// writable box; content editing on a cold CLOUD box is the wake-on-edit path in
|
|
244
296
|
// the editor, not tree mutation.
|
|
245
297
|
const fsReadOnly = capabilities?.FileSystem.readOnly ?? false;
|
|
246
|
-
|
|
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");
|
|
247
303
|
const gitOn = capabilities?.Git.available ?? false;
|
|
248
|
-
const terminalOn = (capabilities?.Terminal.transport ?? null) !== null;
|
|
304
|
+
const terminalOn = terminalEnabled && (capabilities?.Terminal.transport ?? null) !== null;
|
|
249
305
|
// The REAL interactive terminal is the ttyd pty-ws stream. When it's live the
|
|
250
306
|
// legacy HTTP PTY must NOT run; `useSandboxTerminal` opens its HTTP PTY only as
|
|
251
307
|
// the firehose-mode fallback (a backend without ttyd).
|
|
@@ -253,13 +309,14 @@ export function useSandboxWorkspaceTabs(
|
|
|
253
309
|
capabilities?.Terminal.transport === "pty-ws" && Boolean(capabilities?.Terminal.url);
|
|
254
310
|
const ptyCapable = (capabilities?.Terminal.ptyCapable ?? false) && !ptyWsLive;
|
|
255
311
|
const terminal = useSandboxTerminal(sessionId, {
|
|
256
|
-
events,
|
|
257
|
-
interactive: ptyCapable,
|
|
312
|
+
events: terminalEnabled ? events : [],
|
|
313
|
+
interactive: terminalEnabled && ptyCapable,
|
|
258
314
|
liveness,
|
|
259
315
|
});
|
|
260
316
|
const desktopAdvertised =
|
|
261
|
-
|
|
262
|
-
capabilities?.DesktopStream.
|
|
317
|
+
desktopEnabled &&
|
|
318
|
+
((capabilities?.DesktopStream.transport ?? null) !== null ||
|
|
319
|
+
capabilities?.DesktopStream.reason === "lease_cold");
|
|
263
320
|
|
|
264
321
|
// Lazy provisioning (#315) creates the box mid-turn on the first sandbox tool
|
|
265
322
|
// call, emitting sandbox.provision started→completed/failed on the live stream.
|
|
@@ -280,9 +337,27 @@ export function useSandboxWorkspaceTabs(
|
|
|
280
337
|
|
|
281
338
|
// The cold-paint data source: the latest turn-end capture, fetched with a single
|
|
282
339
|
// api round-trip on mount (no machine). Feeds the Files tree + the Changes/Git
|
|
283
|
-
// diff
|
|
284
|
-
|
|
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
|
+
});
|
|
285
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]);
|
|
286
361
|
const notifiedCaptureDegradedReason = useRef<{
|
|
287
362
|
sessionId: string;
|
|
288
363
|
reason: string | null;
|
|
@@ -302,7 +377,9 @@ export function useSandboxWorkspaceTabs(
|
|
|
302
377
|
|
|
303
378
|
const files = useSandboxFiles(sessionId, {
|
|
304
379
|
events,
|
|
305
|
-
|
|
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)),
|
|
306
383
|
liveness,
|
|
307
384
|
capture: captureState.capture,
|
|
308
385
|
// A reverted optimistic mutation (e.g. a 409 rename collision) surfaces as a
|
|
@@ -315,14 +392,18 @@ export function useSandboxWorkspaceTabs(
|
|
|
315
392
|
});
|
|
316
393
|
const git = useSandboxGit(sessionId, {
|
|
317
394
|
events,
|
|
318
|
-
enabled:
|
|
395
|
+
enabled: workspaceDataEnabled && (captureAvailable || (gitOn && liveWorkspaceExpected)),
|
|
396
|
+
repoPaths,
|
|
319
397
|
liveness,
|
|
320
398
|
capture: captureState.capture,
|
|
321
399
|
});
|
|
322
400
|
const stagedGit = useSandboxGit(sessionId, {
|
|
323
401
|
events,
|
|
324
|
-
|
|
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,
|
|
325
405
|
staged: true,
|
|
406
|
+
repoPaths,
|
|
326
407
|
liveness,
|
|
327
408
|
capture: captureState.capture,
|
|
328
409
|
});
|
|
@@ -331,7 +412,7 @@ export function useSandboxWorkspaceTabs(
|
|
|
331
412
|
// belongs in the package (an embedder's theme toggle drives it too).
|
|
332
413
|
const [xtermTheme, setXtermTheme] = useState<XtermTheme | undefined>(undefined);
|
|
333
414
|
useEffect(() => {
|
|
334
|
-
if (typeof document === "undefined") return;
|
|
415
|
+
if (!terminalEnabled || typeof document === "undefined") return;
|
|
335
416
|
const derive = () => setXtermTheme(xtermThemeFromTokens());
|
|
336
417
|
derive();
|
|
337
418
|
const observer = new MutationObserver(derive);
|
|
@@ -340,7 +421,7 @@ export function useSandboxWorkspaceTabs(
|
|
|
340
421
|
attributeFilter: ["data-og-theme", "class"],
|
|
341
422
|
});
|
|
342
423
|
return () => observer.disconnect();
|
|
343
|
-
}, []);
|
|
424
|
+
}, [terminalEnabled]);
|
|
344
425
|
|
|
345
426
|
async function acknowledgeAndWatch() {
|
|
346
427
|
try {
|
|
@@ -375,9 +456,13 @@ export function useSandboxWorkspaceTabs(
|
|
|
375
456
|
capabilitiesState: caps.state,
|
|
376
457
|
activeMachineState: activeMachine?.state ?? null,
|
|
377
458
|
activeIsSelfhosted: activeMachine?.kind === "selfhosted",
|
|
378
|
-
wantsWarm:
|
|
459
|
+
wantsWarm:
|
|
460
|
+
(terminalEnabled && warmTerminal) ||
|
|
461
|
+
(desktopEnabled && watchDesktop) ||
|
|
462
|
+
(workspaceDataEnabled && warmFiles),
|
|
379
463
|
capturedAt: captureState.capturedAt,
|
|
380
464
|
});
|
|
465
|
+
const workspaceWaking = chip.state === "waking";
|
|
381
466
|
|
|
382
467
|
// The pre-paint default tab is decided from the first AUTHORITATIVE workspace
|
|
383
468
|
// source. A capture wins immediately on the cold/offline fast path; a warm box
|
|
@@ -387,49 +472,63 @@ export function useSandboxWorkspaceTabs(
|
|
|
387
472
|
// resolve Changes earlier, but a pure embedder with no event preload still gets
|
|
388
473
|
// the correct live default. The choice latches once so later edits never steal
|
|
389
474
|
// the user's current tab.
|
|
390
|
-
const
|
|
391
|
-
|
|
475
|
+
const defaultIdentity = `${sessionId}\u0000${surfaceIdentity}`;
|
|
476
|
+
const defaultTabRef = useRef<{ identity: string; value: string | null }>({
|
|
477
|
+
identity: defaultIdentity,
|
|
392
478
|
value: null,
|
|
393
479
|
});
|
|
394
|
-
if (defaultTabRef.current.
|
|
395
|
-
defaultTabRef.current = {
|
|
480
|
+
if (defaultTabRef.current.identity !== defaultIdentity) {
|
|
481
|
+
defaultTabRef.current = { identity: defaultIdentity, value: null };
|
|
396
482
|
}
|
|
397
|
-
const liveWorkspaceExpected = liveness === "warm" || liveness === "draining";
|
|
398
483
|
const captureIsAuthoritative =
|
|
399
484
|
!liveWorkspaceExpected && (liveness !== undefined || caps.error !== null);
|
|
400
485
|
const captureUnavailable = captureState.fileCount === 0 || captureState.error !== null;
|
|
401
486
|
if (defaultTabRef.current.value === null) {
|
|
402
|
-
if (initialTab) {
|
|
487
|
+
if (initialTab && (!isWorkbenchSurface(initialTab) || surfaceSet.has(initialTab))) {
|
|
403
488
|
defaultTabRef.current.value = initialTab;
|
|
404
489
|
} else if (git.source === "live") {
|
|
405
|
-
defaultTabRef.current.value =
|
|
406
|
-
git.diff.length > 0
|
|
490
|
+
defaultTabRef.current.value = sourceDrivenDefaultTab(
|
|
491
|
+
git.diff.length > 0,
|
|
492
|
+
changesEnabled,
|
|
493
|
+
filesEnabled,
|
|
494
|
+
);
|
|
407
495
|
} else if (
|
|
408
496
|
captureIsAuthoritative &&
|
|
409
497
|
captureState.fileCount !== null &&
|
|
410
498
|
captureState.available
|
|
411
499
|
) {
|
|
412
|
-
defaultTabRef.current.value =
|
|
413
|
-
captureState.fileCount > 0
|
|
500
|
+
defaultTabRef.current.value = sourceDrivenDefaultTab(
|
|
501
|
+
captureState.fileCount > 0,
|
|
502
|
+
changesEnabled,
|
|
503
|
+
filesEnabled,
|
|
504
|
+
);
|
|
414
505
|
} else if (
|
|
415
506
|
captureIsAuthoritative &&
|
|
416
507
|
captureUnavailable &&
|
|
417
508
|
initialWorkspaceTab(events) === WORKBENCH_TAB_CHANGES
|
|
418
509
|
) {
|
|
419
|
-
defaultTabRef.current.value =
|
|
420
|
-
} else if (captureState.available && git.error && captureState.fileCount !== null) {
|
|
421
|
-
// A warm live read failed but the durable capture is intact: retain an
|
|
422
|
-
// immediate, deterministic review surface instead of hanging unresolved.
|
|
423
|
-
defaultTabRef.current.value =
|
|
424
|
-
captureState.fileCount > 0 ? WORKBENCH_TAB_CHANGES : WORKBENCH_TAB_FILES;
|
|
510
|
+
defaultTabRef.current.value = sourceDrivenDefaultTab(true, changesEnabled, filesEnabled);
|
|
425
511
|
} else if (captureUnavailable && caps.error) {
|
|
426
512
|
// The Changes surface owns the truthful sandbox-unavailable state + retry.
|
|
427
513
|
// Files would misleadingly describe this as a missing filesystem.
|
|
428
|
-
defaultTabRef.current.value =
|
|
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
|
+
);
|
|
429
528
|
} else if (captureUnavailable && git.error) {
|
|
430
529
|
// No capture and live Git failed: Files is the least surprising fallback
|
|
431
530
|
// and preserves the established no-capture degraded behavior.
|
|
432
|
-
defaultTabRef.current.value =
|
|
531
|
+
defaultTabRef.current.value = sourceDrivenDefaultTab(false, changesEnabled, filesEnabled);
|
|
433
532
|
}
|
|
434
533
|
}
|
|
435
534
|
// null only during the brief pre-first-resolve window (no host override yet).
|
|
@@ -438,66 +537,82 @@ export function useSandboxWorkspaceTabs(
|
|
|
438
537
|
const tabs = useMemo(() => {
|
|
439
538
|
const list: WorkspaceTab[] = [];
|
|
440
539
|
|
|
441
|
-
// Changes —
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
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
|
+
});
|
|
467
569
|
|
|
468
|
-
// Files —
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
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
|
+
});
|
|
496
611
|
|
|
497
612
|
// Terminal — capability-gated (appears after negotiation; never the default).
|
|
498
613
|
if (terminalOn) {
|
|
499
614
|
list.push({
|
|
500
|
-
id:
|
|
615
|
+
id: WORKBENCH_TAB_TERMINAL,
|
|
501
616
|
label: "Terminal",
|
|
502
617
|
content: (
|
|
503
618
|
<div className="h-full bg-og-bg p-1">
|
|
@@ -518,10 +633,10 @@ export function useSandboxWorkspaceTabs(
|
|
|
518
633
|
// Desktop — capability-gated + consent-gated.
|
|
519
634
|
if (desktopAdvertised) {
|
|
520
635
|
list.push({
|
|
521
|
-
id:
|
|
636
|
+
id: WORKBENCH_TAB_DESKTOP,
|
|
522
637
|
label: "Desktop",
|
|
523
638
|
badge: watchDesktop ? (
|
|
524
|
-
<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">
|
|
525
640
|
Live
|
|
526
641
|
</span>
|
|
527
642
|
) : undefined,
|
|
@@ -542,6 +657,8 @@ export function useSandboxWorkspaceTabs(
|
|
|
542
657
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
543
658
|
}, [
|
|
544
659
|
fileSystemOn,
|
|
660
|
+
changesEnabled,
|
|
661
|
+
filesEnabled,
|
|
545
662
|
terminalOn,
|
|
546
663
|
desktopAdvertised,
|
|
547
664
|
captureAvailable,
|
|
@@ -559,6 +676,7 @@ export function useSandboxWorkspaceTabs(
|
|
|
559
676
|
terminal,
|
|
560
677
|
xtermTheme,
|
|
561
678
|
capabilities,
|
|
679
|
+
workspaceWaking,
|
|
562
680
|
caps.state,
|
|
563
681
|
caps.error,
|
|
564
682
|
caps.viewerCapReached,
|
|
@@ -569,6 +687,7 @@ export function useSandboxWorkspaceTabs(
|
|
|
569
687
|
tabs,
|
|
570
688
|
defaultTab,
|
|
571
689
|
machine: {
|
|
690
|
+
enabled: anySurfaceEnabled,
|
|
572
691
|
chip,
|
|
573
692
|
activeMachine,
|
|
574
693
|
loading: machines.loading,
|
|
@@ -584,6 +703,11 @@ export type SandboxWorkspaceProps = ClientOverride & {
|
|
|
584
703
|
events: SessionEvent[];
|
|
585
704
|
/** The chat / primary pane shown beside the dock. */
|
|
586
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;
|
|
587
711
|
/** Host tabs injected BEFORE the workbench tabs (e.g. a "Run" landing tab). */
|
|
588
712
|
leadingTabs?: WorkspaceTab[] | undefined;
|
|
589
713
|
/** Host tabs injected AFTER the workbench tabs (e.g. a "Debug" tab). */
|
|
@@ -617,6 +741,7 @@ export function SandboxWorkspace(props: SandboxWorkspaceProps): ReactNode {
|
|
|
617
741
|
sessionId,
|
|
618
742
|
events,
|
|
619
743
|
primary,
|
|
744
|
+
surfaces,
|
|
620
745
|
leadingTabs,
|
|
621
746
|
trailingTabs,
|
|
622
747
|
initialTab,
|
|
@@ -659,6 +784,7 @@ export function SandboxWorkspace(props: SandboxWorkspaceProps): ReactNode {
|
|
|
659
784
|
...(props.workspaceId ? { workspaceId: props.workspaceId } : {}),
|
|
660
785
|
sessionId,
|
|
661
786
|
events,
|
|
787
|
+
...(surfaces ? { surfaces } : {}),
|
|
662
788
|
...(initialTab ? { initialTab } : {}),
|
|
663
789
|
...(onNotify ? { onNotify } : {}),
|
|
664
790
|
requestedFilePath: requestedFile?.sessionId === sessionId ? requestedFile.path : null,
|
|
@@ -673,7 +799,9 @@ export function SandboxWorkspace(props: SandboxWorkspaceProps): ReactNode {
|
|
|
673
799
|
// so committing the real default at first-resolve produces no CONTENT switch.
|
|
674
800
|
const tabs: WorkspaceTab[] = [...(leadingTabs ?? []), ...workbenchTabs, ...(trailingTabs ?? [])];
|
|
675
801
|
const selectedTab = storedSelection?.sessionId === sessionId ? storedSelection.tab : null;
|
|
676
|
-
const
|
|
802
|
+
const preferredTab = selectedTab ?? defaultTab;
|
|
803
|
+
const activeTab =
|
|
804
|
+
preferredTab && tabs.some((tab) => tab.id === preferredTab) ? preferredTab : tabs[0]?.id;
|
|
677
805
|
const selectTab = useCallback(
|
|
678
806
|
(tab: string) => setStoredSelection({ sessionId, tab }),
|
|
679
807
|
[sessionId],
|
|
@@ -685,14 +813,18 @@ export function SandboxWorkspace(props: SandboxWorkspaceProps): ReactNode {
|
|
|
685
813
|
tabs={tabs}
|
|
686
814
|
{...(activeTab !== undefined ? { activeTab } : {})}
|
|
687
815
|
onActiveTabChange={selectTab}
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
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
|
+
: {})}
|
|
696
828
|
{...(mobileLeadingControl !== undefined ? { mobileLeadingControl } : {})}
|
|
697
829
|
{...(collapsed !== undefined ? { collapsed } : {})}
|
|
698
830
|
{...(onCollapsedChange ? { onCollapsedChange } : {})}
|
|
@@ -710,7 +842,9 @@ export type { WorkspaceDockProps };
|
|
|
710
842
|
|
|
711
843
|
function DirtyBadge({ count }: { count: number }) {
|
|
712
844
|
return (
|
|
713
|
-
<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>
|
|
714
848
|
);
|
|
715
849
|
}
|
|
716
850
|
|
|
@@ -737,7 +871,7 @@ function chipDotClass(state: MachineChip["state"]): string {
|
|
|
737
871
|
* with a popover carrying the machine identity, connection state, the "shown as
|
|
738
872
|
* of <time>" staleness note, the shared-session disclosure, and a retry when the
|
|
739
873
|
* fleet failed to resolve (the old per-surface machine bar + Sandbox info tab,
|
|
740
|
-
* folded into one header affordance —
|
|
874
|
+
* folded into one header affordance — recommendation).
|
|
741
875
|
*/
|
|
742
876
|
function MachineStateChip({
|
|
743
877
|
chip,
|
|
@@ -750,10 +884,13 @@ function MachineStateChip({
|
|
|
750
884
|
error: Error | null;
|
|
751
885
|
onRetry: () => void;
|
|
752
886
|
}) {
|
|
887
|
+
const triggerRef = useRef<HTMLButtonElement | null>(null);
|
|
888
|
+
const portalStyle = usePortalTokenStyle(triggerRef);
|
|
753
889
|
return (
|
|
754
890
|
<Popover.Root>
|
|
755
891
|
<Popover.Trigger asChild>
|
|
756
892
|
<button
|
|
893
|
+
ref={triggerRef}
|
|
757
894
|
type="button"
|
|
758
895
|
aria-label={`Machine: ${chip.label}`}
|
|
759
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"
|
|
@@ -768,9 +905,12 @@ function MachineStateChip({
|
|
|
768
905
|
<Popover.Portal forceMount>
|
|
769
906
|
<Popover.Content
|
|
770
907
|
forceMount
|
|
908
|
+
data-machine-state-popover
|
|
771
909
|
align="end"
|
|
772
910
|
sideOffset={6}
|
|
773
|
-
|
|
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}
|
|
774
914
|
>
|
|
775
915
|
<div className="flex min-w-0 items-center gap-1.5">
|
|
776
916
|
<MachineKindIcon kind={activeMachine?.kind} />
|
|
@@ -802,7 +942,7 @@ function MachineStateChip({
|
|
|
802
942
|
) : null}
|
|
803
943
|
{error ? (
|
|
804
944
|
<div className="mt-2.5 space-y-1.5 border-t border-og-border pt-2.5">
|
|
805
|
-
<p className="text-og-xs leading-4 text-og-
|
|
945
|
+
<p className="text-og-xs leading-4 text-og-danger">
|
|
806
946
|
Couldn't reach the sandbox for this session.
|
|
807
947
|
</p>
|
|
808
948
|
<DockActionButton onClick={onRetry}>
|
|
@@ -840,30 +980,69 @@ function ChangesTabBody({
|
|
|
840
980
|
git,
|
|
841
981
|
captureAvailable,
|
|
842
982
|
captureRevision,
|
|
983
|
+
capturePending,
|
|
984
|
+
liveWorkspaceExpected,
|
|
985
|
+
workspaceWaking,
|
|
843
986
|
capabilitiesState,
|
|
844
987
|
capabilitiesError,
|
|
845
988
|
onRetry,
|
|
989
|
+
onWake,
|
|
846
990
|
onOpenFile,
|
|
847
991
|
}: {
|
|
848
992
|
git: UseSandboxGitResult;
|
|
849
993
|
captureAvailable: boolean;
|
|
850
994
|
captureRevision: number | null;
|
|
995
|
+
capturePending: boolean;
|
|
996
|
+
liveWorkspaceExpected: boolean;
|
|
997
|
+
workspaceWaking: boolean;
|
|
851
998
|
capabilitiesState: string;
|
|
852
999
|
capabilitiesError: Error | null;
|
|
853
1000
|
onRetry: () => void;
|
|
1001
|
+
onWake: () => void;
|
|
854
1002
|
onOpenFile?: ((path: string) => void) | undefined;
|
|
855
1003
|
}) {
|
|
856
1004
|
const diff = git.diff;
|
|
857
1005
|
|
|
858
1006
|
if (diff.length > 0) {
|
|
859
1007
|
return (
|
|
860
|
-
<
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
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>
|
|
867
1046
|
);
|
|
868
1047
|
}
|
|
869
1048
|
|
|
@@ -871,7 +1050,7 @@ function ChangesTabBody({
|
|
|
871
1050
|
return (
|
|
872
1051
|
<CenteredState icon={<TriangleAlertIcon className="size-5" aria-hidden />} tone="danger">
|
|
873
1052
|
<p className="text-og-sm font-medium text-og-fg">Sandbox unavailable</p>
|
|
874
|
-
<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">
|
|
875
1054
|
{capabilitiesError.message || "Couldn't reach the sandbox for this session."}
|
|
876
1055
|
</p>
|
|
877
1056
|
<DockActionButton onClick={onRetry}>
|
|
@@ -882,7 +1061,11 @@ function ChangesTabBody({
|
|
|
882
1061
|
);
|
|
883
1062
|
}
|
|
884
1063
|
|
|
885
|
-
if (
|
|
1064
|
+
if (
|
|
1065
|
+
(capturePending || capabilitiesState === "negotiating") &&
|
|
1066
|
+
!captureAvailable &&
|
|
1067
|
+
!workspaceWaking
|
|
1068
|
+
) {
|
|
886
1069
|
return (
|
|
887
1070
|
<CenteredState
|
|
888
1071
|
icon={
|
|
@@ -900,6 +1083,38 @@ function ChangesTabBody({
|
|
|
900
1083
|
);
|
|
901
1084
|
}
|
|
902
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
|
+
|
|
903
1118
|
if (git.loading && git.source === null) {
|
|
904
1119
|
return (
|
|
905
1120
|
<CenteredState
|
|
@@ -936,7 +1151,12 @@ function CenteredState({
|
|
|
936
1151
|
tone?: "neutral" | "success" | "danger";
|
|
937
1152
|
}) {
|
|
938
1153
|
return (
|
|
939
|
-
<div
|
|
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
|
+
>
|
|
940
1160
|
<div className="flex max-w-sm flex-col items-center gap-2.5">
|
|
941
1161
|
{icon ? (
|
|
942
1162
|
<span
|