@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
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { Loader2Icon, RotateCwIcon } from "lucide-react";
|
|
2
|
+
|
|
3
|
+
import type { UseTurnQueueResult } from "../hooks/use-turn-queue";
|
|
4
|
+
|
|
5
|
+
type QueueStateProps = {
|
|
6
|
+
queue: UseTurnQueueResult;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export function EmptyQueueStateSurface({ queue }: QueueStateProps) {
|
|
10
|
+
const hasError = Boolean(queue.error || queue.mutationError);
|
|
11
|
+
return (
|
|
12
|
+
<div
|
|
13
|
+
className="mx-auto mb-2 w-full max-w-3xl shrink-0 px-4 sm:px-6"
|
|
14
|
+
data-testid="queue-surface"
|
|
15
|
+
>
|
|
16
|
+
<div className="overflow-hidden rounded-lg border border-border bg-surface/80 shadow-sm">
|
|
17
|
+
{queue.stoppingPreviousAttempt ? (
|
|
18
|
+
<QueueStoppingStatus queue={queue} dividerAfter={hasError} />
|
|
19
|
+
) : null}
|
|
20
|
+
{hasError ? <QueueErrorAlert queue={queue} /> : null}
|
|
21
|
+
</div>
|
|
22
|
+
</div>
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function QueueStoppingStatus({
|
|
27
|
+
queue,
|
|
28
|
+
dividerAfter = false,
|
|
29
|
+
}: QueueStateProps & { dividerAfter?: boolean }) {
|
|
30
|
+
return (
|
|
31
|
+
<div
|
|
32
|
+
role="status"
|
|
33
|
+
aria-live="polite"
|
|
34
|
+
className={`flex min-h-11 items-center gap-2.5 bg-status-waiting/[0.07] px-3 py-2 text-xs text-fg ${
|
|
35
|
+
dividerAfter ? "border-b border-status-waiting/20" : ""
|
|
36
|
+
}`}
|
|
37
|
+
data-testid="stopping-previous-attempt"
|
|
38
|
+
>
|
|
39
|
+
<Loader2Icon
|
|
40
|
+
aria-hidden="true"
|
|
41
|
+
className="size-3.5 shrink-0 animate-spin text-status-waiting motion-reduce:animate-none"
|
|
42
|
+
/>
|
|
43
|
+
<span className="min-w-0">
|
|
44
|
+
<span className="font-medium">
|
|
45
|
+
{queue.effectiveControl?.state === "paused"
|
|
46
|
+
? "Stopping current attempt…"
|
|
47
|
+
: "Stopping previous attempt…"}
|
|
48
|
+
</span>{" "}
|
|
49
|
+
<span className="text-fg-muted">
|
|
50
|
+
{queue.effectiveControl?.state === "paused"
|
|
51
|
+
? "Queued work stays saved until you resume."
|
|
52
|
+
: "Queued work is saved and starts automatically."}
|
|
53
|
+
</span>
|
|
54
|
+
</span>
|
|
55
|
+
</div>
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function QueueErrorAlert({
|
|
60
|
+
queue,
|
|
61
|
+
dividerBefore = false,
|
|
62
|
+
}: QueueStateProps & { dividerBefore?: boolean }) {
|
|
63
|
+
return (
|
|
64
|
+
<div className={`${dividerBefore ? "border-t border-border" : ""} p-2`}>
|
|
65
|
+
<div
|
|
66
|
+
role="alert"
|
|
67
|
+
className="flex min-w-0 max-w-full flex-wrap items-start gap-2 rounded-md bg-status-failed/10 px-2 py-1.5 text-xs text-status-failed"
|
|
68
|
+
>
|
|
69
|
+
<span
|
|
70
|
+
role="region"
|
|
71
|
+
aria-label="Queue error details"
|
|
72
|
+
tabIndex={0}
|
|
73
|
+
className="max-h-[min(5rem,20dvh)] min-w-0 max-w-full flex-[1_1_5rem] overflow-auto overscroll-contain whitespace-pre-wrap rounded-sm outline-none [overflow-wrap:anywhere] [unicode-bidi:plaintext] focus-visible:ring-2 focus-visible:ring-ring/40"
|
|
74
|
+
data-testid="queue-error-message"
|
|
75
|
+
dir="auto"
|
|
76
|
+
>
|
|
77
|
+
{(queue.mutationError ?? queue.error)?.message}
|
|
78
|
+
</span>
|
|
79
|
+
<button
|
|
80
|
+
type="button"
|
|
81
|
+
onClick={() => {
|
|
82
|
+
queue.clearMutationError();
|
|
83
|
+
void queue.refresh();
|
|
84
|
+
}}
|
|
85
|
+
aria-label="Dismiss queue error and retry"
|
|
86
|
+
title="Retry loading the queue"
|
|
87
|
+
className="ms-auto inline-flex size-7 shrink-0 items-center justify-center self-start rounded-md outline-none transition-colors hover:bg-surface-3 focus-visible:ring-2 focus-visible:ring-ring/40 motion-reduce:transition-none pointer-coarse:size-[44px]"
|
|
88
|
+
>
|
|
89
|
+
<RotateCwIcon className="size-3.5" />
|
|
90
|
+
</button>
|
|
91
|
+
</div>
|
|
92
|
+
</div>
|
|
93
|
+
);
|
|
94
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { Loader2Icon } from "lucide-react";
|
|
2
|
+
import { lazy, Suspense, type ComponentType } from "react";
|
|
3
|
+
|
|
4
|
+
import type { QueueSurfaceProps } from "./queue-surface-implementation";
|
|
5
|
+
import { EmptyQueueStateSurface } from "./queue-surface-state";
|
|
6
|
+
|
|
7
|
+
export type { QueueSurfaceProps } from "./queue-surface-implementation";
|
|
8
|
+
|
|
9
|
+
const loadQueueSurface = () => import("./queue-surface-implementation");
|
|
10
|
+
type QueueSurfaceModule = { QueueSurface: ComponentType<QueueSurfaceProps> };
|
|
11
|
+
type QueueSurfaceLoader = () => Promise<QueueSurfaceModule>;
|
|
12
|
+
|
|
13
|
+
/** The sole human prompt queue: compact above Goal, Agents, and composer. */
|
|
14
|
+
export const QueueSurface = createQueueSurface(loadQueueSurface);
|
|
15
|
+
|
|
16
|
+
/** @internal Deterministic Suspense seam for the QueueSurface regression suite. */
|
|
17
|
+
export function createQueueSurfaceForTest(loadImplementation: QueueSurfaceLoader) {
|
|
18
|
+
return createQueueSurface(loadImplementation);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function createQueueSurface(loadImplementation: QueueSurfaceLoader) {
|
|
22
|
+
const LazyQueueSurface = lazy(async () => ({
|
|
23
|
+
default: (await loadImplementation()).QueueSurface,
|
|
24
|
+
}));
|
|
25
|
+
|
|
26
|
+
return function QueueSurfaceBoundary(props: QueueSurfaceProps) {
|
|
27
|
+
const { queue } = props;
|
|
28
|
+
if (queue.queue.length === 0) {
|
|
29
|
+
if (!queue.stoppingPreviousAttempt && !queue.error && !queue.mutationError) return null;
|
|
30
|
+
return <EmptyQueueStateSurface queue={queue} />;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return (
|
|
34
|
+
<Suspense fallback={<QueueSurfaceFallback count={queue.queue.length} />}>
|
|
35
|
+
<LazyQueueSurface {...props} />
|
|
36
|
+
</Suspense>
|
|
37
|
+
);
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function QueueSurfaceFallback({ count }: { count: number }) {
|
|
42
|
+
return (
|
|
43
|
+
<div
|
|
44
|
+
aria-live="polite"
|
|
45
|
+
className="mx-auto mb-2 w-full max-w-3xl shrink-0 px-4 sm:px-6"
|
|
46
|
+
data-testid="queue-surface-loading"
|
|
47
|
+
role="status"
|
|
48
|
+
>
|
|
49
|
+
<div className="overflow-hidden rounded-lg border border-border bg-surface/80 shadow-sm">
|
|
50
|
+
<div className="flex items-center gap-2 px-3 py-2 text-xs text-fg-muted pointer-coarse:min-h-[44px]">
|
|
51
|
+
<Loader2Icon
|
|
52
|
+
aria-hidden="true"
|
|
53
|
+
className="size-3.5 shrink-0 animate-spin motion-reduce:animate-none"
|
|
54
|
+
/>
|
|
55
|
+
Loading {count} queued prompt{count === 1 ? "" : "s"}…
|
|
56
|
+
</div>
|
|
57
|
+
</div>
|
|
58
|
+
</div>
|
|
59
|
+
);
|
|
60
|
+
}
|
|
@@ -1,8 +1,11 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { FileCode2Icon, FileWarningIcon, LoaderCircleIcon } from "lucide-react";
|
|
2
2
|
import { type ReactNode, useCallback, useEffect, useRef, useState } from "react";
|
|
3
3
|
import { cn } from "../lib/cn";
|
|
4
4
|
import { useThemeType } from "../lib/use-theme-type";
|
|
5
|
-
import
|
|
5
|
+
import {
|
|
6
|
+
CapturedFileUnavailableError,
|
|
7
|
+
type UseSandboxFilesResult,
|
|
8
|
+
} from "../hooks/use-sandbox-files";
|
|
6
9
|
import type { UseSandboxGitResult } from "../hooks/use-sandbox-git";
|
|
7
10
|
import { CodeEditor } from "./code-editor";
|
|
8
11
|
import { FileBrowser } from "./file-browser";
|
|
@@ -27,6 +30,23 @@ export type SandboxFilesProps = {
|
|
|
27
30
|
* dock warms the box on this so the save lands fast; opening/reading never fires
|
|
28
31
|
* it. Browsing the tree/diff must not warm a box. */
|
|
29
32
|
onEditIntent?: (() => void) | undefined;
|
|
33
|
+
/** A guarded diff path routed here by the parent workspace. */
|
|
34
|
+
requestedPath?: string | undefined;
|
|
35
|
+
/** Identity for one guarded-file request. Increment this when the same path is
|
|
36
|
+
* deliberately requested again; it also lets a pending request be consumed
|
|
37
|
+
* without overriding later manual tree navigation. Defaults to the path. */
|
|
38
|
+
requestedPathRequestId?: string | number | undefined;
|
|
39
|
+
/** False while the parent is waking a cold sandbox for `requestedPath`. */
|
|
40
|
+
requestedPathReady?: boolean | undefined;
|
|
41
|
+
/** The machine is cold and no durable capture exists yet. Render an explicit
|
|
42
|
+
* wake gate instead of an empty-tree lie or an implicit Channel-A read. */
|
|
43
|
+
workspaceResting?: boolean | undefined;
|
|
44
|
+
/** A deliberate wake has started but the live file surface is not ready yet. */
|
|
45
|
+
workspaceWaking?: boolean | undefined;
|
|
46
|
+
/** Whether live reads are currently authoritative. */
|
|
47
|
+
liveWorkspaceReady?: boolean | undefined;
|
|
48
|
+
/** Deliberately wake the machine to read content absent from the capture. */
|
|
49
|
+
onWakeWorkspace?: (() => void) | undefined;
|
|
30
50
|
themeType?: "dark" | "light" | undefined;
|
|
31
51
|
className?: string | undefined;
|
|
32
52
|
};
|
|
@@ -45,6 +65,13 @@ export function SandboxFiles({
|
|
|
45
65
|
usePierre = true,
|
|
46
66
|
editable = true,
|
|
47
67
|
onEditIntent,
|
|
68
|
+
requestedPath,
|
|
69
|
+
requestedPathRequestId,
|
|
70
|
+
requestedPathReady = true,
|
|
71
|
+
workspaceResting = false,
|
|
72
|
+
workspaceWaking = false,
|
|
73
|
+
liveWorkspaceReady = true,
|
|
74
|
+
onWakeWorkspace,
|
|
48
75
|
themeType,
|
|
49
76
|
className,
|
|
50
77
|
}: SandboxFilesProps) {
|
|
@@ -52,6 +79,30 @@ export function SandboxFiles({
|
|
|
52
79
|
// View vs Edit for the selected file. Resets to View on every new selection so
|
|
53
80
|
// opening a file never lands you in a stale dirty editor for a different path.
|
|
54
81
|
const [editMode, setEditMode] = useState(false);
|
|
82
|
+
const [liveRequestedPath, setLiveRequestedPath] = useState<string | null>(null);
|
|
83
|
+
const [viewReloadRevision, setViewReloadRevision] = useState(0);
|
|
84
|
+
const pendingRequestRef = useRef<string | number | null>(null);
|
|
85
|
+
const handledRequestRef = useRef<string | number | null>(null);
|
|
86
|
+
const requestKey = requestedPath ? (requestedPathRequestId ?? requestedPath) : null;
|
|
87
|
+
|
|
88
|
+
useEffect(() => {
|
|
89
|
+
if (!requestedPath || requestKey === null) {
|
|
90
|
+
pendingRequestRef.current = null;
|
|
91
|
+
handledRequestRef.current = null;
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
if (handledRequestRef.current === requestKey) {
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
if (!requestedPathReady) {
|
|
98
|
+
pendingRequestRef.current = requestKey;
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
handledRequestRef.current = requestKey;
|
|
102
|
+
pendingRequestRef.current = null;
|
|
103
|
+
setSelected(requestedPath);
|
|
104
|
+
setEditMode(false);
|
|
105
|
+
}, [requestKey, requestedPath, requestedPathReady]);
|
|
55
106
|
|
|
56
107
|
// Side-by-side (tree left, viewer right) once the surface is wide enough;
|
|
57
108
|
// stacked (tree over viewer) on a narrow dock. Tracked off the container so it
|
|
@@ -77,13 +128,21 @@ export function SandboxFiles({
|
|
|
77
128
|
// writable). Nothing is auto-selected — the pane waits for a tree click, so the
|
|
78
129
|
// Files tab opens as a calm browser, not a diff.
|
|
79
130
|
const viewPath = selected;
|
|
80
|
-
const fileView = useFileView(viewPath, files.readFile);
|
|
131
|
+
const fileView = useFileView(viewPath, files.readFile, viewReloadRevision);
|
|
81
132
|
|
|
82
133
|
// Selecting a (different) file always returns to View — never drop the user into
|
|
83
|
-
// an editor whose buffer belongs to the previously-selected path.
|
|
134
|
+
// an editor whose buffer belongs to the previously-selected path. Manual
|
|
135
|
+
// navigation also consumes a pending guarded-file request: a late cold→warm
|
|
136
|
+
// transition must never pull the user away from the file they chose meanwhile.
|
|
84
137
|
const selectFile = useCallback((path: string) => {
|
|
138
|
+
if (pendingRequestRef.current !== null) {
|
|
139
|
+
handledRequestRef.current = pendingRequestRef.current;
|
|
140
|
+
pendingRequestRef.current = null;
|
|
141
|
+
}
|
|
85
142
|
setSelected(path);
|
|
86
143
|
setEditMode(false);
|
|
144
|
+
setLiveRequestedPath(null);
|
|
145
|
+
setViewReloadRevision(0);
|
|
87
146
|
}, []);
|
|
88
147
|
|
|
89
148
|
// A tree file is editable only when it is a real, fully-loaded text file: not
|
|
@@ -99,9 +158,54 @@ export function SandboxFiles({
|
|
|
99
158
|
!fileView.truncated &&
|
|
100
159
|
fileView.content !== null;
|
|
101
160
|
const showEditor = canEdit && editMode;
|
|
161
|
+
const captureFileUnavailable =
|
|
162
|
+
fileView.error instanceof CapturedFileUnavailableError ? fileView.error : null;
|
|
163
|
+
const waitingForSelectedFile =
|
|
164
|
+
liveRequestedPath === viewPath &&
|
|
165
|
+
(!liveWorkspaceReady || files.loading) &&
|
|
166
|
+
captureFileUnavailable !== null;
|
|
102
167
|
|
|
103
168
|
if (!fileSystemAvailable) {
|
|
104
|
-
return
|
|
169
|
+
return (
|
|
170
|
+
<Notice
|
|
171
|
+
className={className}
|
|
172
|
+
icon={<FileWarningIcon className="size-5" aria-hidden />}
|
|
173
|
+
title="Files unavailable"
|
|
174
|
+
announce="alert"
|
|
175
|
+
>
|
|
176
|
+
This sandbox does not expose a file system.
|
|
177
|
+
</Notice>
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (workspaceResting || workspaceWaking) {
|
|
182
|
+
return (
|
|
183
|
+
<div className={cn("h-full", className)} data-opengeni-workspace-resting>
|
|
184
|
+
<Notice
|
|
185
|
+
icon={
|
|
186
|
+
workspaceWaking ? (
|
|
187
|
+
<LoaderCircleIcon
|
|
188
|
+
className="size-5 animate-spin motion-reduce:animate-none"
|
|
189
|
+
aria-hidden
|
|
190
|
+
/>
|
|
191
|
+
) : (
|
|
192
|
+
<FileCode2Icon className="size-5" aria-hidden />
|
|
193
|
+
)
|
|
194
|
+
}
|
|
195
|
+
title={workspaceWaking ? "Waking workspace" : "Workspace is resting"}
|
|
196
|
+
announce="status"
|
|
197
|
+
>
|
|
198
|
+
<p>
|
|
199
|
+
{workspaceWaking
|
|
200
|
+
? "Connecting to the live file system…"
|
|
201
|
+
: "No captured revision is available yet. Wake the sandbox to browse its current files."}
|
|
202
|
+
</p>
|
|
203
|
+
{!workspaceWaking && onWakeWorkspace ? (
|
|
204
|
+
<WakeButton onClick={onWakeWorkspace}>Open live workspace</WakeButton>
|
|
205
|
+
) : null}
|
|
206
|
+
</Notice>
|
|
207
|
+
</div>
|
|
208
|
+
);
|
|
105
209
|
}
|
|
106
210
|
|
|
107
211
|
return (
|
|
@@ -138,7 +242,10 @@ export function SandboxFiles({
|
|
|
138
242
|
)}
|
|
139
243
|
>
|
|
140
244
|
<div className="flex shrink-0 items-center justify-between gap-2 border-b border-og-border bg-og-surface-1 px-2 py-1">
|
|
141
|
-
<span
|
|
245
|
+
<span
|
|
246
|
+
data-opengeni-selected-file
|
|
247
|
+
className="min-w-0 truncate font-og-mono text-og-xs text-og-fg-muted"
|
|
248
|
+
>
|
|
142
249
|
{selected ?? "No file selected"}
|
|
143
250
|
</span>
|
|
144
251
|
{/* View/Edit toggle — only for a real, fully-loaded text file the editor
|
|
@@ -163,16 +270,54 @@ export function SandboxFiles({
|
|
|
163
270
|
path={viewPath}
|
|
164
271
|
initialContents={fileView.content}
|
|
165
272
|
themeType={resolvedTheme}
|
|
166
|
-
onSave={(contents) =>
|
|
273
|
+
onSave={(contents) =>
|
|
274
|
+
files.writeFile(viewPath, contents, { expectedContent: fileView.content! })
|
|
275
|
+
}
|
|
276
|
+
onOverwrite={(contents) => files.writeFile(viewPath, contents, { force: true })}
|
|
277
|
+
onReload={() => setViewReloadRevision((revision) => revision + 1)}
|
|
167
278
|
{...(onEditIntent ? { onEditIntent } : {})}
|
|
168
279
|
className="h-full"
|
|
169
280
|
/>
|
|
281
|
+
) : waitingForSelectedFile ? (
|
|
282
|
+
<Notice
|
|
283
|
+
icon={
|
|
284
|
+
<LoaderCircleIcon
|
|
285
|
+
className="size-5 animate-spin motion-reduce:animate-none"
|
|
286
|
+
aria-hidden
|
|
287
|
+
/>
|
|
288
|
+
}
|
|
289
|
+
title="Waking workspace"
|
|
290
|
+
announce="status"
|
|
291
|
+
>
|
|
292
|
+
Opening {viewPath} when the live file system is ready…
|
|
293
|
+
</Notice>
|
|
294
|
+
) : captureFileUnavailable ? (
|
|
295
|
+
<Notice icon={<FileCode2Icon className="size-5" aria-hidden />} title="On machine">
|
|
296
|
+
<p>
|
|
297
|
+
{captureFileUnavailable.reason === "too-large"
|
|
298
|
+
? "This file is larger than the captured preview limit."
|
|
299
|
+
: captureFileUnavailable.reason === "content-missing"
|
|
300
|
+
? "The captured copy is no longer available."
|
|
301
|
+
: "This file was indexed, but it was not changed in the captured turn."}
|
|
302
|
+
</p>
|
|
303
|
+
{onWakeWorkspace ? (
|
|
304
|
+
<WakeButton
|
|
305
|
+
onClick={() => {
|
|
306
|
+
setLiveRequestedPath(viewPath);
|
|
307
|
+
if (liveWorkspaceReady) void files.refresh();
|
|
308
|
+
else onWakeWorkspace();
|
|
309
|
+
}}
|
|
310
|
+
>
|
|
311
|
+
{liveWorkspaceReady ? "Retry live file" : "Open live file"}
|
|
312
|
+
</WakeButton>
|
|
313
|
+
) : null}
|
|
314
|
+
</Notice>
|
|
170
315
|
) : fileView.error ? (
|
|
171
|
-
<Notice>
|
|
316
|
+
<Notice announce="alert">
|
|
172
317
|
Could not open {viewPath}: {fileView.error.message}
|
|
173
318
|
</Notice>
|
|
174
319
|
) : fileView.loading ? (
|
|
175
|
-
<Notice>Loading {viewPath}…</Notice>
|
|
320
|
+
<Notice announce="status">Loading {viewPath}…</Notice>
|
|
176
321
|
) : fileView.isBinary ? (
|
|
177
322
|
<Notice>
|
|
178
323
|
{viewPath} is a binary file ({fileView.sizeBytes ?? 0} bytes).
|
|
@@ -204,11 +349,26 @@ export function SandboxFiles({
|
|
|
204
349
|
)}
|
|
205
350
|
</>
|
|
206
351
|
) : (
|
|
207
|
-
<Notice>Loading {viewPath}…</Notice>
|
|
352
|
+
<Notice announce="status">Loading {viewPath}…</Notice>
|
|
208
353
|
)
|
|
354
|
+
) : // Nothing selected — the tree shows the whole workspace; pick a file.
|
|
355
|
+
requestedPath && !requestedPathReady ? (
|
|
356
|
+
<Notice
|
|
357
|
+
icon={
|
|
358
|
+
<LoaderCircleIcon
|
|
359
|
+
className="size-5 animate-spin motion-reduce:animate-none"
|
|
360
|
+
aria-hidden
|
|
361
|
+
/>
|
|
362
|
+
}
|
|
363
|
+
title="Waking sandbox"
|
|
364
|
+
announce="status"
|
|
365
|
+
>
|
|
366
|
+
Opening {requestedPath} when the live workspace is ready…
|
|
367
|
+
</Notice>
|
|
209
368
|
) : (
|
|
210
|
-
|
|
211
|
-
|
|
369
|
+
<Notice icon={<FileCode2Icon className="size-5" aria-hidden />} title="Choose a file">
|
|
370
|
+
Select a file in the tree to preview it.
|
|
371
|
+
</Notice>
|
|
212
372
|
)}
|
|
213
373
|
</div>
|
|
214
374
|
</div>
|
|
@@ -217,19 +377,36 @@ export function SandboxFiles({
|
|
|
217
377
|
);
|
|
218
378
|
}
|
|
219
379
|
|
|
380
|
+
function WakeButton({ children, onClick }: { children: ReactNode; onClick: () => void }) {
|
|
381
|
+
return (
|
|
382
|
+
<button
|
|
383
|
+
type="button"
|
|
384
|
+
onClick={onClick}
|
|
385
|
+
className="mt-1 inline-flex min-h-11 items-center justify-center rounded-og-md bg-og-accent px-3 py-2 text-og-sm font-medium text-og-on-accent shadow-sm transition-colors hover:bg-og-accent-hover focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-og-accent focus-visible:ring-offset-2 focus-visible:ring-offset-og-bg"
|
|
386
|
+
>
|
|
387
|
+
{children}
|
|
388
|
+
</button>
|
|
389
|
+
);
|
|
390
|
+
}
|
|
391
|
+
|
|
220
392
|
function GitHeader({ git, dirtyCount }: { git: UseSandboxGitResult; dirtyCount: number }) {
|
|
221
393
|
const dirty = dirtyCount > 0;
|
|
222
394
|
return (
|
|
223
395
|
<div className="flex shrink-0 items-center gap-2 border-b border-og-border bg-og-surface-1 px-2 py-1 text-og-sm">
|
|
224
396
|
<span
|
|
397
|
+
aria-hidden="true"
|
|
225
398
|
className={cn(
|
|
226
399
|
"size-2 shrink-0 rounded-full",
|
|
227
400
|
dirty ? "bg-og-status-running" : "bg-og-status-idle",
|
|
228
401
|
)}
|
|
229
|
-
title={dirty ? `${dirtyCount} changed` : "clean"}
|
|
230
402
|
/>
|
|
403
|
+
<span className="sr-only">
|
|
404
|
+
{dirty ? `Working tree has ${dirtyCount} changed files` : "Working tree clean"}
|
|
405
|
+
</span>
|
|
231
406
|
<span className="truncate font-og-mono text-og-fg">
|
|
232
|
-
{git.
|
|
407
|
+
{git.repoCount > 1
|
|
408
|
+
? `${git.repoCount} repositories`
|
|
409
|
+
: (git.branch ?? (git.isRepo ? "(detached)" : "no repo"))}
|
|
233
410
|
</span>
|
|
234
411
|
{(git.ahead > 0 || git.behind > 0) && (
|
|
235
412
|
<span className="flex shrink-0 items-center gap-1.5 text-og-xs text-og-fg-subtle">
|
|
@@ -238,7 +415,13 @@ function GitHeader({ git, dirtyCount }: { git: UseSandboxGitResult; dirtyCount:
|
|
|
238
415
|
</span>
|
|
239
416
|
)}
|
|
240
417
|
{dirty && (
|
|
241
|
-
<span
|
|
418
|
+
<span
|
|
419
|
+
aria-hidden="true"
|
|
420
|
+
data-contrast-audited
|
|
421
|
+
className="ml-auto shrink-0 text-og-xs text-og-fg-subtle"
|
|
422
|
+
>
|
|
423
|
+
{dirtyCount} changed
|
|
424
|
+
</span>
|
|
242
425
|
)}
|
|
243
426
|
</div>
|
|
244
427
|
);
|
|
@@ -261,7 +444,7 @@ function Segmented({
|
|
|
261
444
|
type="button"
|
|
262
445
|
onClick={() => onChange(opt.value)}
|
|
263
446
|
className={cn(
|
|
264
|
-
"min-h-7 rounded-og-xs px-1.5 py-0.5 text-og-xs max-[1023px]:min-h-11 pointer-coarse:min-h-11",
|
|
447
|
+
"min-h-7 rounded-og-xs px-1.5 py-0.5 text-og-xs focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-og-accent max-[1023px]:min-h-11 max-[1023px]:min-w-11 pointer-coarse:min-h-11 pointer-coarse:min-w-11",
|
|
265
448
|
opt.value === value
|
|
266
449
|
? "bg-og-accent-soft text-og-fg"
|
|
267
450
|
: "text-og-fg-subtle hover:text-og-fg",
|
|
@@ -294,7 +477,8 @@ type FileViewState = {
|
|
|
294
477
|
*/
|
|
295
478
|
function useFileView(
|
|
296
479
|
path: string | null,
|
|
297
|
-
readFile:
|
|
480
|
+
readFile: UseSandboxFilesResult["readFile"],
|
|
481
|
+
reloadRevision = 0,
|
|
298
482
|
): FileViewState {
|
|
299
483
|
const [state, setState] = useState<FileViewState>({
|
|
300
484
|
content: null,
|
|
@@ -317,6 +501,7 @@ function useFileView(
|
|
|
317
501
|
return;
|
|
318
502
|
}
|
|
319
503
|
let cancelled = false;
|
|
504
|
+
const abort = new AbortController();
|
|
320
505
|
setState({
|
|
321
506
|
content: null,
|
|
322
507
|
isBinary: false,
|
|
@@ -325,7 +510,7 @@ function useFileView(
|
|
|
325
510
|
loading: true,
|
|
326
511
|
error: null,
|
|
327
512
|
});
|
|
328
|
-
void readFile(path)
|
|
513
|
+
void readFile(path, { signal: abort.signal })
|
|
329
514
|
.then((res) => {
|
|
330
515
|
if (cancelled) return;
|
|
331
516
|
const content = res.isBinary
|
|
@@ -355,8 +540,9 @@ function useFileView(
|
|
|
355
540
|
});
|
|
356
541
|
return () => {
|
|
357
542
|
cancelled = true;
|
|
543
|
+
abort.abort();
|
|
358
544
|
};
|
|
359
|
-
}, [path, readFile]);
|
|
545
|
+
}, [path, readFile, reloadRevision]);
|
|
360
546
|
return state;
|
|
361
547
|
}
|
|
362
548
|
|
|
@@ -375,15 +561,38 @@ function decodeBase64Utf8(b64: string): string {
|
|
|
375
561
|
return b64;
|
|
376
562
|
}
|
|
377
563
|
|
|
378
|
-
function Notice({
|
|
564
|
+
function Notice({
|
|
565
|
+
children,
|
|
566
|
+
className,
|
|
567
|
+
icon,
|
|
568
|
+
title,
|
|
569
|
+
announce,
|
|
570
|
+
}: {
|
|
571
|
+
children: ReactNode;
|
|
572
|
+
className?: string | undefined;
|
|
573
|
+
icon?: ReactNode | undefined;
|
|
574
|
+
title?: string | undefined;
|
|
575
|
+
announce?: "status" | "alert" | undefined;
|
|
576
|
+
}) {
|
|
379
577
|
return (
|
|
380
578
|
<div
|
|
579
|
+
role={announce}
|
|
580
|
+
aria-atomic={announce ? "true" : undefined}
|
|
581
|
+
aria-live={announce === "alert" ? "assertive" : announce === "status" ? "polite" : undefined}
|
|
381
582
|
className={cn(
|
|
382
583
|
"flex h-full items-center justify-center p-4 text-center text-og-sm text-og-fg-subtle",
|
|
383
584
|
className,
|
|
384
585
|
)}
|
|
385
586
|
>
|
|
386
|
-
|
|
587
|
+
<div className="flex max-w-sm flex-col items-center gap-2.5">
|
|
588
|
+
{icon ? (
|
|
589
|
+
<span className="grid size-10 place-items-center rounded-og-lg border border-og-border bg-og-surface-1 text-og-fg-muted shadow-sm">
|
|
590
|
+
{icon}
|
|
591
|
+
</span>
|
|
592
|
+
) : null}
|
|
593
|
+
{title ? <p className="font-medium text-og-fg">{title}</p> : null}
|
|
594
|
+
<div className="leading-5">{children}</div>
|
|
595
|
+
</div>
|
|
387
596
|
</div>
|
|
388
597
|
);
|
|
389
598
|
}
|
|
@@ -221,7 +221,13 @@ export function SandboxTerminal({
|
|
|
221
221
|
// status lines INSIDE xterm instead of an overlay — but only when there is no
|
|
222
222
|
// firehose transcript to show yet (otherwise the projected output is shown).
|
|
223
223
|
const warm = liveness === "warm" || liveness === "draining";
|
|
224
|
-
const booting =
|
|
224
|
+
const booting =
|
|
225
|
+
activated &&
|
|
226
|
+
!ptyMode &&
|
|
227
|
+
!warm &&
|
|
228
|
+
liveness !== null &&
|
|
229
|
+
liveness !== undefined &&
|
|
230
|
+
result.chunks.length === 0;
|
|
225
231
|
|
|
226
232
|
function handleActivate() {
|
|
227
233
|
if (!activated) setActivated(true);
|
|
@@ -502,7 +508,7 @@ export function SandboxTerminal({
|
|
|
502
508
|
writtenRef.current = new Set();
|
|
503
509
|
wroteFirehoseRef.current = false;
|
|
504
510
|
}}
|
|
505
|
-
className="rounded-og-sm px-1.5 py-0.5 text-og-xs hover:text-og-fg pointer-coarse:min-h-
|
|
511
|
+
className="rounded-og-sm px-1.5 py-0.5 text-og-xs hover:text-og-fg pointer-coarse:min-h-11"
|
|
506
512
|
>
|
|
507
513
|
Clear
|
|
508
514
|
</button>
|