@opengeni/react 0.3.1 → 0.4.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 +1035 -14
- package/dist/index.js +6867 -1884
- package/dist/index.js.map +1 -1
- package/package.json +65 -2
- package/src/client.ts +21 -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/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-terminal-stream.ts +207 -0
- package/src/index.ts +111 -2
- package/src/lib/cn.ts +20 -1
- package/src/lib/git-patch.ts +37 -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 +248 -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,214 @@
|
|
|
1
|
+
import {
|
|
2
|
+
desktopSocketUrl,
|
|
3
|
+
nextDesktopState,
|
|
4
|
+
type DesktopConnectionState,
|
|
5
|
+
type DesktopRfbFactory,
|
|
6
|
+
type DesktopRfbLike,
|
|
7
|
+
type DesktopStreamCapability,
|
|
8
|
+
} from "@opengeni/sdk";
|
|
9
|
+
import { type RefObject, useEffect, useRef, useState } from "react";
|
|
10
|
+
|
|
11
|
+
export type UseDesktopStreamOptions = {
|
|
12
|
+
/** The desktop cell of the negotiated capabilities (`capabilities.DesktopStream`). */
|
|
13
|
+
capability: DesktopStreamCapability | null;
|
|
14
|
+
/** The mount target. RFB attaches here on connect. */
|
|
15
|
+
containerRef: RefObject<HTMLDivElement | null>;
|
|
16
|
+
/** Read-only by default (v1 ruling H). interactive only when cap.mode allows. */
|
|
17
|
+
interactive?: boolean | undefined;
|
|
18
|
+
scaleViewport?: boolean | undefined;
|
|
19
|
+
/** Custom RFB factory (tests / a WebRTC swap). Defaults to a lazy @novnc/novnc. */
|
|
20
|
+
rfbFactory?: DesktopRfbFactory | undefined;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export type UseDesktopStreamResult = {
|
|
24
|
+
state: DesktopConnectionState;
|
|
25
|
+
error: Error | null;
|
|
26
|
+
/** Manual reconnect (e.g. after a securityfailure once a fresh URL arrives). */
|
|
27
|
+
reconnect: () => void;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
/** Lazy-load @novnc/novnc's RFB as the default factory. Imported inside the
|
|
31
|
+
* connect effect so SSR / non-desktop bundles never pull the DOM-only lib.
|
|
32
|
+
* @novnc/novnc ships no types and a single `export default class RFB`. The
|
|
33
|
+
* specifier is STATIC (`import("@novnc/novnc")`) so Vite can pre-bundle and
|
|
34
|
+
* resolve it — a runtime-string indirection with `@vite-ignore` (the previous
|
|
35
|
+
* approach) hands the browser a bare specifier and throws
|
|
36
|
+
* "Failed to resolve module specifier '@novnc/novnc'". The dynamic form keeps
|
|
37
|
+
* it out of the SSR / non-desktop critical path while staying resolvable. */
|
|
38
|
+
async function defaultRfbFactory(): Promise<DesktopRfbFactory> {
|
|
39
|
+
const mod = (await import("@novnc/novnc")) as unknown as {
|
|
40
|
+
default: new (
|
|
41
|
+
t: HTMLElement,
|
|
42
|
+
u: string,
|
|
43
|
+
o: { credentials?: { password?: string | undefined } | undefined },
|
|
44
|
+
) => DesktopRfbLike;
|
|
45
|
+
};
|
|
46
|
+
const RFB = mod.default;
|
|
47
|
+
return (target, url, opts) => new RFB(target, url, opts);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Drive the noVNC RFB lifecycle from a `DesktopStreamCapability`, using the
|
|
52
|
+
* SDK's `desktop.ts` reducer + `desktopSocketUrl`. SSR-safe: the RFB import and
|
|
53
|
+
* the DOM attach happen inside `useEffect`, so a server render is a no-op and
|
|
54
|
+
* the component shows its placeholder until hydration.
|
|
55
|
+
*
|
|
56
|
+
* Read-only is enforced at three layers: `capability.mode` (server) →
|
|
57
|
+
* `interactive` prop → `RFB.viewOnly`. v1 always resolves to read-only. On a
|
|
58
|
+
* capability `url` change (a rotation), the old RFB disconnects and a fresh one
|
|
59
|
+
* connects to the new URL — a brief "desktop blink", acceptable on rollover.
|
|
60
|
+
*/
|
|
61
|
+
export function useDesktopStream(options: UseDesktopStreamOptions): UseDesktopStreamResult {
|
|
62
|
+
const { capability, containerRef, interactive, scaleViewport, rfbFactory } = options;
|
|
63
|
+
const [state, setState] = useState<DesktopConnectionState>("idle");
|
|
64
|
+
const [error, setError] = useState<Error | null>(null);
|
|
65
|
+
const [nonce, setNonce] = useState(0);
|
|
66
|
+
const stateRef = useRef<DesktopConnectionState>("idle");
|
|
67
|
+
const setBoth = (next: DesktopConnectionState) => {
|
|
68
|
+
stateRef.current = next;
|
|
69
|
+
setState(next);
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const reconnect = () => setNonce((n) => n + 1);
|
|
73
|
+
|
|
74
|
+
const url = capability?.url ?? null;
|
|
75
|
+
// The credential is keyed into the connect effect so a token rotation (new
|
|
76
|
+
// password) reconnects, but NOT folded into the whole-`capability` identity —
|
|
77
|
+
// a benign re-negotiation that re-mints the same cell must not churn the socket.
|
|
78
|
+
const token = capability?.token ?? null;
|
|
79
|
+
const transport = capability?.transport ?? null;
|
|
80
|
+
const mode = capability?.mode ?? "read-only";
|
|
81
|
+
|
|
82
|
+
// The live RFB handle. Holding it in a ref lets us flip view-only (take
|
|
83
|
+
// control / return control) and re-scale on the OPEN connection instead of
|
|
84
|
+
// tearing the socket down — the old behaviour reconnected on every
|
|
85
|
+
// `interactive` change, which read as a constant "refresh" and (combined with
|
|
86
|
+
// the canvas churn) bounced control back to watch.
|
|
87
|
+
const rfbRef = useRef<DesktopRfbLike | null>(null);
|
|
88
|
+
// Latest input/scale/factory the connect effect reads via refs so they are NOT
|
|
89
|
+
// connect-effect dependencies: changing them must update the live RFB in place,
|
|
90
|
+
// never reconnect it.
|
|
91
|
+
const interactiveRef = useRef(interactive);
|
|
92
|
+
const scaleViewportRef = useRef(scaleViewport);
|
|
93
|
+
const modeRef = useRef(mode);
|
|
94
|
+
const rfbFactoryRef = useRef(rfbFactory);
|
|
95
|
+
interactiveRef.current = interactive;
|
|
96
|
+
scaleViewportRef.current = scaleViewport;
|
|
97
|
+
modeRef.current = mode;
|
|
98
|
+
rfbFactoryRef.current = rfbFactory;
|
|
99
|
+
|
|
100
|
+
// read-only is forced when the server says so OR the caller didn't opt in.
|
|
101
|
+
const viewOnlyFor = (m: string, want: boolean | undefined) => m === "read-only" || !want;
|
|
102
|
+
|
|
103
|
+
useEffect(() => {
|
|
104
|
+
// SSR / no DOM / no usable transport: stay idle and show the placeholder.
|
|
105
|
+
if (typeof window === "undefined") return;
|
|
106
|
+
if (transport !== "vnc-ws" || !url) {
|
|
107
|
+
setBoth("idle");
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
const container = containerRef.current;
|
|
111
|
+
if (!container) {
|
|
112
|
+
// The mount target isn't in the DOM yet (e.g. the tab is still hidden).
|
|
113
|
+
// Stay idle (not a stale negotiating/connecting) so a re-attach nudge —
|
|
114
|
+
// fired by the viewer on becoming visible / on mount — can re-run this
|
|
115
|
+
// effect once the container exists. Without this the surface can stick
|
|
116
|
+
// forever on the idle scrim and never open a socket.
|
|
117
|
+
setBoth("idle");
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
let rfb: DesktopRfbLike | null = null;
|
|
122
|
+
let disposed = false;
|
|
123
|
+
setError(null);
|
|
124
|
+
setBoth("negotiating");
|
|
125
|
+
|
|
126
|
+
const onConnect = () => {
|
|
127
|
+
if (!disposed) setBoth(nextDesktopState(stateRef.current, { type: "connected" }));
|
|
128
|
+
};
|
|
129
|
+
const onDisconnect = () => {
|
|
130
|
+
if (!disposed) setBoth(nextDesktopState(stateRef.current, { type: "disconnected" }));
|
|
131
|
+
};
|
|
132
|
+
const onSecurityFailure = () => {
|
|
133
|
+
if (disposed) return;
|
|
134
|
+
setError(new Error("desktop authentication failed (token expired or revoked)"));
|
|
135
|
+
setBoth(nextDesktopState(stateRef.current, { type: "fail" }));
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
void (async () => {
|
|
139
|
+
try {
|
|
140
|
+
const factory = rfbFactoryRef.current ?? (await defaultRfbFactory());
|
|
141
|
+
if (disposed) return;
|
|
142
|
+
const socketUrl = desktopSocketUrl({ url });
|
|
143
|
+
setBoth(nextDesktopState(stateRef.current, { type: "negotiated" }));
|
|
144
|
+
rfb = factory(container, socketUrl, {
|
|
145
|
+
credentials: token ? { password: token } : undefined,
|
|
146
|
+
});
|
|
147
|
+
rfb.viewOnly = viewOnlyFor(modeRef.current, interactiveRef.current);
|
|
148
|
+
// Fit-to-panel: SCALE the 1280x800 framebuffer down to the container
|
|
149
|
+
// (aspect-preserved) and never 1:1-clip. `clipViewport=false` is pinned
|
|
150
|
+
// explicitly — noVNC forces it off while scaling, but a fresh RFB on a
|
|
151
|
+
// url rotation could otherwise start from a stale clip and read as
|
|
152
|
+
// "zoomed in". Order matters: clip off, then scale on.
|
|
153
|
+
rfb.clipViewport = false;
|
|
154
|
+
rfb.scaleViewport = scaleViewportRef.current ?? true;
|
|
155
|
+
rfb.addEventListener("connect", onConnect);
|
|
156
|
+
rfb.addEventListener("disconnect", onDisconnect);
|
|
157
|
+
rfb.addEventListener("securityfailure", onSecurityFailure);
|
|
158
|
+
rfbRef.current = rfb;
|
|
159
|
+
} catch (cause) {
|
|
160
|
+
if (!disposed) {
|
|
161
|
+
setError(cause instanceof Error ? cause : new Error(String(cause)));
|
|
162
|
+
setBoth("error");
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
})();
|
|
166
|
+
|
|
167
|
+
return () => {
|
|
168
|
+
disposed = true;
|
|
169
|
+
if (rfbRef.current === rfb) rfbRef.current = null;
|
|
170
|
+
if (rfb) {
|
|
171
|
+
rfb.removeEventListener?.("connect", onConnect);
|
|
172
|
+
rfb.removeEventListener?.("disconnect", onDisconnect);
|
|
173
|
+
rfb.removeEventListener?.("securityfailure", onSecurityFailure);
|
|
174
|
+
try {
|
|
175
|
+
rfb.disconnect();
|
|
176
|
+
} catch {
|
|
177
|
+
// ignore teardown errors
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
};
|
|
181
|
+
// ONLY a real transport change reconnects: a fresh url (rotation), a new
|
|
182
|
+
// credential, the transport flipping, or an explicit manual reconnect
|
|
183
|
+
// (`nonce`). `interactive`/`scaleViewport`/`mode`/`rfbFactory` are read via
|
|
184
|
+
// refs and applied live below — they must never re-open the socket.
|
|
185
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
186
|
+
}, [url, token, transport, nonce]);
|
|
187
|
+
|
|
188
|
+
// Apply take-control / return-control to the OPEN connection in place. noVNC
|
|
189
|
+
// honours `viewOnly` live, so flipping it neither blinks the surface nor drops
|
|
190
|
+
// the framebuffer — the cure for the take-control "refresh + auto-release" loop.
|
|
191
|
+
useEffect(() => {
|
|
192
|
+
const rfb = rfbRef.current;
|
|
193
|
+
if (rfb) rfb.viewOnly = viewOnlyFor(mode, interactive);
|
|
194
|
+
// viewOnlyFor is pure; re-run only when the inputs change.
|
|
195
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
196
|
+
}, [interactive, mode, state]);
|
|
197
|
+
|
|
198
|
+
// Re-scale the live RFB in place (no reconnect) when the scale preference flips
|
|
199
|
+
// OR the connection state advances (e.g. the first framebuffer paints after a
|
|
200
|
+
// take-control flip / a re-attach). Re-asserting clip-off + scale-on here is the
|
|
201
|
+
// belt-and-suspenders against a "zoomed" surface: it re-fits the canvas to the
|
|
202
|
+
// panel on every meaningful transition, so a transient mis-measure at connect
|
|
203
|
+
// time can never stick.
|
|
204
|
+
useEffect(() => {
|
|
205
|
+
const rfb = rfbRef.current;
|
|
206
|
+
if (rfb) {
|
|
207
|
+
rfb.clipViewport = false;
|
|
208
|
+
rfb.scaleViewport = scaleViewport ?? true;
|
|
209
|
+
}
|
|
210
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
211
|
+
}, [scaleViewport, state]);
|
|
212
|
+
|
|
213
|
+
return { state, error, reconnect };
|
|
214
|
+
}
|