replicas-computer 0.3.26 → 0.3.28

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 CHANGED
@@ -15,7 +15,7 @@ The versioned computer-use runtime for Replicas workspace images.
15
15
  - `src/viewer.ts` owns the optional noVNC fallback.
16
16
  - `assets/desktop/start-services.sh` owns the X11 and stream process lifecycle.
17
17
  - `xsettingsd` broadcasts theme changes to running GTK apps and Chrome. Chrome follows the dashboard theme without reopening tabs; launch it without a `GTK_THEME` override.
18
- - `assets/desktop/` owns the desktop theme: `wallpapers/` (a light and dark JPEG per scene; each new desktop picks a different scene), `tint2rc` dock, `openbox-theme/`, `gtk-settings.ini`, the `replicas-terminal` wrapper, `themes/` palettes that `start-services.sh` renders into those templates (`--theme light|dark` switches a running desktop), and the bundled fonts (static TTF instances of General Sans and Geist Mono; variable and WOFF2 builds render with wrong metrics in Pango and VTE).
18
+ - `assets/desktop/` owns the desktop theme: `wallpapers/` (a light and dark JPEG per scene; each new desktop picks a different scene), `tint2rc` dock, `topbar-tint2rc` top bar (the `replicas-symbol.svg` logo and the clock), `openbox-theme/`, `gtk-settings.ini`, the `replicas-terminal` wrapper, `themes/` palettes that `start-services.sh` renders into those templates (`--theme light|dark` switches a running desktop), and the bundled fonts (static TTF instances of General Sans and Geist Mono; variable and WOFF2 builds render with wrong metrics in Pango and VTE).
19
19
 
20
20
  The viewer and agent command paths are independent. `start-services.sh` runs Xvfb, the compositor, PulseAudio and the stream server (and so the encoders it spawns) at nice −10 when `sudo` is available, so CPU load from the apps on the desktop slows those apps instead of stalling the picture of them. One on-demand encoder pair is shared by all viewers, stops when the last viewer disconnects, and restarts without reloading the page. A viewer out of sight (the dashboard keeps one connected while the Desktop tab is hidden) reports `standby`, detected with an `IntersectionObserver` because a hidden iframe keeps its last layout size; while every viewer is in standby the encoder runs at 5 fps on the 1.5 Mbps rung, and the first viewer to become visible returns it to full rate in place.
21
21
 
@@ -0,0 +1,3 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="@BRAND@" shape-rendering="crispEdges">
2
+ <path d="M3 0h12v6H3zm12 6h6v6h-6zM3 12h12v6H3zm0 6h6v6H3zm12 0h6v6h-6z"/>
3
+ </svg>
@@ -336,6 +336,8 @@ mkdir -p \
336
336
  "$HOME/.icons/default"
337
337
 
338
338
  [ -f "$REPLICAS_DESKTOP_TEMPLATES/tint2rc" ] && render_template "$REPLICAS_DESKTOP_TEMPLATES/tint2rc" "$HOME/.config/tint2/tint2rc"
339
+ [ -f "$REPLICAS_DESKTOP_TEMPLATES/topbar-tint2rc" ] && render_template "$REPLICAS_DESKTOP_TEMPLATES/topbar-tint2rc" "$HOME/.config/tint2/topbar-tint2rc"
340
+ [ -f "$REPLICAS_DESKTOP_TEMPLATES/replicas-symbol.svg" ] && render_template "$REPLICAS_DESKTOP_TEMPLATES/replicas-symbol.svg" "$HOME/.config/tint2/replicas-symbol.svg"
339
341
  [ -f "$REPLICAS_DESKTOP_TEMPLATES/openbox-rc.xml" ] && cp "$REPLICAS_DESKTOP_TEMPLATES/openbox-rc.xml" "$HOME/.config/openbox/rc.xml"
340
342
  [ -f "$REPLICAS_DESKTOP_TEMPLATES/picom.conf" ] && cp "$REPLICAS_DESKTOP_TEMPLATES/picom.conf" "$HOME/.config/picom/picom.conf"
341
343
  [ -f "$REPLICAS_DESKTOP_TEMPLATES/gtk-settings.ini" ] && render_template "$REPLICAS_DESKTOP_TEMPLATES/gtk-settings.ini" "$HOME/.config/gtk-3.0/settings.ini"
@@ -395,14 +397,17 @@ paint_wallpaper
395
397
 
396
398
  if [ -n "$REQUESTED_THEME" ] && [ "$THEME" != "$PREVIOUS_THEME" ]; then
397
399
  DISPLAY=":$DISPLAY_NUM" openbox --reconfigure >/dev/null 2>&1 || true
398
- kill_pids "$(cat "$STATE_DIR/tint2.pid" 2>/dev/null)"
399
- rm -f "$STATE_DIR/tint2.pid"
400
+ kill_pids "$(cat "$STATE_DIR/tint2.pid" "$STATE_DIR/topbar.pid" 2>/dev/null)"
401
+ rm -f "$STATE_DIR/tint2.pid" "$STATE_DIR/topbar.pid"
400
402
  fi
401
403
 
402
404
  # tint2 panel anchors window stacking on bare openbox.
403
405
  start_bg tint2 \
404
406
  "false" \
405
407
  "DISPLAY=:$DISPLAY_NUM exec tint2"
408
+ start_bg topbar \
409
+ "false" \
410
+ "DISPLAY=:$DISPLAY_NUM exec tint2 -c $HOME/.config/tint2/topbar-tint2rc"
406
411
 
407
412
  if [ "$COMPOSITOR" = "1" ]; then
408
413
  start_bg picom \
@@ -12,3 +12,4 @@ INK_DISABLED='#5c5c5c'
12
12
  LINE='#ffffff'
13
13
  ACCENT='#3cba85'
14
14
  ACCENT_INK='#3cba85'
15
+ BRAND='#3eeba3'
@@ -12,3 +12,4 @@ INK_DISABLED='#b9c1b8'
12
12
  LINE='#162118'
13
13
  ACCENT='#2fc487'
14
14
  ACCENT_INK='#2f9d57'
15
+ BRAND='#2f9d57'
@@ -47,7 +47,7 @@ border_width = 1
47
47
  background_color = @SURFACE_ALT@ 96
48
48
  border_color = @LINE@ 16
49
49
 
50
- panel_items = LT:C
50
+ panel_items = LT
51
51
  panel_size = 100% 72
52
52
  panel_margin = 0 12
53
53
  panel_padding = 10 7 6
@@ -103,22 +103,6 @@ task_active_icon_asb = 100 0 0
103
103
  task_iconified_icon_asb = 55 0 0
