replicas-computer 0.3.33 → 0.3.34

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.js CHANGED
@@ -3325,7 +3325,7 @@ async function computerStatusCommand() {
3325
3325
  }
3326
3326
 
3327
3327
  // src/version.ts
3328
- var COMPUTER_USE_VERSION = "0.3.33";
3328
+ var COMPUTER_USE_VERSION = "0.3.34";
3329
3329
  var COMPUTER_USE_PROTOCOL_VERSION = 1;
3330
3330
 
3331
3331
  // src/program.ts
@@ -3568,7 +3568,7 @@ async function runComputerClient(argv) {
3568
3568
  }
3569
3569
 
3570
3570
  // src/media-server.ts
3571
- import { spawn as spawn7 } from "child_process";
3571
+ import { execFile, spawn as spawn7 } from "child_process";
3572
3572
  import { createHash as createHash3 } from "crypto";
3573
3573
  import { createReadStream, existsSync as existsSync8, readdirSync as readdirSync3, readFileSync as readFileSync8, statSync as statSync2 } from "fs";
3574
3574
  import { createServer as createServer3 } from "http";
@@ -4166,6 +4166,11 @@ import { availableParallelism } from "os";
4166
4166
  import { dirname as dirname6, resolve as resolve2 } from "path";
4167
4167
  import { fileURLToPath } from "url";
4168
4168
 
4169
+ // src/media-protocol.ts
4170
+ var STREAM_PROTOCOL_VERSION = 3;
4171
+ var MEDIA_PACKET_HEADER_BYTES = 12;
4172
+ var CLIPBOARD_MAX_CHARS = 1e6;
4173
+
4169
4174
  // src/input-validation.ts
4170
4175
  var finite = (value) => typeof value === "number" && Number.isFinite(value);
4171
4176
  var optionalFinite = (value) => value === void 0 || finite(value);
