replicas-computer 0.3.9 → 0.3.11

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.
@@ -11,9 +11,8 @@
11
11
  </head>
12
12
  <body>
13
13
  <div id="screen"></div>
14
- <div id="status" class="visible" role="status" aria-live="polite">
15
- <img src="./viewer-icon.svg" alt="">
16
- <span id="status-title" aria-hidden="true">Replicas</span>
14
+ <div id="status" class="visible pending" role="status" aria-live="polite">
15
+ <span id="status-spinner" aria-hidden="true"></span>
17
16
  <span id="status-text">connecting…</span>
18
17
  </div>
19
18
  <div id="hint">This desktop runs Linux — ⌘ shortcuts won't work here. Use Ctrl instead (e.g. Ctrl+C / Ctrl+V).</div>
@@ -11,9 +11,8 @@
11
11
  </head>
12
12
  <body>
13
13
  <video id="screen" tabindex="0" aria-label="Replicas desktop" autoplay muted playsinline></video>
14
- <div id="status" class="visible" role="status" aria-live="polite">
15
- <img src="./viewer-icon.svg" alt="">
16
- <span id="status-title" aria-hidden="true">Replicas</span>
14
+ <div id="status" class="visible pending" role="status" aria-live="polite">
15
+ <span id="status-spinner" aria-hidden="true"></span>
17
16
  <span id="status-text">connecting…</span>
18
17
  </div>
19
18
  <button id="audio-prompt" type="button">Enable audio</button>
@@ -11,8 +11,6 @@ html[data-view="native"] body, html[data-view="pixel"] body { width: max-content
11
11
  touch-action: none;
12
12
  }
13
13
 
14
- #status { background: var(--replicas-grid); }
15
-
16
14
  #audio-prompt {
17
15
  position: fixed;
18
16
  right: 16px;
@@ -6,14 +6,6 @@
6
6
  src: url("./geist-mono-400.woff2") format("woff2");
7
7
  }
8
8
 
9
- @font-face {
10
- font-family: "Geist Mono";
11
- font-style: normal;
12
- font-weight: 600;
13
- font-display: swap;
14
- src: url("./geist-mono-600.woff2") format("woff2");
15
- }
16
-
17
9
  @font-face {
18
10
  font-family: "Geist Mono";
19
11
  font-style: normal;
@@ -22,21 +14,10 @@
22
14
  src: url("./geist-mono-700.woff2") format("woff2");
23
15
  }
24
16
 