104
104
  urgent_nb_of_blink = 8
105
105
 
106
- separator = new
107
- separator_background_id = 0
108
- separator_color = @LINE@ 18
109
- separator_style = line
110
- separator_size = 1
111
- separator_padding = 6 14
112
-
113
- time1_format = %H:%M
114
- time2_format = %a %-d %b
115
- time1_font = General Sans Semibold 12
116
- time2_font = Geist Mono 8
117
- clock_font_color = @INK@ 100
118
- clock_padding = 12 0
119
- clock_background_id = 0
120
- clock_tooltip = %A, %B %-d
121
-
122
106
  tooltip_padding = 10 6
123
107
  tooltip_show_timeout = 0.4
124
108
  tooltip_hide_timeout = 0.1
@@ -0,0 +1,33 @@
1
+ rounded = 0
2
+ border_width = 1
3
+ border_sides = B
4
+ background_color = @SURFACE@ 100
5
+ border_color = @LINE@ 10
6
+
7
+ panel_items = PFC
8
+ panel_size = 100% 36
9
+ panel_margin = 0 0
10
+ panel_padding = 0 0 0
11
+ panel_background_id = 1
12
+ panel_position = top center horizontal
13
+ panel_layer = top
14
+ panel_monitor = all
15
+ panel_window_name = tint2-topbar
16
+ disable_transparency = 1
17
+ strut_policy = follow_size
18
+ wm_menu = 0
19
+ font_shadow = 0
20
+ mouse_effects = 0
21
+
22
+ button = new
23
+ button_icon = ~/.config/tint2/replicas-symbol.svg
24
+ button_padding = 13 0 0
25
+ button_background_id = 0
26
+ button_centered = 0
27
+ button_max_icon_size = 24
28
+
29
+ time1_format = %a %-d %b %H:%M
30
+ time1_font = General Sans 11
31
+ clock_font_color = @INK@ 100
32
+ clock_padding = 16 0
33
+ clock_background_id = 0
package/dist/index.js CHANGED
@@ -1,12 +1,12 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/client.ts
4
- import { createConnection as createConnection2 } from "net";
4
+ import { createConnection as createConnection3 } from "net";
5
5
 
6
6
  // src/daemon.ts
7
7
  import { spawnSync as spawnSync5 } from "child_process";
8
- import { chmodSync, existsSync as existsSync5, mkdirSync as mkdirSync5, rmSync as rmSync4 } from "fs";
9
- import { createConnection, createServer } from "net";
8
+ import { chmodSync, existsSync as existsSync6, mkdirSync as mkdirSync6, rmSync as rmSync5 } from "fs";
9
+ import { createConnection as createConnection2, createServer as createServer2 } from "net";
10
10
  import { dirname as dirname5 } from "path";
11
11
 
12
12
  // src/commands/desktop.ts
@@ -14,8 +14,23 @@ import { spawnSync } from "child_process";
14
14
  import { existsSync, mkdirSync, readFileSync } from "fs";
15
15
  import { dirname, isAbsolute, resolve } from "path";
16
16
 
17
+ // ../shared/src/type-guards.ts
18
+ function isRecord(value) {
19
+ return typeof value === "object" && value !== null && !Array.isArray(value);
20
+ }
21
+
17
22
  // ../shared/src/engine/environment.ts
18
23
  var DESKTOP_STREAM_PORT = 6080;
