@opengeni/react 0.45.0 → 0.48.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 +68 -12
- package/dist/{chunk-U3CSCMWA.js → chunk-FK6VG4LL.js} +9 -13
- package/dist/chunk-FK6VG4LL.js.map +1 -0
- package/dist/{chunk-HFO4ERGQ.js → chunk-FWXGSNXT.js} +2 -2
- package/dist/{chunk-SRFUT2ZU.js → chunk-I4MPFDRP.js} +119 -63
- package/dist/chunk-I4MPFDRP.js.map +1 -0
- package/dist/{chunk-EB67J347.js → chunk-KZ73RL76.js} +111 -31
- package/dist/chunk-KZ73RL76.js.map +1 -0
- package/dist/{chunk-LWR4MXSS.js → chunk-LHLYWG5R.js} +10 -1
- package/dist/chunk-LHLYWG5R.js.map +1 -0
- package/dist/{chunk-L3EWO3UK.js → chunk-NJ7M2B6Z.js} +445 -84
- package/dist/chunk-NJ7M2B6Z.js.map +1 -0
- package/dist/{chunk-VCBFUSUZ.js → chunk-O4VXOBGF.js} +3 -3
- package/dist/{chunk-VCBFUSUZ.js.map → chunk-O4VXOBGF.js.map} +1 -1
- package/dist/{chunk-6EDV6YFE.js → chunk-OL6QVXGF.js} +121 -29
- package/dist/chunk-OL6QVXGF.js.map +1 -0
- package/dist/chunk-VZTQ73XT.js +86 -0
- package/dist/chunk-VZTQ73XT.js.map +1 -0
- package/dist/{chunk-EHSYZ4KP.js → chunk-WYA65Y3F.js} +16 -16
- package/dist/chunk-WYA65Y3F.js.map +1 -0
- package/dist/{chunk-FD72GJMK.js → chunk-XCNTXWBB.js} +274 -125
- package/dist/chunk-XCNTXWBB.js.map +1 -0
- package/dist/client.d.ts +1 -1
- package/dist/components/chat-composer.d.ts +7 -2
- package/dist/components/composer-transcription-control.d.ts +2 -0
- package/dist/components/composer.d.ts +14 -0
- package/dist/components/machines/machine-detail.d.ts +3 -1
- package/dist/components/machines-dashboard.d.ts +3 -3
- package/dist/components/message-timeline.d.ts +12 -2
- package/dist/components/sandbox-files.d.ts +2 -2
- package/dist/components/sandbox-terminal.d.ts +29 -0
- package/dist/components/sandbox-workspace.d.ts +5 -1
- package/dist/components/workspace-dock.d.ts +3 -1
- package/dist/composer.d.ts +2 -2
- package/dist/composer.js +6 -6
- package/dist/hooks/internal.d.ts +6 -0
- package/dist/hooks/use-codex-accounts.d.ts +1 -1
- package/dist/hooks/use-environments.d.ts +10 -4
- package/dist/hooks/use-machines.d.ts +7 -0
- package/dist/hooks/use-sandbox-files.d.ts +19 -0
- package/dist/hooks/use-sandbox-git.d.ts +7 -0
- package/dist/hooks/use-transcription.d.ts +2 -2
- package/dist/hooks/use-voice-input.d.ts +13 -4
- package/dist/index.d.ts +6 -5
- package/dist/index.js +904 -538
- package/dist/index.js.map +1 -1
- package/dist/lib/composer-responsive-context.d.ts +9 -0
- package/dist/lib/use-portal-token-style.d.ts +14 -3
- package/dist/machines.js +2 -2
- package/dist/{queue-surface-implementation-J4XJFKRW.js → queue-surface-implementation-U5JFG7HT.js} +19 -18
- package/dist/queue-surface-implementation-U5JFG7HT.js.map +1 -0
- package/dist/realtime/realtime-control.d.ts +9 -0
- package/dist/realtime.js +29 -32
- package/dist/realtime.js.map +1 -1
- package/dist/session-ui.js +5 -5
- package/dist/session.js +4 -4
- package/dist/timeline/activity-rail.d.ts +3 -2
- package/dist/timeline/index.d.ts +2 -2
- package/dist/timeline/parsers.d.ts +3 -3
- package/dist/timeline/registry.d.ts +3 -0
- package/dist/timeline/screenshot-lightbox.d.ts +1 -1
- package/dist/timeline/types.d.ts +4 -0
- package/dist/voice-recording-store.d.ts +8 -2
- package/package.json +20 -4
- package/src/client.ts +3 -0
- package/src/components/chat-composer.tsx +12 -2
- package/src/components/code-editor.tsx +1 -1
- package/src/components/command-palette.tsx +1 -1
- package/src/components/composer-transcription-control.tsx +41 -16
- package/src/components/composer.tsx +52 -26
- package/src/components/copy-button.tsx +2 -2
- package/src/components/desktop-viewer.tsx +2 -2
- package/src/components/file-browser.tsx +39 -17
- package/src/components/human-input-form.tsx +2 -2
- package/src/components/machine-card.tsx +1 -1
- package/src/components/machines/machine-detail.tsx +39 -15
- package/src/components/machines-dashboard.tsx +7 -6
- package/src/components/message-timeline.tsx +44 -11
- package/src/components/model-picker.tsx +2 -2
- package/src/components/model-policy-picker.tsx +16 -15
- package/src/components/queue-surface-implementation.tsx +20 -16
- package/src/components/queue-surface-state.tsx +2 -2
- package/src/components/sandbox-files.tsx +140 -60
- package/src/components/sandbox-terminal.tsx +24 -5
- package/src/components/sandbox-workspace.tsx +180 -142
- package/src/components/session-chrome.tsx +14 -14
- package/src/components/tooltip.tsx +13 -16
- package/src/components/user-message-body.tsx +1 -1
- package/src/components/workbench-changes.tsx +4 -4
- package/src/components/workspace-dock.tsx +13 -5
- package/src/composer.ts +3 -0
- package/src/hooks/internal.ts +132 -38
- package/src/hooks/use-codex-accounts.ts +4 -4
- package/src/hooks/use-environments.ts +24 -5
- package/src/hooks/use-file-attachments.ts +70 -32
- package/src/hooks/use-goal.ts +17 -5
- package/src/hooks/use-machines.ts +47 -1
- package/src/hooks/use-sandbox-files.ts +310 -59
- package/src/hooks/use-sandbox-git.ts +148 -41
- package/src/hooks/use-session-capabilities.ts +35 -24
- package/src/hooks/use-session-events.ts +18 -4
- package/src/hooks/use-transcription.ts +4 -14
- package/src/hooks/use-turn-queue.ts +17 -5
- package/src/hooks/use-voice-input.ts +481 -36
- package/src/hooks/use-windowed-sections.ts +9 -7
- package/src/index.ts +11 -6
- package/src/lib/composer-responsive-context.ts +15 -0
- package/src/lib/use-portal-token-style.ts +60 -15
- package/src/provider.tsx +52 -7
- package/src/realtime/dropdown-menu.tsx +15 -19
- package/src/realtime/realtime-control.tsx +18 -7
- package/src/timeline/activity-rail.tsx +15 -6
- package/src/timeline/index.ts +1 -1
- package/src/timeline/parsers.ts +33 -20
- package/src/timeline/projection.ts +23 -0
- package/src/timeline/registry.ts +7 -0
- package/src/timeline/screenshot-lightbox.tsx +15 -4
- package/src/timeline/shared.tsx +8 -8
- package/src/timeline/tool-renderers.tsx +166 -22
- package/src/timeline/turn-summary.tsx +6 -6
- package/src/timeline/types.ts +4 -0
- package/src/voice-recording-store.ts +15 -0
- package/styles/compiled.css +5065 -0
- package/styles/compiled.d.ts +2 -0
- package/styles/effective-tokens.css +27 -0
- package/styles/index.css +3 -0
- package/styles/responsive.css +196 -0
- package/styles/responsive.d.ts +2 -0
- package/styles/tokens.css +50 -17
- package/dist/chunk-6EDV6YFE.js.map +0 -1
- package/dist/chunk-EB67J347.js.map +0 -1
- package/dist/chunk-EHSYZ4KP.js.map +0 -1
- package/dist/chunk-FD72GJMK.js.map +0 -1
- package/dist/chunk-JB5725BI.js +0 -48
- package/dist/chunk-JB5725BI.js.map +0 -1
- package/dist/chunk-L3EWO3UK.js.map +0 -1
- package/dist/chunk-LWR4MXSS.js.map +0 -1
- package/dist/chunk-SRFUT2ZU.js.map +0 -1
- package/dist/chunk-U3CSCMWA.js.map +0 -1
- package/dist/queue-surface-implementation-J4XJFKRW.js.map +0 -1
- /package/dist/{chunk-HFO4ERGQ.js.map → chunk-FWXGSNXT.js.map} +0 -0
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { FileCode2Icon, FileWarningIcon, LoaderCircleIcon } from "lucide-react";
|
|
2
2
|
import { type ReactNode, useCallback, useEffect, useRef, useState } from "react";
|
|
3
|
+
import { Group, Panel, Separator } from "react-resizable-panels";
|
|
3
4
|
import { cn } from "../lib/cn";
|
|
4
5
|
import { useThemeType } from "../lib/use-theme-type";
|
|
5
6
|
import {
|
|
6
7
|
CapturedFileUnavailableError,
|
|
8
|
+
type SandboxFilesGitSummary,
|
|
7
9
|
type UseSandboxFilesResult,
|
|
8
10
|
} from "../hooks/use-sandbox-files";
|
|
9
11
|
import type { UseSandboxGitResult } from "../hooks/use-sandbox-git";
|
|
@@ -14,8 +16,8 @@ import { PierreFile } from "./pierre-file";
|
|
|
14
16
|
export type SandboxFilesProps = {
|
|
15
17
|
/** From `useSandboxFiles(...)`. */
|
|
16
18
|
files: UseSandboxFilesResult;
|
|
17
|
-
/**
|
|
18
|
-
git
|
|
19
|
+
/** Compatibility fallback for callers that have not yet adopted `files.gitSummary`. */
|
|
20
|
+
git?: UseSandboxGitResult | undefined;
|
|
19
21
|
/** @deprecated Diffs live in the dedicated Changes tab now; accepted for
|
|
20
22
|
* source-compat but unused (the Files surface is a browser + viewer). */
|
|
21
23
|
stagedGit?: UseSandboxGitResult | undefined;
|
|
@@ -111,15 +113,29 @@ export function SandboxFiles({
|
|
|
111
113
|
const [wide, setWide] = useState(false);
|
|
112
114
|
useEffect(() => {
|
|
113
115
|
const el = rootRef.current;
|
|
114
|
-
if (!el
|
|
115
|
-
const
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
116
|
+
if (!el) return;
|
|
117
|
+
const update = () => setWide(el.getBoundingClientRect().width >= 720);
|
|
118
|
+
update();
|
|
119
|
+
if (typeof ResizeObserver === "undefined") return;
|
|
120
|
+
const observer = new ResizeObserver(update);
|
|
119
121
|
observer.observe(el);
|
|
120
|
-
|
|
122
|
+
// WorkspaceDock maximization changes its persistent surface from a sized
|
|
123
|
+
// panel child to `fixed inset-0`. Chromium can retain the child's observer
|
|
124
|
+
// size across that containing-block change even though its visible bounding
|
|
125
|
+
// box is now viewport-wide. Follow the surface mode as a deterministic
|
|
126
|
+
// second signal so the tree/viewer orientation matches what is painted.
|
|
127
|
+
const surface = el.closest("[data-workspace-surface]");
|
|
128
|
+
const surfaceObserver =
|
|
129
|
+
surface && typeof MutationObserver !== "undefined" ? new MutationObserver(update) : null;
|
|
130
|
+
surfaceObserver?.observe(surface!, {
|
|
131
|
+
attributes: true,
|
|
132
|
+
attributeFilter: ["class", "style"],
|
|
133
|
+
});
|
|
134
|
+
return () => {
|
|
135
|
+
observer.disconnect();
|
|
136
|
+
surfaceObserver?.disconnect();
|
|
137
|
+
};
|
|
121
138
|
}, []);
|
|
122
|
-
|
|
123
139
|
// Resolve the effective viewer theme from the host palette (the `data-og-theme`
|
|
124
140
|
// attribute the demo/app sets), defaulting to dark, unless the caller forced one.
|
|
125
141
|
const resolvedTheme = useThemeType(themeType);
|
|
@@ -128,7 +144,11 @@ export function SandboxFiles({
|
|
|
128
144
|
// writable). Nothing is auto-selected — the pane waits for a tree click, so the
|
|
129
145
|
// Files tab opens as a calm browser, not a diff.
|
|
130
146
|
const viewPath = selected;
|
|
131
|
-
const fileView = useFileView(
|
|
147
|
+
const fileView = useFileView(
|
|
148
|
+
viewPath,
|
|
149
|
+
files.readFile,
|
|
150
|
+
(files.contentRevision ?? 0) + viewReloadRevision,
|
|
151
|
+
);
|
|
132
152
|
|
|
133
153
|
// Selecting a (different) file always returns to View — never drop the user into
|
|
134
154
|
// an editor whose buffer belongs to the previously-selected path. Manual
|
|
@@ -165,19 +185,6 @@ export function SandboxFiles({
|
|
|
165
185
|
(!liveWorkspaceReady || files.loading) &&
|
|
166
186
|
captureFileUnavailable !== null;
|
|
167
187
|
|
|
168
|
-
if (!fileSystemAvailable) {
|
|
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
188
|
if (workspaceResting || workspaceWaking) {
|
|
182
189
|
return (
|
|
183
190
|
<div className={cn("h-full", className)} data-opengeni-workspace-resting>
|
|
@@ -208,19 +215,51 @@ export function SandboxFiles({
|
|
|
208
215
|
);
|
|
209
216
|
}
|
|
210
217
|
|
|
218
|
+
if (!fileSystemAvailable) {
|
|
219
|
+
return (
|
|
220
|
+
<Notice
|
|
221
|
+
className={className}
|
|
222
|
+
icon={<FileWarningIcon className="size-5" aria-hidden />}
|
|
223
|
+
title="Files unavailable"
|
|
224
|
+
announce="alert"
|
|
225
|
+
>
|
|
226
|
+
This sandbox does not expose a file system.
|
|
227
|
+
</Notice>
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
|
|
211
231
|
return (
|
|
212
232
|
<div ref={rootRef} className={cn("flex h-full min-h-0 min-w-0 flex-col", className)}>
|
|
213
233
|
{/* Branch + dirty header (context; NOT the changed-files list). */}
|
|
214
|
-
<GitHeader
|
|
234
|
+
<GitHeader
|
|
235
|
+
loading={
|
|
236
|
+
Boolean(files.loading || files.gitLoading || files.source === null || git?.loading) &&
|
|
237
|
+
files.gitSummary === null
|
|
238
|
+
}
|
|
239
|
+
git={
|
|
240
|
+
files.gitSummary ?? {
|
|
241
|
+
branch: git?.branch ?? null,
|
|
242
|
+
isRepo: git?.isRepo ?? false,
|
|
243
|
+
repoCount: git?.repoCount ?? 0,
|
|
244
|
+
ahead: git?.ahead ?? 0,
|
|
245
|
+
behind: git?.behind ?? 0,
|
|
246
|
+
dirtyCount: git?.diff.length ?? 0,
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
/>
|
|
215
250
|
|
|
216
|
-
<
|
|
251
|
+
<Group
|
|
252
|
+
orientation={wide ? "horizontal" : "vertical"}
|
|
253
|
+
className={cn("min-h-0 flex-1", wide ? "flex-row" : "flex-col")}
|
|
254
|
+
>
|
|
217
255
|
{/* Tree pane: the full lazy file tree. A fixed left column when wide, a top
|
|
218
256
|
band when narrow. */}
|
|
219
|
-
<
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
257
|
+
<Panel
|
|
258
|
+
id="sandbox-files-tree"
|
|
259
|
+
defaultSize={wide ? "280px" : "40%"}
|
|
260
|
+
minSize={wide ? "180px" : "120px"}
|
|
261
|
+
maxSize={wide ? "60%" : "70%"}
|
|
262
|
+
className="flex min-h-0 min-w-0 flex-col"
|
|
224
263
|
>
|
|
225
264
|
<FileBrowser
|
|
226
265
|
result={files}
|
|
@@ -230,16 +269,26 @@ export function SandboxFiles({
|
|
|
230
269
|
emptyState="This directory is empty"
|
|
231
270
|
className="min-w-0 flex-1"
|
|
232
271
|
/>
|
|
233
|
-
</
|
|
272
|
+
</Panel>
|
|
273
|
+
|
|
274
|
+
<Separator
|
|
275
|
+
aria-label="Resize file tree"
|
|
276
|
+
className={cn(
|
|
277
|
+
"group relative z-10 shrink-0 cursor-col-resize outline-hidden focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-og-accent",
|
|
278
|
+
"w-px bg-og-border after:absolute after:inset-y-0 after:left-1/2 after:w-3 after:-translate-x-1/2",
|
|
279
|
+
"hover:bg-og-accent focus-visible:bg-og-accent data-[separator-state=dragging]:bg-og-accent",
|
|
280
|
+
!wide &&
|
|
281
|
+
"h-px w-full cursor-row-resize after:inset-x-0 after:inset-y-auto after:top-1/2 after:h-3 after:w-full after:-translate-x-0 after:-translate-y-1/2",
|
|
282
|
+
)}
|
|
283
|
+
/>
|
|
234
284
|
|
|
235
285
|
{/* Viewer pane: the selected file's contents (read-only) or the editor.
|
|
236
286
|
Fills the remaining width when side-by-side, sits below the tree when
|
|
237
287
|
stacked. */}
|
|
238
|
-
<
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
)}
|
|
288
|
+
<Panel
|
|
289
|
+
id="sandbox-files-viewer"
|
|
290
|
+
minSize={wide ? "240px" : "160px"}
|
|
291
|
+
className={cn("flex min-h-0 min-w-0 flex-col", !wide && "border-t border-og-border")}
|
|
243
292
|
>
|
|
244
293
|
<div className="flex shrink-0 items-center justify-between gap-2 border-b border-og-border bg-og-surface-1 px-2 py-1">
|
|
245
294
|
<span
|
|
@@ -371,8 +420,8 @@ export function SandboxFiles({
|
|
|
371
420
|
</Notice>
|
|
372
421
|
)}
|
|
373
422
|
</div>
|
|
374
|
-
</
|
|
375
|
-
</
|
|
423
|
+
</Panel>
|
|
424
|
+
</Group>
|
|
376
425
|
</div>
|
|
377
426
|
);
|
|
378
427
|
}
|
|
@@ -382,15 +431,30 @@ function WakeButton({ children, onClick }: { children: ReactNode; onClick: () =>
|
|
|
382
431
|
<button
|
|
383
432
|
type="button"
|
|
384
433
|
onClick={onClick}
|
|
385
|
-
className="mt-1 inline-flex min-h-11 items-center justify-center rounded-og-md bg-og-accent-deep px-3 py-2 text-og-sm font-medium text-og-accent-fg shadow-sm transition-opacity hover:opacity-90 focus-visible:outline-
|
|
434
|
+
className="mt-1 inline-flex min-h-11 items-center justify-center rounded-og-md bg-og-accent-deep px-3 py-2 text-og-sm font-medium text-og-accent-fg shadow-sm transition-opacity hover:opacity-90 focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-og-accent focus-visible:ring-offset-2 focus-visible:ring-offset-og-bg"
|
|
386
435
|
>
|
|
387
436
|
{children}
|
|
388
437
|
</button>
|
|
389
438
|
);
|
|
390
439
|
}
|
|
391
440
|
|
|
392
|
-
function GitHeader({ git,
|
|
393
|
-
|
|
441
|
+
function GitHeader({ git, loading }: { git: SandboxFilesGitSummary; loading: boolean }) {
|
|
442
|
+
if (loading) {
|
|
443
|
+
return (
|
|
444
|
+
<div
|
|
445
|
+
className="flex shrink-0 items-center gap-2 border-b border-og-border bg-og-surface-1 px-2 py-1 text-og-sm text-og-fg-muted"
|
|
446
|
+
role="status"
|
|
447
|
+
aria-live="polite"
|
|
448
|
+
>
|
|
449
|
+
<LoaderCircleIcon
|
|
450
|
+
className="size-3.5 shrink-0 animate-spin motion-reduce:animate-none"
|
|
451
|
+
aria-hidden
|
|
452
|
+
/>
|
|
453
|
+
<span>Loading repository…</span>
|
|
454
|
+
</div>
|
|
455
|
+
);
|
|
456
|
+
}
|
|
457
|
+
const dirty = git.dirtyCount > 0;
|
|
394
458
|
return (
|
|
395
459
|
<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">
|
|
396
460
|
<span
|
|
@@ -401,7 +465,7 @@ function GitHeader({ git, dirtyCount }: { git: UseSandboxGitResult; dirtyCount:
|
|
|
401
465
|
)}
|
|
402
466
|
/>
|
|
403
467
|
<span className="sr-only">
|
|
404
|
-
{dirty ? `
|
|
468
|
+
{dirty ? `Uncommitted changes in ${git.dirtyCount} files` : "No uncommitted changes"}
|
|
405
469
|
</span>
|
|
406
470
|
<span className="truncate font-og-mono text-og-fg">
|
|
407
471
|
{git.repoCount > 1
|
|
@@ -420,7 +484,7 @@ function GitHeader({ git, dirtyCount }: { git: UseSandboxGitResult; dirtyCount:
|
|
|
420
484
|
data-contrast-audited
|
|
421
485
|
className="ml-auto shrink-0 text-og-xs text-og-fg-subtle"
|
|
422
486
|
>
|
|
423
|
-
{dirtyCount} changed
|
|
487
|
+
{git.dirtyCount} changed
|
|
424
488
|
</span>
|
|
425
489
|
)}
|
|
426
490
|
</div>
|
|
@@ -444,7 +508,7 @@ function Segmented({
|
|
|
444
508
|
type="button"
|
|
445
509
|
onClick={() => onChange(opt.value)}
|
|
446
510
|
className={cn(
|
|
447
|
-
"min-h-7 rounded-og-xs px-1.5 py-0.5 text-og-xs focus-visible:outline-
|
|
511
|
+
"min-h-7 rounded-og-xs px-1.5 py-0.5 text-og-xs focus-visible:outline-hidden 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",
|
|
448
512
|
opt.value === value
|
|
449
513
|
? "bg-og-accent-soft text-og-fg"
|
|
450
514
|
: "text-og-fg-subtle hover:text-og-fg",
|
|
@@ -480,6 +544,7 @@ function useFileView(
|
|
|
480
544
|
readFile: UseSandboxFilesResult["readFile"],
|
|
481
545
|
reloadRevision = 0,
|
|
482
546
|
): FileViewState {
|
|
547
|
+
const previousPathRef = useRef<string | null>(null);
|
|
483
548
|
const [state, setState] = useState<FileViewState>({
|
|
484
549
|
content: null,
|
|
485
550
|
isBinary: false,
|
|
@@ -490,6 +555,7 @@ function useFileView(
|
|
|
490
555
|
});
|
|
491
556
|
useEffect(() => {
|
|
492
557
|
if (!path) {
|
|
558
|
+
previousPathRef.current = null;
|
|
493
559
|
setState({
|
|
494
560
|
content: null,
|
|
495
561
|
isBinary: false,
|
|
@@ -500,16 +566,25 @@ function useFileView(
|
|
|
500
566
|
});
|
|
501
567
|
return;
|
|
502
568
|
}
|
|
569
|
+
const pathChanged = previousPathRef.current !== path;
|
|
570
|
+
previousPathRef.current = path;
|
|
503
571
|
let cancelled = false;
|
|
504
572
|
const abort = new AbortController();
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
573
|
+
if (pathChanged) {
|
|
574
|
+
setState({
|
|
575
|
+
content: null,
|
|
576
|
+
isBinary: false,
|
|
577
|
+
truncated: false,
|
|
578
|
+
sizeBytes: null,
|
|
579
|
+
loading: true,
|
|
580
|
+
error: null,
|
|
581
|
+
});
|
|
582
|
+
} else {
|
|
583
|
+
// Keep the last successful preview visible while a remote Modal read
|
|
584
|
+
// revalidates it. Replacing useful content with a multi-second spinner
|
|
585
|
+
// makes a live workspace feel slower and hides the state being refreshed.
|
|
586
|
+
setState((previous) => ({ ...previous, error: null }));
|
|
587
|
+
}
|
|
513
588
|
void readFile(path, { signal: abort.signal })
|
|
514
589
|
.then((res) => {
|
|
515
590
|
if (cancelled) return;
|
|
@@ -529,14 +604,19 @@ function useFileView(
|
|
|
529
604
|
})
|
|
530
605
|
.catch((cause) => {
|
|
531
606
|
if (cancelled) return;
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
607
|
+
const error = cause instanceof Error ? cause : new Error(String(cause));
|
|
608
|
+
setState((previous) =>
|
|
609
|
+
pathChanged
|
|
610
|
+
? {
|
|
611
|
+
content: null,
|
|
612
|
+
isBinary: false,
|
|
613
|
+
truncated: false,
|
|
614
|
+
sizeBytes: null,
|
|
615
|
+
loading: false,
|
|
616
|
+
error,
|
|
617
|
+
}
|
|
618
|
+
: { ...previous, loading: false, error },
|
|
619
|
+
);
|
|
540
620
|
});
|
|
541
621
|
return () => {
|
|
542
622
|
cancelled = true;
|
|
@@ -110,6 +110,13 @@ type XtermLike = {
|
|
|
110
110
|
};
|
|
111
111
|
type FitAddonLike = { fit: () => void };
|
|
112
112
|
|
|
113
|
+
/** Clear the transient wake line and put the first live prompt at cell 1,1.
|
|
114
|
+
* `Terminal.clear()` removes scrollback but deliberately preserves the cursor,
|
|
115
|
+
* which lets the first PTY frame append to the old wake message. */
|
|
116
|
+
export function clearTerminalBootStatus(term: Pick<XtermLike, "write">): void {
|
|
117
|
+
term.write("\r\x1b[2K\x1b[2J\x1b[H");
|
|
118
|
+
}
|
|
119
|
+
|
|
113
120
|
/** Debug seam (dev/evidence only, never a typed public API): a global hook the
|
|
114
121
|
* screenshot harness reads to probe the settled renderer tier + resolved font
|
|
115
122
|
* without mounting a real WebGL context in unit tests. */
|
|
@@ -215,7 +222,16 @@ export function SandboxTerminal({
|
|
|
215
222
|
// PTY mode = a live ttyd socket drives the screen. Firehose mode = the
|
|
216
223
|
// read-only projection (or the legacy HTTP-write fallback).
|
|
217
224
|
const ptyMode = ptyWs && ptyStatus !== "closed";
|
|
218
|
-
const interactive = !readOnly && (ptyMode
|
|
225
|
+
const interactive = !readOnly && (ptyMode ? ptyConnected : result.write !== null);
|
|
226
|
+
const terminalModeLabel = interactive
|
|
227
|
+
? null
|
|
228
|
+
: readOnly
|
|
229
|
+
? "read-only"
|
|
230
|
+
: ptyMode && ptyStatus === "connecting"
|
|
231
|
+
? "connecting"
|
|
232
|
+
: ptyMode && ptyStatus === "error"
|
|
233
|
+
? "connection error"
|
|
234
|
+
: "output only";
|
|
219
235
|
|
|
220
236
|
// Boot-in-terminal: after the user focuses a not-yet-warm box, show styled
|
|
221
237
|
// status lines INSIDE xterm instead of an overlay — but only when there is no
|
|
@@ -407,7 +423,7 @@ export function SandboxTerminal({
|
|
|
407
423
|
if (!booting) {
|
|
408
424
|
if (bootActiveRef.current) {
|
|
409
425
|
bootActiveRef.current = false;
|
|
410
|
-
if (!wroteFirehoseRef.current) term
|
|
426
|
+
if (!wroteFirehoseRef.current) clearTerminalBootStatus(term);
|
|
411
427
|
}
|
|
412
428
|
return;
|
|
413
429
|
}
|
|
@@ -496,9 +512,12 @@ export function SandboxTerminal({
|
|
|
496
512
|
<span className="truncate font-og-mono">pty: {shell ?? "shell"}</span>
|
|
497
513
|
</span>
|
|
498
514
|
<span className="flex shrink-0 items-center gap-2">
|
|
499
|
-
{
|
|
500
|
-
<span
|
|
501
|
-
|
|
515
|
+
{terminalModeLabel && (
|
|
516
|
+
<span
|
|
517
|
+
className="rounded-og-sm bg-og-surface-2 px-1.5 py-0.5 text-og-xs uppercase tracking-wide"
|
|
518
|
+
role="status"
|
|
519
|
+
>
|
|
520
|
+
{terminalModeLabel}
|
|
502
521
|
</span>
|
|
503
522
|
)}
|
|
504
523
|
<button
|