25
- @font-face {
26
- font-family: "General Sans";
27
- font-style: normal;
28
- font-weight: 300;
29
- font-display: swap;
30
- src: url("./general-sans-300.woff2") format("woff2");
31
- }
32
-
33
17
  :root {
34
18
  --replicas-green: #3cba85;
35
- --replicas-mark-glow: rgb(60 186 133 / 15%);
36
- --replicas-mark-filter: brightness(0.85);
37
19
  --replicas-surface: #121212;
38
20
  --replicas-ink: #ededed;
39
- --replicas-title: #ffffff;
40
21
  --replicas-ink-muted: rgb(255 255 255 / 60%);
41
22
  --replicas-hairline: rgb(255 255 255 / 16%);
42
23
  --replicas-hairline-strong: rgb(255 255 255 / 50%);
@@ -50,11 +31,8 @@
50
31
 
51
32
  :root[data-theme="light"] {
52
33
  --replicas-green: #2fc487;
53
- --replicas-mark-glow: rgb(47 196 135 / 40%);
54
- --replicas-mark-filter: brightness(0.84);
55
34
  --replicas-surface: #f4f5f1;
56
35
  --replicas-ink: #162118;
57
- --replicas-title: #162118;
58
36
  --replicas-ink-muted: rgb(22 33 24 / 60%);
59
37
  --replicas-hairline: rgb(22 33 24 / 14%);
60
38
  --replicas-hairline-strong: rgb(22 33 24 / 45%);
@@ -74,51 +52,52 @@ html, body {
74
52
  #status {
75
53
  position: fixed;
76
54
  inset: 0;
77
- display: flex;
78
- flex-direction: column;
79
- gap: 18px;
80
- align-items: center;
81
- justify-content: center;
55
+ display: grid;
56
+ place-items: center;
82
57
  pointer-events: none;
58
+ background: var(--replicas-surface);
83
59
  opacity: 0;
84
60
  transition: opacity 180ms ease-out;
85
61
  }
86
62
 
87
63
  #status.visible { opacity: 1; }
88
64
 
89
- #status img {
90
- width: 56px;
91
- height: 56px;
92
- filter: var(--replicas-mark-filter) drop-shadow(0 0 18px var(--replicas-mark-glow));
93
- animation: status-pulse 2.4s ease-in-out infinite;
65
+ #status-spinner {
66
+ width: 20px;
67
+ height: 20px;
68
+ box-sizing: border-box;
69
+ border: 2px solid var(--replicas-hairline);
70
+ border-top-color: var(--replicas-ink-muted);
71
+ border-radius: 50%;
72
+ animation: status-spin 0.8s linear infinite;
94
73
  }
95
74
 
96
- @keyframes status-pulse {
97
- 0%, 100% { opacity: 1; }
98
- 50% { opacity: 0.55; }
75
+ @keyframes status-spin {
76
+ to { transform: rotate(360deg); }
99
77
  }
100
78
 
101
79
  @media (prefers-reduced-motion: reduce) {
102
- #status img { animation: none; }
80
+ #status-spinner { animation-duration: 2.4s; }
103
81
  }
104
82
 
105
- #status-title {
106
- color: var(--replicas-title);
107
- font-family: "General Sans", system-ui, sans-serif;
108
- font-size: 30px;
109
- font-weight: 300;
110
- letter-spacing: -0.04em;
111
- line-height: 1;
112
- }
83
+ #status:not(.pending) #status-spinner { display: none; }
113
84
 
114
85
  #status-text {
115
86
  max-width: 72ch;
116
87
  padding: 0 24px;
117
88
  color: var(--replicas-ink-muted);
118
- font-size: 11px;
119
- font-weight: 600;
120
- letter-spacing: 0.12em;
89
+ font-size: 12px;
121
90
  line-height: 1.5;
122
91
  text-align: center;
123
- text-transform: uppercase;
92
+ }
93
+
94
+ /* Progress messages stay readable to screen readers and capture scripts behind the spinner. */
95
+ #status.pending #status-text {
96
+ position: absolute;
97
+ width: 1px;
98
+ height: 1px;
99
+ padding: 0;
100
+ overflow: hidden;
101
+ clip-path: inset(50%);
102
+ white-space: nowrap;
124
103
  }
package/dist/index.js CHANGED
@@ -2937,7 +2937,7 @@ async function computerStatusCommand() {
2937
2937
  }
2938
2938
 
2939
2939
  // src/version.ts
2940
- var COMPUTER_USE_VERSION = "0.3.9";
2940
+ var COMPUTER_USE_VERSION = "0.3.11";
2941
2941
  var COMPUTER_USE_PROTOCOL_VERSION = 1;
2942
2942
 
2943
2943
  // src/program.ts
@@ -3592,7 +3592,8 @@ var DESKTOP_WEBRTC_FAILURE_REASONS = {
3592
3592
  codec_unsupported: "browser cannot receive H.264 over WebRTC",
3593
3593
  negotiation_failed: "negotiation failed",
3594
3594
  video_timeout: "WebRTC did not decode video in time",
3595
- video_stalled: "WebRTC video stopped arriving"
3595
+ video_stalled: "WebRTC video stopped arriving",
3596
+ full_chroma_undecodable: "WebRTC could not decode H.264 High 4:4:4"
3596
3597
  };
