@volter/supercode-teams 0.2.76 → 0.2.78

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.
package/client.mjs CHANGED
@@ -7,6 +7,7 @@ import { randomBytes } from 'node:crypto';
7
7
  import { EventEmitter } from 'node:events';
8
8
  import { Channel } from '@volter/teams/channel';
9
9
  import { openLocalMachine, openRemoteMachine, StreamRpc, fromWebPage } from '@volter/teams/client';
10
+ import { lineSplitter } from './lines.mjs';
10
11
 
11
12
  export { openLocalMachine, openRemoteMachine, StreamRpc, fromWebPage };
12
13
 
@@ -32,15 +33,9 @@ export async function execServerBridge(channel, harness, { host = '127.0.0.1' }
32
33
  catch (error) { if (own) ch?.close?.(); socket.close(1011, String(error.message ?? error).slice(0, 120)); return; }
33
34
  if (own) stream.on('close', () => ch.close?.());
34
35
  if (socket.readyState !== socket.OPEN) { stream.close('client closed'); return; }
35
- let buffer = '';
36
+ const lines = lineSplitter();
36
37
  stream.on('data', (chunk) => {
37
- buffer += Buffer.from(chunk, 'base64').toString('utf8');
38
- let nl;
39
- while ((nl = buffer.indexOf('\n')) >= 0) {
40
- const line = buffer.slice(0, nl);
41
- buffer = buffer.slice(nl + 1);
42
- if (line) socket.send(line);
43
- }
38
+ for (const line of lines(Buffer.from(chunk, 'base64').toString('utf8'))) if (line) socket.send(line);
44
39
  });
45
40
  stream.on('close', (reason) => socket.close(1000, String(reason ?? '').slice(0, 120)));
46
41
  for (const data of early.splice(0)) send(data);
@@ -62,15 +57,9 @@ export function harnessTransport(channel) {
62
57
  get running() { return stream !== null && !stream.closed; },
63
58
  async start(handlers) {
64
59
  stream = await channel.open('harness');
65
- let buffer = '';
60
+ const lines = lineSplitter();
66
61
  stream.on('data', (chunk) => {
67
- buffer += chunk;
68
- let index;
69
- while ((index = buffer.indexOf('\n')) >= 0) {
70
- const line = buffer.slice(0, index);
71
- buffer = buffer.slice(index + 1);
72
- if (line) handlers.onLine(line);
73
- }
62
+ for (const line of lines(chunk)) if (line) handlers.onLine(line);
74
63
  });
75
64
  stream.on('close', (reason) => handlers.onExit(null, reason ?? null));
76
65
  },
package/lines.mjs ADDED
@@ -0,0 +1,19 @@
1
+ /** Splits a stream's chunks into its complete lines, in time linear in its length: each chunk is scanned once and a
2
+ * line joined once, when its newline arrives. Appending to one buffer and searching it from the start again on
3
+ * every chunk is quadratic in a long line, and one answer (a large session's `sessions.load`, a whole machine's
4
+ * `sessions.discover`) is a line of tens of megabytes. */
5
+ export function lineSplitter() {
6
+ let pending = [];
7
+ return (chunk) => {
8
+ const text = String(chunk), lines = [];
9
+ let from = 0, at;
10
+ while ((at = text.indexOf('\n', from)) >= 0) {
11
+ pending.push(text.slice(from, at));
12
+ lines.push(pending.join(''));
13
+ pending = [];
14
+ from = at + 1;
15
+ }
16
+ if (from < text.length) pending.push(text.slice(from));
17
+ return lines;
18
+ };
19
+ }
package/machine.mjs CHANGED
@@ -37,6 +37,7 @@ import { TmuxTerminalHost, classifyAttentionSignal, composerHoldsDraft, composer
37
37
  // than failing the load: a POSIX daemon never needs it. Windows needs a supercode-terminal with PtyTerminalHost.
38
38
  import * as terminal from '@volter/supercode-terminal';
39
39
  import { findLiveReceipt, mintFrontendCredential } from '@volter/supercode-harness-sdk/live-runtime';
40
+ import { lineSplitter } from './lines.mjs';
40
41
  import { NODE_PROTOCOL } from '@volter/teams/channel';
41
42
  import { holds, observesPanes } from '@volter/teams/admission';
42
43
  import { MachineCore, MACHINE_VERSION } from '@volter/teams/machine-core';
@@ -163,7 +164,6 @@ export class TeamsMachine extends MachineCore {
163
164
  // How many pane attachments a principal may hold (limits.json); every refusal is logged.
164
165
  this.attachmentsByPrincipal = new Map(); // principal → live pane attachments
165
166
  this.serve = null;
166
- this.serveBuffer = '';
167
167
  this.routes = new Map(); // routing key (subscription/connection id) → connection index
168
168
  this.portsDir = join(home, 'ports');
169
169
  // Panes whose REMOTE input is refused. It survives a daemon restart:
@@ -228,14 +228,10 @@ export class TeamsMachine extends MachineCore {
228
228
  #startServe() {
229
229
  const child = spawn(this.supercodeBin, ['harness', 'serve'], { stdio: ['pipe', 'pipe', 'pipe'], env: this.env });
230
230
  child.stdout.setEncoding('utf8');
231
+ // A restarted serve starts on a line of its own, never on the tail its predecessor left unfinished.
232
+ const serveLines = lineSplitter();
231
233
  child.stdout.on('data', (chunk) => {
232
- this.serveBuffer += chunk;
233
- let index;
234
- while ((index = this.serveBuffer.indexOf('\n')) >= 0) {
235
- const line = this.serveBuffer.slice(0, index);
236
- this.serveBuffer = this.serveBuffer.slice(index + 1);
237
- if (line) this.#routeServeLine(line);
238
- }
234
+ for (const line of serveLines(chunk)) if (line) this.#routeServeLine(line);
239
235
  });
240
236
  child.stderr.setEncoding('utf8');
241
237
  child.stderr.on('data', (text) => this.emit('serve-stderr', text));
@@ -350,13 +346,9 @@ export class TeamsMachine extends MachineCore {
350
346
  entry.harness = stream;
351
347
  accept();
352
348
  this.log.append({ principal, cap: 'harness', target: '*', outcome: 'opened' });
353
- let buffer = '';
349
+ const lines = lineSplitter();
354
350
  stream.on('data', (chunk) => {
355
- buffer += chunk;
356
- let nl;
357
- while ((nl = buffer.indexOf('\n')) >= 0) {
358
- const line = buffer.slice(0, nl);
359
- buffer = buffer.slice(nl + 1);
351
+ for (const line of lines(chunk)) {
360
352
  if (!line) continue;
361
353
  let message;
362
354
  try { message = JSON.parse(line); } catch { continue; }
@@ -1048,13 +1040,9 @@ export class TeamsMachine extends MachineCore {
1048
1040
  }
1049
1041
 
1050
1042
  #serveNodeRpc(entry, stream) {
1051
- let buffer = '';
1043
+ const lines = lineSplitter();
1052
1044
  stream.on('data', async (chunk) => {
1053
- buffer += chunk;
1054
- let nl;
1055
- while ((nl = buffer.indexOf('\n')) >= 0) {
1056
- const line = buffer.slice(0, nl);
1057
- buffer = buffer.slice(nl + 1);
1045
+ for (const line of lines(chunk)) {
1058
1046
  if (!line) continue;
1059
1047
  let request;
1060
1048
  try { request = JSON.parse(line); } catch { continue; }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@volter/supercode-teams",
3
- "version": "0.2.76",
3
+ "version": "0.2.78",
4
4
  "type": "module",
5
5
  "description": "A machine daemon serving this machine's doors, and a local or remote Teams server for shared session discovery, OpenTelemetry ingestion and access to enrolled machines.",
6
6
  "license": "MIT",
package/terminal-look.mjs CHANGED
@@ -129,6 +129,17 @@ export function weztermConfig(profile, { fontDirs = existsSync(TERMINAL_FONTS) ?
129
129
  // arming the watcher waits on fseventsd: open windows stopped taking input for up to 2.3 s after each popup's
130
130
  // launch (sampled on macOS 26). The file is written for each launch, so the next process takes any change.
131
131
  'config.automatically_reload_config = false',
132
+ // Terminal.app's mouse: a drag, double or triple click selects text in the window and the highlight stays after
133
+ // the button comes up, for Cmd+C to copy. The pane's tmux asks for the mouse (so the wheel scrolls its history);
134
+ // without these the left button went to tmux, whose copy-mode drops the highlight on release.
135
+ 'local act = wezterm.action',
136
+ 'config.mouse_bindings = {',
137
+ ...[[1, 'Cell'], [2, 'Word'], [3, 'Line']].flatMap(([streak, unit]) => [
138
+ ` { event = { Down = { streak = ${streak}, button = 'Left' } }, mods = 'NONE', mouse_reporting = true, action = act.SelectTextAtMouseCursor '${unit}' },`,
139
+ ` { event = { Drag = { streak = ${streak}, button = 'Left' } }, mods = 'NONE', mouse_reporting = true, action = act.ExtendSelectionToMouseCursor '${unit}' },`,
140
+ ` { event = { Up = { streak = ${streak}, button = 'Left' } }, mods = 'NONE', mouse_reporting = true, action = act.CompleteSelection 'PrimarySelection' },`,
141
+ ]),
142
+ '}',
132
143
  'return config',
133
144
  '',
134
145
  ];