@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
|
@@ -3,14 +3,38 @@ import { type ReactNode, useEffect, useRef, useState } from "react";
|
|
|
3
3
|
import { cn } from "../lib/cn";
|
|
4
4
|
import { useTerminalStream } from "../hooks/use-terminal-stream";
|
|
5
5
|
import type { UseSandboxTerminalResult } from "../hooks/use-sandbox-terminal";
|
|
6
|
+
import { resolveTerminalFont, xtermThemeFromTokens } from "../lib/xterm-theme";
|
|
7
|
+
import { attachRenderer, type RendererLoaders, type RendererTier } from "../lib/xterm-renderer";
|
|
6
8
|
|
|
7
|
-
/**
|
|
9
|
+
/**
|
|
10
|
+
* A COMPLETE xterm `ITheme` (subset by intent, but every field xterm colors a
|
|
11
|
+
* cell with is present so it never falls back to its stock VGA palette). Built
|
|
12
|
+
* from the og tokens via `xtermThemeFromTokens`.
|
|
13
|
+
*/
|
|
8
14
|
export type XtermTheme = {
|
|
9
15
|
background?: string;
|
|
10
16
|
foreground?: string;
|
|
11
17
|
cursor?: string;
|
|
12
18
|
cursorAccent?: string;
|
|
13
19
|
selectionBackground?: string;
|
|
20
|
+
selectionForeground?: string;
|
|
21
|
+
selectionInactiveBackground?: string;
|
|
22
|
+
black?: string;
|
|
23
|
+
red?: string;
|
|
24
|
+
green?: string;
|
|
25
|
+
yellow?: string;
|
|
26
|
+
blue?: string;
|
|
27
|
+
magenta?: string;
|
|
28
|
+
cyan?: string;
|
|
29
|
+
white?: string;
|
|
30
|
+
brightBlack?: string;
|
|
31
|
+
brightRed?: string;
|
|
32
|
+
brightGreen?: string;
|
|
33
|
+
brightYellow?: string;
|
|
34
|
+
brightBlue?: string;
|
|
35
|
+
brightMagenta?: string;
|
|
36
|
+
brightCyan?: string;
|
|
37
|
+
brightWhite?: string;
|
|
14
38
|
};
|
|
15
39
|
|
|
16
40
|
export type SandboxTerminalProps = {
|
|
@@ -26,9 +50,21 @@ export type SandboxTerminalProps = {
|
|
|
26
50
|
* (`result.chunks`). Omit to force the legacy firehose-only behavior.
|
|
27
51
|
*/
|
|
28
52
|
terminalCapability?: TerminalCapability | null | undefined;
|
|
53
|
+
/**
|
|
54
|
+
* Full xterm theme. When omitted the terminal self-derives it from the `--og-*`
|
|
55
|
+
* tokens of its own container (correct even inside a nested `data-og-theme`
|
|
56
|
+
* subtree). Passing it lets the host drive re-theme on a theme flip.
|
|
57
|
+
*/
|
|
29
58
|
theme?: XtermTheme | undefined;
|
|
30
59
|
fontFamily?: string | undefined;
|
|
31
60
|
fontSize?: number | undefined;
|
|
61
|
+
/**
|
|
62
|
+
* Lease liveness (`cold` | `warming` | `warm` | `draining`). Drives the
|
|
63
|
+
* boot-in-terminal status lines: once the user has focused the terminal and
|
|
64
|
+
* the box is not yet warm, a styled "● waking machine — Ns…" line is rendered
|
|
65
|
+
* INSIDE xterm (no overlay/spinner) until the live PTY connects.
|
|
66
|
+
*/
|
|
67
|
+
liveness?: string | undefined;
|
|
32
68
|
/**
|
|
33
69
|
* Force read-only even when the PTY accepts stdin. Default: interactive
|
|
34
70
|
* whenever a live pty-ws stream is connected OR `result.write !== null` (the box
|
|
@@ -64,21 +100,69 @@ type XtermLike = {
|
|
|
64
100
|
dispose: () => void;
|
|
65
101
|
cols: number;
|
|
66
102
|
rows: number;
|
|
67
|
-
options: {
|
|
103
|
+
options: {
|
|
104
|
+
theme?: XtermTheme;
|
|
105
|
+
disableStdin?: boolean;
|
|
106
|
+
cursorBlink?: boolean;
|
|
107
|
+
fontFamily?: string;
|
|
108
|
+
fontSize?: number;
|
|
109
|
+
};
|
|
68
110
|
};
|
|
69
111
|
type FitAddonLike = { fit: () => void };
|
|
70
112
|
|
|
113
|
+
/** Debug seam (dev/evidence only, never a typed public API): a global hook the
|
|
114
|
+
* screenshot harness reads to probe the settled renderer tier + resolved font
|
|
115
|
+
* without mounting a real WebGL context in unit tests. */
|
|
116
|
+
type TerminalDebugInfo = {
|
|
117
|
+
renderer: RendererTier | null;
|
|
118
|
+
fontFamily: string | undefined;
|
|
119
|
+
fontSize: number | undefined;
|
|
120
|
+
hasVarInFont: boolean;
|
|
121
|
+
theme: XtermTheme | undefined;
|
|
122
|
+
/** The live xterm instance (opaque) — the evidence harness reads its buffer to
|
|
123
|
+
* prove screen preservation (E5) and drive the burst-responsiveness probe. */
|
|
124
|
+
term: unknown;
|
|
125
|
+
};
|
|
126
|
+
declare global {
|
|
127
|
+
// eslint-disable-next-line no-var
|
|
128
|
+
var __OG_TERMINAL_DEBUG__: ((info: TerminalDebugInfo) => void) | undefined;
|
|
129
|
+
// A space/comma list of renderer tiers to force-fail, so the demo can prove the
|
|
130
|
+
// WebGL→canvas→DOM fallback ladder without a real GPU context loss.
|
|
131
|
+
// eslint-disable-next-line no-var
|
|
132
|
+
var __OG_FORCE_RENDERER_FAIL__: string | undefined;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** Which renderer tiers are force-failed (demo/evidence only). */
|
|
136
|
+
function forcedRendererFailures(): Set<RendererTier> {
|
|
137
|
+
const raw = typeof globalThis !== "undefined" ? globalThis.__OG_FORCE_RENDERER_FAIL__ : undefined;
|
|
138
|
+
const set = new Set<RendererTier>();
|
|
139
|
+
if (typeof raw === "string") {
|
|
140
|
+
for (const tier of raw.split(/[\s,]+/)) {
|
|
141
|
+
if (tier === "webgl" || tier === "canvas") set.add(tier);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return set;
|
|
145
|
+
}
|
|
146
|
+
|
|
71
147
|
/**
|
|
72
148
|
* An xterm.js terminal fed by the Channel-A event projection
|
|
73
|
-
* (`useSandboxTerminal`)
|
|
149
|
+
* (`useSandboxTerminal`) or a live ttyd PTY. xterm + its addons are lazy-imported
|
|
74
150
|
* inside an effect, so SSR renders the placeholder and the terminal mounts on
|
|
75
|
-
* hydration.
|
|
76
|
-
*
|
|
77
|
-
*
|
|
151
|
+
* hydration.
|
|
152
|
+
*
|
|
153
|
+
* Renderer: WebGL (GPU-composited cells) with a canvas → DOM fallback ladder
|
|
154
|
+
* (`attachRenderer`), so bursty output stays smooth but a lost/blocklisted
|
|
155
|
+
* context degrades gracefully. Font + theme are resolved to CONCRETE values
|
|
156
|
+
* from the og tokens BEFORE construction (xterm measures glyphs in a canvas
|
|
157
|
+
* where `var()` never resolves, and colors every cell from a full ANSI palette).
|
|
158
|
+
*
|
|
159
|
+
* The terminal is mounted ONCE and its data source is swapped in place: the
|
|
160
|
+
* read-only firehose (`result.chunks`) → the live PTY. Stdin is toggled via
|
|
161
|
+
* `term.options.disableStdin` at runtime (not a remount), so scrollback and the
|
|
162
|
+
* visible screen survive the projection→PTY handoff.
|
|
78
163
|
*
|
|
79
164
|
* Resizes are tracked with a `ResizeObserver` on the container (not just
|
|
80
|
-
* `window.resize`) so dragging the dock handle refits the grid
|
|
81
|
-
* leaving the terminal mis-sized.
|
|
165
|
+
* `window.resize`) so dragging the dock handle refits the grid.
|
|
82
166
|
*/
|
|
83
167
|
export function SandboxTerminal({
|
|
84
168
|
result,
|
|
@@ -86,6 +170,7 @@ export function SandboxTerminal({
|
|
|
86
170
|
theme,
|
|
87
171
|
fontFamily,
|
|
88
172
|
fontSize,
|
|
173
|
+
liveness,
|
|
89
174
|
readOnly,
|
|
90
175
|
placeholder,
|
|
91
176
|
showHeader,
|
|
@@ -96,20 +181,23 @@ export function SandboxTerminal({
|
|
|
96
181
|
const containerRef = useRef<HTMLDivElement | null>(null);
|
|
97
182
|
const termRef = useRef<XtermLike | null>(null);
|
|
98
183
|
const fitRef = useRef<FitAddonLike | null>(null);
|
|
184
|
+
const rendererRef = useRef<RendererTier | null>(null);
|
|
99
185
|
const writtenRef = useRef<Set<string>>(new Set());
|
|
186
|
+
const wroteFirehoseRef = useRef(false);
|
|
187
|
+
const bootActiveRef = useRef(false);
|
|
100
188
|
const [ready, setReady] = useState(false);
|
|
189
|
+
const [activated, setActivated] = useState(false);
|
|
101
190
|
|
|
102
191
|
// ── Interactive transport switch ─────────────────────────────────────────────
|
|
103
|
-
// The interactive terminal is a REAL bidirectional PTY (ttyd over the Modal
|
|
104
|
-
// tunnel) WHENEVER the Terminal cell advertises `transport: "pty-ws"` + a live
|
|
105
|
-
// `url`; the ttyd socket then OWNS the screen (input + output both ride it). On
|
|
106
|
-
// a cold box / sse-events cell, `useTerminalStream` stays idle and we fall back
|
|
107
|
-
// to the read-only Channel-A command-output firehose (`result.chunks`) — with
|
|
108
|
-
// the legacy `result.write` (ptyWrite-over-HTTP) as the only stdin path there.
|
|
109
192
|
const ptyWs = terminalCapability?.transport === "pty-ws" && Boolean(terminalCapability?.url);
|
|
110
193
|
// Output frames buffer here until xterm has mounted; the write effect drains it.
|
|
111
194
|
const ptyOutputQueueRef = useRef<string[]>([]);
|
|
112
|
-
const
|
|
195
|
+
const {
|
|
196
|
+
status: ptyStatus,
|
|
197
|
+
write: ptyWrite,
|
|
198
|
+
resize: resizePty,
|
|
199
|
+
connected: ptyConnected,
|
|
200
|
+
} = useTerminalStream({
|
|
113
201
|
capability: ptyWs
|
|
114
202
|
? {
|
|
115
203
|
transport: terminalCapability!.transport,
|
|
@@ -124,48 +212,49 @@ export function SandboxTerminal({
|
|
|
124
212
|
},
|
|
125
213
|
});
|
|
126
214
|
|
|
127
|
-
// PTY mode = a live ttyd socket drives the screen.
|
|
128
|
-
//
|
|
129
|
-
|
|
130
|
-
// mode = the read-only projection (or the legacy HTTP-write fallback).
|
|
131
|
-
const ptyMode = ptyWs && ptyStream.status !== "closed";
|
|
132
|
-
// Interactive (stdin enabled) when a live PTY socket is connected, OR (legacy
|
|
133
|
-
// fallback) the projection exposed an HTTP write fn. `readOnly` forces off.
|
|
215
|
+
// PTY mode = a live ttyd socket drives the screen. Firehose mode = the
|
|
216
|
+
// read-only projection (or the legacy HTTP-write fallback).
|
|
217
|
+
const ptyMode = ptyWs && ptyStatus !== "closed";
|
|
134
218
|
const interactive = !readOnly && (ptyMode || result.write !== null);
|
|
135
219
|
|
|
136
|
-
//
|
|
137
|
-
//
|
|
220
|
+
// Boot-in-terminal: after the user focuses a not-yet-warm box, show styled
|
|
221
|
+
// status lines INSIDE xterm instead of an overlay — but only when there is no
|
|
222
|
+
// firehose transcript to show yet (otherwise the projected output is shown).
|
|
223
|
+
const warm = liveness === "warm" || liveness === "draining";
|
|
224
|
+
const booting = activated && !ptyMode && !warm && liveness != null && result.chunks.length === 0;
|
|
225
|
+
|
|
226
|
+
function handleActivate() {
|
|
227
|
+
if (!activated) setActivated(true);
|
|
228
|
+
onActivate?.();
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Mount xterm once (client-only). Never re-mounts on interactive/theme flips —
|
|
232
|
+
// stdin + theme are runtime options synced by the effects below, so the visible
|
|
233
|
+
// screen survives the projection→PTY handoff (E5).
|
|
138
234
|
useEffect(() => {
|
|
139
235
|
if (typeof window === "undefined") return;
|
|
140
236
|
const el = containerRef.current;
|
|
141
237
|
if (!el) return;
|
|
142
238
|
let disposed = false;
|
|
143
239
|
|
|
144
|
-
// Wait for
|
|
145
|
-
//
|
|
146
|
-
|
|
147
|
-
// grid is rasterized into the canvas; a later fit reflows the text but the
|
|
148
|
-
// stale first paint lingers as a `]]]bbb` smear on the top row. Gating the
|
|
149
|
-
// open on a non-zero size removes that first wrong-width render entirely.
|
|
150
|
-
const waitForSize = (el: HTMLElement) =>
|
|
240
|
+
// Wait for a real measured size before opening xterm so the first fit is at
|
|
241
|
+
// the true width (belt-and-braces with the visibility gate below).
|
|
242
|
+
const waitForSize = (node: HTMLElement) =>
|
|
151
243
|
new Promise<void>((resolve) => {
|
|
152
|
-
if (
|
|
244
|
+
if (node.clientWidth > 0 && node.clientHeight > 0) return resolve();
|
|
153
245
|
const ro = new ResizeObserver(() => {
|
|
154
|
-
if (
|
|
246
|
+
if (node.clientWidth > 0 && node.clientHeight > 0) {
|
|
155
247
|
ro.disconnect();
|
|
156
248
|
resolve();
|
|
157
249
|
}
|
|
158
250
|
});
|
|
159
|
-
ro.observe(
|
|
160
|
-
// Safety: never hang if the observer somehow never fires.
|
|
251
|
+
ro.observe(node);
|
|
161
252
|
setTimeout(() => {
|
|
162
253
|
ro.disconnect();
|
|
163
254
|
resolve();
|
|
164
255
|
}, 1000);
|
|
165
256
|
});
|
|
166
257
|
|
|
167
|
-
// Inject xterm's structural CSS before the first open so the helper-textarea
|
|
168
|
-
// ghost is clipped from frame one (no top-left input box flash).
|
|
169
258
|
ensureXtermBaseCss();
|
|
170
259
|
|
|
171
260
|
void (async () => {
|
|
@@ -177,32 +266,68 @@ export function SandboxTerminal({
|
|
|
177
266
|
if (disposed) return;
|
|
178
267
|
await waitForSize(el);
|
|
179
268
|
if (disposed) return;
|
|
269
|
+
|
|
270
|
+
// Resolve font + theme to CONCRETE values BEFORE construction. `var()` in a
|
|
271
|
+
// font family never resolves in xterm's canvas measurement, so we read the
|
|
272
|
+
// computed `--og-font-mono` here; the theme is a full ANSI palette.
|
|
273
|
+
const font = resolveTerminalFont(el, { fontFamily, fontSize });
|
|
274
|
+
const initialTheme = theme ?? xtermThemeFromTokens(el);
|
|
275
|
+
|
|
180
276
|
const term = new Terminal({
|
|
181
277
|
convertEol: true,
|
|
182
278
|
disableStdin: !interactive,
|
|
183
279
|
cursorBlink: interactive,
|
|
184
|
-
fontFamily: fontFamily
|
|
185
|
-
fontSize: fontSize
|
|
186
|
-
lineHeight:
|
|
187
|
-
|
|
188
|
-
// the border (matches a real terminal app's inner gutter).
|
|
189
|
-
...({ padding: 8 } as Record<string, unknown>),
|
|
190
|
-
...(theme ? { theme } : {}),
|
|
280
|
+
fontFamily: font.fontFamily,
|
|
281
|
+
fontSize: font.fontSize,
|
|
282
|
+
lineHeight: font.lineHeight,
|
|
283
|
+
...(initialTheme ? { theme: initialTheme } : {}),
|
|
191
284
|
}) as unknown as XtermLike;
|
|
192
285
|
const fit = new FitAddon() as unknown as FitAddonLike;
|
|
193
286
|
term.loadAddon(fit);
|
|
194
|
-
// Clickable URLs in agent output (open in a new tab).
|
|
195
287
|
term.loadAddon(
|
|
196
|
-
new WebLinksAddon((_e: MouseEvent, uri: string) =>
|
|
288
|
+
new WebLinksAddon((_e: MouseEvent, uri: string) =>
|
|
289
|
+
window.open(uri, "_blank", "noopener,noreferrer"),
|
|
290
|
+
) as unknown,
|
|
197
291
|
);
|
|
198
292
|
term.open(el);
|
|
199
293
|
termRef.current = term;
|
|
200
294
|
fitRef.current = fit;
|
|
201
|
-
|
|
202
|
-
//
|
|
203
|
-
//
|
|
204
|
-
//
|
|
205
|
-
//
|
|
295
|
+
|
|
296
|
+
// Attach the fastest available renderer. For xterm-6 the supported ladder
|
|
297
|
+
// is WebGL → DOM (the standalone 2D-canvas addon targets xterm-5 internals,
|
|
298
|
+
// so it is intentionally NOT in the shipped path). Init failure / context
|
|
299
|
+
// loss steps down to xterm's built-in DOM renderer; the settled tier is
|
|
300
|
+
// exposed for evidence.
|
|
301
|
+
const failSet = forcedRendererFailures();
|
|
302
|
+
const loaders: RendererLoaders = {
|
|
303
|
+
webgl: async (onLoss) => {
|
|
304
|
+
if (failSet.has("webgl")) throw new Error("forced webgl failure");
|
|
305
|
+
const { WebglAddon } = await import("@xterm/addon-webgl");
|
|
306
|
+
const addon = new WebglAddon() as unknown as {
|
|
307
|
+
dispose: () => void;
|
|
308
|
+
onContextLoss?: (cb: () => void) => void;
|
|
309
|
+
};
|
|
310
|
+
addon.onContextLoss?.(() => {
|
|
311
|
+
try {
|
|
312
|
+
addon.dispose();
|
|
313
|
+
} catch {
|
|
314
|
+
// already disposed
|
|
315
|
+
}
|
|
316
|
+
onLoss();
|
|
317
|
+
});
|
|
318
|
+
term.loadAddon(addon);
|
|
319
|
+
return addon;
|
|
320
|
+
},
|
|
321
|
+
};
|
|
322
|
+
await attachRenderer("webgl", loaders, (tier) => {
|
|
323
|
+
rendererRef.current = tier;
|
|
324
|
+
el.setAttribute("data-og-term-renderer", tier);
|
|
325
|
+
});
|
|
326
|
+
if (disposed) return;
|
|
327
|
+
|
|
328
|
+
// Fit BEFORE reveal so the first visible frame is already correctly sized
|
|
329
|
+
// (no 80×24 rasterize-then-reflow flash — the container stays hidden until
|
|
330
|
+
// now via the `ready` visibility gate).
|
|
206
331
|
const settle = () => {
|
|
207
332
|
try {
|
|
208
333
|
fit.fit();
|
|
@@ -214,11 +339,21 @@ export function SandboxTerminal({
|
|
|
214
339
|
requestAnimationFrame(() => {
|
|
215
340
|
if (disposed) return;
|
|
216
341
|
settle();
|
|
217
|
-
// Drain any ttyd OUTPUT that arrived before xterm finished mounting.
|
|
218
342
|
if (ptyOutputQueueRef.current.length > 0) {
|
|
219
343
|
for (const data of ptyOutputQueueRef.current) term.write(data);
|
|
220
344
|
ptyOutputQueueRef.current = [];
|
|
221
345
|
}
|
|
346
|
+
el.setAttribute("data-og-term-ready", "true");
|
|
347
|
+
if (typeof globalThis.__OG_TERMINAL_DEBUG__ === "function") {
|
|
348
|
+
globalThis.__OG_TERMINAL_DEBUG__({
|
|
349
|
+
renderer: rendererRef.current,
|
|
350
|
+
fontFamily: term.options.fontFamily,
|
|
351
|
+
fontSize: term.options.fontSize,
|
|
352
|
+
hasVarInFont: String(term.options.fontFamily ?? "").includes("var("),
|
|
353
|
+
theme: term.options.theme,
|
|
354
|
+
term,
|
|
355
|
+
});
|
|
356
|
+
}
|
|
222
357
|
setReady(true);
|
|
223
358
|
});
|
|
224
359
|
})();
|
|
@@ -228,71 +363,97 @@ export function SandboxTerminal({
|
|
|
228
363
|
termRef.current?.dispose();
|
|
229
364
|
termRef.current = null;
|
|
230
365
|
fitRef.current = null;
|
|
366
|
+
rendererRef.current = null;
|
|
231
367
|
writtenRef.current = new Set();
|
|
368
|
+
wroteFirehoseRef.current = false;
|
|
369
|
+
bootActiveRef.current = false;
|
|
232
370
|
setReady(false);
|
|
233
371
|
};
|
|
372
|
+
// Mount ONCE — see the effects below for the runtime option syncs.
|
|
234
373
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
235
|
-
}, [
|
|
374
|
+
}, []);
|
|
236
375
|
|
|
237
|
-
//
|
|
376
|
+
// Sync stdin/cursor at runtime (NOT a remount) when interactivity flips, so the
|
|
377
|
+
// projection→PTY handoff keeps the single Terminal instance + its scrollback.
|
|
238
378
|
useEffect(() => {
|
|
239
379
|
const term = termRef.current;
|
|
240
|
-
if (!ready || !term
|
|
241
|
-
term.options.
|
|
380
|
+
if (!ready || !term) return;
|
|
381
|
+
term.options.disableStdin = !interactive;
|
|
382
|
+
term.options.cursorBlink = interactive;
|
|
383
|
+
}, [ready, interactive]);
|
|
384
|
+
|
|
385
|
+
// Re-theme live (dark↔light flip) without re-mounting / losing scrollback. Uses
|
|
386
|
+
// the passed theme, or self-derives from the container's tokens.
|
|
387
|
+
useEffect(() => {
|
|
388
|
+
const term = termRef.current;
|
|
389
|
+
if (!ready || !term) return;
|
|
390
|
+
const next = theme ?? xtermThemeFromTokens(containerRef.current);
|
|
391
|
+
if (next) term.options.theme = next;
|
|
242
392
|
}, [ready, theme]);
|
|
243
393
|
|
|
244
|
-
//
|
|
245
|
-
//
|
|
246
|
-
//
|
|
247
|
-
|
|
394
|
+
// Boot-in-terminal status lines. While `booting`, paint a styled "● waking
|
|
395
|
+
// machine — Ns…" line and tick it. On exit, if those lines were the ONLY
|
|
396
|
+
// content (no firehose transcript), clear them for the incoming live screen —
|
|
397
|
+
// never clear a real transcript (E5 preservation).
|
|
248
398
|
useEffect(() => {
|
|
249
399
|
const term = termRef.current;
|
|
250
400
|
if (!ready || !term) return;
|
|
251
|
-
if (
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
401
|
+
if (!booting) {
|
|
402
|
+
if (bootActiveRef.current) {
|
|
403
|
+
bootActiveRef.current = false;
|
|
404
|
+
if (!wroteFirehoseRef.current) term.clear();
|
|
405
|
+
}
|
|
406
|
+
return;
|
|
256
407
|
}
|
|
257
|
-
|
|
408
|
+
bootActiveRef.current = true;
|
|
409
|
+
const start = Date.now();
|
|
410
|
+
const paint = () => {
|
|
411
|
+
const s = Math.max(0, Math.round((Date.now() - start) / 1000));
|
|
412
|
+
// Bright-blue dot (accent) + dim label; `\r` + clear-line keeps it on one
|
|
413
|
+
// line so the counter updates in place.
|
|
414
|
+
term.write(`\r\x1b[2K\x1b[94m●\x1b[0m \x1b[2mwaking machine — ${s}s…\x1b[0m`);
|
|
415
|
+
};
|
|
416
|
+
term.write("\r\n");
|
|
417
|
+
paint();
|
|
418
|
+
const id = setInterval(paint, 1000);
|
|
419
|
+
return () => clearInterval(id);
|
|
420
|
+
}, [ready, booting]);
|
|
258
421
|
|
|
259
|
-
// Write new firehose chunks incrementally —
|
|
260
|
-
//
|
|
261
|
-
//
|
|
422
|
+
// Write new firehose chunks incrementally — ONLY in firehose mode. In PTY mode
|
|
423
|
+
// the ttyd socket owns the screen. Records that a real transcript exists so the
|
|
424
|
+
// boot cleanup never wipes it.
|
|
262
425
|
useEffect(() => {
|
|
263
426
|
const term = termRef.current;
|
|
264
427
|
if (!ready || !term || ptyMode) return;
|
|
265
428
|
for (const chunk of result.chunks) {
|
|
266
429
|
if (writtenRef.current.has(chunk.id)) continue;
|
|
267
430
|
writtenRef.current.add(chunk.id);
|
|
431
|
+
wroteFirehoseRef.current = true;
|
|
268
432
|
term.write(chunk.text);
|
|
269
433
|
}
|
|
270
434
|
}, [ready, result.chunks, ptyMode]);
|
|
271
435
|
|
|
272
436
|
// Wire interactive input when allowed. PTY mode pipes keystrokes to the ttyd
|
|
273
|
-
// socket
|
|
437
|
+
// socket; firehose mode uses the legacy ptyWrite-over-HTTP fn.
|
|
274
438
|
useEffect(() => {
|
|
275
439
|
const term = termRef.current;
|
|
276
440
|
if (!ready || !term || !interactive) return;
|
|
277
|
-
const sink = ptyMode ?
|
|
441
|
+
const sink = ptyMode ? ptyWrite : result.write;
|
|
278
442
|
if (!sink) return;
|
|
279
443
|
const sub = term.onData((data) => sink(data));
|
|
280
444
|
return () => sub.dispose();
|
|
281
|
-
}, [ready, interactive, ptyMode,
|
|
445
|
+
}, [ready, interactive, ptyMode, ptyWrite, result.write]);
|
|
282
446
|
|
|
283
|
-
// In PTY mode, tell ttyd the window size on the xterm resize event
|
|
284
|
-
// addon reflows the grid; ttyd resizes the remote PTY to match), and push the
|
|
285
|
-
// initial geometry once the socket is open.
|
|
447
|
+
// In PTY mode, tell ttyd the window size on the xterm resize event.
|
|
286
448
|
useEffect(() => {
|
|
287
449
|
const term = termRef.current;
|
|
288
450
|
if (!ready || !term || !ptyMode) return;
|
|
289
|
-
|
|
290
|
-
const sub = term.onResize(({ cols, rows }) =>
|
|
451
|
+
resizePty(term.cols, term.rows);
|
|
452
|
+
const sub = term.onResize(({ cols, rows }) => resizePty(cols, rows));
|
|
291
453
|
return () => sub.dispose();
|
|
292
|
-
}, [ready, ptyMode,
|
|
454
|
+
}, [ready, ptyMode, ptyConnected, resizePty]);
|
|
293
455
|
|
|
294
|
-
// Refit on container resize (dock drag) AND window resize.
|
|
295
|
-
// mutates xterm's cols/rows → the onResize handler above forwards to ttyd.
|
|
456
|
+
// Refit on container resize (dock drag) AND window resize.
|
|
296
457
|
useEffect(() => {
|
|
297
458
|
if (typeof window === "undefined") return;
|
|
298
459
|
const refit = () => {
|
|
@@ -326,9 +487,7 @@ export function SandboxTerminal({
|
|
|
326
487
|
result.running ? "bg-og-status-running" : "bg-og-fg-subtle",
|
|
327
488
|
)}
|
|
328
489
|
/>
|
|
329
|
-
<span className="truncate font-og-mono">
|
|
330
|
-
pty: {shell ?? "shell"}
|
|
331
|
-
</span>
|
|
490
|
+
<span className="truncate font-og-mono">pty: {shell ?? "shell"}</span>
|
|
332
491
|
</span>
|
|
333
492
|
<span className="flex shrink-0 items-center gap-2">
|
|
334
493
|
{!interactive && (
|
|
@@ -341,6 +500,7 @@ export function SandboxTerminal({
|
|
|
341
500
|
onClick={() => {
|
|
342
501
|
termRef.current?.clear();
|
|
343
502
|
writtenRef.current = new Set();
|
|
503
|
+
wroteFirehoseRef.current = false;
|
|
344
504
|
}}
|
|
345
505
|
className="rounded-og-sm px-1.5 py-0.5 text-og-xs hover:text-og-fg pointer-coarse:min-h-10"
|
|
346
506
|
>
|
|
@@ -351,14 +511,22 @@ export function SandboxTerminal({
|
|
|
351
511
|
)}
|
|
352
512
|
{/* `onPointerDownCapture`/`onFocusCapture` fire on the FIRST user engagement
|
|
353
513
|
with the terminal surface (capture so they win even though xterm's own
|
|
354
|
-
listeners stop propagation). The host warms the box for pty-ws here.
|
|
514
|
+
listeners stop propagation). The host warms the box for pty-ws here. The
|
|
515
|
+
12px inset + `--og-color-bg` ground match the dock surface. */}
|
|
355
516
|
<div
|
|
356
|
-
className="relative min-h-0 flex-1 bg-og-bg
|
|
357
|
-
onPointerDownCapture={
|
|
358
|
-
onFocusCapture={
|
|
517
|
+
className="relative min-h-0 flex-1 bg-og-bg p-3"
|
|
518
|
+
onPointerDownCapture={handleActivate}
|
|
519
|
+
onFocusCapture={handleActivate}
|
|
359
520
|
>
|
|
360
521
|
{!ready && (placeholder ?? <TerminalPlaceholder />)}
|
|
361
|
-
|
|
522
|
+
{/* Kept `visibility:hidden` until the first successful fit so the first
|
|
523
|
+
VISIBLE frame is already correctly sized (no 80×24 flash — E4). */}
|
|
524
|
+
<div
|
|
525
|
+
ref={containerRef}
|
|
526
|
+
className="h-full w-full"
|
|
527
|
+
style={{ visibility: ready ? "visible" : "hidden" }}
|
|
528
|
+
data-opengeni-terminal
|
|
529
|
+
/>
|
|
362
530
|
</div>
|
|
363
531
|
</div>
|
|
364
532
|
);
|