3597
3598
  function isDesktopWebRtcFailure(value) {
3598
3599
  if (!isRecord(value) || typeof value.reasonCode !== "string" || !Object.hasOwn(DESKTOP_WEBRTC_FAILURE_REASONS, value.reasonCode) || typeof value.turnConfigured !== "boolean") return false;
@@ -5494,13 +5495,13 @@ async function setupComputer(options = {}) {
5494
5495
  mkdirSync6(viewerRoot, { recursive: true });
5495
5496
  cpSync(noVncCore, resolve4(viewerRoot, "core"), { recursive: true, force: true });
5496
5497
  cpSync(noVncVendor, resolve4(viewerRoot, "vendor"), { recursive: true, force: true });
5497
- for (const file of ["novnc-index.html", "viewer.css", "viewer-theme.css", "viewer-theme.js", "viewer-icon.svg", "geist-mono-400.ttf", "geist-mono-600.ttf", "geist-mono-700.ttf", "general-sans-300.ttf"]) {
5498
+ 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"]) {
5498
5499
  cpSync(resolve4(assets, file), resolve4(viewerRoot, file === "novnc-index.html" ? "index.html" : file), { force: true });
5499
5500
  }
5500
5501
  cpSync(resolve4(packageRoot2, "dist", "viewer.js"), resolve4(viewerRoot, "viewer.js"), { force: true });
5501
5502
  const streamRoot = "/usr/local/share/replicas/desktop/stream";
5502
5503
  mkdirSync6(streamRoot, { recursive: true });
5503
- 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-600.ttf", "geist-mono-700.ttf", "general-sans-300.ttf"]) {
5504
+ 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"]) {
5504
5505
  cpSync(resolve4(assets, file), resolve4(streamRoot, file), { force: true });
5505
5506
  }
5506
5507
  cpSync(resolve4(packageRoot2, "dist", "stream-viewer.js"), resolve4(streamRoot, "stream-viewer.js"), { force: true });
@@ -42,7 +42,8 @@ var DESKTOP_WEBRTC_FAILURE_REASONS = {
42
42
  codec_unsupported: "browser cannot receive H.264 over WebRTC",
43
43
  negotiation_failed: "negotiation failed",
44
44
  video_timeout: "WebRTC did not decode video in time",
45
- video_stalled: "WebRTC video stopped arriving"
45
+ video_stalled: "WebRTC video stopped arriving",
46
+ full_chroma_undecodable: "WebRTC could not decode H.264 High 4:4:4"
46
47
  };
47
48
 
48
49
  // src/input-validation.ts
@@ -99,6 +100,13 @@ diagnostics.classList.toggle("visible", showDiagnostics);
99
100
  var webrtcFrameTimeoutMs = 6e3;
100
101
  var webrtcSkipsAfterFailure = 3;
101
102
  var pointerIntervalMs = 4;
103
+ var fullChromaFailureKey = "replicas-desktop-4:4:4-failed";
104
+ var skipFullChroma = false;
105
+ var fullChromaFailure;
106
+ try {
107
+ skipFullChroma = localStorage.getItem(fullChromaFailureKey) === navigator.userAgent;
108
+ } catch {
109
+ }
102
110
  var socket;
103
111
  var config;
104
112
  var transport;
@@ -159,8 +167,9 @@ var note = (event, detail) => {
159
167
  if (viewerEvents.push({ at: Date.now(), event, detail }) > 50) viewerEvents.shift();
160
168
  };
161
169
  var describeEvents = (list) => list.map((entry) => `${new Date(entry.at).toLocaleTimeString([], { hour12: false })} ${entry.event}${entry.detail ? ` (${entry.detail})` : ""}`).join(" \xB7 ");
162
- var showStatus = (message) => {
170
+ var showStatus = (message, pending = false) => {
163
171
  statusText.textContent = message;
172
+ status.classList.toggle("pending", pending);
164
173
  status.classList.add("visible");
165
174
  };
166
175
  var describe = (error) => error instanceof Error ? error.message : String(error);
@@ -298,7 +307,7 @@ var configureVideo = async (next, preferHardware = true) => {
298
307
  videoDecoder = void 0;
299
308
  waitingForKeyframe = true;
300
309
  note("decoder error", error.message);
301
- showStatus(`recovering video: ${error.message}`);
310
+ showStatus(`recovering video: ${error.message}`, true);
302
311
  if (config) void configureVideo(config, false).catch((reason) => showStatus(describe(reason)));
303
312
  }
304
313
  });
