deckide 3.5.34 → 3.5.36

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.
@@ -10,6 +10,10 @@ const DEFAULT_PROFILE_DIR = path.join(os.homedir(), '.local', 'share', 'agent-br
10
10
  const DEFAULT_OUTPUT_DIR = path.join(os.homedir(), '.local', 'share', 'agent-browser', 'output');
11
11
  const START_TIMEOUT_MS = 30_000;
12
12
  const MAX_CLIENT_BUFFERED_AMOUNT = 4 * 1024 * 1024;
13
+ // Above this many bytes already queued on a client socket we hold the newest
14
+ // frame instead of piling on, then flush it once the socket drains.
15
+ const FRAME_COALESCE_THRESHOLD = 256 * 1024;
16
+ const FRAME_FLUSH_INTERVAL_MS = 16;
13
17
  const DEFAULT_VIEWPORT = { width: 1280, height: 720 };
14
18
  const MIN_VIEWPORT = { width: 320, height: 240 };
15
19
  const MAX_VIEWPORT = { width: 3840, height: 2160 };
@@ -243,6 +247,14 @@ export class AgentBrowserService {
243
247
  cdpSocket = null;
244
248
  cdpConnecting = null;
245
249
  screencastStarted = false;
250
+ // Raw JPEG bytes of the most recent screencast frame, kept so reconnecting or
251
+ // late-joining clients can be shown the current page immediately. Frames are
252
+ // pushed to clients as binary WebSocket messages (no base64) to cut ~33% size
253
+ // and avoid per-frame string allocation.
254
+ lastFrameBuffer = null;
255
+ // Newest frame held back per slow client, flushed when its socket drains.
256
+ pendingFrames = new Map();
257
+ frameFlushTimer = null;
246
258
  clients = new Set();
247
259
  viewport = { ...DEFAULT_VIEWPORT };
248
260
  pageUrl = null;
@@ -304,6 +316,12 @@ export class AgentBrowserService {
304
316
  this.pageUrl = null;
305
317
  this.pageTitle = null;
306
318
  this.screencastStarted = false;
319
+ this.lastFrameBuffer = null;
320
+ this.pendingFrames.clear();
321
+ if (this.frameFlushTimer) {
322
+ clearInterval(this.frameFlushTimer);
323
+ this.frameFlushTimer = null;
324
+ }
307
325
  if (proc && proc.exitCode == null && proc.signalCode == null) {
308
326
  await new Promise((resolve) => {
309
327
  const killTimer = setTimeout(() => {
@@ -341,6 +359,7 @@ export class AgentBrowserService {
341
359
  });
342
360
  socket.on('close', () => {
343
361
  this.clients.delete(socket);
362
+ this.pendingFrames.delete(socket);
344
363
  if (this.clients.size === 0) {
345
364
  void this.stopScreencast();
346
365
  }
@@ -438,6 +457,13 @@ export class AgentBrowserService {
438
457
  await this.start();
439
458
  await this.ensureCdp();
440
459
  await this.sendStatus(socket);
460
+ // Replay the most recent frame so a reconnecting or late-joining client
461
+ // sees the current page immediately. Screencast only emits frames on
462
+ // change, so without this a client attaching to a static page would stay
463
+ // blank until the page next repaints.
464
+ if (this.lastFrameBuffer) {
465
+ this.writeFrame(socket, this.lastFrameBuffer);
466
+ }
441
467
  if (this.clients.size > 0) {
442
468
  await this.startScreencast();
443
469
  }
@@ -660,11 +686,9 @@ export class AgentBrowserService {
660
686
  void this.cdp?.send('Page.screencastFrameAck', { sessionId }).catch(() => undefined);
661
687
  }
662
688
  if (data) {
663
- this.broadcast({
664
- type: 'frame',
665
- data,
666
- metadata: params.metadata ?? null,
667
- });
689
+ const buffer = Buffer.from(data, 'base64');
690
+ this.lastFrameBuffer = buffer;
691
+ this.broadcastBinary(buffer);
668
692
  }
669
693
  return;
670
694
  }
@@ -719,6 +743,79 @@ export class AgentBrowserService {
719
743
  send(socket, payload) {
720
744
  this.sendRaw(socket, JSON.stringify(payload));
721
745
  }
746
+ broadcastBinary(buffer) {
747
+ for (const socket of this.clients) {
748
+ this.queueFrameForClient(socket, buffer);
749
+ }
750
+ }
751
+ // Per-client, backpressure-aware frame delivery. A fast client (e.g. on
752
+ // localhost) drains instantly and receives every frame; a slow client (remote
753
+ // or mobile) has only the *newest* frame held and gets it once its send buffer
754
+ // clears. This trades frame rate for latency so motion stays smooth instead of
755
+ // accumulating a backlog that stutters and then bursts.
756
+ queueFrameForClient(socket, buffer) {
757
+ if (!isOpen(socket)) {
758
+ this.pendingFrames.delete(socket);
759
+ return;
760
+ }
761
+ if (socket.bufferedAmount > FRAME_COALESCE_THRESHOLD) {
762
+ this.pendingFrames.set(socket, buffer);
763
+ this.ensureFrameFlushTimer();
764
+ return;
765
+ }
766
+ this.pendingFrames.delete(socket);
767
+ this.writeFrame(socket, buffer);
768
+ }
769
+ ensureFrameFlushTimer() {
770
+ if (this.frameFlushTimer) {
771
+ return;
772
+ }
773
+ this.frameFlushTimer = setInterval(() => this.flushPendingFrames(), FRAME_FLUSH_INTERVAL_MS);
774
+ this.frameFlushTimer.unref?.();
775
+ }
776
+ flushPendingFrames() {
777
+ for (const [socket, buffer] of this.pendingFrames) {
778
+ if (!isOpen(socket)) {
779
+ this.pendingFrames.delete(socket);
780
+ continue;
781
+ }
782
+ if (socket.bufferedAmount > FRAME_COALESCE_THRESHOLD) {
783
+ continue; // still draining; keep holding the newest frame
784
+ }
785
+ this.pendingFrames.delete(socket);
786
+ this.writeFrame(socket, buffer);
787
+ }
788
+ if (this.pendingFrames.size === 0 && this.frameFlushTimer) {
789
+ clearInterval(this.frameFlushTimer);
790
+ this.frameFlushTimer = null;
791
+ }
792
+ }
793
+ writeFrame(socket, buffer) {
794
+ if (!isOpen(socket)) {
795
+ return;
796
+ }
797
+ // Hard ceiling: a client this far behind is unrecoverable, so drop it.
798
+ if (socket.bufferedAmount > MAX_CLIENT_BUFFERED_AMOUNT) {
799
+ try {
800
+ socket.close(1009, 'Browser stream overflow');
801
+ }
802
+ catch {
803
+ // ignore
804
+ }
805
+ return;
806
+ }
807
+ try {
808
+ socket.send(buffer, { binary: true });
809
+ }
810
+ catch {
811
+ try {
812
+ socket.close(1011, 'Browser stream send failed');
813
+ }
814
+ catch {
815
+ // ignore
816
+ }
817
+ }
818
+ }
722
819
  sendRaw(socket, payload) {
723
820
  if (!isOpen(socket)) {
724
821
  return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "deckide",
3
- "version": "3.5.34",
3
+ "version": "3.5.36",
4
4
  "description": "Deck IDE - Browser-based IDE with terminal, file explorer, and git integration",
5
5
  "type": "module",
6
6
  "bin": {