@co0ontty/wand 4.39.0 → 4.40.0

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.
@@ -68,6 +68,7 @@ export declare class WsBroadcastManager {
68
68
  private discardClient;
69
69
  private acquirePtyPause;
70
70
  private releasePtyPause;
71
+ private releaseAllPtyPauses;
71
72
  private sendPtyError;
72
73
  private processWsQueue;
73
74
  }
@@ -92,10 +92,7 @@ export class WsBroadcastManager {
92
92
  outputSeqBySession: new Map(),
93
93
  pendingResyncSessions: new Set(),
94
94
  lastSeenAt: Date.now(),
95
- subscribedSessionId: null,
96
- supportsPtyAck: false,
97
- unackedPtyBytes: 0,
98
- ptyPausedSessionId: null,
95
+ ptySubscriptions: new Map(),
99
96
  };
100
97
  this.clients.add(client);
101
98
  ws.on("close", () => {
@@ -120,12 +117,20 @@ export class WsBroadcastManager {
120
117
  if (Number.isSafeInteger(msg.blockBudget) && msg.blockBudget > 0) {
121
118
  client.blockBudget = Math.min(msg.blockBudget, MAX_BLOCK_BUDGET);
122
119
  }
123
- this.releasePtyPause(client);
124
- client.subscribedSessionId = null;
125
- client.supportsPtyAck = msg.capabilities?.ptyAck === true;
120
+ // 默认仍是历史的“切换当前会话”;分屏池显式用 mode:add 叠加订阅。
121
+ if (msg.mode !== "add") {
122
+ this.releaseAllPtyPauses(client);
123
+ client.ptySubscriptions.clear();
124
+ }
125
+ else {
126
+ this.releasePtyPause(client, msg.sessionId);
127
+ }
128
+ client.ptySubscriptions.set(msg.sessionId, {
129
+ supportsAck: msg.capabilities?.ptyAck === true,
130
+ unackedBytes: 0,
131
+ paused: false,
132
+ });
126
133
  this.flushOutput(msg.sessionId);
127
- client.subscribedSessionId = msg.sessionId;
128
- client.unackedPtyBytes = 0;
129
134
  const snapshot = this.port?.getSession(msg.sessionId) ?? null;
130
135
  if (snapshot) {
131
136
  this.sendInit(client, msg.sessionId, snapshot, false);
@@ -138,6 +143,10 @@ export class WsBroadcastManager {
138
143
  }));
139
144
  }
140
145
  }