@@ -396,38 +405,40 @@ var unlockAudio = () => {
396
405
  });
397
406
  });
398
407
  };
408
+ var captureFailure = (reasonCode, current) => {
409
+ const captured = {
410
+ id: crypto.randomUUID?.() ?? `${Date.now()}-${Math.random().toString(36).slice(2)}`,
411
+ reasonCode,
412
+ elapsedMs: Math.round(performance.now() - connectionStartedAt),
413
+ connectionState: peer?.connectionState ?? "unavailable",
414
+ iceConnectionState: peer?.iceConnectionState ?? "unavailable",
415
+ iceGatheringState: peer?.iceGatheringState ?? "unavailable",
416
+ signalingState: peer?.signalingState ?? "unavailable",
417
+ iceTransportPolicy: request.iceTransportPolicy,
418
+ turnConfigured: current.iceServers?.some((server) => /^turns?:/i.test(server.urls)) ?? false,
419
+ serverVersion: current.serverVersion ?? "unknown",
420
+ serverUnavailableReason: current.webrtcUnavailableReason ?? "",
421
+ localCandidateTypes: [...localCandidateTypes].join(","),
422
+ remoteCandidateTypes: [...remoteCandidateTypes].join(","),
423
+ iceErrorCodes: [...iceErrorCodes].join(","),
424
+ candidateErrors,
425
+ errorName: negotiationErrorName,
426
+ path: route.path,
427
+ codec: route.codec,
428
+ framesDecoded: inbound?.framesDecoded ?? 0,
429
+ packetsReceived: inbound?.packetsReceived ?? 0,
430
+ packetsLost: inbound?.packetsLost ?? 0,
431
+ bytesReceived: inbound?.bytesReceived ?? 0,
432
+ lastFrameAgeMs: lastFrameAt ? Math.round(performance.now() - lastFrameAt) : -1,
433
+ encoderFps: telemetry?.encoder.fps ?? -1
434
+ };
435
+ state.lastWebRtcFailure = captured;
436
+ return captured;
437
+ };
399
438
  var useFallback = (reasonCode, failed = false) => {
400
439
  if (transport === "websocket" || !config) return;
401
440
  const reason = reasonCode === "websocket_selected" ? "WebSocket transport selected" : reasonCode === "recent_failure" ? "recent WebRTC failure" : DESKTOP_WEBRTC_FAILURE_REASONS[reasonCode];
402
- if (request.transport === "webrtc" && reasonCode !== "websocket_selected" && reasonCode !== "recent_failure") {
403
- failure = {
404
- id: crypto.randomUUID?.() ?? `${Date.now()}-${Math.random().toString(36).slice(2)}`,
405
- reasonCode,
406
- elapsedMs: Math.round(performance.now() - connectionStartedAt),
407
- connectionState: peer?.connectionState ?? "unavailable",
408
- iceConnectionState: peer?.iceConnectionState ?? "unavailable",
409
- iceGatheringState: peer?.iceGatheringState ?? "unavailable",
410
- signalingState: peer?.signalingState ?? "unavailable",
411
- iceTransportPolicy: request.iceTransportPolicy,
412
- turnConfigured: config.iceServers?.some((server) => /^turns?:/i.test(server.urls)) ?? false,
413
- serverVersion: config.serverVersion ?? "unknown",
414
- serverUnavailableReason: config.webrtcUnavailableReason ?? "",
415
- localCandidateTypes: [...localCandidateTypes].join(","),
416
- remoteCandidateTypes: [...remoteCandidateTypes].join(","),
417
- iceErrorCodes: [...iceErrorCodes].join(","),
418
- candidateErrors,
419
- errorName: negotiationErrorName,
420
- path: route.path,
421
- codec: route.codec,
422
- framesDecoded: inbound?.framesDecoded ?? 0,
423
- packetsReceived: inbound?.packetsReceived ?? 0,
424
- packetsLost: inbound?.packetsLost ?? 0,
425
- bytesReceived: inbound?.bytesReceived ?? 0,
426
- lastFrameAgeMs: lastFrameAt ? Math.round(performance.now() - lastFrameAt) : -1,
427
- encoderFps: telemetry?.encoder.fps ?? -1
428
- };
429
- state.lastWebRtcFailure = failure;
430
- }
441
+ if (request.transport === "webrtc" && reasonCode !== "websocket_selected" && reasonCode !== "recent_failure") failure = captureFailure(reasonCode, config);
431
442
  closePeer();
432
443
  if (failed) webrtcSkips = webrtcSkipsAfterFailure;
433
444
  note("fallback to socket", reason);
@@ -503,6 +514,10 @@ var negotiate = async (sdp) => {
503
514
  try {
504
515
  await pc.setRemoteDescription({ type: "offer", sdp });
505
516
  if (peer !== pc) return;
517
+ if (skipFullChroma) {
518
+ const codecs = RTCRtpReceiver.getCapabilities("video")?.codecs.filter((codec) => h264ProfileLabel(codec.sdpFmtpLine ?? "") !== H264_PROFILES.high444.label);
519
+ if (codecs) pc.getTransceivers().find((transceiver) => transceiver.receiver.track.kind === "video")?.setCodecPreferences(codecs);
520
+ }
506
521
  const answer = await pc.createAnswer();
507
522
  if (peer !== pc) return;
508
523
  await pc.setLocalDescription(answer);
@@ -571,8 +586,17 @@ var handleServerMessage = (message, viaSocket) => {
571
586
  webrtcSkips -= 1;
572
587
  useFallback("recent_failure");
573
588
  } else if (!transport) {
574
- showStatus("Establishing connection\u2026");
575
- webrtcDeadline ??= setTimeout(() => useFallback("video_timeout", true), webrtcFrameTimeoutMs);
589
+ showStatus("Establishing connection\u2026", true);
590
+ webrtcDeadline ??= setTimeout(() => {
591
+ if (skipFullChroma || !inbound?.packetsReceived || !route.codec.endsWith(H264_PROFILES.high444.label)) {
592
+ useFallback("video_timeout", true);
593
+ return;
594
+ }
595
+ skipFullChroma = true;
596
+ if (config) fullChromaFailure = captureFailure("full_chroma_undecodable", config);
597
+ note("retrying WebRTC without 4:4:4");
598
+ socket?.close();
599
+ }, webrtcFrameTimeoutMs);
576
600
  }
577
601
  };
578
602
  var handleServerText = (data, viaSocket) => {
@@ -603,7 +627,7 @@ var connect = () => {
603
627
  route = { path: "", codec: "", decoded: "", kbps: 0, displayedFps: 0 };
604
628
  announceTransport();
605
629
  socket?.close();
606
- showStatus(reconnectAttempt ? "reconnecting\u2026" : "connecting\u2026");
630
+ showStatus(reconnectAttempt ? "reconnecting\u2026" : "connecting\u2026", true);
607
631
  const query = new URLSearchParams([...params].filter(([key]) => ["transport", "profile", "adapt", "ice"].includes(key)));
608
632
  const nextSocket = new WebSocket(`${location.protocol === "https:" ? "wss" : "ws"}://${location.host}/stream?${query}`);
609
633
  nextSocket.binaryType = "arraybuffer";
@@ -793,6 +817,15 @@ var reportWebRtcStats = async (pc, stats) => {
793
817
  lastFrameAt = now;
794
818
  clearTimeout(webrtcDeadline);
795
819
  webrtcDeadline = void 0;
820
+ if (fullChromaFailure) {
821
+ try {
822
+ localStorage.setItem(fullChromaFailureKey, navigator.userAgent);
823
+ } catch {
824
+ }
825
+ failure = fullChromaFailure;
826
+ fullChromaFailure = void 0;
827
+ announceTransport();
828
+ }
796
829
  }
797
830
  if (inbound && (inbound.id !== counters.id || counters.framesDecoded < inbound.framesDecoded)) inbound = void 0;
798
831
  if (!inbound) inboundHistory = [];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "replicas-computer",
3
- "version": "0.3.9",
3
+ "version": "0.3.11",
4
4
  "description": "Replicas workspace computer-use runtime",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
Binary file