@opengeni/react 0.3.1 → 0.5.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/dist/index.d.ts +1055 -27
- package/dist/index.js +6993 -1954
- package/dist/index.js.map +1 -1
- package/package.json +65 -2
- package/src/client.ts +22 -0
- package/src/components/code-editor.tsx +398 -0
- package/src/components/desktop-viewer.tsx +647 -0
- package/src/components/diff-view.tsx +230 -0
- package/src/components/file-browser.tsx +838 -0
- package/src/components/fleet-tile.tsx +5 -0
- package/src/components/message-timeline.tsx +70 -196
- package/src/components/pierre-diff.tsx +140 -0
- package/src/components/pierre-file.tsx +142 -0
- package/src/components/sandbox-files.tsx +509 -0
- package/src/components/sandbox-terminal.tsx +425 -0
- package/src/components/workspace-dock.tsx +247 -0
- package/src/hooks/use-desktop-stream.ts +214 -0
- package/src/hooks/use-sandbox-files.ts +670 -0
- package/src/hooks/use-sandbox-git.ts +105 -0
- package/src/hooks/use-sandbox-terminal.ts +226 -0
- package/src/hooks/use-session-capabilities.ts +415 -0
- package/src/hooks/use-session.ts +80 -12
- package/src/hooks/use-terminal-stream.ts +207 -0
- package/src/index.ts +112 -3
- package/src/lib/cn.ts +20 -1
- package/src/lib/git-patch.ts +43 -0
- package/src/lib/use-theme-type.ts +40 -0
- package/src/lib/xterm-theme.ts +34 -0
- package/src/timeline/activity-rail.tsx +207 -0
- package/src/timeline/disclosure-context.tsx +34 -0
- package/src/timeline/index.ts +85 -0
- package/src/timeline/parsers.ts +253 -0
- package/src/{timeline.ts → timeline/projection.ts} +59 -134
- package/src/timeline/registry.ts +96 -0
- package/src/timeline/screenshot-lightbox.tsx +152 -0
- package/src/timeline/shared.tsx +481 -0
- package/src/timeline/tool-diff.tsx +91 -0
- package/src/timeline/tool-renderers.tsx +882 -0
- package/src/timeline/turn-summary.tsx +125 -0
- package/src/timeline/types.ts +131 -0
- package/src/types/external.d.ts +7 -0
- package/styles/index.css +72 -0
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import {
|
|
2
|
+
TTYD_SUBPROTOCOL,
|
|
3
|
+
TerminalCapability,
|
|
4
|
+
terminalSocketUrl,
|
|
5
|
+
ttydAuthFrame,
|
|
6
|
+
ttydInputFrame,
|
|
7
|
+
ttydResizeFrame,
|
|
8
|
+
TtydServerCommand,
|
|
9
|
+
} from "@opengeni/sdk";
|
|
10
|
+
import { useEffect, useMemo, useRef, useState } from "react";
|
|
11
|
+
|
|
12
|
+
/** The ttyd connection lifecycle as surfaced to the component. */
|
|
13
|
+
export type TerminalStreamStatus = "connecting" | "open" | "closed" | "error";
|
|
14
|
+
|
|
15
|
+
export type UseTerminalStreamOptions = {
|
|
16
|
+
/** The Terminal cell of the negotiated capabilities (`capabilities.Terminal`).
|
|
17
|
+
* The stream connects ONLY when `transport === "pty-ws"` and `url` is set; on a
|
|
18
|
+
* cold box (`transport === "sse-events"` / no url) it stays idle and the caller
|
|
19
|
+
* falls back to the Channel-A read-only firehose. */
|
|
20
|
+
capability: Pick<TerminalCapability, "transport" | "url" | "token"> | null;
|
|
21
|
+
/** Called for each OUTPUT payload from ttyd (write verbatim into xterm). */
|
|
22
|
+
onOutput?: ((data: string) => void) | undefined;
|
|
23
|
+
/** Called when ttyd sends a SET_WINDOW_TITLE frame. */
|
|
24
|
+
onTitle?: ((title: string) => void) | undefined;
|
|
25
|
+
/** Initial PTY size to seed the ttyd auth frame + first resize. */
|
|
26
|
+
initialCols?: number | undefined;
|
|
27
|
+
initialRows?: number | undefined;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export type UseTerminalStreamResult = {
|
|
31
|
+
/** True once the ttyd socket is open (and the auth frame has been sent). */
|
|
32
|
+
connected: boolean;
|
|
33
|
+
status: TerminalStreamStatus;
|
|
34
|
+
/** Pipe a keystroke/paste to the PTY stdin. No-op until the socket is open. */
|
|
35
|
+
write: (data: string) => void;
|
|
36
|
+
/** Tell ttyd the PTY window changed size (on xterm fit/resize). */
|
|
37
|
+
resize: (cols: number, rows: number) => void;
|
|
38
|
+
/** Tear the socket down (the effect also tears down on unmount / url change). */
|
|
39
|
+
disconnect: () => void;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
/** Decode an inbound ttyd frame's payload (everything after the 1-char command).
|
|
43
|
+
* ttyd may send either a text frame (string) or a binary frame (ArrayBuffer);
|
|
44
|
+
* for binary we slice off the first byte (the command) and utf-8 decode the rest. */
|
|
45
|
+
function decodeFrame(data: string | ArrayBuffer): { command: string; payload: string } {
|
|
46
|
+
if (typeof data === "string") {
|
|
47
|
+
return { command: data.charAt(0), payload: data.slice(1) };
|
|
48
|
+
}
|
|
49
|
+
const bytes = new Uint8Array(data);
|
|
50
|
+
const command = bytes.length > 0 ? String.fromCharCode(bytes[0]!) : "";
|
|
51
|
+
const payload = bytes.length > 1 ? new TextDecoder().decode(bytes.subarray(1)) : "";
|
|
52
|
+
return { command, payload };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Drive a ttyd PTY-over-websocket connection from a `pty-ws` Terminal capability,
|
|
57
|
+
* symmetric with `use-desktop-stream` (the noVNC-over-tunnel hook). The scoped
|
|
58
|
+
* stream token is already embedded in the minted tunnel `url`; the WebSocket is
|
|
59
|
+
* opened with the REQUIRED ttyd subprotocol "tty".
|
|
60
|
+
*
|
|
61
|
+
* ttyd wire protocol (see `@opengeni/sdk/terminal`):
|
|
62
|
+
* - first frame: `JSON.stringify({ AuthToken: "" })` (+ optional columns/rows).
|
|
63
|
+
* - client→server: INPUT = "0"+data ; RESIZE = "1"+JSON({columns,rows}).
|
|
64
|
+
* - server→client: "0" = OUTPUT (→ xterm) ; "1" = SET_WINDOW_TITLE ;
|
|
65
|
+
* "2" = SET_PREFERENCES (ignored). Binary frames are decoded the same way.
|
|
66
|
+
*
|
|
67
|
+
* On a `url`/`token` rotation (a box rollover folds a fresh address into the cell)
|
|
68
|
+
* the effect re-runs: the old socket closes and a fresh one connects — a brief
|
|
69
|
+
* terminal blink, acceptable on rollover (mirrors the desktop's RFB hot-swap).
|
|
70
|
+
* SSR-safe: the socket open lives in `useEffect`, so a server render is a no-op.
|
|
71
|
+
*/
|
|
72
|
+
export function useTerminalStream(options: UseTerminalStreamOptions): UseTerminalStreamResult {
|
|
73
|
+
const { capability, onOutput, onTitle, initialCols, initialRows } = options;
|
|
74
|
+
const [status, setStatus] = useState<TerminalStreamStatus>("closed");
|
|
75
|
+
const wsRef = useRef<WebSocket | null>(null);
|
|
76
|
+
// Latest size, so a resize() before the socket opens is replayed on open, and a
|
|
77
|
+
// reconnect seeds the right geometry.
|
|
78
|
+
const sizeRef = useRef<{ cols: number; rows: number }>({
|
|
79
|
+
cols: initialCols ?? 80,
|
|
80
|
+
rows: initialRows ?? 24,
|
|
81
|
+
});
|
|
82
|
+
// Keep the callbacks current without re-running the connect effect on every
|
|
83
|
+
// render (the parent passes fresh closures each time).
|
|
84
|
+
const onOutputRef = useRef(onOutput);
|
|
85
|
+
const onTitleRef = useRef(onTitle);
|
|
86
|
+
onOutputRef.current = onOutput;
|
|
87
|
+
onTitleRef.current = onTitle;
|
|
88
|
+
|
|
89
|
+
const transport = capability?.transport ?? null;
|
|
90
|
+
const url = capability?.url ?? null;
|
|
91
|
+
const token = capability?.token ?? null;
|
|
92
|
+
|
|
93
|
+
useEffect(() => {
|
|
94
|
+
// SSR / no WebSocket / not a live pty-ws cell: stay closed; the caller falls
|
|
95
|
+
// back to the Channel-A read-only firehose.
|
|
96
|
+
if (typeof window === "undefined" || typeof WebSocket === "undefined") return;
|
|
97
|
+
if (transport !== "pty-ws" || !url) {
|
|
98
|
+
setStatus("closed");
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
let disposed = false;
|
|
103
|
+
let socket: WebSocket;
|
|
104
|
+
setStatus("connecting");
|
|
105
|
+
try {
|
|
106
|
+
// The ttyd "tty" subprotocol is REQUIRED — ttyd rejects a handshake without
|
|
107
|
+
// it. The scoped token is already in the tunnel `url`.
|
|
108
|
+
socket = new WebSocket(terminalSocketUrl({ url }), TTYD_SUBPROTOCOL);
|
|
109
|
+
} catch {
|
|
110
|
+
setStatus("error");
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
socket.binaryType = "arraybuffer";
|
|
114
|
+
wsRef.current = socket;
|
|
115
|
+
|
|
116
|
+
socket.onopen = () => {
|
|
117
|
+
if (disposed) return;
|
|
118
|
+
// ttyd's required first frame: the auth message (empty token — the gate is
|
|
119
|
+
// the tunnel url + scoped stream token, not a ttyd -c credential), seeded
|
|
120
|
+
// with the current PTY geometry. Then an explicit resize to be safe.
|
|
121
|
+
try {
|
|
122
|
+
socket.send(ttydAuthFrame({ columns: sizeRef.current.cols, rows: sizeRef.current.rows }));
|
|
123
|
+
socket.send(ttydResizeFrame(sizeRef.current.cols, sizeRef.current.rows));
|
|
124
|
+
} catch {
|
|
125
|
+
// a closed socket between open and send — onclose handles state.
|
|
126
|
+
}
|
|
127
|
+
setStatus("open");
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
socket.onmessage = (ev: MessageEvent) => {
|
|
131
|
+
if (disposed) return;
|
|
132
|
+
const { command, payload } = decodeFrame(ev.data as string | ArrayBuffer);
|
|
133
|
+
switch (command) {
|
|
134
|
+
case TtydServerCommand.OUTPUT:
|
|
135
|
+
onOutputRef.current?.(payload);
|
|
136
|
+
break;
|
|
137
|
+
case TtydServerCommand.SET_WINDOW_TITLE:
|
|
138
|
+
onTitleRef.current?.(payload);
|
|
139
|
+
break;
|
|
140
|
+
// SET_PREFERENCES ("2") and anything else: ignored.
|
|
141
|
+
default:
|
|
142
|
+
break;
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
socket.onerror = () => {
|
|
147
|
+
if (!disposed) setStatus("error");
|
|
148
|
+
};
|
|
149
|
+
socket.onclose = () => {
|
|
150
|
+
if (!disposed) setStatus("closed");
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
return () => {
|
|
154
|
+
disposed = true;
|
|
155
|
+
wsRef.current = null;
|
|
156
|
+
// Drop handlers so an in-flight close/error doesn't mutate state post-unmount.
|
|
157
|
+
socket.onopen = null;
|
|
158
|
+
socket.onmessage = null;
|
|
159
|
+
socket.onerror = null;
|
|
160
|
+
socket.onclose = null;
|
|
161
|
+
try {
|
|
162
|
+
socket.close();
|
|
163
|
+
} catch {
|
|
164
|
+
// ignore teardown errors
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
// A url/token change (rotation) re-runs this effect → close old, open new.
|
|
168
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
169
|
+
}, [transport, url, token]);
|
|
170
|
+
|
|
171
|
+
return useMemo<UseTerminalStreamResult>(() => {
|
|
172
|
+
const write = (data: string) => {
|
|
173
|
+
const ws = wsRef.current;
|
|
174
|
+
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
175
|
+
try {
|
|
176
|
+
ws.send(ttydInputFrame(data));
|
|
177
|
+
} catch {
|
|
178
|
+
// socket raced closed — the reconnect effect will re-establish.
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
};
|
|
182
|
+
const resize = (cols: number, rows: number) => {
|
|
183
|
+
if (cols <= 0 || rows <= 0) return;
|
|
184
|
+
sizeRef.current = { cols, rows };
|
|
185
|
+
const ws = wsRef.current;
|
|
186
|
+
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
187
|
+
try {
|
|
188
|
+
ws.send(ttydResizeFrame(cols, rows));
|
|
189
|
+
} catch {
|
|
190
|
+
// socket raced closed — geometry is replayed on the next open.
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
};
|
|
194
|
+
const disconnect = () => {
|
|
195
|
+
const ws = wsRef.current;
|
|
196
|
+
wsRef.current = null;
|
|
197
|
+
if (ws) {
|
|
198
|
+
try {
|
|
199
|
+
ws.close();
|
|
200
|
+
} catch {
|
|
201
|
+
// ignore
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
};
|
|
205
|
+
return { connected: status === "open", status, write, resize, disconnect };
|
|
206
|
+
}, [status]);
|
|
207
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -9,7 +9,7 @@ export { OpenGeniProvider, useOpenGeni, useOpenGeniClient } from "./provider";
|
|
|
9
9
|
export type { ClientOverride, OpenGeniContextValue, OpenGeniProviderProps } from "./provider";
|
|
10
10
|
|
|
11
11
|
// Hooks
|
|
12
|
-
export { useSession } from "./hooks/use-session";
|
|
12
|
+
export { useSession, isTitleEvent } from "./hooks/use-session";
|
|
13
13
|
export type { UseSessionOptions, UseSessionResult } from "./hooks/use-session";
|
|
14
14
|
export { useSessionEvents } from "./hooks/use-session-events";
|
|
15
15
|
export type { SessionEventsConnectionState, UseSessionEventsOptions, UseSessionEventsResult } from "./hooks/use-session-events";
|
|
@@ -46,6 +46,37 @@ export type { UseBillingUsageOptions, UseBillingUsageResult } from "./hooks/use-
|
|
|
46
46
|
export { useAvailableModels } from "./hooks/use-available-models";
|
|
47
47
|
export type { UseAvailableModelsOptions, UseAvailableModelsResult } from "./hooks/use-available-models";
|
|
48
48
|
|
|
49
|
+
// Sandbox surfacing (Phase 5): capability negotiation + terminal/files/diff/desktop
|
|
50
|
+
export { useSessionCapabilities } from "./hooks/use-session-capabilities";
|
|
51
|
+
export type {
|
|
52
|
+
SessionCapabilitiesState,
|
|
53
|
+
UseSessionCapabilitiesOptions,
|
|
54
|
+
UseSessionCapabilitiesResult,
|
|
55
|
+
} from "./hooks/use-session-capabilities";
|
|
56
|
+
export { useDesktopStream } from "./hooks/use-desktop-stream";
|
|
57
|
+
export type { UseDesktopStreamOptions, UseDesktopStreamResult } from "./hooks/use-desktop-stream";
|
|
58
|
+
export { useTerminalStream } from "./hooks/use-terminal-stream";
|
|
59
|
+
export type {
|
|
60
|
+
TerminalStreamStatus,
|
|
61
|
+
UseTerminalStreamOptions,
|
|
62
|
+
UseTerminalStreamResult,
|
|
63
|
+
} from "./hooks/use-terminal-stream";
|
|
64
|
+
export { useSandboxTerminal } from "./hooks/use-sandbox-terminal";
|
|
65
|
+
export type {
|
|
66
|
+
TerminalChunk,
|
|
67
|
+
UseSandboxTerminalOptions,
|
|
68
|
+
UseSandboxTerminalResult,
|
|
69
|
+
} from "./hooks/use-sandbox-terminal";
|
|
70
|
+
export { useSandboxFiles } from "./hooks/use-sandbox-files";
|
|
71
|
+
export type {
|
|
72
|
+
FileTreeNode,
|
|
73
|
+
FileTreeStatus,
|
|
74
|
+
UseSandboxFilesOptions,
|
|
75
|
+
UseSandboxFilesResult,
|
|
76
|
+
} from "./hooks/use-sandbox-files";
|
|
77
|
+
export { useSandboxGit } from "./hooks/use-sandbox-git";
|
|
78
|
+
export type { UseSandboxGitOptions, UseSandboxGitResult } from "./hooks/use-sandbox-git";
|
|
79
|
+
|
|
49
80
|
// Pending-approvals projection
|
|
50
81
|
export { approvalsFromRequiresAction, projectPendingApprovals } from "./approvals";
|
|
51
82
|
export type { PendingApproval } from "./approvals";
|
|
@@ -53,13 +84,13 @@ export type { PendingApproval } from "./approvals";
|
|
|
53
84
|
// Timeline projection
|
|
54
85
|
export {
|
|
55
86
|
buildTimeline,
|
|
56
|
-
compactPayloadPreview,
|
|
57
87
|
extractSessionRef,
|
|
58
88
|
groupTimeline,
|
|
59
89
|
sessionStatusFromEvents,
|
|
60
90
|
toolDisplayName,
|
|
61
91
|
} from "./timeline";
|
|
62
92
|
export type {
|
|
93
|
+
ActivityItem,
|
|
63
94
|
AgentMessageItem,
|
|
64
95
|
GoalItem,
|
|
65
96
|
NoticeItem,
|
|
@@ -73,6 +104,60 @@ export type {
|
|
|
73
104
|
WorkerItem,
|
|
74
105
|
} from "./timeline";
|
|
75
106
|
|
|
107
|
+
// Tool-renderer registry + the per-tool renderers (the timeline's extension API)
|
|
108
|
+
export { createDefaultToolRegistry, createToolRegistry, defaultToolRegistry, rawTypeOf } from "./timeline";
|
|
109
|
+
export type {
|
|
110
|
+
CreateToolRegistryOptions,
|
|
111
|
+
ToolRegistry,
|
|
112
|
+
ToolRegistryEntry,
|
|
113
|
+
ToolRenderer,
|
|
114
|
+
ToolRendererProps,
|
|
115
|
+
} from "./timeline";
|
|
116
|
+
|
|
117
|
+
// Timeline rendering primitives + the screenshot lightbox (compose custom renderers)
|
|
118
|
+
export {
|
|
119
|
+
ActivityDisclosure,
|
|
120
|
+
ActivityRail,
|
|
121
|
+
BodyNote,
|
|
122
|
+
DisclosureDefaultsProvider,
|
|
123
|
+
LightboxProvider,
|
|
124
|
+
MediaEmpty,
|
|
125
|
+
MediaSkeleton,
|
|
126
|
+
PayloadBlock,
|
|
127
|
+
ScreenshotFigure,
|
|
128
|
+
TermBlock,
|
|
129
|
+
Thumbnail,
|
|
130
|
+
TurnSummary,
|
|
131
|
+
useLightbox,
|
|
132
|
+
useLightboxOptional,
|
|
133
|
+
} from "./timeline";
|
|
134
|
+
export type {
|
|
135
|
+
ActivityDisclosureProps,
|
|
136
|
+
ActivityRailProps,
|
|
137
|
+
DisclosureChip,
|
|
138
|
+
TurnOutcome,
|
|
139
|
+
TurnSummaryProps,
|
|
140
|
+
} from "./timeline";
|
|
141
|
+
|
|
142
|
+
// Pure provider-shape parsers (exec banner, V4A diff, secret redaction, …)
|
|
143
|
+
export {
|
|
144
|
+
applyPatchOps,
|
|
145
|
+
controlCaret,
|
|
146
|
+
execTruncated,
|
|
147
|
+
isApplyPatch,
|
|
148
|
+
isExecSessionLostBanner,
|
|
149
|
+
looksBinary,
|
|
150
|
+
parseExecBannerSessionId,
|
|
151
|
+
parseToolArgs,
|
|
152
|
+
redactSecrets,
|
|
153
|
+
sandboxCommandExitCode,
|
|
154
|
+
stripExecBanner,
|
|
155
|
+
tailPeek,
|
|
156
|
+
unwrapMcpOutput,
|
|
157
|
+
v4aToGitFileDiff,
|
|
158
|
+
} from "./timeline";
|
|
159
|
+
export type { ApplyPatchOperation } from "./timeline";
|
|
160
|
+
|
|
76
161
|
// Slash-command palette (registry + UI + hook)
|
|
77
162
|
export {
|
|
78
163
|
argHint,
|
|
@@ -107,7 +192,7 @@ export { ChatComposer } from "./components/chat-composer";
|
|
|
107
192
|
export type { ChatComposerProps } from "./components/chat-composer";
|
|
108
193
|
export { ModelPicker } from "./components/model-picker";
|
|
109
194
|
export type { ModelPickerProps } from "./components/model-picker";
|
|
110
|
-
export { MessageTimeline } from "./components/message-timeline";
|
|
195
|
+
export { MessageTimeline, TimelineRow } from "./components/message-timeline";
|
|
111
196
|
export type { MessageTimelineProps } from "./components/message-timeline";
|
|
112
197
|
export { Markdown } from "./components/markdown";
|
|
113
198
|
export type { MarkdownProps } from "./components/markdown";
|
|
@@ -116,6 +201,30 @@ export type { SessionStatusProps, StatusDotProps, SessionStatusMeta } from "./co
|
|
|
116
201
|
export { FleetTile, sessionDisplayTitle } from "./components/fleet-tile";
|
|
117
202
|
export type { FleetTileProps } from "./components/fleet-tile";
|
|
118
203
|
|
|
204
|
+
// Sandbox surfacing components (Phase 5)
|
|
205
|
+
export { SandboxTerminal } from "./components/sandbox-terminal";
|
|
206
|
+
export type { SandboxTerminalProps, XtermTheme } from "./components/sandbox-terminal";
|
|
207
|
+
export { FileBrowser } from "./components/file-browser";
|
|
208
|
+
export type { FileBrowserProps } from "./components/file-browser";
|
|
209
|
+
export { DiffView } from "./components/diff-view";
|
|
210
|
+
export type { DiffViewProps, DiffTheme } from "./components/diff-view";
|
|
211
|
+
export { PierreDiff } from "./components/pierre-diff";
|
|
212
|
+
export type { PierreDiffProps } from "./components/pierre-diff";
|
|
213
|
+
export { PierreFile } from "./components/pierre-file";
|
|
214
|
+
export type { PierreFileProps } from "./components/pierre-file";
|
|
215
|
+
export { CodeEditor, languageForPath } from "./components/code-editor";
|
|
216
|
+
export type { CodeEditorProps } from "./components/code-editor";
|
|
217
|
+
export { SandboxFiles } from "./components/sandbox-files";
|
|
218
|
+
export type { SandboxFilesProps } from "./components/sandbox-files";
|
|
219
|
+
export { DesktopViewer } from "./components/desktop-viewer";
|
|
220
|
+
export type { DesktopViewerProps } from "./components/desktop-viewer";
|
|
221
|
+
export { WorkspaceDock } from "./components/workspace-dock";
|
|
222
|
+
export type { WorkspaceDockProps, WorkspaceTab } from "./components/workspace-dock";
|
|
223
|
+
|
|
224
|
+
// Sandbox helpers
|
|
225
|
+
export { gitFileDiffToPatch } from "./lib/git-patch";
|
|
226
|
+
export { xtermThemeFromTokens } from "./lib/xterm-theme";
|
|
227
|
+
|
|
119
228
|
// Utilities
|
|
120
229
|
export { cn } from "./lib/cn";
|
|
121
230
|
export { formatBytes, formatRelativeTime, stringifyPayload, truncate, tryParseJson } from "./lib/format";
|
package/src/lib/cn.ts
CHANGED
|
@@ -1,5 +1,24 @@
|
|
|
1
1
|
import { clsx, type ClassValue } from "clsx";
|
|
2
|
-
import {
|
|
2
|
+
import { extendTailwindMerge } from "tailwind-merge";
|
|
3
|
+
|
|
4
|
+
/* ----------------------------------------------------------------------------
|
|
5
|
+
tailwind-merge, taught our design tokens.
|
|
6
|
+
|
|
7
|
+
Our type scale and color ramp share the `og-` prefix: `text-og-base` is a
|
|
8
|
+
FONT SIZE, `text-og-fg-muted` is a COLOR. Stock tailwind-merge can't tell
|
|
9
|
+
them apart — it lumps every `text-og-*` into one `font-size` group and, when
|
|
10
|
+
two land in the same class list, drops the earlier one. That silently ate the
|
|
11
|
+
size off rows like `text-og-base text-og-fg-muted`, leaving titles at the
|
|
12
|
+
browser-default 16px. Register the custom font-size scale so the size and the
|
|
13
|
+
color live in different conflict groups and both survive a merge.
|
|
14
|
+
-------------------------------------------------------------------------- */
|
|
15
|
+
const twMerge = extendTailwindMerge({
|
|
16
|
+
extend: {
|
|
17
|
+
classGroups: {
|
|
18
|
+
"font-size": [{ text: ["og-xs", "og-sm", "og-base", "og-md"] }],
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
});
|
|
3
22
|
|
|
4
23
|
/** Merge class names with Tailwind-aware conflict resolution. */
|
|
5
24
|
export function cn(...inputs: ClassValue[]): string {
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { GitFileDiff } from "@opengeni/sdk";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Reconstruct a unified-diff patch string for a single `GitFileDiff` so it can
|
|
5
|
+
* be fed to a generic patch renderer (e.g. Pierre's `PatchDiff`). The hook
|
|
6
|
+
* already carries per-line old/new numbers and a hunk header, so we emit a
|
|
7
|
+
* conventional `--- / +++ / @@` patch the parser understands.
|
|
8
|
+
*/
|
|
9
|
+
export function gitFileDiffToPatch(file: GitFileDiff): string {
|
|
10
|
+
const oldPath = file.oldPath ?? file.path;
|
|
11
|
+
const newPath = file.path;
|
|
12
|
+
const lines: string[] = [];
|
|
13
|
+
lines.push(`diff --git a/${oldPath} b/${newPath}`);
|
|
14
|
+
if (file.status === "deleted") {
|
|
15
|
+
lines.push(`--- a/${oldPath}`);
|
|
16
|
+
lines.push(`+++ /dev/null`);
|
|
17
|
+
} else if (file.status === "added" || file.status === "untracked") {
|
|
18
|
+
lines.push(`--- /dev/null`);
|
|
19
|
+
lines.push(`+++ b/${newPath}`);
|
|
20
|
+
} else {
|
|
21
|
+
lines.push(`--- a/${oldPath}`);
|
|
22
|
+
lines.push(`+++ b/${newPath}`);
|
|
23
|
+
}
|
|
24
|
+
for (const hunk of file.hunks) {
|
|
25
|
+
// Only trust a pre-parsed header if it carries the full unified range form
|
|
26
|
+
// `@@ -<o>[,<n>] +<o>[,<n>] @@`. A synthesized create_file hunk (parsers.ts)
|
|
27
|
+
// can carry a degenerate `@@ +1 @@` with no `-`/`+` ranges; a generic patch
|
|
28
|
+
// parser (Pierre) renders zero lines from it, so the expanded diff comes up
|
|
29
|
+
// empty while the collapsed chip still shows the (correct) addition count.
|
|
30
|
+
// In that case regenerate a valid header from the hunk's range fields.
|
|
31
|
+
const headerIsValid = /^@@ -\d+(?:,\d+)? \+\d+(?:,\d+)? @@/.test(hunk.header ?? "");
|
|
32
|
+
const header = headerIsValid
|
|
33
|
+
? hunk.header
|
|
34
|
+
: `@@ -${hunk.oldStart},${hunk.oldLines} +${hunk.newStart},${hunk.newLines} @@`;
|
|
35
|
+
lines.push(header);
|
|
36
|
+
for (const line of hunk.lines) {
|
|
37
|
+
if (line.type === "meta") continue;
|
|
38
|
+
const prefix = line.type === "add" ? "+" : line.type === "del" ? "-" : " ";
|
|
39
|
+
lines.push(`${prefix}${line.text}`);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return lines.join("\n") + "\n";
|
|
43
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { useEffect, useState } from "react";
|
|
2
|
+
|
|
3
|
+
/* ----------------------------------------------------------------------------
|
|
4
|
+
useThemeType
|
|
5
|
+
|
|
6
|
+
Resolve the effective dark/light theme for surfaces (the Pierre/Shiki diff)
|
|
7
|
+
that render outside the reach of host CSS — they need the theme as a value,
|
|
8
|
+
not a cascade. An explicit prop always wins; otherwise read the host's
|
|
9
|
+
`data-og-theme` attribute (set on `<html>` or any ancestor by the same opt-in
|
|
10
|
+
the tokens use) and default to dark, the first-class theme. A MutationObserver
|
|
11
|
+
keeps it live across runtime theme flips.
|
|
12
|
+
|
|
13
|
+
One detector, shared by every diff surface (the Files tab and the timeline),
|
|
14
|
+
so the two can never drift onto different themes.
|
|
15
|
+
-------------------------------------------------------------------------- */
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Resolve the diff theme. An explicit `forced` value wins; otherwise auto-detect
|
|
19
|
+
* from the host `data-og-theme` (defaulting to dark) and track live flips.
|
|
20
|
+
*/
|
|
21
|
+
export function useThemeType(forced: "dark" | "light" | undefined): "dark" | "light" {
|
|
22
|
+
const [detected, setDetected] = useState<"dark" | "light">("dark");
|
|
23
|
+
useEffect(() => {
|
|
24
|
+
if (forced || typeof document === "undefined") return;
|
|
25
|
+
const read = () => {
|
|
26
|
+
const el = document.querySelector("[data-og-theme]");
|
|
27
|
+
const value = el?.getAttribute("data-og-theme");
|
|
28
|
+
setDetected(value === "light" ? "light" : "dark");
|
|
29
|
+
};
|
|
30
|
+
read();
|
|
31
|
+
const observer = new MutationObserver(read);
|
|
32
|
+
observer.observe(document.documentElement, {
|
|
33
|
+
attributes: true,
|
|
34
|
+
attributeFilter: ["data-og-theme"],
|
|
35
|
+
subtree: true,
|
|
36
|
+
});
|
|
37
|
+
return () => observer.disconnect();
|
|
38
|
+
}, [forced]);
|
|
39
|
+
return forced ?? detected;
|
|
40
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { XtermTheme } from "../components/sandbox-terminal";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Derive an xterm `ITheme` subset from the live OKLCH `--og-*` token system (or
|
|
5
|
+
* the app's `--color-*` aliases). Reads the COMPUTED values so xterm — which
|
|
6
|
+
* paints into a canvas and can't consume CSS vars — gets concrete colors. Call
|
|
7
|
+
* on mount and re-derive on a `data-og-theme` flip.
|
|
8
|
+
*
|
|
9
|
+
* SSR-safe: returns undefined off the DOM (the caller keeps xterm's defaults).
|
|
10
|
+
*/
|
|
11
|
+
export function xtermThemeFromTokens(root?: HTMLElement | null): XtermTheme | undefined {
|
|
12
|
+
if (typeof window === "undefined" || typeof getComputedStyle === "undefined") return undefined;
|
|
13
|
+
const el = root ?? document.documentElement;
|
|
14
|
+
const style = getComputedStyle(el);
|
|
15
|
+
const read = (names: string[]): string | undefined => {
|
|
16
|
+
for (const name of names) {
|
|
17
|
+
const value = style.getPropertyValue(name).trim();
|
|
18
|
+
if (value) return value;
|
|
19
|
+
}
|
|
20
|
+
return undefined;
|
|
21
|
+
};
|
|
22
|
+
const bg = read(["--og-color-bg", "--color-bg"]);
|
|
23
|
+
const fg = read(["--og-color-fg", "--color-fg"]);
|
|
24
|
+
const accent = read(["--og-color-accent", "--color-brand", "--color-accent"]);
|
|
25
|
+
const theme: XtermTheme = {};
|
|
26
|
+
if (bg) theme.background = bg;
|
|
27
|
+
if (fg) theme.foreground = fg;
|
|
28
|
+
if (accent) {
|
|
29
|
+
theme.cursor = accent;
|
|
30
|
+
theme.selectionBackground = accent;
|
|
31
|
+
}
|
|
32
|
+
if (bg) theme.cursorAccent = bg;
|
|
33
|
+
return Object.keys(theme).length > 0 ? theme : undefined;
|
|
34
|
+
}
|