@av-pi-studio/server 0.0.93 → 0.0.95

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.
@@ -5,6 +5,13 @@ export function registerTerminalHandlers(registry, deps, getActiveSessions) {
5
5
  // Per (session, slot) stream unsubscribers.
6
6
  const streamUnsubs = new Map();
7
7
  const key = (session, slot) => `${session.id}:${slot}`;
8
+ // A terminal exit — self-exit (`exit`, a crash) or `kill()` — always broadcasts the same
9
+ // `terminals_update` signal, unconditionally to every active session. This is the ONLY exit
10
+ // broadcast: `kill_terminal_request` below relies on it rather than sending its own, since
11
+ // `manager.kill()` invokes this listener synchronously and a second broadcast would duplicate it.
12
+ manager.onTerminalExit(() => {
13
+ deps.broadcast(getActiveSessions(), { type: "terminals_update", terminals: manager.list() });
14
+ });
8
15
  registry.register("list_terminals_request", () => ({
9
16
  type: "list_terminals_response",
10
17
  terminals: manager.list(),
@@ -40,10 +47,15 @@ export function registerTerminalHandlers(registry, deps, getActiveSessions) {
40
47
  const slot = Number(ctx.message.slot);
41
48
  const session = ctx.session;
42
49
  // Restore mode is honored only when the daemon advertises the feature AND the client advertised
43
- // the reflowable-snapshot capability. Otherwise fall back to the basic snapshot (ignore mode).
50
+ // the reflowable-snapshot capability AND asked for it by name. The wire literal is exactly
51
+ // "reflowable" (sprint-053/task-004) — any other requested value (including a future/typo'd
52
+ // one) is served and echoed as "basic", so the response never names a tier that was not
53
+ // actually served.
44
54
  const requestedMode = ctx.message.restoreMode;
45
55
  const clientReflowable = session.supports("terminal_reflowable_snapshot");
46
- const restoreMode = deps.restoreModesEnabled && clientReflowable ? (requestedMode ?? "basic") : "basic";
56
+ const restoreMode = deps.restoreModesEnabled && clientReflowable && requestedMode === "reflowable"
57
+ ? "reflowable"
58
+ : "basic";
47
59
  streamUnsubs.get(key(session, slot))?.(); // replace existing subscription
48
60
  try {
49
61
  // Resize BEFORE subscribing, using the grid the attaching client sent (if any).
@@ -62,9 +74,11 @@ export function registerTerminalHandlers(registry, deps, getActiveSessions) {
62
74
  //
63
75
  // Validation and the same-size no-op both live in `manager.resize` — the one choke point every
64
76
  // size path funnels through — so this passes the raw values straight through rather than
65
- // growing a second, drifting copy of those rules here.
66
- manager.resize(slot, Number(ctx.message.cols), Number(ctx.message.rows));
67
- const unsub = manager.subscribe(slot, (frame) => session.sendBinary(frame));
77
+ // growing a second, drifting copy of those rules here. Broadcasts `terminals_update` when the
78
+ // grid actually changed (sprint-053/task-007), so an already-attached second client's stale
79
+ // belief gets corrected without it having to guess from a redundant `Resize` of its own.
80
+ resizeAndBroadcast(manager, deps.broadcast, getActiveSessions, slot, Number(ctx.message.cols), Number(ctx.message.rows));
81
+ const unsub = manager.subscribe(slot, (frame) => session.sendBinary(frame), { restoreMode });
68
82
  streamUnsubs.set(key(session, slot), unsub);
69
83
  // Echo the PTY's real size so the client can seed its belief instead of guessing. Without
70
84
  // this a reattaching client cannot know what it is attaching to, and has to send a blind
@@ -100,7 +114,6 @@ export function registerTerminalHandlers(registry, deps, getActiveSessions) {
100
114
  registry.register("kill_terminal_request", (ctx) => {
101
115
  const slot = Number(ctx.message.slot);
102
116
  const ok = manager.kill(slot);
103
- deps.broadcast(getActiveSessions(), { type: "terminals_update", terminals: manager.list() });
104
117
  return { type: "kill_terminal_response", slot, ok };
105
118
  });
106
119
  registry.register("capture_terminal_request", (ctx) => {
@@ -135,8 +148,30 @@ export function registerTerminalHandlers(registry, deps, getActiveSessions) {
135
148
  };
136
149
  });
137
150
  }
151
+ /**
152
+ * Apply a resize and broadcast the refreshed inventory to every active session, but only when the
153
+ * grid actually changed (sprint-053/task-007). An unknown slot, an invalid grid, and a same-size
154
+ * no-op must all stay silent — the binary `Resize` frame path is the hot path of every coalesced
155
+ * pane-divider drag, and a broadcast per intermediate frame would defeat that coalescing.
156
+ *
157
+ * Compares `TerminalManager.get(slot)` before and after rather than trusting `resize`'s boolean
158
+ * return (which is `true` for both an applied change and a same-size no-op) — `get` returns the
159
+ * live entry object, which `resize` mutates in place, so the cols/rows are captured into locals
160
+ * before the call rather than held as a stale reference to the same object.
161
+ */
162
+ function resizeAndBroadcast(manager, broadcast, getActiveSessions, slot, cols, rows) {
163
+ const before = manager.get(slot);
164
+ const beforeCols = before?.cols;
165
+ const beforeRows = before?.rows;
166
+ if (!manager.resize(slot, cols, rows))
167
+ return;
168
+ const after = manager.get(slot);
169
+ if (after && (after.cols !== beforeCols || after.rows !== beforeRows)) {
170
+ broadcast(getActiveSessions(), { type: "terminals_update", terminals: manager.list() });
171
+ }
172
+ }
138
173
  /** Binary terminal-input frame handler for the frame dispatcher (Input/Resize opcodes). */
139
- export function makeTerminalBinaryHandler(manager) {
174
+ export function makeTerminalBinaryHandler(manager, broadcast, getActiveSessions) {
140
175
  return (_session, bytes) => {
141
176
  const frame = tryDecodeTerminalFrame(bytes);
142
177
  if (!frame)
@@ -144,7 +179,7 @@ export function makeTerminalBinaryHandler(manager) {
144
179
  if (frame.opcode === "Input")
145
180
  manager.input(frame.slot, frame.data);
146
181
  else if (frame.opcode === "Resize")
147
- manager.resize(frame.slot, frame.cols, frame.rows);
182
+ resizeAndBroadcast(manager, broadcast, getActiveSessions, frame.slot, frame.cols, frame.rows);
148
183
  };
149
184
  }
150
185
  //# sourceMappingURL=terminal-rpc.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@av-pi-studio/server",
3
- "version": "0.0.93",
3
+ "version": "0.0.95",
4
4
  "bin": {
5
5
  "pi-studio-daemon": "dist/daemon/main.js"
6
6
  },
@@ -24,10 +24,11 @@
24
24
  "clean": "rm -rf dist *.tsbuildinfo"
25
25
  },
26
26
  "dependencies": {
27
- "@av-pi-studio/highlight": "^0.0.93",
28
- "@av-pi-studio/protocol": "^0.0.93",
29
- "@av-pi-studio/relay": "^0.0.93",
27
+ "@av-pi-studio/highlight": "^0.0.95",
28
+ "@av-pi-studio/protocol": "^0.0.95",
29
+ "@av-pi-studio/relay": "^0.0.95",
30
30
  "@earendil-works/pi-coding-agent": "^0.84.1",
31
+ "@xterm/addon-serialize": "^0.14.0",
31
32
  "@xterm/headless": "^6.0.0",
32
33
  "bcryptjs": "^3.0.3",
33
34
  "lru-cache": "^11.5.1",