@opengeni/react 0.12.0 → 0.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +20 -14
- package/dist/chunk-TOJR776I.js +2280 -0
- package/dist/chunk-TOJR776I.js.map +1 -0
- package/dist/index.d.ts +1040 -363
- package/dist/index.js +10938 -6281
- package/dist/index.js.map +1 -1
- package/dist/{machines-BeZ3bD3t.d.ts → machines-BpdwuQcD.d.ts} +134 -14
- package/dist/machines.d.ts +1 -1
- package/dist/machines.js +23 -1
- package/package.json +20 -12
- package/src/approvals.ts +16 -4
- package/src/client.ts +41 -5
- package/src/commands/index.ts +1 -7
- package/src/commands/registry.ts +46 -12
- package/src/components/chat-composer.tsx +417 -69
- package/src/components/code-editor.tsx +28 -36
- package/src/components/command-palette.tsx +26 -5
- package/src/components/desktop-viewer.tsx +29 -16
- package/src/components/diff-view.tsx +27 -189
- package/src/components/enrollment-consent.tsx +28 -10
- package/src/components/enrollment-device-flow.tsx +8 -5
- package/src/components/file-browser.tsx +190 -56
- package/src/components/fleet-tile.tsx +13 -4
- package/src/components/machine-card.tsx +93 -15
- package/src/components/machine-dock-bar.tsx +20 -8
- package/src/components/machine-health-pill.tsx +68 -0
- package/src/components/machine-metrics.tsx +28 -19
- package/src/components/machine-status-pill.tsx +26 -5
- package/src/components/machines/health.ts +146 -0
- package/src/components/machines/machine-detail.tsx +220 -0
- package/src/components/machines/metric-history-chart.tsx +298 -0
- package/src/components/machines/metric-sparkline.tsx +76 -0
- package/src/components/machines/series.ts +113 -0
- package/src/components/machines-dashboard.tsx +21 -3
- package/src/components/markdown.tsx +39 -9
- package/src/components/message-timeline.tsx +633 -109
- package/src/components/pierre-diff.tsx +83 -15
- package/src/components/pierre-file.tsx +10 -9
- package/src/components/queue-surface.tsx +578 -0
- package/src/components/sandbox-files.tsx +193 -225
- package/src/components/sandbox-terminal.tsx +256 -88
- package/src/components/sandbox-workspace.tsx +957 -0
- package/src/components/session-status.tsx +22 -2
- package/src/components/workbench-changes.tsx +585 -0
- package/src/components/workspace-dock.tsx +213 -59
- package/src/hooks/internal.ts +5 -2
- package/src/hooks/use-available-models.ts +7 -2
- package/src/hooks/use-billing-usage.ts +4 -1
- package/src/hooks/use-codex-accounts.ts +74 -44
- package/src/hooks/use-composer.ts +414 -63
- package/src/hooks/use-environments.ts +92 -47
- package/src/hooks/use-file-attachments.ts +101 -55
- package/src/hooks/use-goal.ts +52 -16
- package/src/hooks/use-machine-chip.ts +134 -0
- package/src/hooks/use-machines.ts +34 -16
- package/src/hooks/use-packs.ts +24 -19
- package/src/hooks/use-relay-frame-stream.ts +5 -2
- package/src/hooks/use-rigs.ts +335 -0
- package/src/hooks/use-sandbox-files.ts +213 -39
- package/src/hooks/use-sandbox-git.ts +188 -11
- package/src/hooks/use-sandbox-terminal.ts +18 -14
- package/src/hooks/use-scheduled-tasks.ts +10 -2
- package/src/hooks/use-session-capabilities.ts +77 -20
- package/src/hooks/use-session-control.ts +50 -23
- package/src/hooks/use-session-events.ts +50 -21
- package/src/hooks/use-session-lineage.ts +128 -0
- package/src/hooks/use-session.ts +53 -30
- package/src/hooks/use-slash-commands.ts +6 -4
- package/src/hooks/use-turn-queue.ts +202 -147
- package/src/hooks/use-windowed-sections.ts +204 -0
- package/src/hooks/use-workspace-capture.ts +233 -0
- package/src/hooks/use-workspace-edit.ts +231 -0
- package/src/hooks/use-workspace-sessions.ts +48 -5
- package/src/hooks/use-workspaces.ts +18 -15
- package/src/index.ts +133 -23
- package/src/lib/format.ts +3 -3
- package/src/lib/xterm-renderer.ts +65 -0
- package/src/lib/xterm-theme.ts +224 -18
- package/src/machines.ts +29 -3
- package/src/provider.tsx +195 -6
- package/src/timeline/activity-rail.tsx +213 -54
- package/src/timeline/disclosure-context.tsx +12 -2
- package/src/timeline/index.ts +21 -2
- package/src/timeline/parsers.ts +87 -16
- package/src/timeline/projection.ts +412 -87
- package/src/timeline/shared.tsx +111 -24
- package/src/timeline/tool-diff.tsx +24 -20
- package/src/timeline/tool-renderers.tsx +98 -21
- package/src/timeline/turn-summary.tsx +76 -23
- package/src/timeline/types.ts +96 -12
- package/styles/index.css +22 -0
- package/styles/tokens.css +2 -2
- package/dist/chunk-5TIXRCSI.js +0 -1291
- package/dist/chunk-5TIXRCSI.js.map +0 -1
|
@@ -1,85 +1,94 @@
|
|
|
1
|
-
import type { FsReadResponse
|
|
2
|
-
import {
|
|
3
|
-
import { type ReactNode, useCallback, useEffect,
|
|
1
|
+
import type { FsReadResponse } from "@opengeni/sdk";
|
|
2
|
+
import { FileCode2Icon, FileWarningIcon, LoaderCircleIcon } from "lucide-react";
|
|
3
|
+
import { type ReactNode, useCallback, useEffect, useRef, useState } from "react";
|
|
4
4
|
import { cn } from "../lib/cn";
|
|
5
5
|
import { useThemeType } from "../lib/use-theme-type";
|
|
6
6
|
import type { UseSandboxFilesResult } from "../hooks/use-sandbox-files";
|
|
7
7
|
import type { UseSandboxGitResult } from "../hooks/use-sandbox-git";
|
|
8
8
|
import { CodeEditor } from "./code-editor";
|
|
9
9
|
import { FileBrowser } from "./file-browser";
|
|
10
|
-
import { DiffView } from "./diff-view";
|
|
11
|
-
import { PierreDiff } from "./pierre-diff";
|
|
12
10
|
import { PierreFile } from "./pierre-file";
|
|
13
11
|
|
|
14
12
|
export type SandboxFilesProps = {
|
|
15
13
|
/** From `useSandboxFiles(...)`. */
|
|
16
14
|
files: UseSandboxFilesResult;
|
|
17
|
-
/** From `useSandboxGit(...)` — the
|
|
15
|
+
/** From `useSandboxGit(...)` — drives the branch/dirty header only. */
|
|
18
16
|
git: UseSandboxGitResult;
|
|
19
|
-
/**
|
|
17
|
+
/** @deprecated Diffs live in the dedicated Changes tab now; accepted for
|
|
18
|
+
* source-compat but unused (the Files surface is a browser + viewer). */
|
|
20
19
|
stagedGit?: UseSandboxGitResult | undefined;
|
|
21
20
|
/** Whether a FileSystem surface is advertised (drives the unavailable notice). */
|
|
22
21
|
fileSystemAvailable?: boolean | undefined;
|
|
23
|
-
/** Use Pierre's Shiki
|
|
22
|
+
/** Use Pierre's Shiki highlighter for the viewer (default true; plain fallback). */
|
|
24
23
|
usePierre?: boolean | undefined;
|
|
25
24
|
/** Allow in-place editing of tree files (CodeMirror). Default true. When false
|
|
26
25
|
* the surface is review-only: every text file opens in the read-only viewer. */
|
|
27
26
|
editable?: boolean | undefined;
|
|
27
|
+
/** Fired once when the user first edits an open file (wake-on-edit intent). The
|
|
28
|
+
* dock warms the box on this so the save lands fast; opening/reading never fires
|
|
29
|
+
* it. Browsing the tree/diff must not warm a box. */
|
|
30
|
+
onEditIntent?: (() => void) | undefined;
|
|
31
|
+
/** A guarded diff path routed here by the parent workspace. */
|
|
32
|
+
requestedPath?: string | undefined;
|
|
33
|
+
/** Identity for one guarded-file request. Increment this when the same path is
|
|
34
|
+
* deliberately requested again; it also lets a pending request be consumed
|
|
35
|
+
* without overriding later manual tree navigation. Defaults to the path. */
|
|
36
|
+
requestedPathRequestId?: string | number | undefined;
|
|
37
|
+
/** False while the parent is waking a cold sandbox for `requestedPath`. */
|
|
38
|
+
requestedPathReady?: boolean | undefined;
|
|
28
39
|
themeType?: "dark" | "light" | undefined;
|
|
29
40
|
className?: string | undefined;
|
|
30
41
|
};
|
|
31
42
|
|
|
32
|
-
const STATUS_TINT: Record<GitFileDiff["status"], string> = {
|
|
33
|
-
added: "text-og-status-idle",
|
|
34
|
-
modified: "text-og-status-running",
|
|
35
|
-
deleted: "text-og-status-failed",
|
|
36
|
-
renamed: "text-og-accent",
|
|
37
|
-
copied: "text-og-accent",
|
|
38
|
-
untracked: "text-og-fg-subtle",
|
|
39
|
-
ignored: "text-og-fg-subtle",
|
|
40
|
-
conflicted: "text-og-status-failed",
|
|
41
|
-
typechange: "text-og-status-running",
|
|
42
|
-
};
|
|
43
|
-
|
|
44
|
-
const STATUS_LETTER: Record<GitFileDiff["status"], string> = {
|
|
45
|
-
added: "A",
|
|
46
|
-
modified: "M",
|
|
47
|
-
deleted: "D",
|
|
48
|
-
renamed: "R",
|
|
49
|
-
copied: "C",
|
|
50
|
-
untracked: "U",
|
|
51
|
-
ignored: "I",
|
|
52
|
-
conflicted: "!",
|
|
53
|
-
typechange: "T",
|
|
54
|
-
};
|
|
55
|
-
|
|
56
43
|
/**
|
|
57
|
-
* The
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
44
|
+
* The Files surface: a branch/dirty header, the full lazy file tree, and a
|
|
45
|
+
* viewer/editor pane for the selected file. This is the workspace BROWSER — pick
|
|
46
|
+
* a file to read or edit it. Diff review lives in the dedicated Changes tab (this
|
|
47
|
+
* surface deliberately does NOT replicate the changed-files list, and does not
|
|
48
|
+
* diff here — one job per tab). The agent commits; the human reviews.
|
|
62
49
|
*/
|
|
63
50
|
export function SandboxFiles({
|
|
64
51
|
files,
|
|
65
52
|
git,
|
|
66
|
-
stagedGit,
|
|
67
53
|
fileSystemAvailable = true,
|
|
68
54
|
usePierre = true,
|
|
69
55
|
editable = true,
|
|
56
|
+
onEditIntent,
|
|
57
|
+
requestedPath,
|
|
58
|
+
requestedPathRequestId,
|
|
59
|
+
requestedPathReady = true,
|
|
70
60
|
themeType,
|
|
71
61
|
className,
|
|
72
62
|
}: SandboxFilesProps) {
|
|
73
63
|
const [selected, setSelected] = useState<string | null>(null);
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
// View vs Edit for the read-only-viewer branch (a tree file that is NOT a
|
|
77
|
-
// diff). Resets to View on every new selection so opening a file never lands
|
|
78
|
-
// you in a stale dirty editor for a different path.
|
|
64
|
+
// View vs Edit for the selected file. Resets to View on every new selection so
|
|
65
|
+
// opening a file never lands you in a stale dirty editor for a different path.
|
|
79
66
|
const [editMode, setEditMode] = useState(false);
|
|
67
|
+
const pendingRequestRef = useRef<string | number | null>(null);
|
|
68
|
+
const handledRequestRef = useRef<string | number | null>(null);
|
|
69
|
+
const requestKey = requestedPath ? (requestedPathRequestId ?? requestedPath) : null;
|
|
80
70
|
|
|
81
|
-
|
|
82
|
-
|
|
71
|
+
useEffect(() => {
|
|
72
|
+
if (!requestedPath || requestKey === null) {
|
|
73
|
+
pendingRequestRef.current = null;
|
|
74
|
+
handledRequestRef.current = null;
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
if (handledRequestRef.current === requestKey) {
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
if (!requestedPathReady) {
|
|
81
|
+
pendingRequestRef.current = requestKey;
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
handledRequestRef.current = requestKey;
|
|
85
|
+
pendingRequestRef.current = null;
|
|
86
|
+
setSelected(requestedPath);
|
|
87
|
+
setEditMode(false);
|
|
88
|
+
}, [requestKey, requestedPath, requestedPathReady]);
|
|
89
|
+
|
|
90
|
+
// Side-by-side (tree left, viewer right) once the surface is wide enough;
|
|
91
|
+
// stacked (tree over viewer) on a narrow dock. Tracked off the container so it
|
|
83
92
|
// reacts to the dock resize, not just the viewport.
|
|
84
93
|
const rootRef = useRef<HTMLDivElement | null>(null);
|
|
85
94
|
const [wide, setWide] = useState(false);
|
|
@@ -94,42 +103,33 @@ export function SandboxFiles({
|
|
|
94
103
|
return () => observer.disconnect();
|
|
95
104
|
}, []);
|
|
96
105
|
|
|
97
|
-
// Resolve the effective
|
|
98
|
-
// attribute the demo/app sets), defaulting to dark, unless the caller forced
|
|
99
|
-
// one. Keeps the Pierre diff dark-on-dark instead of a white slab.
|
|
106
|
+
// Resolve the effective viewer theme from the host palette (the `data-og-theme`
|
|
107
|
+
// attribute the demo/app sets), defaulting to dark, unless the caller forced one.
|
|
100
108
|
const resolvedTheme = useThemeType(themeType);
|
|
101
109
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
// The selection is EITHER a changed file (-> diff pane) or any tree file
|
|
107
|
-
// (-> read-only viewer pane). An explicit pick wins; otherwise default to the
|
|
108
|
-
// first changed file so a review-first dock opens on a diff. Crucially this is
|
|
109
|
-
// NOT gated on a repo: with no changes (or no repo) the pane simply waits for a
|
|
110
|
-
// tree click, and a clicked tree file always resolves to the viewer.
|
|
111
|
-
const effectiveSelected = selected ?? changed[0]?.path ?? null;
|
|
112
|
-
const selectedDiff = changed.find((f) => f.path === effectiveSelected) ?? null;
|
|
113
|
-
// A selected path that is not a changed file is viewed (fs.read), not diffed.
|
|
114
|
-
const viewPath = effectiveSelected && !selectedDiff ? effectiveSelected : null;
|
|
115
|
-
|
|
116
|
-
// Read the selected file's contents for the viewer pane (text/base64). The
|
|
117
|
-
// hook caps size + flags binary; we surface a notice for binary rather than
|
|
118
|
-
// dumping base64 into the highlighter.
|
|
110
|
+
// The selected tree file opens in the viewer (read-only) or the editor (when
|
|
111
|
+
// writable). Nothing is auto-selected — the pane waits for a tree click, so the
|
|
112
|
+
// Files tab opens as a calm browser, not a diff.
|
|
113
|
+
const viewPath = selected;
|
|
119
114
|
const fileView = useFileView(viewPath, files.readFile);
|
|
120
115
|
|
|
121
|
-
// Selecting a (different) file always returns to View — never drop the user
|
|
122
|
-
//
|
|
116
|
+
// Selecting a (different) file always returns to View — never drop the user into
|
|
117
|
+
// an editor whose buffer belongs to the previously-selected path. Manual
|
|
118
|
+
// navigation also consumes a pending guarded-file request: a late cold→warm
|
|
119
|
+
// transition must never pull the user away from the file they chose meanwhile.
|
|
123
120
|
const selectFile = useCallback((path: string) => {
|
|
121
|
+
if (pendingRequestRef.current !== null) {
|
|
122
|
+
handledRequestRef.current = pendingRequestRef.current;
|
|
123
|
+
pendingRequestRef.current = null;
|
|
124
|
+
}
|
|
124
125
|
setSelected(path);
|
|
125
126
|
setEditMode(false);
|
|
126
127
|
}, []);
|
|
127
128
|
|
|
128
129
|
// A tree file is editable only when it is a real, fully-loaded text file: not
|
|
129
|
-
// binary (would corrupt on save) and not truncated (we only hold a PREFIX
|
|
130
|
-
//
|
|
131
|
-
//
|
|
132
|
-
// read-only in the viewer.
|
|
130
|
+
// binary (would corrupt on save) and not truncated (we only hold a PREFIX). The
|
|
131
|
+
// editor is then additionally gated on the `editable` prop; anything failing this
|
|
132
|
+
// opens read-only in the viewer.
|
|
133
133
|
const canEdit =
|
|
134
134
|
editable &&
|
|
135
135
|
viewPath !== null &&
|
|
@@ -142,151 +142,72 @@ export function SandboxFiles({
|
|
|
142
142
|
|
|
143
143
|
if (!fileSystemAvailable) {
|
|
144
144
|
return (
|
|
145
|
-
<Notice
|
|
145
|
+
<Notice
|
|
146
|
+
className={className}
|
|
147
|
+
icon={<FileWarningIcon className="size-5" aria-hidden />}
|
|
148
|
+
title="Files unavailable"
|
|
149
|
+
>
|
|
150
|
+
This sandbox does not expose a file system.
|
|
151
|
+
</Notice>
|
|
146
152
|
);
|
|
147
153
|
}
|
|
148
154
|
|
|
149
155
|
return (
|
|
150
156
|
<div ref={rootRef} className={cn("flex h-full min-h-0 min-w-0 flex-col", className)}>
|
|
151
|
-
{/* Branch + dirty header */}
|
|
152
|
-
<GitHeader git={git} dirtyCount={
|
|
157
|
+
{/* Branch + dirty header (context; NOT the changed-files list). */}
|
|
158
|
+
<GitHeader git={git} dirtyCount={git.diff.length} />
|
|
153
159
|
|
|
154
160
|
<div className={cn("flex min-h-0 flex-1", wide ? "flex-row" : "flex-col")}>
|
|
155
|
-
{/* Tree pane:
|
|
156
|
-
|
|
161
|
+
{/* Tree pane: the full lazy file tree. A fixed left column when wide, a top
|
|
162
|
+
band when narrow. */}
|
|
157
163
|
<div
|
|
158
164
|
className={cn(
|
|
159
|
-
"min-h-0
|
|
160
|
-
wide
|
|
161
|
-
? "w-[280px] shrink-0 border-r border-og-border"
|
|
162
|
-
: "flex-1",
|
|
165
|
+
"flex min-h-0 flex-col",
|
|
166
|
+
wide ? "w-[280px] shrink-0 border-r border-og-border" : "flex-1",
|
|
163
167
|
)}
|
|
164
168
|
>
|
|
165
|
-
{changed.length > 0 && (
|
|
166
|
-
<div className="border-b border-og-border">
|
|
167
|
-
<div className="px-2 py-1 text-og-xs font-medium uppercase tracking-wide text-og-fg-subtle">
|
|
168
|
-
Changes · {changed.length}
|
|
169
|
-
</div>
|
|
170
|
-
<ul className="pb-1">
|
|
171
|
-
{changed.map((file) => (
|
|
172
|
-
<li key={file.path}>
|
|
173
|
-
<button
|
|
174
|
-
type="button"
|
|
175
|
-
onClick={() => selectFile(file.path)}
|
|
176
|
-
className={cn(
|
|
177
|
-
"flex w-full items-center gap-1.5 truncate px-2 py-0.5 text-left text-og-sm hover:bg-og-surface-2 pointer-coarse:min-h-10",
|
|
178
|
-
file.path === effectiveSelected && "bg-og-surface-2",
|
|
179
|
-
)}
|
|
180
|
-
>
|
|
181
|
-
<span
|
|
182
|
-
className={cn(
|
|
183
|
-
"w-3 shrink-0 text-center font-og-mono text-og-xs",
|
|
184
|
-
STATUS_TINT[file.status],
|
|
185
|
-
)}
|
|
186
|
-
>
|
|
187
|
-
{STATUS_LETTER[file.status]}
|
|
188
|
-
</span>
|
|
189
|
-
<FileIcon className="size-3.5 shrink-0 opacity-70" />
|
|
190
|
-
<span className="truncate">{file.path}</span>
|
|
191
|
-
<span className="ml-auto flex shrink-0 items-center gap-1.5 pl-2 text-og-xs">
|
|
192
|
-
<span className="text-og-status-idle">
|
|
193
|
-
+{file.additions}
|
|
194
|
-
</span>
|
|
195
|
-
<span className="text-og-status-failed">
|
|
196
|
-
−{file.deletions}
|
|
197
|
-
</span>
|
|
198
|
-
</span>
|
|
199
|
-
</button>
|
|
200
|
-
</li>
|
|
201
|
-
))}
|
|
202
|
-
</ul>
|
|
203
|
-
</div>
|
|
204
|
-
)}
|
|
205
|
-
|
|
206
|
-
<div className="px-2 py-1 text-og-xs font-medium uppercase tracking-wide text-og-fg-subtle">
|
|
207
|
-
Files
|
|
208
|
-
</div>
|
|
209
169
|
<FileBrowser
|
|
210
170
|
result={files}
|
|
211
|
-
selectedPath={
|
|
171
|
+
selectedPath={selected ?? undefined}
|
|
212
172
|
onSelectFile={selectFile}
|
|
173
|
+
editable={editable}
|
|
213
174
|
emptyState="This directory is empty"
|
|
214
|
-
className="min-w-0"
|
|
175
|
+
className="min-w-0 flex-1"
|
|
215
176
|
/>
|
|
216
177
|
</div>
|
|
217
178
|
|
|
218
|
-
{/*
|
|
219
|
-
side-by-side, sits below the tree when
|
|
179
|
+
{/* Viewer pane: the selected file's contents (read-only) or the editor.
|
|
180
|
+
Fills the remaining width when side-by-side, sits below the tree when
|
|
181
|
+
stacked. */}
|
|
220
182
|
<div
|
|
221
183
|
className={cn(
|
|
222
184
|
"flex min-h-0 min-w-0 flex-col",
|
|
223
|
-
wide
|
|
224
|
-
? "flex-1"
|
|
225
|
-
: "flex-[1.4] border-t border-og-border",
|
|
185
|
+
wide ? "flex-1" : "flex-[1.4] border-t border-og-border",
|
|
226
186
|
)}
|
|
227
187
|
>
|
|
228
188
|
<div className="flex shrink-0 items-center justify-between gap-2 border-b border-og-border bg-og-surface-1 px-2 py-1">
|
|
229
|
-
<span
|
|
230
|
-
|
|
189
|
+
<span
|
|
190
|
+
data-opengeni-selected-file
|
|
191
|
+
className="min-w-0 truncate font-og-mono text-og-xs text-og-fg-muted"
|
|
192
|
+
>
|
|
193
|
+
{selected ?? "No file selected"}
|
|
231
194
|
</span>
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
{selectedDiff && (
|
|
246
|
-
<Segmented
|
|
247
|
-
options={[
|
|
248
|
-
{ value: "unified", label: "Unified" },
|
|
249
|
-
{ value: "split", label: "Split" },
|
|
250
|
-
]}
|
|
251
|
-
value={layout}
|
|
252
|
-
onChange={(v) => setLayout(v as "unified" | "split")}
|
|
253
|
-
/>
|
|
254
|
-
)}
|
|
255
|
-
{/* View/Edit toggle — only for a real, fully-loaded text file the
|
|
256
|
-
editor can safely round-trip. Binary/truncated files never get
|
|
257
|
-
an Edit affordance (they'd corrupt on save). */}
|
|
258
|
-
{!selectedDiff && canEdit && (
|
|
259
|
-
<Segmented
|
|
260
|
-
options={[
|
|
261
|
-
{ value: "view", label: "View" },
|
|
262
|
-
{ value: "edit", label: "Edit" },
|
|
263
|
-
]}
|
|
264
|
-
value={editMode ? "edit" : "view"}
|
|
265
|
-
onChange={(v) => setEditMode(v === "edit")}
|
|
266
|
-
/>
|
|
267
|
-
)}
|
|
268
|
-
</div>
|
|
195
|
+
{/* View/Edit toggle — only for a real, fully-loaded text file the editor
|
|
196
|
+
can safely round-trip. Binary/truncated/read-only files never get an
|
|
197
|
+
Edit affordance (they'd corrupt on save or can't be written). */}
|
|
198
|
+
{canEdit && (
|
|
199
|
+
<Segmented
|
|
200
|
+
options={[
|
|
201
|
+
{ value: "view", label: "View" },
|
|
202
|
+
{ value: "edit", label: "Edit" },
|
|
203
|
+
]}
|
|
204
|
+
value={editMode ? "edit" : "view"}
|
|
205
|
+
onChange={(v) => setEditMode(v === "edit")}
|
|
206
|
+
/>
|
|
207
|
+
)}
|
|
269
208
|
</div>
|
|
270
209
|
<div className="min-h-0 flex-1 overflow-auto">
|
|
271
|
-
{
|
|
272
|
-
// A changed file -> diff pane (HEAD vs working/staged).
|
|
273
|
-
usePierre ? (
|
|
274
|
-
<PierreDiff
|
|
275
|
-
diff={[selectedDiff]}
|
|
276
|
-
layout={layout}
|
|
277
|
-
themeType={resolvedTheme}
|
|
278
|
-
fallback={
|
|
279
|
-
<DiffView diff={[selectedDiff]} layout={layout} isRepo={git.isRepo} className="p-1" />
|
|
280
|
-
}
|
|
281
|
-
className="p-1"
|
|
282
|
-
/>
|
|
283
|
-
) : (
|
|
284
|
-
<DiffView diff={[selectedDiff]} layout={layout} isRepo={git.isRepo} className="p-1" />
|
|
285
|
-
)
|
|
286
|
-
) : viewPath ? (
|
|
287
|
-
// Any other tree file -> editable editor (Edit mode) or read-only
|
|
288
|
-
// single-file viewer (fs.read). This works with NO repo — viewing
|
|
289
|
-
// and editing files never requires git.
|
|
210
|
+
{viewPath ? (
|
|
290
211
|
showEditor && fileView.content !== null ? (
|
|
291
212
|
<CodeEditor
|
|
292
213
|
key={viewPath}
|
|
@@ -294,49 +215,64 @@ export function SandboxFiles({
|
|
|
294
215
|
initialContents={fileView.content}
|
|
295
216
|
themeType={resolvedTheme}
|
|
296
217
|
onSave={(contents) => files.writeFile(viewPath, contents)}
|
|
218
|
+
{...(onEditIntent ? { onEditIntent } : {})}
|
|
297
219
|
className="h-full"
|
|
298
220
|
/>
|
|
299
221
|
) : fileView.error ? (
|
|
300
|
-
<Notice>
|
|
222
|
+
<Notice>
|
|
223
|
+
Could not open {viewPath}: {fileView.error.message}
|
|
224
|
+
</Notice>
|
|
301
225
|
) : fileView.loading ? (
|
|
302
226
|
<Notice>Loading {viewPath}…</Notice>
|
|
303
227
|
) : fileView.isBinary ? (
|
|
304
|
-
<Notice>
|
|
228
|
+
<Notice>
|
|
229
|
+
{viewPath} is a binary file ({fileView.sizeBytes ?? 0} bytes).
|
|
230
|
+
</Notice>
|
|
305
231
|
) : fileView.content !== null ? (
|
|
306
232
|
<>
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
233
|
+
{fileView.truncated && (
|
|
234
|
+
<div className="border-b border-og-border bg-og-surface-1 px-2 py-1 text-og-xs text-og-status-running">
|
|
235
|
+
Large file — showing a truncated preview ({fileView.sizeBytes ?? 0} bytes
|
|
236
|
+
loaded). Editing is disabled to avoid corrupting the file.
|
|
237
|
+
</div>
|
|
238
|
+
)}
|
|
239
|
+
{usePierre ? (
|
|
240
|
+
<PierreFile
|
|
241
|
+
path={viewPath}
|
|
242
|
+
contents={fileView.content}
|
|
243
|
+
themeType={resolvedTheme}
|
|
244
|
+
fallback={
|
|
245
|
+
<pre className="overflow-auto whitespace-pre p-2 font-og-mono text-og-sm text-og-fg">
|
|
246
|
+
{fileView.content}
|
|
247
|
+
</pre>
|
|
248
|
+
}
|
|
249
|
+
className="p-1"
|
|
250
|
+
/>
|
|
251
|
+
) : (
|
|
252
|
+
<pre className="overflow-auto whitespace-pre p-2 font-og-mono text-og-sm text-og-fg">
|
|
253
|
+
{fileView.content}
|
|
254
|
+
</pre>
|
|
255
|
+
)}
|
|
329
256
|
</>
|
|
330
257
|
) : (
|
|
331
258
|
<Notice>Loading {viewPath}…</Notice>
|
|
332
259
|
)
|
|
260
|
+
) : // Nothing selected — the tree shows the whole workspace; pick a file.
|
|
261
|
+
requestedPath && !requestedPathReady ? (
|
|
262
|
+
<Notice
|
|
263
|
+
icon={
|
|
264
|
+
<LoaderCircleIcon
|
|
265
|
+
className="size-5 animate-spin motion-reduce:animate-none"
|
|
266
|
+
aria-hidden
|
|
267
|
+
/>
|
|
268
|
+
}
|
|
269
|
+
title="Waking sandbox"
|
|
270
|
+
>
|
|
271
|
+
Opening {requestedPath} when the live workspace is ready…
|
|
272
|
+
</Notice>
|
|
333
273
|
) : (
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
<Notice>
|
|
337
|
-
{changed.length > 0
|
|
338
|
-
? "Select a changed file to review its diff, or a file in the tree to view it."
|
|
339
|
-
: "Select a file in the tree to view it."}
|
|
274
|
+
<Notice icon={<FileCode2Icon className="size-5" aria-hidden />} title="Choose a file">
|
|
275
|
+
Select a file in the tree to preview it.
|
|
340
276
|
</Notice>
|
|
341
277
|
)}
|
|
342
278
|
</div>
|
|
@@ -367,7 +303,7 @@ function GitHeader({ git, dirtyCount }: { git: UseSandboxGitResult; dirtyCount:
|
|
|
367
303
|
</span>
|
|
368
304
|
)}
|
|
369
305
|
{dirty && (
|
|
370
|
-
<span className="ml-auto shrink-0 text-og-xs text-og-fg-subtle">
|
|
306
|
+
<span data-contrast-audited className="ml-auto shrink-0 text-og-xs text-og-fg-subtle">
|
|
371
307
|
{dirtyCount} changed
|
|
372
308
|
</span>
|
|
373
309
|
)}
|
|
@@ -392,7 +328,7 @@ function Segmented({
|
|
|
392
328
|
type="button"
|
|
393
329
|
onClick={() => onChange(opt.value)}
|
|
394
330
|
className={cn(
|
|
395
|
-
"rounded-og-xs px-1.5 py-0.5 text-og-xs pointer-coarse:min-h-
|
|
331
|
+
"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 pointer-coarse:min-h-11",
|
|
396
332
|
opt.value === value
|
|
397
333
|
? "bg-og-accent-soft text-og-fg"
|
|
398
334
|
: "text-og-fg-subtle hover:text-og-fg",
|
|
@@ -437,11 +373,25 @@ function useFileView(
|
|
|
437
373
|
});
|
|
438
374
|
useEffect(() => {
|
|
439
375
|
if (!path) {
|
|
440
|
-
setState({
|
|
376
|
+
setState({
|
|
377
|
+
content: null,
|
|
378
|
+
isBinary: false,
|
|
379
|
+
truncated: false,
|
|
380
|
+
sizeBytes: null,
|
|
381
|
+
loading: false,
|
|
382
|
+
error: null,
|
|
383
|
+
});
|
|
441
384
|
return;
|
|
442
385
|
}
|
|
443
386
|
let cancelled = false;
|
|
444
|
-
setState({
|
|
387
|
+
setState({
|
|
388
|
+
content: null,
|
|
389
|
+
isBinary: false,
|
|
390
|
+
truncated: false,
|
|
391
|
+
sizeBytes: null,
|
|
392
|
+
loading: true,
|
|
393
|
+
error: null,
|
|
394
|
+
});
|
|
445
395
|
void readFile(path)
|
|
446
396
|
.then((res) => {
|
|
447
397
|
if (cancelled) return;
|
|
@@ -492,7 +442,17 @@ function decodeBase64Utf8(b64: string): string {
|
|
|
492
442
|
return b64;
|
|
493
443
|
}
|
|
494
444
|
|
|
495
|
-
function Notice({
|
|
445
|
+
function Notice({
|
|
446
|
+
children,
|
|
447
|
+
className,
|
|
448
|
+
icon,
|
|
449
|
+
title,
|
|
450
|
+
}: {
|
|
451
|
+
children: ReactNode;
|
|
452
|
+
className?: string | undefined;
|
|
453
|
+
icon?: ReactNode | undefined;
|
|
454
|
+
title?: string | undefined;
|
|
455
|
+
}) {
|
|
496
456
|
return (
|
|
497
457
|
<div
|
|
498
458
|
className={cn(
|
|
@@ -500,7 +460,15 @@ function Notice({ children, className }: { children: ReactNode; className?: stri
|
|
|
500
460
|
className,
|
|
501
461
|
)}
|
|
502
462
|
>
|
|
503
|
-
|
|
463
|
+
<div className="flex max-w-sm flex-col items-center gap-2.5">
|
|
464
|
+
{icon ? (
|
|
465
|
+
<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">
|
|
466
|
+
{icon}
|
|
467
|
+
</span>
|
|
468
|
+
) : null}
|
|
469
|
+
{title ? <p className="font-medium text-og-fg">{title}</p> : null}
|
|
470
|
+
<div className="leading-5">{children}</div>
|
|
471
|
+
</div>
|
|
504
472
|
</div>
|
|
505
473
|
);
|
|
506
474
|
}
|