replicas-computer 0.1.0 → 0.2.1

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.
@@ -0,0 +1,308 @@
1
+ // src/media-protocol.ts
2
+ var STREAM_PROTOCOL_VERSION = 1;
3
+ var MEDIA_PACKET_HEADER_BYTES = 12;
4
+
5
+ // src/stream-viewer.ts
6
+ var screen = document.querySelector("#screen");
7
+ var status = document.querySelector("#status");
8
+ var statusText = document.querySelector("#status-text");
9
+ var audioPrompt = document.querySelector("#audio-prompt");
10
+ var diagnostics = document.querySelector("#diagnostics");
11
+ var TrackGenerator = globalThis.MediaStreamTrackGenerator;
12
+ if (!screen || !status || !statusText || !audioPrompt || !diagnostics) throw new Error("Desktop stream markup is incomplete");
13
+ if (!TrackGenerator) throw new Error("This browser cannot render the desktop stream");
14
+ var params = new URLSearchParams(location.search);
15
+ var showDiagnostics = params.get("diagnostics") === "1";
16
+ diagnostics.classList.toggle("visible", showDiagnostics);
17
+ var socket;
18
+ var config;
19
+ var videoTrack;
20
+ var videoWriter;
21
+ var videoDecoder;
22
+ var activeVideoConfig;
23
+ var videoConfiguration = 0;
24
+ var audioDecoder;
25
+ var audioContext;
26
+ var nextAudioTime = 0;
27
+ var waitingForKeyframe = true;
28
+ var reconnectAttempt = 0;
29
+ var reconnectTimer;
30
+ var pointerFrame;
31
+ var pendingPointer;
32
+ var renderedFrames = 0;
33
+ var decodedAudioPackets = 0;
34
+ var renderedWindowStartedAt = performance.now();
35
+ var rtt = 0;
36
+ var showStatus = (message) => {
37
+ statusText.textContent = message;
38
+ status.classList.add("visible");
39
+ };
40
+ var fitScreen = () => {
41
+ if (!config) return;
42
+ const scale = Math.min(innerWidth / config.width, innerHeight / config.height);
43
+ screen.style.width = `${Math.floor(config.width * scale)}px`;
44
+ screen.style.height = `${Math.floor(config.height * scale)}px`;
45
+ };
46
+ var send = (message) => {
47
+ if (socket?.readyState === WebSocket.OPEN) socket.send(JSON.stringify(message));
48
+ };
49
+ var resetVideoTrack = () => {
50
+ void videoWriter?.close().catch(() => {
51
+ });
52
+ videoTrack?.stop();
53
+ videoTrack = new TrackGenerator({ kind: "video" });
54
+ videoWriter = videoTrack.writable.getWriter();
55
+ screen.srcObject = new MediaStream([videoTrack]);
56
+ };
57
+ var decoderConfig = (next, preferHardware = false) => {
58
+ const decoder = {
59
+ codec: next.videoCodec,
60
+ codedWidth: next.width,
61
+ codedHeight: next.height,
62
+ optimizeForLatency: true,
63
+ avc: { format: "annexb" }
64
+ };
65
+ if (preferHardware) decoder.hardwareAcceleration = "prefer-hardware";
66
+ return decoder;
67
+ };
68
+ var configureVideo = async (next) => {
69
+ const configuration = ++videoConfiguration;
70
+ const previousDecoder = videoDecoder;
71
+ videoDecoder = void 0;
72
+ activeVideoConfig = void 0;
73
+ if (previousDecoder && previousDecoder.state !== "closed") previousDecoder.close();
74
+ const preferred = decoderConfig(next, true);
75
+ const support = await VideoDecoder.isConfigSupported(preferred);
76
+ if (configuration !== videoConfiguration) return;
77
+ const selectedConfig = support.supported && support.config ? support.config : decoderConfig(next);
78
+ activeVideoConfig = selectedConfig;
79
+ const decoder = new VideoDecoder({
80
+ output: (frame) => {
81
+ const writer = videoWriter;
82
+ if (!writer || (writer.desiredSize ?? 1) <= 0) {
83
+ frame.close();
84
+ return;
85
+ }
86
+ try {
87
+ void writer.write(frame).catch((error) => showStatus(error instanceof Error ? error.message : String(error))).finally(() => frame.close());
88
+ } catch (error) {
89
+ frame.close();
90
+ showStatus(error instanceof Error ? error.message : String(error));
91
+ }
92
+ },
93
+ error: (error) => {
94
+ if (videoDecoder !== decoder) return;
95
+ videoDecoder = void 0;
96
+ waitingForKeyframe = true;
97
+ showStatus(`recovering video: ${error.message}`);
98
+ if (config) void configureVideo(config).catch((reason) => showStatus(reason instanceof Error ? reason.message : String(reason)));
99
+ }
100
+ });
101
+ videoDecoder = decoder;
102
+ decoder.configure(selectedConfig);
103
+ waitingForKeyframe = true;
104
+ };
105
+ var enableAudio = async () => {
106
+ if (!config || typeof AudioDecoder === "undefined") return;
107
+ audioContext ??= new AudioContext({ latencyHint: "interactive", sampleRate: config.audioSampleRate });
108
+ await audioContext.resume();
109
+ if (!audioDecoder || audioDecoder.state === "closed") {
110
+ const decoderConfig2 = {
111
+ codec: config.audioCodec,
112
+ sampleRate: config.audioSampleRate,
113
+ numberOfChannels: config.audioChannels
114
+ };
115
+ const support = await AudioDecoder.isConfigSupported(decoderConfig2);
116
+ if (!support.supported) throw new Error(`Browser cannot decode ${config.audioCodec}`);
117
+ audioDecoder = new AudioDecoder({
118
+ output: (data) => {
119
+ if (!audioContext || audioContext.state !== "running") {
120
+ data.close();
121
+ return;
122
+ }
123
+ const buffer = audioContext.createBuffer(data.numberOfChannels, data.numberOfFrames, data.sampleRate);
124
+ for (let channel = 0; channel < data.numberOfChannels; channel += 1) {
125
+ data.copyTo(buffer.getChannelData(channel), { planeIndex: channel, format: "f32-planar" });
126
+ }
127
+ const source = audioContext.createBufferSource();
128
+ source.buffer = buffer;
129
+ source.connect(audioContext.destination);
130
+ const minimumStart = audioContext.currentTime + 0.035;
131
+ if (nextAudioTime < minimumStart || nextAudioTime > audioContext.currentTime + 0.25) nextAudioTime = minimumStart;
132
+ source.start(nextAudioTime);
133
+ nextAudioTime += data.numberOfFrames / data.sampleRate;
134
+ data.close();
135
+ decodedAudioPackets += 1;
136
+ window.dispatchEvent(new Event("replicasdesktopaudio"));
137
+ },
138
+ error: () => {
139
+ audioPrompt.hidden = false;
140
+ }
141
+ });
142
+ audioDecoder.configure(support.config ?? decoderConfig2);
143
+ }
144
+ audioPrompt.hidden = true;
145
+ };
146
+ var handleBinary = (data) => {
147
+ if (!config || data.byteLength <= MEDIA_PACKET_HEADER_BYTES) return;
148
+ const view = new DataView(data);
149
+ if (view.getUint8(1) !== STREAM_PROTOCOL_VERSION) return;
150
+ const kind = view.getUint8(0);
151
+ if (kind !== 1 /* VideoKey */ && kind !== 2 /* VideoDelta */ && kind !== 3 /* Audio */) return;
152
+ const timestamp = Number(view.getBigUint64(4));
153
+ const payload = new Uint8Array(data, MEDIA_PACKET_HEADER_BYTES);
154
+ if (kind === 3 /* Audio */) {
155
+ if (!audioDecoder || audioDecoder.state !== "configured" || audioDecoder.decodeQueueSize > 12) return;
156
+ audioDecoder.decode(new EncodedAudioChunk({ type: "key", timestamp, data: payload }));
157
+ return;
158
+ }
159
+ if (!videoDecoder || !activeVideoConfig || videoDecoder.state !== "configured") return;
160
+ const key = kind === 1 /* VideoKey */;
161
+ if (videoDecoder.decodeQueueSize > 12) {
162
+ videoDecoder.reset();
163
+ videoDecoder.configure(activeVideoConfig);
164
+ waitingForKeyframe = true;
165
+ }
166
+ if (waitingForKeyframe && !key) return;
167
+ waitingForKeyframe = false;
168
+ try {
169
+ videoDecoder.decode(new EncodedVideoChunk({ type: key ? "key" : "delta", timestamp, data: payload }));
170
+ } catch {
171
+ videoDecoder.reset();
172
+ videoDecoder.configure(activeVideoConfig);
173
+ waitingForKeyframe = true;
174
+ }
175
+ };
176
+ var connect = () => {
177
+ clearTimeout(reconnectTimer);
178
+ socket?.close();
179
+ showStatus(reconnectAttempt ? "reconnecting\u2026" : "connecting\u2026");
180
+ const nextSocket = new WebSocket(`${location.protocol === "https:" ? "wss" : "ws"}://${location.host}/stream`);
181
+ nextSocket.binaryType = "arraybuffer";
182
+ socket = nextSocket;
183
+ nextSocket.addEventListener("open", () => {
184
+ reconnectAttempt = 0;
185
+ });
186
+ nextSocket.addEventListener("message", (event) => {
187
+ if (event.data instanceof ArrayBuffer) {
188
+ handleBinary(event.data);
189
+ return;
190
+ }
191
+ if (typeof event.data !== "string") return;
192
+ const message = JSON.parse(event.data);
193
+ if (message.type === "pong") {
194
+ rtt = performance.now() - message.sentAt;
195
+ return;
196
+ }
197
+ if (message.type !== "config" || message.protocolVersion !== STREAM_PROTOCOL_VERSION) return;
198
+ config = message;
199
+ fitScreen();
200
+ resetVideoTrack();
201
+ void configureVideo(message).catch((error) => showStatus(error instanceof Error ? error.message : String(error)));
202
+ void screen.play().catch(() => showStatus("Click the stream to begin playback"));
203
+ enableAudio().catch(() => {
204
+ audioPrompt.hidden = false;
205
+ });
206
+ });
207
+ nextSocket.addEventListener("close", () => {
208
+ if (socket !== nextSocket) return;
209
+ reconnectAttempt += 1;
210
+ const delay = Math.min(8e3, 250 * 2 ** Math.min(reconnectAttempt, 5));
211
+ reconnectTimer = setTimeout(connect, delay * (0.85 + Math.random() * 0.3));
212
+ });
213
+ nextSocket.addEventListener("error", () => nextSocket.close());
214
+ };
215
+ var normalizedPointer = (event) => {
216
+ const bounds = screen.getBoundingClientRect();
217
+ return {
218
+ x: (event.clientX - bounds.left) / bounds.width,
219
+ y: (event.clientY - bounds.top) / bounds.height
220
+ };
221
+ };
222
+ var flushPointer = () => {
223
+ pointerFrame = void 0;
224
+ if (!pendingPointer) return;
225
+ send({ type: "pointer", ...pendingPointer });
226
+ pendingPointer = void 0;
227
+ };
228
+ var frameCallback = 0;
229
+ var observeFrame = () => {
230
+ frameCallback = screen.requestVideoFrameCallback(() => {
231
+ renderedFrames += 1;
232
+ status.classList.remove("visible");
233
+ window.dispatchEvent(new Event("replicasdesktopframe"));
234
+ observeFrame();
235
+ });
236
+ };
237
+ observeFrame();
238
+ screen.addEventListener("pointermove", (event) => {
239
+ pendingPointer = normalizedPointer(event);
240
+ pointerFrame ??= requestAnimationFrame(flushPointer);
241
+ });
242
+ screen.addEventListener("pointerdown", (event) => {
243
+ event.preventDefault();
244
+ screen.focus({ preventScroll: true });
245
+ screen.setPointerCapture(event.pointerId);
246
+ enableAudio().catch(() => {
247
+ audioPrompt.hidden = false;
248
+ });
249
+ send({ type: "button", ...normalizedPointer(event), button: event.button, down: true });
250
+ });
251
+ screen.addEventListener("pointerup", (event) => {
252
+ event.preventDefault();
253
+ send({ type: "button", ...normalizedPointer(event), button: event.button, down: false });
254
+ });
255
+ screen.addEventListener("pointercancel", () => send({ type: "release" }));
256
+ screen.addEventListener("wheel", (event) => {
257
+ event.preventDefault();
258
+ send({ type: "wheel", ...normalizedPointer(event), deltaX: event.deltaX, deltaY: event.deltaY });
259
+ }, { passive: false });
260
+ screen.addEventListener("contextmenu", (event) => event.preventDefault());
261
+ window.addEventListener("keydown", (event) => {
262
+ if (!screen.matches(":focus")) return;
263
+ event.preventDefault();
264
+ enableAudio().catch(() => {
265
+ audioPrompt.hidden = false;
266
+ });
267
+ send({ type: "key", code: event.code, down: true, repeat: event.repeat });
268
+ }, { capture: true });
269
+ window.addEventListener("keyup", (event) => {
270
+ if (!screen.matches(":focus")) return;
271
+ event.preventDefault();
272
+ send({ type: "key", code: event.code, down: false });
273
+ }, { capture: true });
274
+ window.addEventListener("blur", () => send({ type: "release" }));
275
+ window.addEventListener("resize", fitScreen);
276
+ window.addEventListener("online", connect);
277
+ window.addEventListener("pagehide", () => {
278
+ clearTimeout(reconnectTimer);
279
+ send({ type: "release" });
280
+ socket?.close();
281
+ screen.cancelVideoFrameCallback(frameCallback);
282
+ videoDecoder?.close();
283
+ audioDecoder?.close();
284
+ audioContext?.close();
285
+ void videoWriter?.close();
286
+ videoTrack?.stop();
287
+ });
288
+ audioPrompt.addEventListener("click", () => enableAudio().catch((error) => showStatus(error instanceof Error ? error.message : String(error))));
289
+ setInterval(() => {
290
+ if (socket?.readyState === WebSocket.OPEN) send({ type: "ping", sentAt: performance.now() });
291
+ const now = performance.now();
292
+ const elapsedSeconds = (now - renderedWindowStartedAt) / 1e3;
293
+ const measuredFps = renderedFrames / elapsedSeconds;
294
+ send({
295
+ type: "stats",
296
+ videoFps: measuredFps,
297
+ audioPacketsPerSecond: decodedAudioPackets / elapsedSeconds,
298
+ decodeQueue: videoDecoder?.decodeQueueSize ?? 0,
299
+ rttMs: rtt
300
+ });
301
+ if (showDiagnostics) {
302
+ diagnostics.value = `${measuredFps.toFixed(1)} fps \xB7 ${rtt.toFixed(0)} ms RTT \xB7 ${videoDecoder?.decodeQueueSize ?? 0} queued`;
303
+ }
304
+ renderedFrames = 0;
305
+ decodedAudioPackets = 0;
306
+ renderedWindowStartedAt = now;
307
+ }, 1e3);
308
+ connect();
package/dist/viewer.js ADDED
@@ -0,0 +1,199 @@
1
+ // src/viewer.ts
2
+ import RFB from "./core/rfb.js";
3
+ import { encodings } from "./core/encodings.js";
4
+ var screen = document.querySelector("#screen");
5
+ var status = document.querySelector("#status");
6
+ var statusText = document.querySelector("#status-text");
7
+ var hint = document.querySelector("#hint");
8
+ if (!screen || !status || !statusText || !hint) throw new Error("Desktop viewer markup is incomplete");
9
+ var params = new URLSearchParams(location.search);
10
+ var intParam = (name, fallback, min = 0, max = 9) => {
11
+ const value = Number.parseInt(params.get(name) ?? "", 10);
12
+ return Number.isInteger(value) && value >= min && value <= max ? value : fallback;
13
+ };
14
+ var requestedProfile = params.get("profile");
15
+ var streamProfile = requestedProfile === "raw" || requestedProfile === "hextile" ? requestedProfile : "tight";
16
+ var requestFps = intParam("requestFps", streamProfile === "tight" ? 120 : 0, 0, 240);
17
+ var requestBatch = intParam("requestBatch", 1, 1, 8);
18
+ var inputRefresh = params.get("inputRefresh") === "1" || streamProfile === "tight";
19
+ var optimisticScroll = params.get("optimisticScroll") === "1";
20
+ var optimisticScrollMax = intParam("optimisticScrollMax", 220, 0, 720);
21
+ var streamDiagnostics = params.get("diagnostics") === "1";
22
+ var modernCursor = 'url("data:image/svg+xml,%3Csvg xmlns=%27http://www.w3.org/2000/svg%27 width=%2724%27 height=%2724%27 viewBox=%270 0 24 24%27%3E%3Cpath d=%27M5 3.5l12.2 11.4-5.8.7 3.2 5.3-2.6 1.5-3.2-5.3-4.1 4.3z%27 fill=%27white%27 stroke=%27%2310161f%27 stroke-width=%272%27 stroke-linejoin=%27round%27/%3E%3C/svg%3E") 5 4, default';
23
+ var wsUrl = `${location.protocol === "https:" ? "wss" : "ws"}://${location.host}/websockify`;
24
+ var activeRfb = null;
25
+ var connectionGeneration = 0;
26
+ var reconnectAttempt = 0;
27
+ var reconnectTimer;
28
+ var updatePump;
29
+ var lastInputRefreshAt = 0;
30
+ var showStatus = (message) => {
31
+ statusText.textContent = message;
32
+ status.classList.add("visible");
33
+ };
34
+ var applyModernCursor = () => {
35
+ screen.style.cursor = modernCursor;
36
+ const canvas = screen.querySelector("canvas");
37
+ if (canvas) canvas.style.cursor = modernCursor;
38
+ };
39
+ var requestFramebufferUpdate = (duringContinuousUpdates = false) => {
40
+ const rfb = activeRfb;
41
+ if (!rfb || rfb._rfbConnectionState !== "connected" || !duringContinuousUpdates && rfb._enabledContinuousUpdates || !rfb._sock || !rfb._fbWidth || !rfb._fbHeight) return;
42
+ for (let i = 0; i < requestBatch; i += 1) {
43
+ RFB.messages.fbUpdateRequest(rfb._sock, true, 0, 0, rfb._fbWidth, rfb._fbHeight);
44
+ }
45
+ };
46
+ var stopUpdatePump = () => {
47
+ clearInterval(updatePump);
48
+ updatePump = void 0;
49
+ };
50
+ var startUpdatePump = () => {
51
+ stopUpdatePump();
52
+ if (requestFps) updatePump = setInterval(requestFramebufferUpdate, Math.max(8, Math.round(1e3 / requestFps)));
53
+ };
54
+ var queueInputRefresh = (duringContinuousUpdates = false) => {
55
+ if (!inputRefresh) return;
56
+ const now = performance.now();
57
+ if (now - lastInputRefreshAt < 16) return;
58
+ lastInputRefreshAt = now;
59
+ requestFramebufferUpdate(duringContinuousUpdates);
60
+ for (const delay of [8, 16, 32, 48, 64, 96]) setTimeout(() => requestFramebufferUpdate(duringContinuousUpdates), delay);
61
+ };
62
+ var configureEncodings = (rfb) => {
63
+ if (streamProfile === "tight") return;
64
+ const sendDefaultEncodings = rfb._sendEncodings.bind(rfb);
65
+ rfb._sendEncodings = () => {
66
+ if (!rfb._sock || rfb._fbDepth !== 24) {
67
+ sendDefaultEncodings();
68
+ return;
69
+ }
70
+ RFB.messages.clientEncodings(rfb._sock, [
71
+ encodings.encodingCopyRect,
72
+ streamProfile === "hextile" ? encodings.encodingHextile : encodings.encodingRaw,
73
+ encodings.encodingRaw,
74
+ encodings.pseudoEncodingDesktopSize,
75
+ encodings.pseudoEncodingLastRect,
76
+ encodings.pseudoEncodingQEMUExtendedKeyEvent,
77
+ encodings.pseudoEncodingExtendedDesktopSize,
78
+ encodings.pseudoEncodingXvp,
79
+ encodings.pseudoEncodingFence,
80
+ encodings.pseudoEncodingContinuousUpdates,
81
+ encodings.pseudoEncodingDesktopName,
82
+ encodings.pseudoEncodingExtendedClipboard,
83
+ encodings.pseudoEncodingVMwareCursor,
84
+ encodings.pseudoEncodingCursor
85
+ ]);
86
+ };
87
+ };
88
+ var scheduleReconnect = (immediate = false) => {
89
+ if (reconnectTimer || document.visibilityState === "hidden" || !navigator.onLine) return;
90
+ const delay = immediate ? 0 : Math.min(1e4, 250 * 2 ** Math.min(reconnectAttempt, 6)) * (0.8 + Math.random() * 0.4);
91
+ reconnectAttempt += 1;
92
+ showStatus(reconnectAttempt > 1 ? "reconnecting\u2026" : "connecting\u2026");
93
+ reconnectTimer = setTimeout(() => {
94
+ reconnectTimer = void 0;
95
+ connect();
96
+ }, delay);
97
+ };
98
+ var connect = () => {
99
+ clearTimeout(reconnectTimer);
100
+ reconnectTimer = void 0;
101
+ stopUpdatePump();
102
+ const generation = ++connectionGeneration;
103
+ const previousRfb = activeRfb;
104
+ activeRfb = null;
105
+ previousRfb?.disconnect();
106
+ showStatus(reconnectAttempt ? "reconnecting\u2026" : "connecting\u2026");
107
+ let rfb;
108
+ try {
109
+ rfb = new RFB(screen, wsUrl);
110
+ } catch {
111
+ scheduleReconnect();
112
+ return;
113
+ }
114
+ activeRfb = rfb;
115
+ rfb.scaleViewport = true;
116
+ rfb.resizeSession = false;
117
+ rfb.background = "#000";
118
+ rfb.showDotCursor = false;
119
+ if (streamProfile === "tight") {
120
+ rfb.compressionLevel = intParam("compression", 1);
121
+ rfb.qualityLevel = intParam("quality", 8);
122
+ }
123
+ configureEncodings(rfb);
124
+ if (streamDiagnostics) {
125
+ const framebufferUpdate = rfb._framebufferUpdate.bind(rfb);
126
+ rfb._framebufferUpdate = () => {
127
+ const complete = framebufferUpdate();
128
+ if (complete) window.dispatchEvent(new Event("replicasdesktopframe"));
129
+ return complete;
130
+ };
131
+ }
132
+ rfb.addEventListener("connect", () => {
133
+ if (generation !== connectionGeneration) return;
134
+ reconnectAttempt = 0;
135
+ status.classList.remove("visible");
136
+ applyModernCursor();
137
+ startUpdatePump();
138
+ });
139
+ rfb.addEventListener("disconnect", () => {
140
+ if (generation !== connectionGeneration) return;
141
+ activeRfb = null;
142
+ stopUpdatePump();
143
+ scheduleReconnect();
144
+ });
145
+ rfb.addEventListener("securityfailure", () => {
146
+ if (generation !== connectionGeneration) return;
147
+ activeRfb = null;
148
+ stopUpdatePump();
149
+ scheduleReconnect();
150
+ });
151
+ };
152
+ var shiftCanvasForWheel = (event) => {
153
+ if (!optimisticScroll || event.ctrlKey || event.metaKey || Math.abs(event.deltaY) <= Math.abs(event.deltaX)) return;
154
+ const canvas = screen.querySelector("canvas");
155
+ const context = canvas?.getContext("2d");
156
+ if (!canvas || !context || !canvas.width || !canvas.height) return;
157
+ const scale = event.deltaMode === WheelEvent.DOM_DELTA_LINE ? 32 : event.deltaMode === WheelEvent.DOM_DELTA_PAGE ? canvas.height : 1;
158
+ const limit = Math.max(0, Math.min(optimisticScrollMax, canvas.height - 1));
159
+ const offset = Math.trunc(Math.max(-limit, Math.min(limit, event.deltaY * scale)));
160
+ if (offset > 0) {
161
+ context.drawImage(canvas, 0, offset, canvas.width, canvas.height - offset, 0, 0, canvas.width, canvas.height - offset);
162
+ context.drawImage(canvas, 0, canvas.height - offset - 1, canvas.width, 1, 0, canvas.height - offset, canvas.width, offset);
163
+ } else if (offset < 0) {
164
+ const size = -offset;
165
+ context.drawImage(canvas, 0, 0, canvas.width, canvas.height - size, 0, size, canvas.width, canvas.height - size);
166
+ context.drawImage(canvas, 0, size, canvas.width, 1, 0, 0, canvas.width, size);
167
+ }
168
+ };
169
+ var hintTimer;
170
+ var isMac = /Mac|iPhone|iPad/.test(navigator.userAgentData?.platform ?? navigator.platform ?? "");
171
+ window.addEventListener("keydown", (event) => {
172
+ if (isMac && event.metaKey && event.key !== "Meta") {
173
+ hint.classList.add("visible");
174
+ clearTimeout(hintTimer);
175
+ hintTimer = setTimeout(() => hint.classList.remove("visible"), 4e3);
176
+ }
177
+ queueInputRefresh(true);
178
+ }, { capture: true, passive: true });
179
+ window.addEventListener("wheel", (event) => {
180
+ shiftCanvasForWheel(event);
181
+ queueInputRefresh(true);
182
+ }, { capture: true, passive: true });
183
+ for (const eventName of ["pointerdown", "pointerup", "keyup"]) {
184
+ window.addEventListener(eventName, () => queueInputRefresh(true), { passive: true });
185
+ }
186
+ window.addEventListener("pointermove", () => queueInputRefresh(), { passive: true });
187
+ new MutationObserver(applyModernCursor).observe(screen, { attributes: true, childList: true, subtree: true, attributeFilter: ["style"] });
188
+ window.addEventListener("online", () => scheduleReconnect(true));
189
+ document.addEventListener("visibilitychange", () => {
190
+ if (document.visibilityState === "visible" && !activeRfb) scheduleReconnect(true);
191
+ });
192
+ window.addEventListener("pagehide", () => {
193
+ connectionGeneration += 1;
194
+ clearTimeout(reconnectTimer);
195
+ stopUpdatePump();
196
+ activeRfb?.disconnect();
197
+ activeRfb = null;
198
+ });
199
+ connect();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "replicas-computer",
3
- "version": "0.1.0",
3
+ "version": "0.2.1",
4
4
  "description": "Replicas workspace computer-use runtime",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -31,11 +31,15 @@
31
31
  "directory": "computer-use"
32
32
  },
33
33
  "dependencies": {
34
+ "@novnc/novnc": "1.7.0",
34
35
  "chalk": "^4.1.2",
35
- "commander": "^12.0.0"
36
+ "commander": "^12.0.0",
37
+ "ws": "8.21.2"
36
38
  },
37
39
  "devDependencies": {
40
+ "@replicas/shared": "*",
38
41
  "@types/node": "^20.11.17",
42
+ "@types/ws": "8.18.1",
39
43
  "tsup": "^8.5.0",
40
44
  "typescript": "^5.9.3"
41
45
  }