@@ -4178,7 +4183,10 @@ function validInput(value) {
4178
4183
  case "keyframe":
4179
4184
  case "retry-webrtc":
4180
4185
  case "webrtc-ready":
4186
+ case "copy":
4181
4187
  return true;
4188
+ case "paste":
4189
+ return typeof value.text === "string" && value.text.length <= CLIPBOARD_MAX_CHARS;
4182
4190
  case "theme":
4183
4191
  return value.theme === "light" || value.theme === "dark";
4184
4192
  case "standby":
@@ -4216,10 +4224,6 @@ function parseInput(raw) {
4216
4224
  return validInput(value) ? value : void 0;
4217
4225
  }
4218
4226
 
4219
- // src/media-protocol.ts
4220
- var STREAM_PROTOCOL_VERSION = 3;
4221
- var MEDIA_PACKET_HEADER_BYTES = 12;
4222
-
4223
4227
  // src/pacer.ts
4224
4228
  import { performance } from "perf_hooks";
4225
4229
  var ringSize = 4096;
@@ -4850,9 +4854,14 @@ var InputBridge = class {
4850
4854
  latestPosition;
4851
4855
  pressedButtons = /* @__PURE__ */ new Set();
4852
4856
  pressedKeys = /* @__PURE__ */ new Set();
4857
+ /** Input that arrived while xclip was taking the clipboard, held so it lands after the paste. */
4858
+ queue;
4853
4859
  close() {
4854
4860
  if (this.closed) return;
4855
4861
  this.closed = true;
4862
+ const queue = this.queue;
4863
+ this.queue = void 0;
4864
+ if (queue) this.drain(queue);
4856
4865
  this.release();
4857
4866
  const child = this.process;
4858
4867
  this.process = void 0;
@@ -4937,8 +4946,53 @@ var InputBridge = class {
4937
4946
  }
4938
4947
  return;
4939
4948
  }
4949
+ if (message.type === "paste") {
4950
+ if (this.queue) this.queue.push(() => this.paste(message.text));
4951
+ else this.paste(message.text);
4952
+ return;
4953
+ }
4940
4954
  if (message.type === "release") this.release();
4941
4955
  }
4956
+ paste(text) {
4957
+ if (this.closed) return;
4958
+ if (!text) {
4959
+ this.write("keydown v");
4960
+ this.write("keyup v");
4961
+ return;
4962
+ }
4963
+ const queue = [];
4964
+ this.queue = queue;
4965
+ const done = () => {
4966
+ if (this.queue !== queue) return;
4967
+ this.queue = void 0;
4968
+ this.write("keydown v");
4969
+ this.write("keyup v");
4970
+ this.queue = queue;
4971
+ setTimeout(() => {
4972
+ if (this.queue !== queue) return;
4973
+ this.queue = void 0;
4974
+ this.drain(queue);
4975
+ }, 200);
4976
+ };
4977
+ const child = spawn6("xclip", ["-selection", "clipboard", "-i"], {
4978
+ env: { ...process.env, DISPLAY: this.display },
4979
+ stdio: ["pipe", "ignore", "ignore"]
4980
+ });
4981
+ child.stdin.on("error", () => {
4982
+ });
4983
+ child.once("error", done);
4984
+ child.once("exit", done);
4985
+ child.stdin.end(text);
4986
+ }
4987
+ drain(queue) {
4988
+ for (let run = queue.shift(); run; run = queue.shift()) {
4989
+ run();
4990
+ if (this.queue) {
4991
+ this.queue.push(...queue);
4992
+ return;
4993
+ }
4994
+ }
4995
+ }
4942
4996
  release() {
4943
4997
  if (this.pointerFlush) clearImmediate(this.pointerFlush);
4944
4998
  this.pointerFlush = void 0;
@@ -4964,6 +5018,10 @@ var InputBridge = class {
4964
5018
  this.pointer = void 0;
4965
5019
  }
4966
5020
  write(command) {
5021
+ if (this.queue) {
5022
+ this.queue.push(() => this.write(command));
5023
+ return;
5024
+ }
4967
5025
  if (!this.process || this.process.exitCode !== null || this.process.stdin.destroyed) {
4968
5026
  const child = spawn6("xte", [], {
4969
5027
  env: { ...process.env, DISPLAY: this.display },
@@ -5080,6 +5138,9 @@ var standbyRung = 5;
5080
5138
  var port = Number.parseInt(process.env.REPLICAS_DESKTOP_STREAM_PORT || String(DESKTOP_STREAM_PORT), 10);
5081
5139
  var display = process.env.REPLICAS_DESKTOP_DISPLAY?.replace(/^([^:])/, ":$1") || ":99";
5082
5140
  var webrtcEnabled = process.env.REPLICAS_DESKTOP_STREAM_WEBRTC !== "0";
5141
+ var readClipboard = () => new Promise((done) => {
5142
+ execFile("xclip", ["-selection", "clipboard", "-o"], { env: { ...process.env, DISPLAY: display }, timeout: 1e3, maxBuffer: CLIPBOARD_MAX_CHARS * 4 }, (error, stdout) => done(error ? "" : stdout));
5143
+ });
5083
5144
  var colorMode = process.env.REPLICAS_DESKTOP_STREAM_COLOR === "legacy" ? "legacy" : "bt709";
5084
5145
  var defaultRequest = parseStreamRequest(new URLSearchParams({
5085
5146
  transport: process.env.REPLICAS_DESKTOP_STREAM_TRANSPORT ?? "",
@@ -5865,6 +5926,17 @@ async function runMediaServer() {
5865
5926
  case "theme":
5866
5927
  applyDesktopTheme(message.theme);
5867
5928
  return;
5929
+ case "copy":
5930
+ void (async () => {
5931
+ const before = await readClipboard();
5932
+ let text = before;
5933
+ for (let attempt = 0; attempt < 10 && text === before; attempt += 1) {
5934
+ await new Promise((wait) => setTimeout(wait, 50));
5935
+ text = await readClipboard();
5936
+ }
5937
+ if (text) reply({ type: "clipboard", text });
5938
+ })();
5939
+ return;
5868
5940
  case "standby":
5869
5941
  if (viewer.standby === message.standby) return;
5870
5942
  viewer.standby = message.standby;
@@ -6011,7 +6083,7 @@ async function runMediaServer() {
6011
6083
  response.setHeader("cache-control", "private, max-age=31536000, immutable");
6012
6084
  createReadStream(path).pipe(response);
6013
6085
  });
6014
- const webSocketServer = new WebSocketServer({ noServer: true, maxPayload: 64 * 1024 });
6086
+ const webSocketServer = new WebSocketServer({ noServer: true, maxPayload: CLIPBOARD_MAX_CHARS * 8 });
6015
6087
  httpServer.on("upgrade", (request2, socket, head) => {
6016
6088
  const url = new URL(request2.url || "/", "http://localhost");
6017
6089
  if (url.pathname !== "/stream") {
@@ -6136,6 +6208,7 @@ var dependencies = [
6136
6208
  ["PulseAudio", "pulseaudio"],
6137
6209
  ["PulseAudio control", "pactl"],
6138
6210
  ["X automation", "xte"],
6211
+ ["xclip", "xclip"],
6139
6212
  ["x11vnc", "x11vnc"],
6140
6213
  ["websockify", "websockify"],
6141
6214
  ["Google Chrome", "google-chrome-stable"],
@@ -100,6 +100,11 @@ function isDesktopElement(value) {
100
100
  return isRecord(value) && isDesktopRect(value.rect) && ["url", "tag", "selector"].every((key) => typeof value[key] === "string") && ["text", "label", "source"].every((key) => value[key] === void 0 || typeof value[key] === "string") && (value.components === void 0 || Array.isArray(value.components) && value.components.every((name) => typeof name === "string"));
101
101
  }
102
102
 
103
+ // src/media-protocol.ts
104
+ var STREAM_PROTOCOL_VERSION = 3;
105
+ var MEDIA_PACKET_HEADER_BYTES = 12;
106
+ var CLIPBOARD_MAX_CHARS = 1e6;
107
+
103
108
  // src/input-validation.ts
104
109
  var finite = (value) => typeof value === "number" && Number.isFinite(value);
105
110
  function validServerMessage(value) {
@@ -110,6 +115,8 @@ function validServerMessage(value) {
110
115
  return finite(value.sentAt);
111
116
  case "webrtc-failed":
112
117
  return true;
118
+ case "clipboard":
119
+ return typeof value.text === "string";
113
120
  case "offer":
114
121
  return typeof value.sdp === "string" && value.sdp.length < 65536;
115
122
  case "candidate":
@@ -129,10 +136,6 @@ function wheelDeltaPixels(event, pageHeight) {
129
136
  return { deltaX: event.deltaX * scale, deltaY: event.deltaY * scale };
130
137
  }
131
138
 
132
- // src/media-protocol.ts
133
- var STREAM_PROTOCOL_VERSION = 3;
134
- var MEDIA_PACKET_HEADER_BYTES = 12;
135
-
136
139
  // src/stream-viewer.ts
137
140
  var screen = document.querySelector("#screen");
138
141
  var status = document.querySelector("#status");
@@ -285,7 +288,7 @@ var sendInput = (message, unreliable = false) => {
285
288
  pendingPointer = void 0;
286
289
  }
287
290
  }
288
- if (config?.inputRace && transport && (message.type === "button" || message.type === "wheel" || message.type === "key" || message.type === "release")) {
291
+ if (config?.inputRace && transport && (message.type === "button" || message.type === "wheel" || message.type === "key" || message.type === "release" || message.type === "paste")) {
289
292
  const ordered = socket?.readyState === WebSocket.OPEN ? socket : void 0;
290
293
  const reliable = transport === "webrtc" && inputChannel?.readyState === "open" ? inputChannel : void 0;
291
294
  const nextAction = actionId + 1;
@@ -302,7 +305,7 @@ var sendInput = (message, unreliable = false) => {
302
305
  const viaSocket = deliver(ordered);
303
306
  if (!viaSocket && !deliver(reliable)) return;
304
307
  actionId = nextAction;
305
- if (viaSocket && transport === "webrtc" && pointerChannel?.readyState === "open" && pointerChannel.bufferedAmount < 16384) {
308
+ if (viaSocket && message.type !== "paste" && transport === "webrtc" && pointerChannel?.readyState === "open" && pointerChannel.bufferedAmount < 16384) {
306
309
  deliver(pointerChannel);
307
310
  }
308
311
  return;
@@ -315,7 +318,7 @@ var sendInput = (message, unreliable = false) => {
315
318
  pointerChannel.send(JSON.stringify(message));
316
319
  return;
317
320
  }
318
- if (!unreliable && ["pointer", "button", "wheel", "key", "release"].includes(message.type) && socket?.readyState === WebSocket.OPEN) {
321
+ if (!unreliable && ["pointer", "button", "wheel", "key", "release", "paste"].includes(message.type) && socket?.readyState === WebSocket.OPEN) {
319
322
  sendSocket(message);
320
323
  return;
321
324
  }
@@ -801,7 +804,15 @@ var completeRetry = (attempt) => {
801
804
  announceTransport();
802
805
  unlockAudio();
803
806
  };
807
+ var syncedClipboard;
804
808
  var handleServerMessage = (message, viaSocket) => {
809
+ if (message.type === "clipboard") {
810
+ void navigator.clipboard?.writeText(message.text).then(() => {
811
+ syncedClipboard = message.text;
812
+ }, () => {
813
+ });
814
+ return;
815
+ }
805
816
  if (message.type === "pong") {
806
817
  const elapsed = performance.now() - message.sentAt;
807
818
  if ((!viaSocket || transport !== "webrtc") && message.sentAt > rttSentAt) {
@@ -1009,17 +1020,32 @@ screen.addEventListener("wheel", (event) => {
1009
1020
  send({ type: "wheel", ...normalizedPointer(event), ...wheelDeltaPixels(event, screen.clientHeight) });
1010
1021
  }, { passive: false });
1011
1022
  screen.addEventListener("contextmenu", (event) => event.preventDefault());
1023
+ var isMac = /Mac|iPhone|iPad/.test(navigator.platform);
1024
+ var desktopKey = (code) => isMac && code.startsWith("Meta") ? code.replace("Meta", "Control") : code;
1012
1025
  window.addEventListener("keydown", (event) => {
1013
1026
  if (!screen.matches(":focus")) return;
1014
- event.preventDefault();
1015
1027
  if (screen.muted || audioContext?.state !== "running") unlockAudio();
1016
- send({ type: "key", code: event.code, down: true, repeat: event.repeat });
1028
+ const shortcut = (event.ctrlKey || event.metaKey) && !event.altKey;
1029
+ if (shortcut && event.code === "KeyV") return;
1030
+ event.preventDefault();
1031
+ send({ type: "key", code: desktopKey(event.code), down: true, repeat: event.repeat });
1032
+ if (isMac && event.metaKey && !["Meta", "Control", "Shift", "Alt", "CapsLock"].includes(event.key)) send({ type: "key", code: event.code, down: false });
1033
+ if (shortcut && !event.repeat && (event.code === "KeyC" || event.code === "KeyX")) sendSocket({ type: "copy" });
1017
1034
  }, { capture: true });
1018
1035
  window.addEventListener("keyup", (event) => {
1019
1036
  if (!screen.matches(":focus")) return;
1020
1037
  event.preventDefault();
1021
- send({ type: "key", code: event.code, down: false });
1038
+ send({ type: "key", code: desktopKey(event.code), down: false });
1022
1039
  }, { capture: true });
1040
+ window.addEventListener("paste", (event) => {
1041
+ if (!screen.matches(":focus")) return;
1042
+ event.preventDefault();
1043
+ const text = event.clipboardData?.getData("text/plain") ?? "";
1044
+ if (text.length > CLIPBOARD_MAX_CHARS) return;
1045
+ const fresh = text !== syncedClipboard;
1046
+ if (fresh) syncedClipboard = text;
1047
+ send({ type: "paste", text: fresh ? text : "" });
1048
+ });
1023
1049
  window.addEventListener("blur", () => send({ type: "release" }));
1024
1050
  window.addEventListener("resize", fitScreen);
1025
1051
  var setStandby = (next) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "replicas-computer",
3
- "version": "0.3.33",
3
+ "version": "0.3.34",
4
4
  "description": "Replicas workspace computer-use runtime",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",