@av-pi-studio/server 0.0.94 → 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.
@@ -482,7 +482,7 @@ export function startDaemon(opts) {
482
482
  restoreModesEnabled: true,
483
483
  projectConfigPath: (cwd) => join(cwd, "pi-studio.json"),
484
484
  }, getActiveSessions);
485
- const terminalBinaryHandler = makeTerminalBinaryHandler(terminalManager);
485
+ const terminalBinaryHandler = makeTerminalBinaryHandler(terminalManager, broadcast, getActiveSessions);
486
486
  // ── Orchestration: schedules / chat / loops (real, disk-backed) ───────────────
487
487
  const scheduleExecutor = {
488
488
  async createAndPrompt(agentConfig, prompt) {
@@ -9,6 +9,7 @@
9
9
  */
10
10
  export declare class ScreenBuffer {
11
11
  private readonly term;
12
+ private readonly serializeAddon;
12
13
  constructor(cols: number, rows: number, scrollback?: number);
13
14
  write(data: Uint8Array): void;
14
15
  resize(cols: number, rows: number): void;
@@ -16,6 +17,18 @@ export declare class ScreenBuffer {
16
17
  flush(): Promise<void>;
17
18
  /** The visible viewport as plain text, with trailing blank lines trimmed. */
18
19
  snapshotText(): string;
20
+ /**
21
+ * A reflowable redraw of the current screen — SGR colours/attributes and cursor position, not
22
+ * just text (`terminals.md` § Restore / snapshot, tier 2: the daemon's raw byte ring is
23
+ * approximate at a different width; this is the payload sent instead when both ends support
24
+ * it). Computed on demand, not maintained continuously, so an idle terminal costs nothing extra
25
+ * beyond what `capture`/`snapshotText` already require. Bounded to
26
+ * `RESTORE_SCROLLBACK_LINES` — verified empirically against `@xterm/addon-serialize@0.14.0`
27
+ * paired with `@xterm/headless@6.0.0` (no published peer range covers this pairing yet; the
28
+ * addon's actual API — reading `buffer.active` cells/modes — has been runtime-compatible across
29
+ * this xterm major since it predates the `@xterm/*` scoped rename).
30
+ */
31
+ serialize(): string;
19
32
  dispose(): void;
20
33
  }
21
34
  //# sourceMappingURL=screen-buffer.d.ts.map
@@ -2,9 +2,23 @@ import { createRequire } from "node:module";
2
2
  import stripAnsi from "strip-ansi";
3
3
  // `@xterm/headless` ships a UMD bundle whose `module.exports` Node's ESM loader cannot statically
4
4
  // read, so a named `import { Terminal }` resolves at type-check time but throws at runtime. Load it
5
- // through `createRequire` (CJS) to get the real `Terminal` constructor.
5
+ // through `createRequire` (CJS) to get the real `Terminal` constructor. `@xterm/addon-serialize`
6
+ // ships the same way (also a UMD bundle, no `exports` map in its `package.json`) and needs the
7
+ // identical treatment (sprint-053/task-004) — expected, not a surprise discovered at runtime. Both
8
+ // `import type`s above are erased at compile time (no runtime `import`, so no UMD-load failure);
9
+ // they only give the two `require(...)` results below a real type instead of `any`.
6
10
  const require = createRequire(import.meta.url);
7
11
  const { Terminal } = require("@xterm/headless");
12
+ const { SerializeAddon, } = require("@xterm/addon-serialize");
13
+ /**
14
+ * Lines of scrollback history a reflowable `Restore` payload includes, on top of the viewport
15
+ * itself (`terminals.md` § Restore / snapshot, tier 2; `feature-panels-ui.md` § Reconnect/restore:
16
+ * "a visible-snapshot restore (bounded scrollback)"). A redraw needs the current screen, not the
17
+ * terminal's whole retained history (`ScreenBuffer`'s own `scrollback` constructor default is
18
+ * 1000 lines) replayed on every reattach — bounding this is what keeps the payload size
19
+ * predictable regardless of how long the terminal has been running.
20
+ */
21
+ const RESTORE_SCROLLBACK_LINES = 200;
8
22
  /**
9
23
  * Server-side terminal screen model backed by `@xterm/headless` (features/terminals.md § capture).
10
24
  *
@@ -16,8 +30,17 @@ const { Terminal } = require("@xterm/headless");
16
30
  */
17
31
  export class ScreenBuffer {
18
32
  term;
33
+ serializeAddon;
19
34
  constructor(cols, rows, scrollback = 1000) {
20
35
  this.term = new Terminal({ cols, rows, scrollback, allowProposedApi: true });
36
+ this.serializeAddon = new SerializeAddon();
37
+ // `@xterm/addon-serialize`'s published types declare `activate(terminal: Terminal)` against
38
+ // `@xterm/xterm` (the browser package) specifically, so it is not structurally assignable to
39
+ // headless's own `ITerminalAddon` (which wants its OWN `Terminal` type) — even though the
40
+ // addon's real implementation only reads `buffer`/`cols`/`rows`, fields both `Terminal` types
41
+ // share, and works correctly headless (verified empirically: colours, cursor position, and
42
+ // text all round-trip — see `serialize()` below and its tests).
43
+ this.term.loadAddon(this.serializeAddon);
21
44
  }
22
45
  write(data) {
23
46
  this.term.write(Buffer.from(data));
@@ -48,6 +71,20 @@ export class ScreenBuffer {
48
71
  // translateToString already yields plain text; strip-ansi defends against any passthrough.
49
72
  return stripAnsi(lines.join("\n"));
50
73
  }
74
+ /**
75
+ * A reflowable redraw of the current screen — SGR colours/attributes and cursor position, not
76
+ * just text (`terminals.md` § Restore / snapshot, tier 2: the daemon's raw byte ring is
77
+ * approximate at a different width; this is the payload sent instead when both ends support
78
+ * it). Computed on demand, not maintained continuously, so an idle terminal costs nothing extra
79
+ * beyond what `capture`/`snapshotText` already require. Bounded to
80
+ * `RESTORE_SCROLLBACK_LINES` — verified empirically against `@xterm/addon-serialize@0.14.0`
81
+ * paired with `@xterm/headless@6.0.0` (no published peer range covers this pairing yet; the
82
+ * addon's actual API — reading `buffer.active` cells/modes — has been runtime-compatible across
83
+ * this xterm major since it predates the `@xterm/*` scoped rename).
84
+ */
85
+ serialize() {
86
+ return this.serializeAddon.serialize({ scrollback: RESTORE_SCROLLBACK_LINES });
87
+ }
51
88
  dispose() {
52
89
  this.term.dispose();
53
90
  }
@@ -17,6 +17,13 @@ import { type PtyBackend } from "./pty-backend.js";
17
17
  */
18
18
  /** A subscriber sink receives fully-encoded binary terminal frames. */
19
19
  export type TerminalFrameSink = (frame: Uint8Array) => void;
20
+ /**
21
+ * Tier negotiated per subscription (`terminals.md` § Restore / snapshot). `"basic"` is the raw
22
+ * byte-ring `Snapshot` (always available); `"reflowable"` is a `Restore` frame carrying
23
+ * `ScreenBuffer.serialize()` — correct at any client width, gated on both sides supporting it
24
+ * (`terminal-rpc.ts`'s negotiation). Exactly one of the two is ever sent per subscribe.
25
+ */
26
+ export type RestoreMode = "basic" | "reflowable";
20
27
  export interface TerminalRuntimeEntry {
21
28
  slot: number;
22
29
  workspaceId: string;
@@ -76,10 +83,22 @@ export declare class TerminalManager {
76
83
  private readonly logger?;
77
84
  /** Rotating hand-out point in the one-byte slot space (see `nextFreeSlot`). */
78
85
  private slotCursor;
86
+ /** Exit listeners (`onTerminalExit`) — fired once per terminal, covering both `kill()` and a
87
+ * PTY exiting on its own (`exit`, a crash). See `onExit` below for the single call site. */
88
+ private readonly exitListeners;
79
89
  constructor(options?: TerminalManagerOptions);
80
90
  /** All live terminal runtime entries. */
81
91
  list(): TerminalRuntimeEntry[];
82
92
  get(slot: number): TerminalRuntimeEntry | undefined;
93
+ /**
94
+ * Subscribe to every terminal exit — self-exit (the `exit` command, a crash) or an explicit
95
+ * `kill()` — regardless of which session, if any, triggered it. Fires exactly once per
96
+ * terminal, after its entry has already been removed from `list()`. The daemon has no
97
+ * dedicated close opcode on the binary terminal stream (`onExit` below); this is the seam a
98
+ * caller uses to relay the fact out-of-band, e.g. `registerTerminalHandlers` broadcasting
99
+ * `terminals_update`. Returns an unsubscribe fn.
100
+ */
101
+ onTerminalExit(listener: (slot: number) => void): () => void;
83
102
  /**
84
103
  * Spawn a PTY in the backend, assign a slot, and track the runtime entry.
85
104
  *
@@ -90,10 +109,15 @@ export declare class TerminalManager {
90
109
  */
91
110
  createTerminal(options: CreateTerminalOptions): TerminalRuntimeEntry;
92
111
  /**
93
- * Subscribe to a slot: emit a Snapshot frame (current screen) immediately, then live Output frames.
94
- * Does NOT resize the PTY (passive attach must not claim size). Returns an unsubscribe fn.
112
+ * Subscribe to a slot: emit exactly one restore frame (current screen) immediately, then live
113
+ * Output frames. `restoreMode: "reflowable"` (default `"basic"`) sends a `Restore` frame
114
+ * carrying `ScreenBuffer.serialize()` instead of the raw byte-ring `Snapshot` — computed here,
115
+ * on subscribe, not maintained continuously (an idle terminal must cost nothing extra). Does
116
+ * NOT resize the PTY (passive attach must not claim size). Returns an unsubscribe fn.
95
117
  */
96
- subscribe(slot: number, sink: TerminalFrameSink): () => void;
118
+ subscribe(slot: number, sink: TerminalFrameSink, opts?: {
119
+ restoreMode?: RestoreMode;
120
+ }): () => void;
97
121
  rename(slot: number, name: string): boolean;
98
122
  /** Forward input bytes to the PTY. */
99
123
  input(slot: number, bytes: Uint8Array): boolean;
@@ -127,9 +151,14 @@ export declare class TerminalManager {
127
151
  * whether `from` lands inside a sequence depends on where that sequence began, which may be
128
152
  * before `from` — the exact case a raw byte-offset cut produces.
129
153
  *
130
- * Bounded: if the sequence straddling `from` never terminates before `buffer.length`, drops to
131
- * `buffer.length` (nothing left of that unterminated tail is safe to replay) rather than scanning
132
- * past the buffer or emitting a partial sequence.
154
+ * Fallback (sprint-053/task-007): if the sequence straddling `from` never terminates before
155
+ * `buffer.length`, no position from `from` onward is provably safe but returning `buffer.length`
156
+ * (dropping the entire retained region) is needlessly pessimistic for what is usually a few stray
157
+ * bytes, e.g. `cat` on a binary file leaving one unterminated DCS. Falls back to the naive cut
158
+ * (`from` itself) instead: the emulator eats whatever garbage remains of that one sequence on
159
+ * replay (bounded — at most one sequence's worth), which is strictly more readable than an empty
160
+ * snapshot. This fallback only applies when no safe boundary exists at all; a legitimate
161
+ * mid-sequence cut with a real boundary later in the buffer still returns that boundary unchanged.
133
162
  */
134
163
  export declare function safeReplayStart(buffer: Uint8Array, from: number): number;
135
164
  //# sourceMappingURL=terminal-manager.d.ts.map
@@ -1,6 +1,7 @@
1
1
  import { encodeTerminalFrame, nextFreeSlot, SLOT_SPACE } from "@av-pi-studio/protocol";
2
2
  import { createDefaultPtyBackend, resolveExecutable, } from "./pty-backend.js";
3
3
  import { ScreenBuffer } from "./screen-buffer.js";
4
+ const textEncoder = new TextEncoder();
4
5
  /**
5
6
  * Grid bounds for any client-supplied size (`terminals.md` § PTY size ownership: "the daemon MUST
6
7
  * validate every requested size, whatever path it arrives on").
@@ -44,6 +45,9 @@ export class TerminalManager {
44
45
  logger;
45
46
  /** Rotating hand-out point in the one-byte slot space (see `nextFreeSlot`). */
46
47
  slotCursor = 1;
48
+ /** Exit listeners (`onTerminalExit`) — fired once per terminal, covering both `kill()` and a
49
+ * PTY exiting on its own (`exit`, a crash). See `onExit` below for the single call site. */
50
+ exitListeners = new Set();
47
51
  constructor(options = {}) {
48
52
  this.backend = options.backend ?? createDefaultPtyBackend();
49
53
  this.coalesceMs = options.coalesceMs ?? 4;
@@ -58,6 +62,18 @@ export class TerminalManager {
58
62
  get(slot) {
59
63
  return this.terminals.get(slot)?.entry;
60
64
  }
65
+ /**
66
+ * Subscribe to every terminal exit — self-exit (the `exit` command, a crash) or an explicit
67
+ * `kill()` — regardless of which session, if any, triggered it. Fires exactly once per
68
+ * terminal, after its entry has already been removed from `list()`. The daemon has no
69
+ * dedicated close opcode on the binary terminal stream (`onExit` below); this is the seam a
70
+ * caller uses to relay the fact out-of-band, e.g. `registerTerminalHandlers` broadcasting
71
+ * `terminals_update`. Returns an unsubscribe fn.
72
+ */
73
+ onTerminalExit(listener) {
74
+ this.exitListeners.add(listener);
75
+ return () => this.exitListeners.delete(listener);
76
+ }
61
77
  /**
62
78
  * Spawn a PTY in the backend, assign a slot, and track the runtime entry.
63
79
  *
@@ -132,15 +148,24 @@ export class TerminalManager {
132
148
  return entry;
133
149
  }
134
150
  /**
135
- * Subscribe to a slot: emit a Snapshot frame (current screen) immediately, then live Output frames.
136
- * Does NOT resize the PTY (passive attach must not claim size). Returns an unsubscribe fn.
151
+ * Subscribe to a slot: emit exactly one restore frame (current screen) immediately, then live
152
+ * Output frames. `restoreMode: "reflowable"` (default `"basic"`) sends a `Restore` frame
153
+ * carrying `ScreenBuffer.serialize()` instead of the raw byte-ring `Snapshot` — computed here,
154
+ * on subscribe, not maintained continuously (an idle terminal must cost nothing extra). Does
155
+ * NOT resize the PTY (passive attach must not claim size). Returns an unsubscribe fn.
137
156
  */
138
- subscribe(slot, sink) {
157
+ subscribe(slot, sink, opts) {
139
158
  const managed = this.terminals.get(slot);
140
159
  if (!managed)
141
160
  throw new Error(`no terminal in slot ${slot}`);
142
- // Snapshot first (rebuilds screen state), then live output.
143
- sink(encodeTerminalFrame({ opcode: "Snapshot", slot, data: managed.screen.bytes() }));
161
+ // Exactly one restore-tier frame first (rebuilds screen state), then live output.
162
+ if (opts?.restoreMode === "reflowable") {
163
+ const data = textEncoder.encode(managed.screenModel.serialize());
164
+ sink(encodeTerminalFrame({ opcode: "Restore", slot, data }));
165
+ }
166
+ else {
167
+ sink(encodeTerminalFrame({ opcode: "Snapshot", slot, data: managed.screen.bytes() }));
168
+ }
144
169
  managed.subscribers.add(sink);
145
170
  return () => {
146
171
  managed.subscribers.delete(sink);
@@ -246,6 +271,8 @@ export class TerminalManager {
246
271
  // Notify subscribers the terminal closed (empty Output then drop). Clients treat an exited
247
272
  // terminal as closed; no dedicated close opcode exists in the binary protocol.
248
273
  managed.subscribers.clear();
274
+ for (const listener of this.exitListeners)
275
+ listener(managed.entry.slot);
249
276
  }
250
277
  }
251
278
  /**
@@ -254,7 +281,8 @@ export class TerminalManager {
254
281
  * the sequence's tail — parameter digits, an SGR/cursor final byte, an OSC payload — which the
255
282
  * emulator on replay consumes as garbage input instead of the printable text it actually is,
256
283
  * corrupting everything after it. `safeReplayStart` finds the nearest safe boundary at or after the
257
- * naive cut instead; `SnapshotRing.compact` is its only caller.
284
+ * naive cut instead; `SnapshotRing.compact` AND the oversized-single-chunk path in
285
+ * `SnapshotRing.append` both call it.
258
286
  */
259
287
  const ESC = 0x1b;
260
288
  const BEL = 0x07;
@@ -275,9 +303,14 @@ function startsStringSequence(byte) {
275
303
  * whether `from` lands inside a sequence depends on where that sequence began, which may be
276
304
  * before `from` — the exact case a raw byte-offset cut produces.
277
305
  *
278
- * Bounded: if the sequence straddling `from` never terminates before `buffer.length`, drops to
279
- * `buffer.length` (nothing left of that unterminated tail is safe to replay) rather than scanning
280
- * past the buffer or emitting a partial sequence.
306
+ * Fallback (sprint-053/task-007): if the sequence straddling `from` never terminates before
307
+ * `buffer.length`, no position from `from` onward is provably safe but returning `buffer.length`
308
+ * (dropping the entire retained region) is needlessly pessimistic for what is usually a few stray
309
+ * bytes, e.g. `cat` on a binary file leaving one unterminated DCS. Falls back to the naive cut
310
+ * (`from` itself) instead: the emulator eats whatever garbage remains of that one sequence on
311
+ * replay (bounded — at most one sequence's worth), which is strictly more readable than an empty
312
+ * snapshot. This fallback only applies when no safe boundary exists at all; a legitimate
313
+ * mid-sequence cut with a real boundary later in the buffer still returns that boundary unchanged.
281
314
  */
282
315
  export function safeReplayStart(buffer, from) {
283
316
  if (from <= 0)
@@ -333,8 +366,9 @@ export function safeReplayStart(buffer, from) {
333
366
  }
334
367
  }
335
368
  // Ran off the end still inside an unterminated sequence (or a run of continuation bytes with no
336
- // following lead byte) — nothing from `from` onward is a safe start.
337
- return buffer.length;
369
+ // following lead byte) — no position is provably safe. Fall back to the naive cut rather than
370
+ // dropping everything (see the function doc comment's "Fallback" paragraph).
371
+ return from;
338
372
  }
339
373
  /**
340
374
  * Fraction of the cap the ring keeps when it compacts. The reclaimed headroom (the remaining
@@ -16,5 +16,5 @@ export interface TerminalRpcDeps {
16
16
  }
17
17
  export declare function registerTerminalHandlers(registry: HandlerRegistry, deps: TerminalRpcDeps, getActiveSessions: () => Iterable<Session>): void;
18
18
  /** Binary terminal-input frame handler for the frame dispatcher (Input/Resize opcodes). */
19
- export declare function makeTerminalBinaryHandler(manager: TerminalManager): BinaryHandler;
19
+ export declare function makeTerminalBinaryHandler(manager: TerminalManager, broadcast: TerminalRpcDeps["broadcast"], getActiveSessions: () => Iterable<Session>): BinaryHandler;
20
20
  //# sourceMappingURL=terminal-rpc.d.ts.map
@@ -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.94",
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.94",
28
- "@av-pi-studio/protocol": "^0.0.94",
29
- "@av-pi-studio/relay": "^0.0.94",
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",