146
+ else if (msg.type === "unsubscribe" && msg.sessionId) {
147
+ this.releasePtyPause(client, msg.sessionId);
148
+ client.ptySubscriptions.delete(msg.sessionId);
149
+ }
141
150
  else if (msg.type === "resync" && msg.sessionId) {
142
151
  this.flushOutput(msg.sessionId);
143
152
  const snapshot = this.port?.getSession(msg.sessionId) ?? null;
@@ -145,7 +154,7 @@ export class WsBroadcastManager {
145
154
  this.sendInit(client, msg.sessionId, snapshot, true);
146
155
  }
147
156
  else if (msg.type === "pty_input" && msg.sessionId && typeof msg.data === "string") {
148
- if (client.subscribedSessionId !== msg.sessionId || msg.data.length > MAX_PTY_INPUT_CHARS)
157
+ if (!client.ptySubscriptions.has(msg.sessionId) || msg.data.length > MAX_PTY_INPUT_CHARS)
149
158
  return;
150
159
  try {
151
160
  this.port?.sendPtyInput?.(msg.sessionId, msg.data, typeof msg.shortcutKey === "string" ? msg.shortcutKey : undefined, msg.userInput !== false);
@@ -155,7 +164,7 @@ export class WsBroadcastManager {
155
164
  }
156
165
  }
157
166
  else if (msg.type === "pty_resize" && msg.sessionId) {
158
- if (client.subscribedSessionId !== msg.sessionId)
167
+ if (!client.ptySubscriptions.has(msg.sessionId))
159
168
  return;
160
169
  if (!Number.isFinite(msg.cols) || !Number.isFinite(msg.rows))
161
170
  return;
@@ -167,11 +176,12 @@ export class WsBroadcastManager {
167
176
  }
168
177
  }
169
178
  else if (msg.type === "pty_ack" && msg.sessionId && Number.isFinite(msg.bytes)) {
170
- if (!client.supportsPtyAck || client.subscribedSessionId !== msg.sessionId)
179
+ const subscription = client.ptySubscriptions.get(msg.sessionId);
180
+ if (!subscription?.supportsAck)
171
181
  return;
172
- client.unackedPtyBytes = Math.max(0, client.unackedPtyBytes - Math.max(0, Math.floor(msg.bytes)));
173
- if (client.ptyPausedSessionId && client.unackedPtyBytes <= PTY_UNACKED_LOW_WATER) {
174
- this.releasePtyPause(client);
182
+ subscription.unackedBytes = Math.max(0, subscription.unackedBytes - Math.max(0, Math.floor(msg.bytes)));
183
+ if (subscription.paused && subscription.unackedBytes <= PTY_UNACKED_LOW_WATER) {
184
+ this.releasePtyPause(client, msg.sessionId);
175
185
  }
176
186
  }
177
187
  else if (msg.type === "pong") {
@@ -389,7 +399,7 @@ export class WsBroadcastManager {
389
399
  for (const client of this.clients) {
390
400
  if (client.ws.readyState !== WebSocket.OPEN)
391
401
  continue;
392
- if (isRawPtyOutput && client.subscribedSessionId !== event.sessionId)
402
+ if (isRawPtyOutput && !client.ptySubscriptions.has(event.sessionId))
393
403
  continue;
394
404
  const clientEvent = eventForClient(client);
395
405
  // Stamp output events with a per-(client, session) sequence number so
@@ -400,11 +410,12 @@ export class WsBroadcastManager {
400
410
  client.outputSeqBySession.set(event.sessionId, seq);
401
411
  outgoing = { ...clientEvent, seq };
402
412
  }
403
- const usesPtyAckFlowControl = isRawPtyOutput && client.supportsPtyAck;
413
+ const subscription = client.ptySubscriptions.get(event.sessionId);
414
+ const usesPtyAckFlowControl = isRawPtyOutput && subscription?.supportsAck === true;
404
415
  if (usesPtyAckFlowControl) {
405
416
  outgoing = { ...outgoing, ptyBytes };
406
- client.unackedPtyBytes += ptyBytes;
407
- if (client.unackedPtyBytes >= PTY_UNACKED_HIGH_WATER) {
417
+ subscription.unackedBytes += ptyBytes;
418
+ if (subscription.unackedBytes >= PTY_UNACKED_HIGH_WATER) {
408
419
  this.acquirePtyPause(client, event.sessionId);
409
420
  }
410
421
  }
@@ -451,7 +462,7 @@ export class WsBroadcastManager {
451
462
  }
452
463
  }
453
464
  discardClient(client, terminate) {
454
- this.releasePtyPause(client);
465
+ this.releaseAllPtyPauses(client);
455
466
  client.sendQueue.length = 0;
456
467
  client.pendingResyncSessions.clear();
457
468
  client.sendInProgress = false;
@@ -465,20 +476,20 @@ export class WsBroadcastManager {
465
476
  }
466
477
  }
467
478
  acquirePtyPause(client, sessionId) {
468
- if (client.ptyPausedSessionId === sessionId)
479
+ const subscription = client.ptySubscriptions.get(sessionId);
480
+ if (!subscription || subscription.paused)
469
481
  return;
470
- this.releasePtyPause(client);
471
- client.ptyPausedSessionId = sessionId;
482
+ subscription.paused = true;
472
483
  const next = (this.ptyPauseRefs.get(sessionId) ?? 0) + 1;
473
484
  this.ptyPauseRefs.set(sessionId, next);
474
485
  if (next === 1)
475
486
  this.port?.pausePtyOutput?.(sessionId);
476
487
  }
477
- releasePtyPause(client) {
478
- const sessionId = client.ptyPausedSessionId;
479
- if (!sessionId)
488
+ releasePtyPause(client, sessionId) {
489
+ const subscription = client.ptySubscriptions.get(sessionId);
490
+ if (!subscription?.paused)
480
491
  return;
481
- client.ptyPausedSessionId = null;
492
+ subscription.paused = false;
482
493
  const next = Math.max(0, (this.ptyPauseRefs.get(sessionId) ?? 1) - 1);
483
494
  if (next === 0) {
484
495
  this.ptyPauseRefs.delete(sessionId);
@@ -488,6 +499,11 @@ export class WsBroadcastManager {
488
499
  this.ptyPauseRefs.set(sessionId, next);
489
500
  }
490
501
  }
502
+ releaseAllPtyPauses(client) {
503
+ for (const sessionId of client.ptySubscriptions.keys()) {
504
+ this.releasePtyPause(client, sessionId);
505
+ }
506
+ }
491
507
  sendPtyError(client, sessionId, error) {
492
508
  if (client.ws.readyState !== WebSocket.OPEN)
493
509
  return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@co0ontty/wand",
3
- "version": "4.39.0",
3
+ "version": "4.40.0",
4
4
  "description": "A web console for Claude Code, Codex, OpenCode, and other local CLI tools.",
5
5
  "type": "module",
6
6
  "bin": {