24
+ var DEVICES_DIR = ".ssh/replicas-devices";
25
+ var DEVICE_SCREEN_FILE_SUFFIX = ".screen.json";
26
+ function parseDeviceScreenState(value) {
27
+ if (!isRecord(value)) return null;
28
+ if (value.state === "connected" && typeof value.width === "number" && typeof value.height === "number") {
29
+ return { state: "connected", width: value.width, height: value.height };
30
+ }
31
+ if (value.state === "failed" && typeof value.reason === "string") return { state: "failed", reason: value.reason };
32
+ return value.state === "connecting" ? { state: "connecting" } : null;
33
+ }
19
34
  function getDesktopViewerUrl(publicUrl) {
20
35
  const url = new URL(`${publicUrl.replace(/\/$/, "")}/`);
21
36
  url.searchParams.set("v", "15");
@@ -31,6 +46,7 @@ var STREAM_PORT = process.env.REPLICAS_DESKTOP_STREAM_PORT ? parseInt(process.en
31
46
  var SERVICES_SCRIPT = process.env.REPLICAS_COMPUTER_SERVICES_SCRIPT || "/usr/local/bin/replicas-start-desktop-services";
32
47
  var INPUT_LOCK_FILE = process.env.REPLICAS_DESKTOP_INPUT_LOCK_FILE || `${STATE_DIR}/input.lock`;
33
48
  var CHROME_WRAPPER = "/usr/local/bin/replicas-chrome";
49
+ var NOVNC_VIEWER_ROOT = "/usr/local/share/replicas/desktop/novnc";
34
50
  var TERMINAL_WRAPPER = "/usr/local/bin/replicas-terminal";
35
51
  var INFO_WAIT_TIMEOUT_MS = 1e4;
36
52
  var INFO_WAIT_INTERVAL_MS = 500;
@@ -58,7 +74,7 @@ function getDesktopBridgeStatus() {
58
74
  }
59
75
  function desktopStackHealthy() {
60
76
  const pids = {};
61
- for (const name of ["openbox", "tint2", "stream"]) {
77
+ for (const name of ["openbox", "tint2", "topbar", "stream"]) {
62
78
  try {
63
79
  const pid = Number.parseInt(readFileSync(`${STATE_DIR}/${name}.pid`, "utf8").trim(), 10);
64
80
  if (!Number.isFinite(pid)) return false;
@@ -1111,11 +1127,6 @@ async function computerRecordStopCommand() {
1111
1127
  clearRecordingState();
1112
1128
  }
1113
1129
 
1114
- // ../shared/src/type-guards.ts
1115
- function isRecord(value) {
1116
- return typeof value === "object" && value !== null && !Array.isArray(value);
1117
- }
1118
-
1119
1130
  // src/lib/chrome-session.ts
1120
1131
  async function openChromeSession(webSocketDebuggerUrl) {
1121
1132
  const socket = new WebSocket(webSocketDebuggerUrl);
@@ -2996,11 +3007,193 @@ async function computerDragCommand(fx, fy, tx, ty) {
2996
3007
  console.log(`dragged (${fromX},${fromY}) -> (${toX},${toY})`);
2997
3008
  }
2998
3009
 
2999
- // src/commands/system.ts
3000
- import { spawnSync as spawnSync4 } from "child_process";
3001
- import { readFileSync as readFileSync5 } from "fs";
3010
+ // src/commands/screen.ts
3011
+ import { spawn as spawn3 } from "child_process";
3012
+ import { closeSync, existsSync as existsSync5, mkdirSync as mkdirSync5, openSync, readdirSync as readdirSync2, readFileSync as readFileSync5, rmSync as rmSync4, symlinkSync, writeFileSync as writeFileSync5 } from "fs";
3013
+ import { createConnection, createServer } from "net";
3002
3014
  import { homedir } from "os";
3003
3015
  import { join } from "path";
3016
+ import { setTimeout as sleep2 } from "timers/promises";
3017
+ var SCREEN_PORT = 5900;
3018
+ var CONNECT_TIMEOUT_MS = 3e4;
3019
+ var devicesDir = join(homedir(), DEVICES_DIR);
3020
+ function ownsProcess(session, pid) {
3021
+ try {
3022
+ return readFileSync5(`/proc/${pid}/cmdline`, "utf8").split("\0").some((arg) => arg.includes(`127.0.0.1:${session.tunnelPort}`));
3023
+ } catch {
3024
+ return false;
3025
+ }
3026
+ }
3027
+ function isListening(port2) {
3028
+ return new Promise((resolve5) => {
3029
+ const socket = createConnection({ host: "127.0.0.1", port: port2 });
3030
+ const done = (listening) => {
3031
+ socket.destroy();
3032
+ resolve5(listening);
3033
+ };
3034
+ socket.once("connect", () => done(true));
3035
+ socket.once("error", () => done(false));
3036
+ socket.setTimeout(1e3, () => done(false));
3037
+ });
3038
+ }
3039
+ async function freePorts(count) {
3040
+ const servers = await Promise.all(Array.from({ length: count }, () => new Promise((resolve5, reject) => {
3041
+ const server = createServer().once("error", reject);
3042
+ server.listen(0, "127.0.0.1", () => resolve5(server));
3043
+ })));
3044
+ const ports = servers.map((server) => {
3045
+ const address = server.address();
3046
+ return typeof address === "object" && address ? address.port : fail("Could not reserve a local port");
3047
+ });
3048
+ await Promise.all(servers.map((server) => new Promise((resolve5) => server.close(resolve5))));
3049
+ return ports;
3050
+ }
3051
+ function spawnLogged(bin, args, log) {
3052
+ const fd = openSync(log, "a");
3053
+ const child = spawn3(bin, args, { detached: true, stdio: ["ignore", fd, fd] });
3054
+ closeSync(fd);
3055
+ child.unref();
3056
+ let exited = false;
3057
+ child.once("exit", () => {
3058
+ exited = true;
3059
+ });
3060
+ return { pid: child.pid ?? fail(`Could not start ${bin}`), exited: () => exited };
3061
+ }
3062
+ async function waitForPort(port2, child) {
3063
+ const deadline = Date.now() + CONNECT_TIMEOUT_MS;
3064
+ while (Date.now() < deadline && !child.exited()) {
3065
+ if (await isListening(port2)) return true;
3066
+ await sleep2(200);
3067
+ }
3068
+ return false;
3069
+ }
3070
+ function readBanner(port2) {
3071
+ return new Promise((resolve5) => {
3072
+ const socket = createConnection({ host: "127.0.0.1", port: port2 });
3073
+ let banner = "";
3074
+ const done = () => {
3075
+ socket.destroy();
3076
+ resolve5(banner);
3077
+ };
3078
+ socket.setTimeout(1e4, done);
3079
+ socket.on("data", (chunk) => {
3080
+ banner += chunk.toString("latin1");
3081
+ if (banner.length >= 12) done();
3082
+ });
3083
+ socket.once("error", done);
3084
+ socket.once("close", done);
3085
+ });
3086
+ }
3087
+ function readSession(file) {
3088
+ try {
3089
+ const value = JSON.parse(readFileSync5(file, "utf8"));
3090
+ return isRecord(value) && typeof value.webPort === "number" && typeof value.tunnelPort === "number" && Array.isArray(value.pids) && value.pids.every((pid) => typeof pid === "number") ? { webPort: value.webPort, tunnelPort: value.tunnelPort, pids: value.pids } : null;
3091
+ } catch {
3092
+ return null;
3093
+ }
3094
+ }
3095
+ async function closeScreen(dir, session) {
3096
+ if (session) {
3097
+ const pages = await getChromePages().catch(() => []);
3098
+ for (const page of pages) {
3099
+ if (page.webSocketDebuggerUrl && page.url?.startsWith(`http://127.0.0.1:${session.webPort}/`)) {
3100
+ await sendChromeCommand(page.webSocketDebuggerUrl, "Page.close", {}).catch(() => void 0);
3101
+ }
3102
+ }
3103
+ for (const pid of session.pids.filter((candidate) => ownsProcess(session, candidate))) {
3104
+ try {
3105
+ process.kill(pid);
3106
+ } catch {
3107
+ }
3108
+ }
3109
+ }
3110
+ rmSync4(dir, { recursive: true, force: true });
3111
+ }
3112
+ async function openSession(device, dir, previous) {
3113
+ await closeScreen(dir, previous);
3114
+ const web = join(dir, "web");
3115
+ mkdirSync5(web, { recursive: true, mode: 448 });
3116
+ for (const entry of readdirSync2(NOVNC_VIEWER_ROOT)) symlinkSync(join(NOVNC_VIEWER_ROOT, entry), join(web, entry));
3117
+ const credentials = join(devicesDir, `${device}${DEVICE_SCREEN_FILE_SUFFIX}`);
3118
+ if (existsSync5(credentials)) symlinkSync(credentials, join(web, "credentials.json"));
3119
+ const [tunnelPort, webPort] = await freePorts(2);
3120
+ const tunnelLog = join(dir, "tunnel.log");
3121
+ const tunnel = spawnLogged("ssh", ["-N", "-o", "BatchMode=yes", "-o", "ExitOnForwardFailure=yes", "-L", `127.0.0.1:${tunnelPort}:localhost:${SCREEN_PORT}`, device], tunnelLog);
3122
+ const pids = [tunnel.pid];
3123
+ const abort = (message) => {
3124
+ for (const pid of pids) {
3125
+ try {
3126
+ process.kill(pid);
3127
+ } catch {
3128
+ }
3129
+ }
3130
+ return fail(message);
3131
+ };
3132
+ if (!await waitForPort(tunnelPort, tunnel)) abort(`Could not connect to ${device} over SSH: ${readFileSync5(tunnelLog, "utf8").trim() || "timed out"}`);
3133
+ if (!(await readBanner(tunnelPort)).startsWith("RFB ")) {
3134
+ abort(`${device} isn't sharing its screen: nothing answered on port ${SCREEN_PORT}. On a Mac, turn on Screen Sharing in System Settings \u2192 General \u2192 Sharing.`);
3135
+ }
3136
+ const bridgeLog = join(dir, "bridge.log");
3137
+ const bridge = spawnLogged("websockify", ["--web", web, `127.0.0.1:${webPort}`, `127.0.0.1:${tunnelPort}`], bridgeLog);
3138
+ pids.push(bridge.pid);
3139
+ if (!await waitForPort(webPort, bridge)) abort(`Could not start the screen viewer: ${readFileSync5(bridgeLog, "utf8").trim() || "timed out"}`);
3140
+ const session = { webPort, tunnelPort, pids };
3141
+ writeFileSync5(join(dir, "session.json"), JSON.stringify(session));
3142
+ return session;
3143
+ }
3144
+ async function computerScreenCommand(device, options) {
3145
+ if (!/^[a-z0-9-]+$/.test(device)) fail(`Invalid device name: ${device}`);
3146
+ const dir = join(STATE_DIR, "screens", device);
3147
+ const previous = readSession(join(dir, "session.json"));
3148
+ if (options.close) {
3149
+ await closeScreen(dir, previous);
3150
+ console.log(`Closed ${device}'s screen`);
3151
+ return;
3152
+ }
3153
+ if (!existsSync5(join(devicesDir, device))) {
3154
+ const names = existsSync5(devicesDir) ? readdirSync2(devicesDir).filter((name) => name !== "devices.conf" && !name.endsWith(DEVICE_SCREEN_FILE_SUFFIX)) : [];
3155
+ fail(names.length > 0 ? `No device named ${device}. Devices: ${names.join(", ")}.` : "This workspace has no devices. Add them in the environment's Devices settings.");
3156
+ }
3157
+ ensureServicesRunning();
3158
+ const session = previous && previous.pids.every((pid) => ownsProcess(previous, pid)) && await isListening(previous.webPort) ? previous : await openSession(device, dir, previous);
3159
+ try {
3160
+ await showScreen(device, session);
3161
+ } catch (error) {
3162
+ await closeScreen(dir, session);
3163
+ throw error;
3164
+ }
3165
+ }
3166
+ async function showScreen(device, session) {
3167
+ const origin = `http://127.0.0.1:${session.webPort}/`;
3168
+ const findPage = async () => (await getChromePages().catch(() => [])).find((page2) => page2.webSocketDebuggerUrl && page2.url?.startsWith(origin));
3169
+ let page = await findPage();
3170
+ if (!page) {
3171
+ spawn3(CHROME_WRAPPER, ["--new-window", `--app=${origin}?device=${device}&requestFps=0`], { env: withDisplay(), detached: true, stdio: "ignore" }).unref();
3172
+ const deadline2 = Date.now() + CONNECT_TIMEOUT_MS;
3173
+ while (!page && Date.now() < deadline2) {
3174
+ await sleep2(250);
3175
+ page = await findPage();
3176
+ }
3177
+ }
3178
+ const target = page?.webSocketDebuggerUrl ?? fail("Chrome did not open the screen viewer");
3179
+ let state = { state: "connecting" };
3180
+ const deadline = Date.now() + CONNECT_TIMEOUT_MS;
3181
+ while (state.state === "connecting" && Date.now() < deadline) {
3182
+ state = parseDeviceScreenState(await evaluateChromeTarget(target, "globalThis.__replicasScreen ?? null").catch(() => null)) ?? state;
3183
+ if (state.state === "connecting") await sleep2(250);
3184
+ }
3185
+ if (state.state !== "connected") {
3186
+ fail(state.state === "failed" ? state.reason : `${device}'s screen didn't connect within ${CONNECT_TIMEOUT_MS / 1e3} seconds.`);
3187
+ }
3188
+ await sendChromeCommand(target, "Page.bringToFront", {});
3189
+ console.log(`Showing ${device}'s screen (${state.width}\xD7${state.height}) in a window on this desktop. Drive it with \`replicas computer observe\`, \`click\`, \`type\` and \`key\`; \`replicas computer screen ${device} --close\` closes it.`);
3190
+ }
3191
+
3192
+ // src/commands/system.ts
3193
+ import { spawnSync as spawnSync4 } from "child_process";
3194
+ import { readFileSync as readFileSync6 } from "fs";
3195
+ import { homedir as homedir2 } from "os";
3196
+ import { join as join2 } from "path";
3004
3197
  import chalk from "chalk";
3005
3198
  function bridgeStatus(details, includeBacklog = false, rootsOnly = false) {
3006
3199
  const pids = rootsOnly ? details.rootPids ?? [] : details.listenerPids ?? [];
@@ -3014,7 +3207,7 @@ function bridgeStatus(details, includeBacklog = false, rootsOnly = false) {
3014
3207
  }
3015
3208
  async function lookupDesktopViewerUrl() {
3016
3209
  try {
3017
- const value = JSON.parse(readFileSync5(join(homedir(), ".replicas", "preview-ports.json"), "utf8"));
3210
+ const value = JSON.parse(readFileSync6(join2(homedir2(), ".replicas", "preview-ports.json"), "utf8"));
3018
3211
  if (!isRecord(value) || !Array.isArray(value.previews)) return null;
3019
3212
  const preview = value.previews.find(
3020
3213
  (item) => isRecord(item) && item.port === STREAM_PORT && typeof item.publicUrl === "string"
@@ -3050,7 +3243,8 @@ async function computerStatusCommand() {
3050
3243
  ["Xvfb", "Xvfb"],
3051
3244
  ["openbox", "openbox"],
3052
3245
  ["XSETTINGS", "xsettingsd"],
3053
- ["tint2", "tint2"],
3246
+ ["dock", "^tint2$"],
3247
+ ["top bar", "tint2 -c .*topbar"],
3054
3248
  ["PulseAudio", "pulseaudio"],
3055
3249
  ["desktop stream", "replicas-computer.*stream"]
3056
3250
  ];
@@ -3069,7 +3263,7 @@ async function computerStatusCommand() {
3069
3263
  }
3070
3264
 
3071
3265
  // src/version.ts
3072
- var COMPUTER_USE_VERSION = "0.3.26";
3266
+ var COMPUTER_USE_VERSION = "0.3.28";
3073
3267
  var COMPUTER_USE_PROTOCOL_VERSION = 1;
3074
3268
 
3075
3269
  // src/program.ts
@@ -3107,6 +3301,7 @@ function createProgram() {
3107
3301
  program.command("scroll <direction>").description("Scroll the desktop").option("--amount <n>", "Wheel ticks").option("--x <x>", "Pointer x").option("--y <y>", "Pointer y").action((direction, options) => computerScrollCommand(direction, options));
3108
3302
  program.command("drag <fromX> <fromY> <toX> <toY>").description("Drag between desktop coordinates").action((fromX, fromY, toX, toY) => computerDragCommand(fromX, fromY, toX, toY));
3109
3303
  program.command("launch <app> [args...]").description("Launch an application on the desktop").action((app, args) => computerLaunchCommand(app, args));
3304
+ program.command("screen <device>", { hidden: true }).description("Show a device's screen in a window on this desktop, where the other commands drive it").option("--close", "Close the window and its connection").action((device, options) => computerScreenCommand(device, options));
3110
3305
  const record = program.command("record").description("Record the workspace display");
3111
3306
  record.command("start <path>").description("Start a recording").option("--fps <n>", "Frame rate. Default 60.").action((path, options) => computerRecordStartCommand(path, options));
3112
3307
  record.command("stop").description("Stop and finalize the active recording").action(() => computerRecordStopCommand());
@@ -3158,9 +3353,9 @@ function writeResult(socket, result) {
3158
3353
  `);
3159
3354
  }
3160
3355
  async function hasSocketListener() {
3161
- if (!existsSync5(COMPUTER_USE_SOCKET_PATH)) return false;
3356
+ if (!existsSync6(COMPUTER_USE_SOCKET_PATH)) return false;
3162
3357
  return await new Promise((resolve5) => {
3163
- const socket = createConnection(COMPUTER_USE_SOCKET_PATH);
3358
+ const socket = createConnection2(COMPUTER_USE_SOCKET_PATH);
3164
3359
  socket.setTimeout(500, () => socket.destroy());
3165
3360
  socket.once("connect", () => {
3166
3361
  socket.destroy();
@@ -3173,11 +3368,11 @@ async function hasSocketListener() {
3173
3368
  async function runComputerDaemon() {
3174
3369
  if (await hasSocketListener()) throw new Error(`Computer-use daemon is already listening on ${COMPUTER_USE_SOCKET_PATH}`);
3175
3370
  ensureServicesRunning();
3176
- mkdirSync5(dirname5(COMPUTER_USE_SOCKET_PATH), { recursive: true, mode: 448 });
3177
- rmSync4(COMPUTER_USE_SOCKET_PATH, { force: true });
3371
+ mkdirSync6(dirname5(COMPUTER_USE_SOCKET_PATH), { recursive: true, mode: 448 });
3372
+ rmSync5(COMPUTER_USE_SOCKET_PATH, { force: true });
3178
3373
  let queue = Promise.resolve();
3179
3374
  const clients = /* @__PURE__ */ new Set();
3180
- const server = createServer({ allowHalfOpen: true }, (socket) => {
3375
+ const server = createServer2({ allowHalfOpen: true }, (socket) => {
3181
3376
  if (clients.size >= MAX_CLIENTS) {
3182
3377
  writeResult(socket, { exitCode: 75, stdout: "", stderr: "Computer-use daemon is busy; retry the request\n" });
3183
3378
  return;
@@ -3250,7 +3445,7 @@ async function runComputerDaemon() {
3250
3445
  process.once("SIGINT", close);
3251
3446
  });
3252
3447
  spawnSync5("bash", [SERVICES_SCRIPT, "--stop"], { stdio: "inherit" });
3253
- rmSync4(COMPUTER_USE_SOCKET_PATH, { force: true });
3448
+ rmSync5(COMPUTER_USE_SOCKET_PATH, { force: true });
3254
3449
  }
3255
3450
 
3256
3451
  // src/client.ts
@@ -3259,7 +3454,7 @@ var ComputerDaemonUnavailableError = class extends Error {
3259
3454
  };
3260
3455
  function sendComputerRequest(argv) {
3261
3456
  return new Promise((resolve5, reject) => {
3262
- const socket = createConnection2(COMPUTER_USE_SOCKET_PATH);
3457
+ const socket = createConnection3(COMPUTER_USE_SOCKET_PATH);
3263
3458
  let response = "";
3264
3459
  let settled = false;
3265
3460
  const finish = (error, result) => {
@@ -3310,17 +3505,17 @@ async function runComputerClient(argv) {
3310
3505
  }
3311
3506
 
3312
3507
  // src/media-server.ts
3313
- import { spawn as spawn5 } from "child_process";
3508
+ import { spawn as spawn6 } from "child_process";
3314
3509
  import { createHash as createHash3 } from "crypto";
3315
- import { createReadStream, existsSync as existsSync7, readdirSync as readdirSync2, readFileSync as readFileSync7, statSync as statSync2 } from "fs";
3316
- import { createServer as createServer2 } from "http";
3510
+ import { createReadStream, existsSync as existsSync8, readdirSync as readdirSync3, readFileSync as readFileSync8, statSync as statSync2 } from "fs";
3511
+ import { createServer as createServer3 } from "http";
3317
3512
  import { availableParallelism as availableParallelism2, loadavg } from "os";
3318
3513
  import { dirname as dirname7, extname, resolve as resolve3 } from "path";
3319
3514
  import { fileURLToPath as fileURLToPath2 } from "url";
3320
3515
  import WebSocket2, { WebSocketServer } from "ws";
3321
3516
 
3322
3517
  // src/encoder.ts
3323
- import { spawn as spawn3 } from "child_process";
3518
+ import { spawn as spawn4 } from "child_process";
3324
3519
  import { createSocket } from "dgram";
3325
3520
  import { createInterface } from "readline";
3326
3521
 
@@ -3555,7 +3750,7 @@ function superviseEncoder(options) {
3555
3750
  const warmupFrames = active ? options.warmupFrames ?? 0 : 0;
3556
3751
  const command = options.command?.() ?? "ffmpeg";
3557
3752
  const args = options.args(socket.address().port, warmupFrames);
3558
- const child = spawn3(command, args, { env: options.env, stdio: [options.controllable?.(command) ? "pipe" : "ignore", options.onOutput ? "pipe" : "ignore", "pipe"] });
3753
+ const child = spawn4(command, args, { env: options.env, stdio: [options.controllable?.(command) ? "pipe" : "ignore", options.onOutput ? "pipe" : "ignore", "pipe"] });
3559
3754
  child.stdin?.on("error", () => {
3560
3755
  });
3561
3756
  child.stderr?.on("data", (chunk) => process.stderr.write(`[desktop-stream] ${chunk}`));
@@ -3657,7 +3852,7 @@ function superviseEncoder(options) {
3657
3852
  }
3658
3853
 
3659
3854
  // src/ice-servers.ts
3660
- import { readFileSync as readFileSync6 } from "fs";
3855
+ import { readFileSync as readFileSync7 } from "fs";
3661
3856
 
3662
3857
  // ../shared/src/desktop-stream.ts
3663
3858
  function isStreamIceServer(value) {
@@ -3802,7 +3997,7 @@ var refreshing;
3802
3997
  async function currentIceServers() {
3803
3998
  let env = process.env;
3804
3999
  try {
3805
- env = { ...env, ...parsePosixEnvFile(readFileSync6(runtimeEnvFile, "utf8")) };
4000
+ env = { ...env, ...parsePosixEnvFile(readFileSync7(runtimeEnvFile, "utf8")) };
3806
4001
  } catch {
3807
4002
  }
3808
4003
  const monolithUrl = readReplicasRuntimeEnv((name) => env[name], REPLICAS_RUNTIME_ENV_ALIASES.monolithUrl);
@@ -3856,7 +4051,7 @@ function toIceServers(spec = "stun:stun.l.google.com:19302", relayServers) {
3856
4051
  }
3857
4052
 
3858
4053
  // src/video-encoder.ts
3859
- import { existsSync as existsSync6 } from "fs";
4054
+ import { existsSync as existsSync7 } from "fs";
3860
4055
  import { availableParallelism } from "os";
3861
4056
  import { dirname as dirname6, resolve as resolve2 } from "path";
3862
4057
  import { fileURLToPath } from "url";
@@ -4305,7 +4500,7 @@ var captureBinary = resolve2(dirname6(fileURLToPath(import.meta.url)), "..", "di
4305
4500
  function superviseVideoEncoder(options) {
4306
4501
  const { display: display2, width: width2, height: height2, colorMode: colorMode2 } = options;
4307
4502
  const env = { ...process.env, DISPLAY: display2 };
4308
- let nativeCapture = process.env.REPLICAS_DESKTOP_STREAM_CAPTURE !== "ffmpeg" && existsSync6(captureBinary);
4503
+ let nativeCapture = process.env.REPLICAS_DESKTOP_STREAM_CAPTURE !== "ffmpeg" && existsSync7(captureBinary);
4309
4504
  let captureMetrics;
4310
4505
  const encoder = superviseEncoder({
4311
4506
  label: "video",
@@ -4479,7 +4674,7 @@ function superviseVideoEncoder(options) {
4479
4674
  }
4480
4675
 
4481
4676
  // src/input-bridge.ts
4482
- import { spawn as spawn4 } from "child_process";
4677
+ import { spawn as spawn5 } from "child_process";
4483
4678
  var scrollPixelsPerStep = 80;
4484
4679
  var namedKeys = {
4485
4680
  AltLeft: "Alt_L",
@@ -4660,7 +4855,7 @@ var InputBridge = class {
4660
4855
  }
4661
4856
  write(command) {
4662
4857
  if (!this.process || this.process.exitCode !== null || this.process.stdin.destroyed) {
4663
- const child = spawn4("xte", [], {
4858
+ const child = spawn5("xte", [], {
4664
4859
  env: { ...process.env, DISPLAY: this.display },
4665
4860
  stdio: ["pipe", "pipe", "pipe"]
4666
4861
  });
@@ -4761,7 +4956,7 @@ function drainDesktopTheme() {
4761
4956
  if (applied) appliedTheme = theme;
4762
4957
  drainDesktopTheme();
4763
4958
  };
4764
- const child = spawn5("bash", [SERVICES_SCRIPT, "--theme", theme], { detached: true, stdio: "ignore" });
4959
+ const child = spawn6("bash", [SERVICES_SCRIPT, "--theme", theme], { detached: true, stdio: "ignore" });
4765
4960
  child.unref();
4766
4961
  child.once("exit", (code) => finish(code === 0));
4767
4962
  child.once("error", () => finish(false));
@@ -4769,7 +4964,7 @@ function drainDesktopTheme() {
4769
4964
  function processCpuMs(pid) {
4770
4965
  if (!pid) return 0;
4771
4966
  try {
4772
- const stat = readFileSync7(`/proc/${pid}/stat`, "utf8");
4967
+ const stat = readFileSync8(`/proc/${pid}/stat`, "utf8");
4773
4968
  const fields = stat.slice(stat.lastIndexOf(")") + 2).split(" ");
4774
4969
  return (Number(fields[11]) + Number(fields[12])) * 10;
4775
4970
  } catch {
@@ -5497,7 +5692,7 @@ async function runMediaServer() {
5497
5692
  audioSampleRate: 48e3,
5498
5693
  audioChannels: 2
5499
5694
  };
5500
- const httpServer = createServer2((request, response) => {
5695
+ const httpServer = createServer3((request, response) => {
5501
5696
  const url = new URL(request.url || "/", "http://localhost");
5502
5697
  if (url.pathname === "/health" || url.pathname === "/telemetry") {
5503
5698
  const all = [...viewers.values()];
@@ -5551,7 +5746,7 @@ async function runMediaServer() {
5551
5746
  return;
5552
5747
  }
5553
5748
  if (url.pathname === "/snapshot") {
5554
- const capture = spawn5("ffmpeg", [
5749
+ const capture = spawn6("ffmpeg", [
5555
5750
  "-hide_banner",
5556
5751
  "-loglevel",
5557
5752
  "error",
@@ -5588,7 +5783,7 @@ async function runMediaServer() {
5588
5783
  }
5589
5784
  const requested = url.pathname === "/" ? "stream-index.html" : url.pathname.slice(1);
5590
5785
  const path = resolve3(webroot, requested);
5591
- if (!path.startsWith(resolve3(webroot) + "/") || !existsSync7(path) || !statSync2(path).isFile()) {
5786
+ if (!path.startsWith(resolve3(webroot) + "/") || !existsSync8(path) || !statSync2(path).isFile()) {
5592
5787
  response.statusCode = 404;
5593
5788
  response.end("Not found");
5594
5789
  return;
@@ -5600,8 +5795,8 @@ async function runMediaServer() {
5600
5795
  if (webrtc) void currentIceServers();
5601
5796
  response.setHeader("cache-control", "no-store");
5602
5797
  const digest = createHash3("sha1");
5603
- for (const name of readdirSync2(webroot).filter((file) => /\.(js|css)$/.test(file)).sort()) digest.update(readFileSync7(resolve3(webroot, name)));
5604
- response.end(readFileSync7(path, "utf8").replaceAll("__VERSION__", `${COMPUTER_USE_VERSION}-${digest.digest("hex").slice(0, 8)}`));
5798
+ for (const name of readdirSync3(webroot).filter((file) => /\.(js|css)$/.test(file)).sort()) digest.update(readFileSync8(resolve3(webroot, name)));
5799
+ response.end(readFileSync8(path, "utf8").replaceAll("__VERSION__", `${COMPUTER_USE_VERSION}-${digest.digest("hex").slice(0, 8)}`));
5605
5800
  return;
5606
5801
  }
5607
5802
  response.setHeader("cache-control", "private, max-age=31536000, immutable");
@@ -5713,7 +5908,7 @@ async function runMediaServer() {
5713
5908
 
5714
5909
  // src/setup.ts
5715
5910
  import { spawnSync as spawnSync6 } from "child_process";
5716
- import { accessSync, chmodSync as chmodSync2, constants, cpSync, existsSync as existsSync8, mkdirSync as mkdirSync6, readdirSync as readdirSync3, writeFileSync as writeFileSync5 } from "fs";
5911
+ import { accessSync, chmodSync as chmodSync2, constants, cpSync, existsSync as existsSync9, mkdirSync as mkdirSync7, readdirSync as readdirSync4, writeFileSync as writeFileSync6 } from "fs";
5717
5912
  import { dirname as dirname8, resolve as resolve4 } from "path";
5718
5913
  import { fileURLToPath as fileURLToPath3 } from "url";
5719
5914
  var dependencies = [
@@ -5814,7 +6009,7 @@ async function setupComputer(options = {}) {
5814
6009
  const noVncPackageRoot = dirname8(dirname8(fileURLToPath3(import.meta.resolve("@novnc/novnc"))));
5815
6010
  const noVncCore = resolve4(noVncPackageRoot, "core");
5816
6011
  const noVncVendor = resolve4(noVncPackageRoot, "vendor");
5817
- if (!existsSync8(noVncCore) || !existsSync8(noVncVendor)) {
6012
+ if (!existsSync9(noVncCore) || !existsSync9(noVncVendor)) {
5818
6013
  missing.push("bundled noVNC client");
5819
6014
  console.error("\u2717 bundled noVNC client");
5820
6015
  } else {
@@ -5831,23 +6026,22 @@ async function setupComputer(options = {}) {
5831
6026
  if (process.getuid?.() !== 0) throw new Error("replicas-computer setup must run as root to install runtime files");
5832
6027
  const packageRoot2 = resolve4(dirname8(fileURLToPath3(import.meta.url)), "..");
5833
6028
  const assets = resolve4(packageRoot2, "assets", "desktop");
5834
- if (!existsSync8(assets)) throw new Error(`Computer-use assets are missing from ${assets}`);
5835
- mkdirSync6("/usr/local/share/replicas/desktop", { recursive: true });
6029
+ if (!existsSync9(assets)) throw new Error(`Computer-use assets are missing from ${assets}`);
6030
+ mkdirSync7("/usr/local/share/replicas/desktop", { recursive: true });
5836
6031
  cpSync(assets, "/usr/local/share/replicas/desktop", { recursive: true, force: true });
5837
6032
  const fontRoot = "/usr/local/share/fonts/replicas";
5838
- mkdirSync6(fontRoot, { recursive: true });
5839
- for (const file of readdirSync3(assets).filter((name) => name.endsWith(".ttf"))) cpSync(resolve4(assets, file), resolve4(fontRoot, file), { force: true });
6033
+ mkdirSync7(fontRoot, { recursive: true });
6034
+ for (const file of readdirSync4(assets).filter((name) => name.endsWith(".ttf"))) cpSync(resolve4(assets, file), resolve4(fontRoot, file), { force: true });
5840
6035
  spawnSync6("fc-cache", ["-f", fontRoot], { stdio: "ignore" });
5841
- const viewerRoot = "/usr/local/share/replicas/desktop/novnc";
5842
- mkdirSync6(viewerRoot, { recursive: true });
5843
- cpSync(noVncCore, resolve4(viewerRoot, "core"), { recursive: true, force: true });
5844
- cpSync(noVncVendor, resolve4(viewerRoot, "vendor"), { recursive: true, force: true });
6036
+ mkdirSync7(NOVNC_VIEWER_ROOT, { recursive: true });
6037
+ cpSync(noVncCore, resolve4(NOVNC_VIEWER_ROOT, "core"), { recursive: true, force: true });
6038
+ cpSync(noVncVendor, resolve4(NOVNC_VIEWER_ROOT, "vendor"), { recursive: true, force: true });
5845
6039
  for (const file of ["novnc-index.html", "viewer.css", "viewer-theme.css", "viewer-theme.js", "viewer-icon.svg", "geist-mono-400.ttf", "geist-mono-700.ttf"]) {
5846
- cpSync(resolve4(assets, file), resolve4(viewerRoot, file === "novnc-index.html" ? "index.html" : file), { force: true });
6040
+ cpSync(resolve4(assets, file), resolve4(NOVNC_VIEWER_ROOT, file === "novnc-index.html" ? "index.html" : file), { force: true });
5847
6041
  }
5848
- cpSync(resolve4(packageRoot2, "dist", "viewer.js"), resolve4(viewerRoot, "viewer.js"), { force: true });
6042
+ cpSync(resolve4(packageRoot2, "dist", "viewer.js"), resolve4(NOVNC_VIEWER_ROOT, "viewer.js"), { force: true });
5849
6043
  const streamRoot = "/usr/local/share/replicas/desktop/stream";
5850
- mkdirSync6(streamRoot, { recursive: true });
6044
+ mkdirSync7(streamRoot, { recursive: true });
5851
6045
  for (const file of ["stream-index.html", "stream-viewer.css", "viewer-theme.css", "viewer-theme.js", "viewer-icon.svg", "geist-mono-400.ttf", "geist-mono-700.ttf"]) {
5852
6046
  cpSync(resolve4(assets, file), resolve4(streamRoot, file), { force: true });
5853
6047
  }
@@ -5859,13 +6053,13 @@ async function setupComputer(options = {}) {
5859
6053
  chmodSync2("/usr/local/bin/replicas-start-desktop-services", 493);
5860
6054
  chmodSync2("/usr/local/bin/replicas-chrome", 493);
5861
6055
  chmodSync2("/usr/local/bin/replicas-terminal", 493);
5862
- writeFileSync5("/etc/systemd/system/replicas-computer.service", serviceUnit, { mode: 420 });
5863
- if (existsSync8("/run/systemd/system")) {
6056
+ writeFileSync6("/etc/systemd/system/replicas-computer.service", serviceUnit, { mode: 420 });
6057
+ if (existsSync9("/run/systemd/system")) {
5864
6058
  const reload = spawnSync6("systemctl", ["daemon-reload"], { stdio: "pipe" });
5865
6059
  if (reload.status !== 0) throw new Error(`Failed to reload systemd: ${reload.stderr?.toString().trim() || `exit ${reload.status}`}`);
5866
6060
  }
5867
- mkdirSync6("/usr/local/share/replicas-computer", { recursive: true });
5868
- writeFileSync5("/usr/local/share/replicas-computer/install.json", `${JSON.stringify({
6061
+ mkdirSync7("/usr/local/share/replicas-computer", { recursive: true });
6062
+ writeFileSync6("/usr/local/share/replicas-computer/install.json", `${JSON.stringify({
5869
6063
  version: COMPUTER_USE_VERSION,
5870
6064
  protocolVersion: COMPUTER_USE_PROTOCOL_VERSION,
5871
6065
  dependencies: found
package/dist/viewer.js CHANGED
@@ -2,6 +2,11 @@
2
2
  import RFB from "./core/rfb.js";
3
3
  import { encodings } from "./core/encodings.js";
4
4
 
5
+ // ../shared/src/type-guards.ts
6
+ function isRecord(value) {
7
+ return typeof value === "object" && value !== null && !Array.isArray(value);
8
+ }
9
+
5
10
  // src/wheel.ts
6
11
  function wheelDeltaPixels(event, pageHeight) {
7
12
  const scale = event.deltaMode === WheelEvent.DOM_DELTA_LINE ? 32 : event.deltaMode === WheelEvent.DOM_DELTA_PAGE ? pageHeight : 1;
@@ -15,6 +20,17 @@ var statusText = document.querySelector("#status-text");
15
20
  var hint = document.querySelector("#hint");
16
21
  if (!screen || !status || !statusText || !hint) throw new Error("Desktop viewer markup is incomplete");
17
22
  var params = new URLSearchParams(location.search);
23
+ var device = params.get("device");
24
+ var credentials = device ? await fetch("./credentials.json").then((response) => response.ok ? response.json() : void 0, () => void 0) : void 0;
25
+ var screenFailed = false;
26
+ var reportScreen = (state) => {
27
+ screenFailed = state.state === "failed";
28
+ globalThis.__replicasScreen = state;
29
+ };
30
+ if (device) {
31
+ document.title = `${device} screen`;
32
+ reportScreen({ state: "connecting" });
33
+ }
18
34
  var intParam = (name, fallback, min = 0, max = 9) => {
19
35
  const value = Number.parseInt(params.get(name) ?? "", 10);
20
36
  return Number.isInteger(value) && value >= min && value <= max ? value : fallback;
@@ -93,7 +109,7 @@ var configureEncodings = (rfb) => {
93
109
  };
94
110
  };
95
111
  var scheduleReconnect = (immediate = false) => {
96
- if (reconnectTimer || document.visibilityState === "hidden" || !navigator.onLine) return;
112
+ if (screenFailed || reconnectTimer || document.visibilityState === "hidden" || !navigator.onLine) return;
97
113
  const delay = immediate ? 0 : Math.min(1e4, 250 * 2 ** Math.min(reconnectAttempt, 6)) * (0.8 + Math.random() * 0.4);
98
114
  reconnectAttempt += 1;
99
115
  showStatus(reconnectAttempt > 1 ? "reconnecting\u2026" : "connecting\u2026");
@@ -113,7 +129,7 @@ var connect = () => {
113
129
  showStatus(reconnectAttempt ? "reconnecting\u2026" : "connecting\u2026");
114
130
  let rfb;
115
131
  try {
116
- rfb = new RFB(screen, wsUrl);
132
+ rfb = new RFB(screen, wsUrl, { credentials });
117
133
  } catch {
118
134
  scheduleReconnect();
119
135
  return;
@@ -132,6 +148,7 @@ var connect = () => {
132
148
  if (generation !== connectionGeneration) return;
133
149
  reconnectAttempt = 0;
134
150
  status.classList.remove("visible");
151
+ if (device) reportScreen({ state: "connected", width: rfb._fbWidth, height: rfb._fbHeight });
135
152
  applyModernCursor();
136
153
  startUpdatePump();
137
154
  });
@@ -139,14 +156,29 @@ var connect = () => {
139
156
  if (generation !== connectionGeneration) return;
140
157
  activeRfb = null;
141
158
  stopUpdatePump();
159
+ if (device && !screenFailed) reportScreen({ state: "connecting" });
142
160
  scheduleReconnect();
143
161
  });
144
- rfb.addEventListener("securityfailure", () => {
162
+ rfb.addEventListener("securityfailure", (event) => {
145
163
  if (generation !== connectionGeneration) return;
146
164
  activeRfb = null;
147
165
  stopUpdatePump();
148
- scheduleReconnect();
166
+ if (!device) {
167
+ scheduleReconnect();
168
+ return;
169
+ }
170
+ const reason = event instanceof CustomEvent && isRecord(event.detail) && typeof event.detail.reason === "string" ? event.detail.reason : void 0;
171
+ showStatus("the device rejected the screen password");
172
+ reportScreen({ state: "failed", reason: `${device} rejected the screen password${reason ? ` (${reason})` : ""}. Update it in the environment's Devices settings.` });
149
173
  });
174
+ if (device) {
175
+ rfb.addEventListener("credentialsrequired", () => {
176
+ if (generation !== connectionGeneration) return;
177
+ reportScreen({ state: "failed", reason: `${device} asks for a password. Add its screen password in the environment's Devices settings.` });
178
+ showStatus("the device asks for a screen password");
179
+ rfb.disconnect();
180
+ });
181
+ }
150
182
  };
151
183
  var shiftCanvasForWheel = (event) => {
152
184
  if (!optimisticScroll || event.ctrlKey || event.metaKey || Math.abs(event.deltaY) <= Math.abs(event.deltaX)) return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "replicas-computer",
3
- "version": "0.3.26",
3
+ "version": "0.3.28",
4
4
  "description": "Replicas workspace computer-use runtime",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",