accelr8 3.8.2 → 4.0.0-next.1

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.
@@ -1,29 +1,41 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
- DEFAULT_MODEL,
4
- addAccount,
5
- defaultHandle,
6
- detectClaudeCode,
3
+ describeEnd,
4
+ rtcNodeMissingHint
5
+ } from "./chunk-CAQJPW54.js";
6
+ import {
7
7
  execute,
8
- loadAccounts,
9
- loadTokens,
10
- removeAccount,
11
- saveAccounts,
12
- saveToken,
13
8
  verifyAccount
14
- } from "./chunk-3JNUWI22.js";
9
+ } from "./chunk-NRXIFJGR.js";
15
10
  import {
16
- VoiceCapture,
17
- VoicePlayback,
18
- detectVoiceTools,
19
- listMicDevices,
20
11
  localTz,
21
12
  tzCity,
22
- tzToCoords,
13
+ tzToCoords
14
+ } from "./chunk-CUUNLVID.js";
15
+ import {
16
+ RelayVoice,
17
+ detectVoiceTools,
18
+ killAllHelpers,
19
+ listMicDevices,
23
20
  vlog,
24
- vlogSession,
25
21
  voiceLogPath
26
- } from "./chunk-HY6ULYGH.js";
22
+ } from "./chunk-UMPU6SAD.js";
23
+ import {
24
+ ensureSignedIn,
25
+ getAccessToken
26
+ } from "./chunk-3OTVRBTJ.js";
27
+ import {
28
+ DEFAULT_MODEL,
29
+ addAccount,
30
+ defaultHandle,
31
+ deleteCredentials,
32
+ detectClaudeCode,
33
+ loadAccounts,
34
+ loadCredentials,
35
+ loadTokens,
36
+ removeAccount,
37
+ saveToken
38
+ } from "./chunk-FVC2UPMV.js";
27
39
  import {
28
40
  CANVAS_H,
29
41
  CANVAS_W,
@@ -32,14 +44,16 @@ import {
32
44
  VERSION,
33
45
  decode,
34
46
  encode
35
- } from "./chunk-7JSMON7M.js";
47
+ } from "./chunk-5QXGOLQ3.js";
48
+ import {
49
+ accelr8Dir
50
+ } from "./chunk-62O2F73J.js";
36
51
 
37
52
  // src/client.ts
38
53
  import tk from "terminal-kit";
39
54
  import { WebSocket } from "ws";
40
55
  import { spawn as spawn2 } from "node:child_process";
41
- import { readFileSync, writeFileSync, mkdirSync, existsSync } from "node:fs";
42
- import { homedir } from "node:os";
56
+ import { readFileSync, writeFileSync, mkdirSync } from "node:fs";
43
57
  import { join } from "node:path";
44
58
 
45
59
  // src/logo.ts
@@ -56,10 +70,346 @@ var LOGO = [
56
70
  ];
57
71
  var LOGO_W = Math.max(...LOGO.map((l) => l.length));
58
72
 
73
+ // src/glyphs.ts
74
+ var ALPHABET = [
75
+ // presence & species
76
+ { ch: "\u25CF", meaning: "alive / on / selected (color disambiguates: good=presence, accent=selected)", surfaces: "everywhere presence or selection is shown" },
77
+ { ch: "\u25CB", meaning: "offline / unselected", surfaces: "roster, radios, join slots" },
78
+ { ch: "\u2726", meaning: "agent \u2014 bright phase / the heart", surfaces: "agent name sigil, orb die window" },
79
+ { ch: "\u2727", meaning: "agent \u2014 soft phase", surfaces: "agent surfaces only (the \u2726 gait)" },
80
+ // voice & input
81
+ { ch: "\u25B8", meaning: "a voice speaks", surfaces: "chat author arrow, HOME row cursor" },
82
+ { ch: "\u25B9", meaning: "sent \u2014 awaiting ack", surfaces: "optimistic chat send" },
83
+ { ch: "\u270E", meaning: "draft in progress", surfaces: "presence lane (humans AND agents)" },
84
+ { ch: "\u276F", meaning: "the machine awaits (input only)", surfaces: "input prompt, typed-name ritual" },
85
+ // structure
86
+ { ch: "\xB7", meaning: "the one connective + system whispers", surfaces: "everywhere" },
87
+ { ch: "\u2500", meaning: "light rule", surfaces: "rules, dividers" },
88
+ { ch: "\u2501", meaning: "heavy rule / active nav segment", surfaces: "underline nav, active tab" },
89
+ { ch: "\u2502", meaning: "light vertical", surfaces: "panels, ledger axis" },
90
+ { ch: "\u2503", meaning: "heavy vertical", surfaces: "canvas edge weight" },
91
+ { ch: "\u256D", meaning: "voice ring corner", surfaces: "trust screen (voice ring)" },
92
+ { ch: "\u256E", meaning: "voice ring corner", surfaces: "trust screen (voice ring)" },
93
+ { ch: "\u2570", meaning: "voice ring corner", surfaces: "trust screen (voice ring)" },
94
+ { ch: "\u256F", meaning: "voice ring corner", surfaces: "trust screen (voice ring)" },
95
+ { ch: "\u2554", meaning: "hands ring corner", surfaces: "trust screen (hands ring)" },
96
+ { ch: "\u2557", meaning: "hands ring corner", surfaces: "trust screen (hands ring)" },
97
+ { ch: "\u255A", meaning: "hands ring corner", surfaces: "trust screen (hands ring)" },
98
+ { ch: "\u255D", meaning: "hands ring corner", surfaces: "trust screen (hands ring)" },
99
+ { ch: "\u2550", meaning: "hands ring / double rule", surfaces: "trust screen, bar sweep" },
100
+ { ch: "\u2551", meaning: "hands ring vertical", surfaces: "trust screen" },
101
+ { ch: "\u250C", meaning: "panel corner", surfaces: "panels, popups" },
102
+ { ch: "\u2510", meaning: "panel corner", surfaces: "panels, popups" },
103
+ { ch: "\u2514", meaning: "panel corner", surfaces: "panels, popups" },
104
+ { ch: "\u2518", meaning: "panel corner", surfaces: "panels, popups" },
105
+ { ch: "\u258E", meaning: "code-fence border", surfaces: "fenced code in chat" },
106
+ { ch: "\u2039", meaning: "off-view / strain (left)", surfaces: "canvas edge chips" },
107
+ { ch: "\u203A", meaning: "off-view / strain (right)", surfaces: "canvas edge chips" },
108
+ // matter — the noise alphabet (condensation) + density ramp
109
+ { ch: " ", meaning: "void / noise floor", surfaces: "everywhere" },
110
+ { ch: ".", meaning: "noise", surfaces: "condensation, boot" },
111
+ { ch: ":", meaning: "noise", surfaces: "condensation, boot" },
112
+ { ch: "-", meaning: "noise", surfaces: "condensation, boot" },
113
+ { ch: "=", meaning: "noise", surfaces: "condensation, boot" },
114
+ { ch: "+", meaning: "noise (also: added, ascii)", surfaces: "condensation, boot, receipts" },
115
+ { ch: "\u2591", meaning: "density 1 / shadow", surfaces: "ramps, overlay shadow" },
116
+ { ch: "\u2592", meaning: "density 2", surfaces: "ramps, collapse" },
117
+ { ch: "\u2593", meaning: "density 3", surfaces: "ramps, collapse" },
118
+ { ch: "\u2588", meaning: "density 4 / block mass", surfaces: "ramps, wordmark, flash" },
119
+ { ch: "\u2580", meaning: "upper half block", surfaces: "wordmark" },
120
+ { ch: "\u2584", meaning: "lower half block", surfaces: "wordmark" },
121
+ { ch: "\u258A", meaning: "stream caret \u2014 live", surfaces: "agent streaming line" },
122
+ { ch: "\u2581", meaning: "stream caret \u2014 dying", surfaces: "agent streaming line" },
123
+ { ch: "\u25B0", meaning: "fuse \u2014 lit", surfaces: "reveal/undo/clear countdowns" },
124
+ { ch: "\u25B1", meaning: "fuse \u2014 spent", surfaces: "reveal/undo/clear countdowns" },
125
+ { ch: "\u258D", meaning: "block cursor", surfaces: "boot cursor, editor" },
126
+ { ch: "\u258F", meaning: "thin caret", surfaces: "modal field" },
127
+ // receipts
128
+ { ch: "\u2713", meaning: "done", surfaces: "copy receipts, allow" },
129
+ { ch: "\u2715", meaning: "refused / failed", surfaces: "verify fail, deny" },
130
+ { ch: "\u21BB", meaning: "retry / reroll", surfaces: "unacked send, name reroll" },
131
+ { ch: "@", meaning: "mention", surfaces: "chat, summon" },
132
+ // reserved
133
+ { ch: "\u25C9", meaning: "the app as one cell", surfaces: "micro face (w<3), the heart landing cameo" }
134
+ ];
135
+ var WHITELIST = new Set(ALPHABET.map((g) => g.ch));
136
+ function inRanges(cp, ranges) {
137
+ let lo = 0, hi = ranges.length - 1;
138
+ while (lo <= hi) {
139
+ const mid = lo + hi >> 1;
140
+ const r = ranges[mid];
141
+ if (cp < r[0]) hi = mid - 1;
142
+ else if (cp > r[1]) lo = mid + 1;
143
+ else return true;
144
+ }
145
+ return false;
146
+ }
147
+ var MARK = /[\p{Mn}\p{Me}]/u;
148
+ var ZERO_WIDTH = [[4448, 4607], [55216, 55295]];
149
+ var FORMAT = new RegExp("\\p{Cf}", "u");
150
+ var WIDE = [
151
+ [4352, 4447],
152
+ // Hangul Jamo
153
+ [8986, 8987],
154
+ // watch, hourglass (emoji presentation, East Asian Wide)
155
+ [9001, 9002],
156
+ [9193, 9196],
157
+ // fast-forward … double-down arrows
158
+ [9200, 9200],
159
+ // alarm clock
160
+ [9203, 9203],
161
+ // hourglass flowing
162
+ [9725, 9726],
163
+ // medium-small squares
164
+ [9748, 9749],
165
+ // umbrella with rain, hot beverage
166
+ [9800, 9811],
167
+ // zodiac
168
+ [9855, 9855],
169
+ // wheelchair
170
+ [9875, 9875],
171
+ // anchor
172
+ [9889, 9889],
173
+ // high voltage
174
+ [9898, 9899],
175
+ // medium circles
176
+ [9917, 9918],
177
+ // soccer ball, baseball
178
+ [9924, 9925],
179
+ // snowman, sun behind cloud
180
+ [9934, 9934],
181
+ // ophiuchus
182
+ [9940, 9940],
183
+ // no entry
184
+ [9962, 9962],
185
+ // church
186
+ [9970, 9971],
187
+ // fountain, flag in hole
188
+ [9973, 9973],
189
+ // sailboat
190
+ [9978, 9978],
191
+ // tent
192
+ [9981, 9981],
193
+ // fuel pump
194
+ [9989, 9989],
195
+ // check mark button
196
+ [9994, 9995],
197
+ // raised fist, raised hand
198
+ [10024, 10024],
199
+ // sparkles
200
+ [10060, 10060],
201
+ // cross mark
202
+ [10062, 10062],
203
+ // cross mark button
204
+ [10067, 10069],
205
+ // question / exclamation ornaments
206
+ [10071, 10071],
207
+ // exclamation mark
208
+ [10133, 10135],
209
+ // heavy plus / minus / divide
210
+ [10160, 10160],
211
+ // curly loop
212
+ [10175, 10175],
213
+ // double curly loop
214
+ [11035, 11036],
215
+ // large squares
216
+ [11088, 11088],
217
+ // star
218
+ [11093, 11093],
219
+ // heavy large circle
220
+ [11904, 12350],
221
+ // CJK radicals … Kangxi
222
+ [12352, 12871],
223
+ // Hiragana, Katakana, CJK symbols …
224
+ [12880, 13311],
225
+ // … (3248-324F, numbers on black squares, are Ambiguous: 1 cell)
226
+ [13312, 19903],
227
+ // CJK ext A
228
+ [19968, 40959],
229
+ // CJK unified
230
+ [40960, 42191],
231
+ // Yi
232
+ [43360, 43388],
233
+ // Hangul Jamo extended-A
234
+ [44032, 55203],
235
+ // Hangul syllables
236
+ [63744, 64255],
237
+ // CJK compatibility ideographs
238
+ [65040, 65049],
239
+ // vertical forms
240
+ [65072, 65135],
241
+ // CJK compatibility forms
242
+ [65280, 65376],
243
+ // FULLWIDTH forms (banned in chrome)
244
+ [65504, 65510],
245
+ // fullwidth signs
246
+ [94176, 94180],
247
+ [94192, 94193],
248
+ // ideographic symbols
249
+ [94208, 100343],
250
+ [100352, 101589],
251
+ [101632, 101640],
252
+ // Tangut, Khitan
253
+ [110576, 110579],
254
+ [110581, 110587],
255
+ [110589, 110590],
256
+ // kana extended-B
257
+ [110592, 111359],
258
+ // kana supplement / extended, small kana, Nushu
259
+ // U+1F000-1FAFF: only what East Asian Width calls W (every Emoji_Presentation
260
+ // emoji, plus the enclosed ideographs). The text-style pictographs between
261
+ // them (🛠 🗓 🖥 🗑 🏷 👁 🌡 🏳 🕊 🎙, most mahjong tiles and playing cards, a
262
+ // lone regional indicator, 🅰) are N: one cell. Unassigned code points in the
263
+ // emoji blocks count as W, because that is where new emoji are added.
264
+ // Generated from Unicode 15.0 EastAsianWidth plus the Unicode 16 emoji.
265
+ [126980, 126980],
266
+ [127183, 127183],
267
+ [127374, 127374],
268
+ [127377, 127386],
269
+ [127488, 127490],
270
+ [127504, 127547],
271
+ [127552, 127560],
272
+ [127568, 127569],
273
+ [127584, 127589],
274
+ [127744, 127776],
275
+ [127789, 127797],
276
+ [127799, 127868],
277
+ [127870, 127891],
278
+ [127904, 127946],
279
+ [127951, 127955],
280
+ [127968, 127984],
281
+ [127988, 127988],
282
+ [127992, 128062],
283
+ [128064, 128064],
284
+ [128066, 128252],
285
+ [128255, 128317],
286
+ [128331, 128334],
287
+ [128336, 128359],
288
+ [128378, 128378],
289
+ [128405, 128406],
290
+ [128420, 128420],
291
+ [128507, 128591],
292
+ [128640, 128709],
293
+ [128716, 128716],
294
+ [128720, 128722],
295
+ [128725, 128735],
296
+ [128747, 128751],
297
+ [128756, 128767],
298
+ [128992, 129003],
299
+ [129008, 129008],
300
+ [129292, 129338],
301
+ [129340, 129349],
302
+ [129351, 129535],
303
+ [129648, 129791],
304
+ [131072, 262141]
305
+ // CJK ext B+ (astral)
306
+ ];
307
+ var isRegionalIndicator = (cp) => cp >= 127462 && cp <= 127487;
308
+ var bmpWidth = new Uint8Array(65536);
309
+ function measure(cp) {
310
+ const ch = String.fromCodePoint(cp);
311
+ if (inRanges(cp, ZERO_WIDTH) || MARK.test(ch) || FORMAT.test(ch)) return 0;
312
+ return inRanges(cp, WIDE) ? 2 : 1;
313
+ }
314
+ function charWidth(cp) {
315
+ if (cp < 32 || cp >= 127 && cp < 160) return 0;
316
+ if (cp < 768) return 1;
317
+ if (cp < 65536) {
318
+ const v = bmpWidth[cp];
319
+ if (v) return v - 1;
320
+ const w = measure(cp);
321
+ bmpWidth[cp] = w + 1;
322
+ return w;
323
+ }
324
+ return measure(cp);
325
+ }
326
+ function stringWidth(s) {
327
+ let w = 0, i = 0;
328
+ for (; i < s.length; i++) {
329
+ const c = s.charCodeAt(i);
330
+ if (c >= 768) break;
331
+ if (c >= 32 && (c < 127 || c >= 160)) w++;
332
+ }
333
+ if (i === s.length) return w;
334
+ w = 0;
335
+ for (const g of clusters(s)) w += clusterWidth(g);
336
+ return w;
337
+ }
338
+ var PICTO = /[\p{Extended_Pictographic}\u{1F3FB}-\u{1F3FF}]/u;
339
+ function clusterWidth(g) {
340
+ let sum = 0, max = 0, ri = 0, emoji = false;
341
+ for (const ch of g) {
342
+ const cp = ch.codePointAt(0);
343
+ const w = charWidth(cp);
344
+ sum += w;
345
+ if (w > max) max = w;
346
+ if (isRegionalIndicator(cp)) {
347
+ ri++;
348
+ emoji = true;
349
+ } else if (!emoji && cp >= 169 && PICTO.test(ch)) emoji = true;
350
+ }
351
+ if (ri >= 2) return 2;
352
+ return emoji ? max : sum;
353
+ }
354
+ function cellPieces(g) {
355
+ const w = clusterWidth(g);
356
+ if (w === 0) return [];
357
+ for (const ch of g) {
358
+ const cp = ch.codePointAt(0);
359
+ if (isRegionalIndicator(cp) || cp >= 169 && PICTO.test(ch)) return [{ text: g, w, emoji: true }];
360
+ }
361
+ const out = [];
362
+ for (const ch of g.normalize("NFC")) {
363
+ const cp = ch.codePointAt(0);
364
+ const cw = charWidth(cp);
365
+ if (cw > 0) out.push({ text: ch, w: cw });
366
+ else if (out.length && !FORMAT.test(ch) && !(cp >= 65024 && cp <= 65039) && !(cp >= 917760 && cp <= 917999))
367
+ out[out.length - 1].text += ch;
368
+ }
369
+ return out;
370
+ }
371
+ function clusters(s) {
372
+ const seg = segmenter();
373
+ if (seg) {
374
+ const out = [];
375
+ for (const p of seg.segment(s)) out.push(p.segment);
376
+ return out;
377
+ }
378
+ return [...s];
379
+ }
380
+ var seg0;
381
+ function segmenter() {
382
+ if (seg0 === void 0) {
383
+ const Seg = Intl.Segmenter;
384
+ seg0 = Seg ? new Seg(void 0, { granularity: "grapheme" }) : null;
385
+ }
386
+ return seg0;
387
+ }
388
+ function truncateWidth(s, cells, tail = "") {
389
+ if (cells <= 0) return "";
390
+ if (stringWidth(s) <= cells) return s;
391
+ const budget = cells - stringWidth(tail);
392
+ if (budget < 0) return "";
393
+ let out = "";
394
+ let w = 0;
395
+ for (const g of clusters(s)) {
396
+ const gw = stringWidth(g);
397
+ if (w + gw > budget) break;
398
+ out += g;
399
+ w += gw;
400
+ }
401
+ return out + tail;
402
+ }
403
+ function padWidth(s, cells) {
404
+ const t = truncateWidth(s, cells);
405
+ return t + " ".repeat(Math.max(0, cells - stringWidth(t)));
406
+ }
407
+
59
408
  // src/chat.ts
60
409
  var CHAT_MAX = 1e3;
61
410
  var SPINNER_COLOR = -1;
62
411
  var SYSTEM_COLOR = -2;
412
+ var NOTICE_COLOR = -4;
63
413
  var DIM = 245;
64
414
  var FAINT = 245;
65
415
  var SELF_MENTION_BG = -3;
@@ -89,35 +439,57 @@ function gutterFor(prev, e) {
89
439
  const d = new Date(e.ts);
90
440
  return `${String(d.getHours()).padStart(2, "0")}:${String(d.getMinutes()).padStart(2, "0")}`;
91
441
  }
92
- function wrapRanges(cps, firstW, contW) {
442
+ var CONTROL = /[\x00-\x09\x0b-\x1f\x7f-\x9f]/;
443
+ var CONTROLS = /[\x00-\x09\x0b-\x1f\x7f-\x9f]/g;
444
+ function displayText(s) {
445
+ return CONTROL.test(s) ? s.replace(/\r\n/g, "\n").replace(CONTROLS, " ") : s;
446
+ }
447
+ function unitsOf(text) {
93
448
  const out = [];
449
+ let cp = 0;
450
+ for (const g of clusters(text)) {
451
+ const n = [...g].length;
452
+ out.push({ s: cp, e: cp + n, w: stringWidth(g), space: g === " ", nl: g.includes("\n") });
453
+ cp += n;
454
+ }
455
+ return out;
456
+ }
457
+ function wrapRanges(u, firstW, contW) {
458
+ const out = [];
459
+ const cpAt = (i) => i < u.length ? u[i].s : u.length ? u[u.length - 1].e : 0;
460
+ const push = (i, j) => out.push({ s: cpAt(i), e: j > i ? u[j - 1].e : cpAt(i) });
94
461
  let first = true;
95
462
  let segStart = 0;
96
- for (let j = 0; j <= cps.length; j++) {
97
- if (j < cps.length && cps[j] !== "\n") continue;
463
+ for (let j = 0; j <= u.length; j++) {
464
+ if (j < u.length && !u[j].nl) continue;
98
465
  let i = segStart;
99
466
  if (i === j) {
100
- out.push({ s: i, e: i });
467
+ push(i, i);
101
468
  first = false;
102
469
  }
103
470
  while (i < j) {
104
471
  const avail = Math.max(1, first ? firstW : contW);
105
472
  first = false;
106
- if (j - i <= avail) {
107
- out.push({ s: i, e: j });
473
+ let m = i, used = 0;
474
+ while (m < j && (m === i || used + u[m].w <= avail)) {
475
+ used += u[m].w;
476
+ m++;
477
+ }
478
+ if (m === j) {
479
+ push(i, j);
108
480
  break;
109
481
  }
110
482
  let brk = -1;
111
- for (let k = i + avail; k > i; k--) if (cps[k] === " ") {
483
+ for (let k = m; k > i; k--) if (u[k].space) {
112
484
  brk = k;
113
485
  break;
114
486
  }
115
487
  if (brk >= 0) {
116
- out.push({ s: i, e: brk });
488
+ push(i, brk);
117
489
  i = brk + 1;
118
490
  } else {
119
- out.push({ s: i, e: i + avail });
120
- i += avail;
491
+ push(i, m);
492
+ i = m;
121
493
  }
122
494
  }
123
495
  segStart = j + 1;
@@ -125,40 +497,53 @@ function wrapRanges(cps, firstW, contW) {
125
497
  return out;
126
498
  }
127
499
  function clampLine(line, width) {
128
- const cps = [...line.text];
129
- if (cps.length <= width) return line;
500
+ if (stringWidth(line.text) <= width) return line;
501
+ let kept = "", used = 0, cps = 0;
502
+ for (const g of clusters(line.text)) {
503
+ const w = stringWidth(g);
504
+ if (used + w > width) break;
505
+ kept += g;
506
+ used += w;
507
+ cps += [...g].length;
508
+ }
130
509
  return {
131
510
  ...line,
132
- text: cps.slice(0, width).join(""),
133
- spans: line.spans.filter((s) => s.start < width).map((s) => ({ ...s, end: Math.min(s.end, width) }))
511
+ text: kept,
512
+ spans: line.spans.filter((s) => s.start < cps).map((s) => ({ ...s, end: Math.min(s.end, cps) }))
134
513
  };
135
514
  }
136
- function systemLines(text, width) {
137
- const cps = [...`\xB7 ${text} \xB7`];
138
- return wrapRanges(cps, width, width).map((r) => clampLine({
515
+ function systemLines(text, width, color = SYSTEM_COLOR) {
516
+ const body = `\xB7 ${displayText(text)} \xB7`;
517
+ const cps = [...body];
518
+ return wrapRanges(unitsOf(body), width, width).map((r) => clampLine({
139
519
  text: cps.slice(r.s, r.e).join(""),
140
- spans: r.e > r.s ? [{ start: 0, end: r.e - r.s, color: SYSTEM_COLOR }] : [],
520
+ spans: r.e > r.s ? [{ start: 0, end: r.e - r.s, color }] : [],
141
521
  centered: true
142
522
  }, width));
143
523
  }
144
- function entryLines(e, mentions, width) {
524
+ function entryLines(e, mentions, width, noticer) {
525
+ const notice = noticer ? noticer(e) : null;
526
+ if (notice) return systemLines(notice, width, NOTICE_COLOR);
145
527
  if (e.kind === "system") return systemLines(e.text, width);
146
528
  const name = (e.kind === "agent" ? "\u2726" : "") + e.authorName;
147
529
  const nameLen = [...name].length;
148
530
  const pre = name + (e.streaming ? " \u283F" : "") + " \u25B8 ";
149
531
  const preLen = [...pre].length;
532
+ const preW = stringWidth(pre);
150
533
  const nameSpan = { start: 0, end: nameLen, color: e.color };
151
534
  if (!e.pending) nameSpan.bold = true;
152
535
  if (e.kind === "agent") nameSpan.italic = true;
153
536
  const preSpans = [nameSpan];
154
537
  if (e.streaming) preSpans.push({ start: nameLen + 1, end: nameLen + 2, color: SPINNER_COLOR });
155
538
  preSpans.push({ start: preLen - 2, end: preLen - 1, color: DIM });
156
- const cps = [...e.partial ? e.text + CUTOFF : e.text];
157
- const textLen = [...e.text].length;
539
+ const text = displayText(e.text);
540
+ const body = e.partial ? text + CUTOFF : text;
541
+ const cps = [...body];
542
+ const textLen = [...text].length;
158
543
  const bodySpans = e.pending ? [{ start: 0, end: cps.length, color: DIM }] : [...mentions];
159
544
  if (e.partial) bodySpans.push({ start: textLen, end: cps.length, color: FAINT });
160
- const indent = Math.min(preLen, INDENT_MAX);
161
- const ranges = wrapRanges(cps, Math.max(1, width - preLen), Math.max(1, width - indent));
545
+ const indent = Math.min(preW, INDENT_MAX);
546
+ const ranges = wrapRanges(unitsOf(body), Math.max(1, width - preW), Math.max(1, width - indent));
162
547
  return ranges.map((r, k) => {
163
548
  const col = k === 0 ? preLen : indent;
164
549
  const spans = k === 0 ? [...preSpans] : [];
@@ -169,10 +554,147 @@ function entryLines(e, mentions, width) {
169
554
  return clampLine({ text: (k === 0 ? pre : " ".repeat(indent)) + cps.slice(r.s, r.e).join(""), spans }, width);
170
555
  });
171
556
  }
557
+ var F_LOGIN = {
558
+ what: (where) => `claude code is logged out on ${where}`,
559
+ fixYou: () => "fix: run claude in a terminal, then type /login",
560
+ fixOther: (o) => `ask ${o} to run claude in a terminal and type /login`
561
+ };
562
+ var F_LIMIT = {
563
+ what: (_w, whose) => `${whose} claude plan hit its usage limit`,
564
+ fixYou: () => "fix: wait for the limit to reset, or press F2 and connect an api key",
565
+ fixOther: () => "try again after the limit resets"
566
+ };
567
+ var F_SPEND = {
568
+ what: (_w, whose) => `${whose} anthropic account hit its spend limit`,
569
+ fixYou: () => "fix: raise the spend limit in the Anthropic console",
570
+ fixOther: (o) => `ask ${o} to raise the spend limit`
571
+ };
572
+ var F_CREDITS = {
573
+ what: (_w, whose) => `${whose} provider account is out of credits`,
574
+ fixYou: () => "fix: add credits in your provider console",
575
+ fixOther: (o) => `ask ${o} to add credits`
576
+ };
577
+ var F_NO_CLI = {
578
+ what: (where) => `claude code is not installed on ${where}`,
579
+ fixYou: () => "fix: install claude code, or press F2 and connect an api key",
580
+ fixOther: (o) => `ask ${o} to install claude code`
581
+ };
582
+ var F_AUTH = {
583
+ what: (_w, whose) => `${whose} api key was rejected`,
584
+ fixYou: (h) => `fix: press F2, remove \u2726${h}, then connect a working key`,
585
+ fixOther: (o) => `ask ${o} to reconnect the key`
586
+ };
587
+ var F_MODEL = {
588
+ what: () => "the model is not available on that key",
589
+ fixYou: (h) => `fix: press F2, remove \u2726${h}, then connect it with another model`,
590
+ fixOther: (o) => `ask ${o} to pick another model`
591
+ };
592
+ var F_RATE = {
593
+ what: () => "the provider is rate limiting it",
594
+ fixYou: () => "try again in a minute",
595
+ fixOther: () => "try again in a minute"
596
+ };
597
+ var F_BUSY = {
598
+ what: () => "the provider is overloaded",
599
+ fixYou: () => "try again in a minute",
600
+ fixOther: () => "try again in a minute"
601
+ };
602
+ var F_NET = {
603
+ what: (where) => `${where} can't reach the provider`,
604
+ fixYou: () => "fix: check your internet connection",
605
+ fixOther: () => "try again later"
606
+ };
607
+ var SEP = "\\s*[\xB7\u2022\u2219]\\s*";
608
+ var CLI_LOGIN = new RegExp(`^(not logged in|invalid api key|oauth token (has )?(expired|been revoked|revoked))${SEP}please run /login\\.?$`);
609
+ var CLI_LIMIT = [
610
+ /^claude ai usage limit reached\|\d+$/,
611
+ // "Claude AI usage limit reached|1760000000"
612
+ /^claude (ai )?usage limit reached\.( your limit will reset at [^\n]{1,60})?$/,
613
+ new RegExp(`^((5-hour|weekly|session|daily|opus|sonnet)( weekly)?|usage) limit reached${SEP}resets? [^\\n]{1,60}$`),
614
+ new RegExp(`^you['\u2019]ve (hit|reached) your (usage )?limit${SEP}resets? [^\\n]{1,60}$`)
615
+ ];
616
+ var CLI_CREDITS = /^credit balance is too low\.?$/;
617
+ var CLI_NET = /^api error:?\s*\(?(request timed out|connection error|fetch failed)[.)]*$/;
618
+ var CLI_LIMIT_ANY = /claude (ai )?usage limit reached|\b(5-hour|weekly|session|daily|opus|sonnet)( weekly)? limit reached|you['’]ve (hit|reached) your (usage )?limit/;
619
+ function cliApiError(raw) {
620
+ const m = /^api error:?\s*(\d{3})\b\s*([\s\S]*)$/i.exec(raw.trim());
621
+ if (!m) return null;
622
+ const login = new RegExp(`${SEP}please run /login\\.?$`, "i");
623
+ const json = m[2].replace(login, "").trim();
624
+ let message = "";
625
+ if (json) {
626
+ if (!json.startsWith("{")) return null;
627
+ try {
628
+ const o = JSON.parse(json);
629
+ message = String(o?.error?.message ?? o?.message ?? "");
630
+ } catch {
631
+ return null;
632
+ }
633
+ }
634
+ const body = (json + (login.test(m[2]) ? " please run /login" : "")).toLowerCase();
635
+ return { status: Number(m[1]), body, message };
636
+ }
637
+ function classifyFailure(raw, reply) {
638
+ const s = raw.trim().toLowerCase();
639
+ if (reply) {
640
+ if (!s || s.includes("\n") || s.length > 600) return null;
641
+ if (CLI_LOGIN.test(s)) return F_LOGIN;
642
+ if (CLI_LIMIT.some((re) => re.test(s))) return F_LIMIT;
643
+ if (CLI_CREDITS.test(s)) return F_CREDITS;
644
+ if (CLI_NET.test(s)) return F_NET;
645
+ const api = cliApiError(raw);
646
+ if (!api) return null;
647
+ if (api.status === 401 || api.status === 403 || /please run \/login|oauth token/.test(api.body)) return F_LOGIN;
648
+ return providerFailure(api.body, api.status) ?? { unknown: `api error ${api.status}${api.message ? " \xB7 " + api.message : ""}` };
649
+ }
650
+ if (/claude cli not found|claude: command not found/.test(s)) return F_NO_CLI;
651
+ if (/please run \/login|^not logged in\b|oauth token (has )?(expired|been revoked|revoked)/.test(s)) return F_LOGIN;
652
+ const status = Number(/^api error:?\s*(\d{3})\b/.exec(s)?.[1] ?? 0);
653
+ return providerFailure(s, status) ?? { unknown: raw };
654
+ }
655
+ function providerFailure(s, status) {
656
+ if (/credit balance is too low|insufficient[ _](credit|balance|quota)|exceeded your current quota/.test(s)) return F_CREDITS;
657
+ if (/specified api usage limits/.test(s)) return F_SPEND;
658
+ if (status === 429 || /rate.?limit|too many requests/.test(s)) return F_RATE;
659
+ if (CLI_LIMIT_ANY.test(s)) return F_LIMIT;
660
+ if (status === 401 || status === 403 || /invalid x-api-key|invalid api key|incorrect api key|authentication|unauthorized|permission/.test(s)) return F_AUTH;
661
+ if (status === 404 || /not_found_error|model.{0,40}(not found|does not exist|not available)|invalid model|^model: \S+$/.test(s)) return F_MODEL;
662
+ if (status >= 500 || /overloaded|internal server error|service unavailable|bad gateway/.test(s)) return F_BUSY;
663
+ if (/network|fetch failed|econnrefused|enotfound|etimedout|econnreset|timed out|socket hang up|getaddrinfo/.test(s)) return F_NET;
664
+ return null;
665
+ }
666
+ var COULDNT_REPLY = /^@([a-z0-9][a-z0-9-]{2,19}) couldn't reply: ([\s\S]*)$/;
667
+ function agentNotice(e, ownerOf) {
668
+ let handle, raw, reply;
669
+ if (e.kind === "agent") {
670
+ if (e.streaming && !e.text) return null;
671
+ handle = e.authorName;
672
+ raw = e.text;
673
+ reply = true;
674
+ } else if (e.kind === "system") {
675
+ const m = COULDNT_REPLY.exec(e.text);
676
+ if (!m) return null;
677
+ handle = m[1];
678
+ raw = m[2];
679
+ reply = false;
680
+ } else return null;
681
+ const owner = ownerOf(handle);
682
+ if (reply && owner?.cli === false) return null;
683
+ const f = classifyFailure(raw, reply);
684
+ if (!f) return null;
685
+ if ("unknown" in f) {
686
+ const why = f.unknown.trim().replace(/\s+/g, " ").slice(0, 160) || "no reason given";
687
+ return `\u2726${handle} can't reply \xB7 ${why}`;
688
+ }
689
+ const where = owner?.you ? "this machine" : owner ? `${owner.name}'s machine` : "its owner's machine";
690
+ const whose = owner?.you ? "your" : owner ? `${owner.name}'s` : "its owner's";
691
+ const fix = owner?.you ? f.fixYou(handle) : f.fixOther(owner?.name ?? "its owner", handle);
692
+ return `\u2726${handle} can't reply \xB7 ${f.what(where, whose)} \xB7 ${fix}`;
693
+ }
172
694
  var makeCell = (e, ents) => ({
173
695
  e,
174
696
  ents,
175
- mentions: ents ? scanMentions(e.text, ents) : [],
697
+ mentions: ents ? scanMentions(displayText(e.text), ents) : [],
176
698
  cache: null
177
699
  });
178
700
  var emptyLine = () => ({ text: "", spans: [] });
@@ -185,13 +707,18 @@ var ChatLog = class {
185
707
  lastGutters = [];
186
708
  pinned = true;
187
709
  unseen = 0;
710
+ unseenRun = 0;
188
711
  /** Older pages may exist above; assume yes until a history page says no. */
189
712
  hasMoreHistory = true;
713
+ /** Optional: swap agent-failure entries for a plain notice (agentNotice).
714
+ * Consulted at wrap time; call dropCaches() when its answers change. */
715
+ noticer = null;
190
716
  get size() {
191
717
  return this.cells.length;
192
718
  }
193
719
  push(e, mentionEntities2) {
194
- this.cells.push(makeCell(e, mentionEntities2));
720
+ const cell = makeCell(e, mentionEntities2);
721
+ this.cells.push(cell);
195
722
  if (this.cells.length > CHAT_MAX) {
196
723
  this.cells.shift();
197
724
  if (this.anchor) {
@@ -199,7 +726,10 @@ var ChatLog = class {
199
726
  else this.anchor.offset = 0;
200
727
  }
201
728
  }
202
- if (!this.pinned) this.unseen++;
729
+ if (!this.pinned) {
730
+ this.unseen++;
731
+ cell.unseenRun = this.unseenRun;
732
+ }
203
733
  }
204
734
  /** Replace the whole log (fresh join / resync); re-pins to the tail. */
205
735
  reset(entries, mentionEntities2) {
@@ -229,13 +759,7 @@ var ChatLog = class {
229
759
  const i = this.cells.findIndex((c) => c.e.pending && c.e.cmid === cmid);
230
760
  if (i < 0) return null;
231
761
  const [cell] = this.cells.splice(i, 1);
232
- if (this.anchor) {
233
- if (this.anchor.index > i) this.anchor.index--;
234
- else if (this.anchor.index === i) {
235
- if (!this.cells.length) this.pinToTail();
236
- else this.anchor = { index: Math.min(i, this.cells.length - 1), offset: 0 };
237
- }
238
- }
762
+ this.forget(cell, i);
239
763
  return cell.e;
240
764
  }
241
765
  /** Append stream text to the matching streaming entry; only that
@@ -246,7 +770,7 @@ var ChatLog = class {
246
770
  const c = this.cells[i];
247
771
  if (!c.e.streaming || !match(c.e)) continue;
248
772
  c.e.text += delta;
249
- if (c.ents) c.mentions = scanMentions(c.e.text, c.ents);
773
+ if (c.ents) c.mentions = scanMentions(displayText(c.e.text), c.ents);
250
774
  c.cache = null;
251
775
  return true;
252
776
  }
@@ -262,6 +786,34 @@ var ChatLog = class {
262
786
  }
263
787
  return false;
264
788
  }
789
+ /** Remove a streaming entry that never received any text (the agent
790
+ * failed or ended silently), so no empty `✦name ▸` row is left behind.
791
+ * Returns the removed entry (so the caller can say what happened), or
792
+ * null when no empty stream matched. */
793
+ dropEmptyStream(match) {
794
+ for (let i = this.cells.length - 1; i >= 0; i--) {
795
+ const c = this.cells[i];
796
+ if (!c.e.streaming || !match(c.e)) continue;
797
+ if (c.e.text.trim()) return null;
798
+ this.cells.splice(i, 1);
799
+ this.forget(c, i);
800
+ return c.e;
801
+ }
802
+ return null;
803
+ }
804
+ /** Bookkeeping after removing `cell` from index i: the read position keeps
805
+ * its place, and a row that counted toward the "↓ N new" pill no longer
806
+ * does (it would count a message that is not there). */
807
+ forget(cell, i) {
808
+ if (cell.unseenRun === this.unseenRun && this.unseen > 0) this.unseen--;
809
+ if (this.anchor) {
810
+ if (this.anchor.index > i) this.anchor.index--;
811
+ else if (this.anchor.index === i) {
812
+ if (!this.cells.length) this.pinToTail();
813
+ else this.anchor = { index: Math.min(i, this.cells.length - 1), offset: 0 };
814
+ }
815
+ }
816
+ }
265
817
  /** End a stream; opts.partial marks it as cut off mid-generation. */
266
818
  finalize(match, opts) {
267
819
  for (let i = this.cells.length - 1; i >= 0; i--) {
@@ -312,6 +864,7 @@ var ChatLog = class {
312
864
  this.pinned = true;
313
865
  this.anchor = null;
314
866
  this.unseen = 0;
867
+ this.unseenRun++;
315
868
  }
316
869
  scrollToTop(width, viewH) {
317
870
  if (!this.topAnchor(width, viewH)) {
@@ -374,7 +927,7 @@ var ChatLog = class {
374
927
  // ---------- internals ----------
375
928
  wrapOf(i, width) {
376
929
  const c = this.cells[i];
377
- if (!c.cache || c.cache.width !== width) c.cache = { width, lines: entryLines(c.e, c.mentions, width) };
930
+ if (!c.cache || c.cache.width !== width) c.cache = { width, lines: entryLines(c.e, c.mentions, width, this.noticer) };
378
931
  return c.cache.lines;
379
932
  }
380
933
  gutterAt(i) {
@@ -838,7 +1391,8 @@ var CinePlayer = class {
838
1391
  const sx = Math.min(CINE_W - 1, Math.floor((x - offX) / scale));
839
1392
  const p = sy * CINE_W + sx;
840
1393
  const color = buf[COLOR_BASE + p * N + idx];
841
- cells.push({ x, y, cell: { ch: CINE_RAMP[buf[p * N + idx]] ?? " ", color, bgColor: color } });
1394
+ const ch = color === BAR ? " " : CINE_RAMP[buf[p * N + idx]] ?? " ";
1395
+ cells.push({ x, y, cell: { ch, color, bgColor: BAR } });
842
1396
  }
843
1397
  }
844
1398
  return { cells };
@@ -1303,6 +1857,408 @@ function microLine(w, engine, frame2, info) {
1303
1857
  return clampTo(tiny, Math.max(0, w));
1304
1858
  }
1305
1859
 
1860
+ // src/livekit-session.ts
1861
+ function voiceModeFrom(v) {
1862
+ const s = (v ?? "").trim().toLowerCase();
1863
+ return s === "livekit" ? "livekit" : s === "relay" ? "relay" : "auto";
1864
+ }
1865
+ var LIVEKIT_JOIN = { t: "voiceJoin", via: "livekit" };
1866
+ var VoiceSession = class {
1867
+ constructor(host, parts = {}) {
1868
+ this.host = host;
1869
+ this.parts = parts;
1870
+ this.mode = parts.mode ?? voiceModeFrom(process.env.ACCELR8_VOICE);
1871
+ this.device = parts.device;
1872
+ this.relay = new RelayVoice({
1873
+ send: (m) => host.send(m),
1874
+ status: (s) => host.status(s),
1875
+ currentProject: () => host.currentProject()?.id ?? null,
1876
+ setPeers: (p) => host.setPeers(p),
1877
+ now: host.now ? () => host.now() : void 0
1878
+ }, parts.relayParts ?? {}, parts.device);
1879
+ }
1880
+ host;
1881
+ parts;
1882
+ mode;
1883
+ /** the server's voiceCaps answer; null until one arrives (old servers never answer) */
1884
+ caps = null;
1885
+ relay;
1886
+ call = null;
1887
+ phase = "off";
1888
+ projectId = null;
1889
+ presence = false;
1890
+ // we told the room we are in voice
1891
+ unsub = null;
1892
+ gen = 0;
1893
+ // bumped by leave(): cancels a start in flight
1894
+ waiters = [];
1895
+ last = null;
1896
+ endStatus = null;
1897
+ device;
1898
+ wantTalk = false;
1899
+ // what ctrl-g last asked for (snapshots lag behind)
1900
+ capsAskedAt = null;
1901
+ // when we last sent voiceCaps
1902
+ capsWaiters = [];
1903
+ /** in voice, or on the way in */
1904
+ get active() {
1905
+ return this.relay.on || this.phase !== "off";
1906
+ }
1907
+ get transport() {
1908
+ return this.relay.on ? "relay" : this.phase !== "off" ? "livekit" : null;
1909
+ }
1910
+ /** 'window' | 'terminal' while a LiveKit call exists */
1911
+ get liveKitKind() {
1912
+ return this.call?.kind ?? null;
1913
+ }
1914
+ /** /voice and /voice headphones */
1915
+ toggle(opts = {}) {
1916
+ if (this.active) this.leave();
1917
+ else this.start(opts);
1918
+ }
1919
+ start(opts = {}) {
1920
+ if (this.active) return;
1921
+ const p = this.host.currentProject();
1922
+ if (!p) {
1923
+ this.host.status("join a project first");
1924
+ return;
1925
+ }
1926
+ if (this.mode === "relay") {
1927
+ if (opts.here) this.host.status("ACCELR8_VOICE=relay \xB7 using the relay");
1928
+ this.startRelay(p.id);
1929
+ return;
1930
+ }
1931
+ if (this.mode === "auto" && !opts.here && this.caps === null && this.capsPending()) {
1932
+ void this.startAfterCaps(p);
1933
+ return;
1934
+ }
1935
+ this.choose(p, opts.here === true);
1936
+ }
1937
+ choose(p, here) {
1938
+ const tryLiveKit = this.mode === "livekit" || here || this.caps === true;
1939
+ if (!tryLiveKit) {
1940
+ this.startRelay(p.id);
1941
+ return;
1942
+ }
1943
+ void this.startLiveKit(p, here);
1944
+ }
1945
+ capsWaitMs() {
1946
+ return this.parts.capsWaitMs ?? 3e3;
1947
+ }
1948
+ capsPending() {
1949
+ return this.capsAskedAt !== null && this.now() - this.capsAskedAt < this.capsWaitMs();
1950
+ }
1951
+ async startAfterCaps(p) {
1952
+ const gen = ++this.gen;
1953
+ this.phase = "asking";
1954
+ this.projectId = p.id;
1955
+ this.host.status("voice \xB7 connecting\u2026");
1956
+ await new Promise((resolve) => {
1957
+ const done = () => {
1958
+ clearTimeout(timer);
1959
+ const i = this.capsWaiters.indexOf(done);
1960
+ if (i >= 0) this.capsWaiters.splice(i, 1);
1961
+ resolve();
1962
+ };
1963
+ const timer = setTimeout(done, Math.max(0, (this.capsAskedAt ?? 0) + this.capsWaitMs() - this.now()));
1964
+ timer.unref?.();
1965
+ this.capsWaiters.push(done);
1966
+ });
1967
+ if (gen !== this.gen) return;
1968
+ this.phase = "off";
1969
+ this.projectId = null;
1970
+ if (this.host.currentProject()?.id !== p.id) return;
1971
+ this.choose(p, false);
1972
+ }
1973
+ startRelay(projectId) {
1974
+ const r = this.relay.start(projectId, { name: this.host.meName(), version: Number(this.host.version) || 0 });
1975
+ if (r === "install") this.host.offerInstall();
1976
+ }
1977
+ /** Tell the room whether this session is in voice (only on a change). */
1978
+ setPresence(on) {
1979
+ if (on === this.presence) return;
1980
+ this.presence = on;
1981
+ this.host.send(on ? { ...LIVEKIT_JOIN } : { t: "voiceLeave" });
1982
+ }
1983
+ async startLiveKit(p, here) {
1984
+ const gen = ++this.gen;
1985
+ this.phase = "asking";
1986
+ this.projectId = p.id;
1987
+ this.host.status("voice \xB7 connecting\u2026");
1988
+ this.setPresence(true);
1989
+ const r = await this.requestToken();
1990
+ if (gen !== this.gen) return;
1991
+ if (!r || !r.ok) {
1992
+ this.phase = "off";
1993
+ this.projectId = null;
1994
+ const why = !r ? "the server did not answer" : r.message;
1995
+ vlog(this.now(), "session", "no livekit token", { why, error: r && !r.ok ? r.error : "timeout" });
1996
+ const noLiveKit = r ? r.error === "voice_unconfigured" : this.caps !== true;
1997
+ if (this.mode === "auto" && noLiveKit) {
1998
+ this.presence = false;
1999
+ if (this.relay.start(p.id, { name: this.host.meName(), version: Number(this.host.version) || 0 }) === "install") {
2000
+ this.host.send({ t: "voiceLeave" });
2001
+ this.host.offerInstall();
2002
+ }
2003
+ return;
2004
+ }
2005
+ this.setPresence(false);
2006
+ if (r && !r.ok && (r.error === "rate" || r.error === "not_in_project" || r.error === "not_in_voice")) {
2007
+ this.host.status("voice \xB7 " + why);
2008
+ return;
2009
+ }
2010
+ if (this.mode === "livekit") {
2011
+ this.host.status(`voice \xB7 no LiveKit here: ${why} \xB7 ACCELR8_VOICE=relay to use the relay`);
2012
+ return;
2013
+ }
2014
+ this.host.status(r ? `voice \xB7 ${why} \xB7 try /voice again` : "voice \xB7 the server did not answer, try /voice again");
2015
+ return;
2016
+ }
2017
+ if (this.host.currentProject()?.id !== p.id) {
2018
+ this.phase = "off";
2019
+ this.projectId = null;
2020
+ this.setPresence(false);
2021
+ return;
2022
+ }
2023
+ this.phase = "opening";
2024
+ let call;
2025
+ try {
2026
+ if (here) {
2027
+ const tools = (this.parts.tools ?? detectVoiceTools)();
2028
+ if (!tools.ffmpegPath) {
2029
+ this.phase = "off";
2030
+ this.projectId = null;
2031
+ this.setPresence(false);
2032
+ this.host.offerInstall();
2033
+ return;
2034
+ }
2035
+ const join2 = this.parts.joinTerminal ?? (await import("./livekit-terminal-22Q2NFB5.js")).joinTerminalVoice;
2036
+ call = await join2({ url: r.url, token: r.token, room: r.room, identity: r.identity, tools, device: this.device });
2037
+ } else {
2038
+ const open = this.parts.openWindow ?? (await import("./livekit-window-XVOCBQOM.js")).openVoiceWindow;
2039
+ let first = { token: r.token, expiresAt: r.expiresAt };
2040
+ call = await open({
2041
+ liveKitUrl: r.url,
2042
+ room: r.room,
2043
+ identity: r.identity,
2044
+ title: p.name,
2045
+ getToken: async () => {
2046
+ if (first && first.expiresAt - this.now() > 6e4) return first.token;
2047
+ first = null;
2048
+ const again = await this.requestToken();
2049
+ if (!again || !again.ok) throw new Error(again && !again.ok ? again.message : "the server did not answer");
2050
+ return again.token;
2051
+ }
2052
+ });
2053
+ }
2054
+ } catch (e) {
2055
+ if (gen === this.gen) {
2056
+ this.phase = "off";
2057
+ this.projectId = null;
2058
+ this.setPresence(false);
2059
+ }
2060
+ const m = e instanceof Error ? e.message : String(e);
2061
+ vlog(this.now(), "session", "livekit client failed to start", { here, err: m });
2062
+ if (gen !== this.gen) return;
2063
+ if (here && /Cannot find (package|module)|ERR_MODULE_NOT_FOUND/i.test(m)) {
2064
+ this.host.status(rtcNodeMissingHint());
2065
+ } else {
2066
+ this.host.status("voice \xB7 " + m.slice(0, 90));
2067
+ }
2068
+ return;
2069
+ }
2070
+ if (gen !== this.gen) {
2071
+ void call.leave();
2072
+ return;
2073
+ }
2074
+ this.call = call;
2075
+ this.phase = "on";
2076
+ this.last = null;
2077
+ this.endStatus = null;
2078
+ this.wantTalk = !call.snapshot().muted;
2079
+ this.host.status(here ? "voice in this terminal \xB7 headphones only \xB7 ctrl-g to talk" : "voice window open \xB7 click Join there");
2080
+ this.unsub = call.subscribe((s) => this.onCallState(s));
2081
+ this.onCallState(call.snapshot());
2082
+ }
2083
+ onCallState(s) {
2084
+ const prev = this.last;
2085
+ this.last = s;
2086
+ const inCall = s.connection === "connected" || s.connection === "reconnecting";
2087
+ if (inCall) this.setPresence(true);
2088
+ if (s.connection === "disconnected") {
2089
+ this.setPresence(false);
2090
+ const why = this.endStatus ?? describeEnd(s.endReason) + (s.error && s.endReason !== "left" ? " \xB7 " + s.error.slice(0, 60) : "");
2091
+ vlog(this.now(), "session", "livekit call ended", { reason: s.endReason, error: s.error });
2092
+ this.unsub?.();
2093
+ this.unsub = null;
2094
+ this.call = null;
2095
+ this.phase = "off";
2096
+ this.projectId = null;
2097
+ this.endStatus = null;
2098
+ this.host.status(why);
2099
+ return;
2100
+ }
2101
+ if (prev && prev.muted !== s.muted) this.wantTalk = !s.muted;
2102
+ if (!prev || prev.needsClick !== s.needsClick) {
2103
+ if (s.needsClick) this.host.status("voice window open \xB7 click Join there");
2104
+ }
2105
+ if (s.connection === "connected" && prev?.connection !== "connected") {
2106
+ this.host.status(s.muted ? "voice on \xB7 muted \xB7 ctrl-g to talk" : "\u25CF live \xB7 ctrl-g or /mute to stop");
2107
+ } else if (s.connection === "reconnecting" && prev?.connection === "connected") {
2108
+ this.host.status("voice \xB7 reconnecting\u2026");
2109
+ } else if (prev && s.connection === "connected" && prev.muted !== s.muted) {
2110
+ this.host.status(s.muted ? "muted \xB7 ctrl-g or /talk to speak" : "\u25CF live \xB7 ctrl-g or /mute to stop");
2111
+ }
2112
+ if (s.error && s.error !== prev?.error) this.host.status("voice \xB7 " + s.error.slice(0, 90));
2113
+ }
2114
+ /** /voice while in voice, goHome, a project switch. */
2115
+ leave(quiet = false, why) {
2116
+ this.gen++;
2117
+ if (this.relay.on) {
2118
+ this.relay.leave(quiet || !!why);
2119
+ if (why) this.host.status(why);
2120
+ return;
2121
+ }
2122
+ if (this.phase === "asking" || this.phase === "opening") {
2123
+ this.phase = "off";
2124
+ this.projectId = null;
2125
+ this.setPresence(false);
2126
+ if (!quiet || why) this.host.status(why ?? "voice off");
2127
+ return;
2128
+ }
2129
+ if (this.call) {
2130
+ this.endStatus = why ?? "voice off";
2131
+ void this.call.leave().catch(() => void 0);
2132
+ }
2133
+ }
2134
+ /** ctrl-g toggles; /talk and /mute set. A typed /mute never opens the mic. */
2135
+ talk(on) {
2136
+ if (this.relay.on) {
2137
+ if (on === void 0) this.relay.toggleTalk();
2138
+ else this.relay.setTalk(on);
2139
+ return;
2140
+ }
2141
+ const c = this.call;
2142
+ if (!c) return;
2143
+ const s = c.snapshot();
2144
+ const next = on ?? !this.wantTalk;
2145
+ this.wantTalk = next;
2146
+ void c.setMuted(!next);
2147
+ if (s.connection !== "connected") {
2148
+ this.host.status(next ? "your mic opens once you are in the call" : "you will join muted");
2149
+ }
2150
+ }
2151
+ /** The room socket (re)joined a project. */
2152
+ onJoined(projectId) {
2153
+ if (this.mode !== "relay") {
2154
+ this.host.send({ t: "voiceCaps" });
2155
+ this.capsAskedAt = this.now();
2156
+ }
2157
+ if (this.relay.on) {
2158
+ this.relay.onJoined(projectId);
2159
+ return;
2160
+ }
2161
+ if (this.phase === "off") return;
2162
+ if (projectId !== this.projectId) {
2163
+ this.leave(true, "voice off \xB7 you switched projects");
2164
+ return;
2165
+ }
2166
+ if (this.presence) this.host.send({ ...LIVEKIT_JOIN });
2167
+ }
2168
+ /** Voice messages from the server. true = handled. */
2169
+ handle(msg) {
2170
+ switch (msg.t) {
2171
+ case "voiceCaps":
2172
+ this.caps = msg.livekit === true;
2173
+ for (const w of [...this.capsWaiters]) w();
2174
+ return true;
2175
+ case "voiceToken": {
2176
+ const w = this.waiters.shift();
2177
+ if (w) {
2178
+ clearTimeout(w.timer);
2179
+ w.resolve(msg);
2180
+ }
2181
+ return true;
2182
+ }
2183
+ case "voiceJoined":
2184
+ if (this.relay.on) return this.relay.handle(msg);
2185
+ if (Array.isArray(msg.peers)) this.host.setPeers(msg.peers);
2186
+ return true;
2187
+ case "voiceDrop":
2188
+ case "voiceStats":
2189
+ case "voiceFrame":
2190
+ return this.relay.on ? this.relay.handle(msg) : true;
2191
+ }
2192
+ return false;
2193
+ }
2194
+ requestToken() {
2195
+ return new Promise((resolve) => {
2196
+ const timer = setTimeout(() => {
2197
+ const i = this.waiters.findIndex((w) => w.timer === timer);
2198
+ if (i >= 0) this.waiters.splice(i, 1);
2199
+ resolve(null);
2200
+ }, this.parts.tokenTimeoutMs ?? 4e3);
2201
+ timer.unref?.();
2202
+ this.waiters.push({ resolve, timer });
2203
+ this.host.send({ t: "voiceToken", client: "tui" });
2204
+ });
2205
+ }
2206
+ indicator(peerCount) {
2207
+ if (this.relay.on) return this.relay.indicator(peerCount);
2208
+ if (this.phase === "off") return null;
2209
+ const s = this.call?.snapshot();
2210
+ if (!s) return { label: " \u25CC connecting\u2026", live: false };
2211
+ if (s.needsClick) return { label: " \u25CC click Join in the voice window", live: false };
2212
+ if (s.connection === "connecting") return { label: " \u25CC connecting\u2026", live: false };
2213
+ if (s.connection === "reconnecting") return { label: " \u25CC reconnecting\u2026", live: false };
2214
+ const n = Math.max(s.participants.length, peerCount);
2215
+ return s.muted ? { label: ` \u25CB voice ${n} \xB7 ctrl-g to talk`, live: false } : { label: " \u25CF live \xB7 ctrl-g to mute", live: true };
2216
+ }
2217
+ debugLines(peerCount) {
2218
+ const head = `mode: ${this.mode} \xB7 server LiveKit: ${this.caps === null ? "no answer" : this.caps ? "yes" : "no"}`;
2219
+ if (this.relay.on) return [head, ...this.relay.debugLines(peerCount)];
2220
+ if (this.call) return [head, ...this.call.debug()];
2221
+ return [head, `voice: ${this.phase === "off" ? "off" : this.phase}`];
2222
+ }
2223
+ async listDevices() {
2224
+ if (this.call) {
2225
+ const list = await this.call.listDevices("audioinput");
2226
+ return list.map((d) => ({ index: d.id, name: d.label }));
2227
+ }
2228
+ return listMicDevices();
2229
+ }
2230
+ setDevice(id) {
2231
+ if (this.call?.kind === "window") {
2232
+ if (id) void this.call.setInputDevice(id);
2233
+ return;
2234
+ }
2235
+ this.device = id;
2236
+ this.relay.setDevice(id);
2237
+ if (this.call && id) void this.call.setInputDevice(id);
2238
+ }
2239
+ resetTools() {
2240
+ this.relay.resetTools();
2241
+ }
2242
+ /** Quit path: synchronous. Every helper dies now; the voice window gets
2243
+ * 'bye' (and hangs up by itself if that never arrives). */
2244
+ shutdown() {
2245
+ try {
2246
+ this.relay.shutdown();
2247
+ } catch {
2248
+ }
2249
+ try {
2250
+ this.call?.quit();
2251
+ } catch {
2252
+ }
2253
+ this.call = null;
2254
+ this.phase = "off";
2255
+ killAllHelpers();
2256
+ }
2257
+ now() {
2258
+ return this.host.now ? this.host.now() : Date.now();
2259
+ }
2260
+ };
2261
+
1306
2262
  // src/world-data.ts
1307
2263
  var PACKED = `-92.32,48.24 -88.13,48.92 -83.11,46.27 -81.66,44.76 -82.09,42.29 -77.1,44.0 -69.95,46.92 -65.92,45.32 -66.37,44.25 -61.22,45.43 -64.94,47.34 -64.12,48.52 -70.68,47.02 -67.24,49.33 -59.82,50.48 -56.14,52.46 -59.07,53.58 -58.26,54.21 -60.69,55.33 -61.97,57.41 -64.35,59.49 -67.29,58.15 -69.89,59.91 -71.31,61.45 -78.22,61.97 -77.28,59.53 -77.09,55.88 -79.06,51.68 -82.23,52.7 -86.75,55.72 -92.17,56.86 -95.61,58.82 -92.66,62.02 -90.65,63.24 -95.96,64.12 -89.88,63.98 -89.3,65.22 -86.86,66.12 -84.54,66.88 -82.3,67.76 -83.1,69.68 -86.05,67.98 -88.18,68.2 -91.0,68.82 -91.72,69.69 -93.15,71.09 -96.58,71.05 -93.35,69.52 -94.23,68.25 -95.96,66.73 -98.83,68.27 -102.45,67.69 -108.34,68.43 -105.83,68.05 -108.15,66.6 -111.15,67.63 -114.1,68.23 -120.92,69.44 -124.32,69.26 -128.76,70.5 -131.86,69.19 -131.15,69.79 -135.81,69.13 -140.19,69.37 -141.2,69.58 -141.21,69.56 -142.49,69.83 -148.09,70.26 -154.37,70.96 -159.53,70.38 -166.64,68.25 -161.56,66.55 -162.99,65.97 -168.23,65.49 -161.12,64.49 -165.29,62.57 -164.58,60.06 -162.06,58.36 -157.85,58.12 -162.34,55.06 -156.52,57.11 -153.53,59.32 -149.18,60.81 -149.9,59.5 -146.54,60.36 -139.98,59.73 -137.12,58.28 -136.01,59.12 -133.84,57.12 -131.46,55.98 -132.08,57.2 -140.37,60.25 -141.21,60.16 -133.38,58.93 -130.88,54.83 -128.86,53.9 -126.58,52.12 -127.08,50.8 -124.42,49.66 -122.56,48.91 -122.44,48.92 -124.42,47.18 -124.52,42.48 -123.09,38.45 -121.73,36.62 -117.6,33.34 -117.28,32.64 -117.29,32.48 -114.75,27.8 -112.53,24.8 -110.55,24.07 -114.23,29.59 -112.58,29.99 -109.57,25.94 -105.61,21.94 -102.09,17.87 -95.75,15.94 -92.21,14.97 -92.22,14.71 -86.74,12.06 -83.03,8.65 -79.93,8.74 -77.0,7.82 -81.99,8.97 -83.92,12.7 -86.33,15.8 -88.4,15.92 -88.45,17.42 -87.01,21.33 -91.65,18.72 -96.96,20.37 -97.65,25.67 -97.62,25.82 -95.62,28.84 -90.77,29.03 -87.33,30.22 -82.69,28.15 -80.16,26.66 -80.74,32.31 -76.89,35.43 -76.47,38.21 -75.66,37.67 -71.31,41.76 -69.44,44.17 -67.69,47.03 -73.18,45.14 -79.26,43.28 -82.84,42.59 -83.49,45.32 -86.36,43.65 -87.75,43.42 -86.01,45.96 -87.0,46.59 -91.39,46.79 -90.05,47.96;-152.62,58.41 -152.6,58.4;-153.3,57.8 -152.4,57.48 -153.32,57.79;-166.96,53.96 -167.01,53.95;-168.36,53.5 -168.19,53.36;-170.73,52.68 -170.6,52.55;-174.47,51.94 -174.47,51.92;-176.58,51.71 -176.64,51.73;-177.55,51.76 -177.41,51.63;-178.27,51.75;177.35,51.8 177.33,51.76;172.44,53.0 172.55,53.03;-123.4,48.33 -128.0,50.84 -123.5,48.34;-132.49,52.88 -132.44,52.91;-132.64,53.02 -131.97,53.71 -132.63,53.02;-55.36,51.56 -54.66,49.52 -53.65,47.48 -52.98,46.31 -56.12,46.84 -58.47,47.57 -57.61,50.38 -55.39,51.53;-61.37,49.01 -61.8,49.29 -61.38,49.03;-63.01,46.71 -64.42,46.61 -63.04,46.68;-60.14,46.48 -60.14,46.5;-71.97,41.11 -71.97,41.15;-80.79,27.03 -81.01,26.99;-113.01,42.09 -113.1,42.01;-155.74,20.02 -155.73,19.98;-156.51,20.78 -156.51,20.78;-157.12,21.21 -157.08,20.95;-157.87,21.42;-159.53,22.07;-117.44,66.46 -119.59,65.24 -123.95,65.03 -123.69,66.44 -119.21,66.22 -117.44,66.44;-120.71,64.03 -114.91,62.3 -109.07,62.72 -112.62,61.19 -118.68,61.19 -117.01,61.17 -115.97,62.56 -119.46,64.0 -120.59,63.94;-112.31,58.46 -108.9,59.44 -104.14,58.9 -102.56,56.72 -101.82,58.73 -104.65,58.91 -111.0,58.51 -112.35,58.62;-98.74,50.09 -99.75,52.24 -99.62,51.47 -98.82,50.39;-97.02,50.21 -97.5,54.02 -98.69,52.93 -97.19,51.09 -96.98,50.2;-95.34,49.04 -92.32,50.34 -94.14,49.47 -95.36,48.82;-80.39,56.16 -79.22,55.94 -80.34,56.08;-103.56,58.6 -103.6,58.58;-101.82,58.03 -102.33,58.1 -101.77,58.06;-101.88,55.79 -97.92,57.15 -101.22,55.85 -101.88,55.74;-77.61,6.8 -78.7,0.97 -80.75,-4.47 -76.19,-14.57 -70.44,-18.75 -70.68,-26.15 -71.44,-32.03 -73.38,-37.27 -73.06,-42.11 -73.17,-46.09 -73.52,-48.05 -73.67,-51.56 -71.06,-53.88 -69.14,-50.77 -67.51,-46.59 -63.49,-42.8 -62.14,-40.16 -57.12,-36.71 -53.17,-34.15 -51.26,-32.02 -48.16,-25.48 -40.73,-22.32 -38.88,-15.24 -34.6,-7.81 -41.95,-3.42 -48.02,-1.84 -48.44,-1.57 -50.81,0.0 -54.47,5.39 -60.59,8.32 -64.19,9.88 -70.78,10.64 -70.97,11.89 -76.26,8.76 -77.61,6.8;-69.14,-52.79 -66.16,-55.08 -70.01,-54.88 -70.55,-53.85 -69.31,-52.81;-59.29,-51.58 -59.35,-51.54;-58.65,-51.55 -58.55,-51.56;-84.39,21.44 -73.9,19.73 -79.27,21.18 -83.74,21.8 -84.32,21.42;-66.96,17.95 -67.05,17.89;-77.88,17.22 -78.06,16.98;-74.47,18.08 -69.88,18.99 -71.1,17.76 -74.45,17.86;-85.28,73.74 -85.79,70.96 -85.13,71.94 -84.74,72.96 -80.61,73.1 -78.45,72.2 -75.44,72.55 -73.89,71.98 -72.56,71.04 -71.49,70.57 -69.78,70.29 -68.12,69.71 -65.91,69.19 -66.92,68.39 -64.08,67.68 -62.5,66.68 -63.07,65.33 -66.11,66.08 -67.48,65.41 -64.05,63.15 -66.58,63.26 -69.04,62.33 -72.22,63.77 -76.88,64.17 -73.25,65.54 -70.09,66.64 -72.05,67.44 -76.32,68.36 -78.34,70.17 -82.12,69.71 -87.64,70.12 -89.68,71.43 -85.28,73.74;-80.9,76.1 -84.21,76.28 -88.94,76.38 -85.47,77.4 -85.43,77.93 -87.01,78.54 -83.17,78.94 -84.87,79.93 -81.33,79.82 -76.27,80.92 -82.88,80.62 -82.58,81.16 -86.51,81.05 -89.36,81.21 -90.45,81.38 -89.28,81.86 -87.21,82.3 -80.51,82.05 -80.16,82.55 -77.83,82.86 -75.51,83.05 -71.18,82.9 -65.1,82.78 -63.34,81.8 -68.26,81.26 -69.46,80.34 -71.05,79.82 -74.4,79.46 -75.42,79.03 -75.48,78.92 -76.01,78.2 -80.66,77.28 -78.07,76.98 -80.9,76.13;-92.86,74.13 -92.5,72.7 -94.89,73.16 -92.96,74.14;-94.8,76.95 -89.68,76.04 -88.52,75.4 -82.36,75.67 -79.39,74.65 -86.15,74.22 -91.7,74.94 -95.6,76.91 -94.87,76.96;-99.96,73.74 -97.89,72.9 -98.28,71.13 -102.04,72.92 -101.34,73.14 -99.69,73.59;-107.58,73.25 -104.59,71.02 -101.71,69.56 -104.07,68.62 -106.61,69.12 -114.09,69.05 -113.89,70.12 -115.88,70.32 -116.1,71.32 -117.45,72.48 -113.53,72.44 -109.84,72.24 -106.62,71.71 -107.43,73.04;-120.96,74.29 -118.37,72.53 -123.06,71.18 -123.4,73.77 -120.93,74.27;-108.83,76.74 -106.25,75.54 -107.08,74.78 -112.99,74.16 -112.28,74.99 -116.04,75.33 -115.27,76.2 -110.95,75.56 -109.77,76.31 -108.82,76.7;-115.7,77.46 -118.1,76.3 -121.13,76.37 -116.04,77.28;-110.01,77.86 -112.36,77.68 -109.96,77.86;-109.6,78.48 -112.2,78.01 -109.6,78.48;-97.87,76.61 -99.21,75.31 -100.86,75.6 -99.4,76.26 -97.79,76.6;-94.72,75.53 -94.66,75.52;-104.1,79.01 -99.19,77.54 -103.22,78.08 -104.3,78.95;-93.74,77.52 -93.74,77.52;-96.88,78.5 -96.91,77.77 -96.94,78.48;-84.69,65.84 -81.58,63.87 -85.0,62.96 -84.63,65.71;-81.84,62.75 -82.01,62.63;-79.88,62.12 -79.88,62.12;-43.53,59.89 -45.29,60.67 -47.91,60.83 -49.9,62.41 -50.71,64.42 -51.39,64.94 -52.96,66.09 -53.62,67.19 -53.51,67.51 -51.84,68.65 -52.19,70.0 -51.85,71.03 -55.41,71.41 -54.63,72.97 -56.98,74.7 -61.95,76.09 -66.38,75.83 -71.13,77.0 -66.81,77.6 -70.78,77.78 -64.96,79.7 -63.38,81.16 -56.89,82.17 -48.18,82.15 -42.08,82.74 -38.02,83.54 -23.96,82.94 -25.97,81.97 -25.99,80.64 -13.57,80.97 -16.6,80.16 -19.82,78.82 -18.8,77.54 -21.98,76.46 -20.69,75.12 -21.78,74.4 -24.1,73.69 -26.54,73.08 -24.63,72.69 -21.84,71.69 -24.62,71.24 -27.16,70.89 -27.21,70.0 -24.1,69.35 -28.35,68.43 -32.48,68.56 -35.26,66.26 -37.9,65.9 -40.04,65.0 -40.49,64.04 -42.01,63.14 -42.88,61.15 -43.09,60.07 -43.56,59.9;-16.26,66.41 -15.32,64.29 -20.14,63.47 -21.76,64.21 -21.33,64.97 -23.04,65.62 -21.76,66.26 -18.77,66.12 -16.23,66.35;0.56,51.47 -1.71,54.94 -3.41,57.52 -5.42,58.14 -5.77,55.59 -3.48,54.82 -4.68,52.88 -2.68,51.58 -3.8,50.08 1.26,51.14 0.65,51.41;-7.17,54.91 -9.97,53.47 -8.52,51.76 -5.69,54.79 -7.34,55.25;-1.33,60.66 -1.17,60.38;-6.18,58.44 -6.09,58.36;-6.47,57.58 -6.33,57.54;-7.3,57.54;-7.46,57.05;-6.54,56.94;-6.0,55.94;-5.09,55.55;-4.44,54.38 -4.3,54.19;-8.08,71.02 -8.21,70.86;16.92,79.52 22.26,78.46 16.86,76.41 16.0,77.39 16.03,77.92 16.81,79.5;14.71,79.4 16.05,79.12 14.02,77.8 13.56,78.46 12.63,79.26 14.68,79.4;22.01,78.24 21.86,78.23;21.54,77.75 23.88,77.26 21.53,77.67 22.79,77.79;23.5,79.97 28.24,79.54 20.85,78.94 19.0,79.34 21.05,79.88 23.41,79.96;46.98,80.23 43.13,79.97 47.18,80.22;50.43,80.19 50.55,79.88 47.77,79.86 50.45,80.14;61.79,80.18 61.79,80.18;65.08,80.69 64.27,80.59 65.13,80.68;-5.13,35.66 4.06,36.63 10.4,37.12 11.36,33.61 20.1,30.1 23.49,32.17 31.65,30.8 35.76,23.74 39.75,14.82 42.93,11.34 51.52,11.45 49.82,6.99 43.13,-0.62 39.15,-7.58 40.37,-13.2 37.74,-18.17 35.33,-22.71 32.84,-28.15 26.5,-34.39 19.55,-35.51 17.5,-30.88 12.24,-18.75 13.89,-12.81 12.05,-5.55 9.67,0.14 7.19,3.79 1.74,5.39 -4.77,4.59 -12.0,6.75 -15.54,10.98 -16.33,15.5 -16.1,22.29 -12.9,27.12 -9.52,31.09 -5.41,35.58;33.71,0.0 33.48,-3.42 33.34,-0.2 33.71,0.0;49.3,-12.5 49.28,-18.79 43.95,-25.5 44.37,-20.08 46.34,-16.31 47.91,-14.08 49.3,-12.5;178.88,69.1 181.2,68.42 183.52,67.78 188.87,66.38 186.54,64.74 182.87,65.63 180.13,65.14 179.48,64.88 178.2,64.29 177.46,62.62 170.42,60.17 164.48,59.89 162.92,57.34 161.82,54.88 156.42,51.09 156.4,57.76 163.79,61.73 159.9,60.73 156.81,61.68 153.83,59.1 148.57,59.46 140.77,58.39 137.1,54.07 140.72,52.43 138.77,47.3 129.92,42.04 128.33,38.46 126.15,35.18 125.12,39.08 121.62,40.15 117.58,38.21 121.77,36.9 120.73,32.65 121.28,30.25 118.83,24.93 112.69,21.81 108.53,21.73 107.55,16.34 107.32,10.45 104.39,10.37 100.01,13.52 100.26,8.3 103.22,1.56 98.21,9.17 97.66,15.36 94.21,17.79 90.05,21.74 90.06,21.03 82.06,15.95 80.05,11.72 76.41,8.6 72.79,17.43 72.02,20.0 68.98,21.99 64.62,24.41 57.83,24.77 53.11,26.2 49.67,29.41 50.96,25.15 54.33,23.44 59.03,22.57 57.87,18.86 52.95,15.74 47.26,12.96 42.75,14.68 39.93,19.61 36.92,25.78 33.3,28.46 32.6,30.63 32.18,30.58 36.08,35.03 32.53,36.17 27.77,36.94 26.51,39.18 31.54,40.82 38.53,40.48 40.35,43.17 39.88,46.45 35.18,44.99 33.5,44.96 30.24,45.14 28.7,41.48 26.55,39.84 23.62,39.67 23.8,37.34 21.9,36.92 18.79,42.02 14.52,44.31 14.58,42.25 18.32,39.57 16.05,39.35 11.52,42.36 6.87,43.08 2.8,41.09 -1.11,37.14 -6.24,36.7 -8.67,39.57 -6.51,43.13 -0.84,45.55 -3.93,48.4 0.48,49.09 4.2,51.29 6.44,52.92 8.42,55.94 11.72,55.49 11.73,53.66 16.78,54.14 21.4,56.32 24.67,57.2 28.94,59.18 24.16,59.52 22.07,62.66 23.76,65.35 18.7,62.54 19.11,59.67 18.4,58.54 15.34,55.73 11.74,58.08 8.37,57.68 5.8,59.2 7.38,60.86 7.51,61.86 9.62,62.99 13.37,65.46 15.46,67.12 18.54,68.62 22.32,69.64 24.77,70.17 25.93,69.79 28.56,70.46 29.75,69.76 33.83,69.11 41.9,66.85 35.14,66.25 33.3,66.07 35.46,64.15 37.68,64.03 41.71,64.09 44.8,65.58 44.87,68.16 45.92,66.83 51.79,67.85 53.7,67.89 59.68,68.09 65.07,69.08 68.56,69.19 68.38,70.97 73.03,71.62 73.8,68.29 69.42,66.45 73.43,66.36 77.51,68.36 80.74,66.74 75.27,68.67 75.11,71.8 78.62,70.56 78.43,71.9 82.72,71.23 84.25,70.03 81.4,72.76 86.5,74.01 87.68,74.78 90.25,75.23 89.68,75.57 95.12,75.95 99.69,76.09 104.1,77.52 106.34,76.4 112.99,75.6 107.88,73.72 110.43,73.71 113.34,73.37 123.1,73.28 128.94,73.02 126.1,72.24 130.53,70.86 135.49,71.51 139.6,72.23 146.04,72.39 146.92,72.21 150.77,71.28 159.92,70.14 167.68,69.63 170.2,69.99 178.88,69.1;68.33,76.71 66.03,75.62 59.1,74.11 54.92,73.03 56.67,74.1 58.56,75.09 63.86,75.87 68.19,76.7;53.04,72.57 58.29,70.39 55.03,70.78 53.44,72.26 53.63,72.61;52.22,46.5 51.73,44.73 52.56,41.8 53.43,40.4 54.22,37.86 49.04,38.45 48.17,42.76 49.33,45.64 52.22,46.5;62.32,46.32 60.32,43.06 59.57,45.58 61.94,46.33;79.55,46.12 74.3,44.44 78.62,45.79 79.66,46.07;76.81,41.96 76.73,41.86;35.15,35.15 34.61,34.84 35.18,35.17;23.84,35.33 24.3,34.91 24.09,35.39;15.54,37.89 13.47,37.89 15.54,37.89;9.56,40.95 8.46,39.99 9.12,40.69;9.72,42.6 9.54,42.35;80.6,8.95 79.73,5.96 80.1,8.3;11.04,57.44 10.67,57.25;-77.92,24.67 -77.98,24.22;-77.61,23.62 -77.18,23.64;-75.55,24.13 -75.41,24.31;-91.4,-0.17 -91.52,-0.26;-60.25,46.68 -60.71,46.33;-63.89,49.47 -63.45,49.43;142.53,-10.6 145.62,-16.34 149.79,-22.09 153.21,-26.82 150.52,-35.19 145.6,-38.53 140.13,-37.69 137.34,-34.77 135.76,-34.56 131.5,-31.34 121.72,-33.65 115.62,-33.25 114.09,-26.01 114.88,-21.27 122.34,-18.13 125.32,-14.53 128.39,-14.9 132.35,-11.42 136.16,-12.43 138.07,-16.45 142.25,-10.78;144.72,-40.68 148.32,-42.14 145.57,-42.77 146.47,-41.19;172.86,-34.23 176.1,-37.52 177.06,-39.49 174.77,-38.03 172.83,-34.27;172.36,-40.53 172.92,-43.81 168.41,-46.13 170.26,-43.21 173.69,-40.94;150.74,-10.18 143.04,-8.26 138.48,-6.97 131.95,-2.94 130.91,-1.35 134.38,-2.64 141.24,-2.62 148.19,-8.15 150.75,-10.27;117.24,7.01 117.9,0.76 113.89,-3.5 109.44,-0.82 113.13,3.38 117.24,7.01;95.31,5.75 102.32,1.4 106.03,-2.98 101.46,-2.81 95.2,5.73;140.91,41.53 140.79,35.75 136.82,34.56 133.56,34.72 132.49,35.41 136.73,37.2 139.82,40.0 140.68,41.43;133.71,34.3 131.41,31.58 129.38,33.1 133.9,34.37;141.89,45.5 144.12,42.92 140.3,41.64 141.53,45.3 141.89,45.53;142.57,54.36 143.64,49.19 141.99,45.88 141.92,50.85 142.6,54.34;121.92,25.48 120.53,24.7 121.7,25.51;110.81,20.07 109.2,19.66 110.81,20.07;106.51,-6.16 114.15,-7.72 108.71,-7.89 106.51,-6.16;164.27,-20.01 164.16,-20.27;178.61,-17.04 178.61,-17.04;179.45,-16.43 179.35,-16.43;-172.55,-13.39 -172.61,-13.78;122.26,18.67 123.05,13.86 120.73,13.8 120.43,16.43 121.72,18.4;125.34,9.79 125.56,6.28 122.38,7.0 125.1,9.38;119.64,11.35 118.81,10.16 119.59,10.86 119.64,11.35;-179.87,65.14 -177.13,65.63 -173.46,64.74 -171.13,66.38 -176.48,67.78 -178.8,68.42;101.96,79.08 101.31,77.86 101.22,79.04;94.29,79.29 95.31,78.68 100.02,79.43 97.26,79.62 95.44,79.65;95.46,80.62 92.39,79.66 95.07,80.54;138.54,76.05 144.93,75.45 140.3,74.99 137.27,75.44 138.29,75.98;146.08,75.29 147.75,74.73 145.85,75.06;141.44,73.88 141.48,73.84;0.01,-71.68 6.57,-70.57 15.04,-70.44 25.1,-70.75 33.37,-69.1 38.46,-69.77 42.85,-68.16 46.59,-67.23 49.35,-66.96 52.9,-65.97 58.46,-67.2 63.6,-67.58 70.63,-68.41 69.24,-70.36 76.2,-69.44 88.08,-66.64 94.98,-66.52 101.53,-66.09 111.31,-65.91 118.64,-66.87 126.24,-66.24 133.09,-66.18 139.85,-66.72 146.86,-67.96 153.65,-68.82 159.94,-69.57 164.1,-70.67 170.19,-71.94 165.68,-74.64 163.82,-77.6 162.1,-78.95 166.72,-82.84 175.58,-83.86;-178.56,-84.37 -147.96,-85.4 -152.96,-81.12 -153.95,-79.5 -151.24,-77.48 -146.74,-76.44 -137.68,-75.16 -131.63,-74.63 -123.05,-74.41 -114.76,-73.97 -111.91,-75.41 -105.05,-74.77 -100.9,-74.21 -101.04,-73.18 -100.28,-73.06 -93.06,-73.33 -85.4,-73.18 -79.82,-73.04 -78.21,-72.52 -71.9,-73.41 -67.51,-71.1 -67.57,-68.92 -66.65,-66.83 -64.3,-65.28 -59.14,-63.74 -59.58,-64.37 -62.5,-65.94 -62.48,-66.66 -65.64,-68.02 -63.85,-69.07 -61.69,-70.87 -60.89,-72.71 -61.07,-74.3 -63.33,-75.88 -76.05,-77.06 -83.04,-77.12 -74.3,-80.83 -56.4,-82.14 -42.46,-81.65 -31.6,-80.17 -34.01,-79.2 -32.48,-77.28 -26.28,-76.18 -17.18,-73.45 -11.2,-72.01 -8.67,-71.98 -5.45,-71.45 -0.82,-71.74 0.07,-71.68;164.65,-77.89 170.95,-77.37 179.67,-78.25;-178.74,-78.24 -165.76,-78.47 -158.42,-77.73;-58.98,-64.63 -60.99,-68.62 -61.02,-71.7;-62.01,-74.94 -52.0,-77.07 -42.23,-77.8 -36.22,-78.03;-35.03,-77.81 -26.13,-75.54 -19.35,-73.04 -12.16,-71.86 -6.15,-70.65 -0.57,-69.14 4.93,-70.25 10.91,-69.99 16.52,-69.87 25.41,-70.22 32.13,-69.29 33.62,-69.58;70.56,-68.53 73.91,-69.51;81.42,-67.87 84.67,-66.41 89.07,-66.73;-135.79,-74.67 -124.34,-73.22 -116.65,-74.08 -109.93,-74.64 -105.36,-74.56 -105.83,-74.77;-69.3,-70.06 -71.33,-72.68 -71.42,-71.85 -75.1,-71.46 -71.79,-70.55 -70.34,-69.26 -69.34,-70.13;-49.2,-77.83 -44.59,-78.79 -44.14,-80.13 -59.04,-79.95 -49.28,-77.84 -48.24,-77.81;-58.13,-80.12 -63.25,-80.2 -58.32,-80.12;-163.64,-78.74 -161.2,-79.93 -163.62,-78.74;66.82,66.82 66.82,66.82`;
1308
2264
  var POLYLINES = PACKED.split(";").map(
@@ -1452,9 +2408,45 @@ function hover(rendered, x, y) {
1452
2408
  // src/client.ts
1453
2409
  var term = tk.terminal;
1454
2410
  var ScreenBuffer = tk.ScreenBuffer;
1455
- var CONF_DIR = join(homedir(), ".accelr8");
1456
- var ID_FILE = join(CONF_DIR, "identity.json");
1457
- var USER_COLORS = [39, 213, 220, 46, 208, 141, 51, 203, 118, 165];
2411
+ var CELL_TEXT_BYTES = 20;
2412
+ var ASCII_CHARS = Array.from({ length: 128 }, (_, i) => String.fromCharCode(i));
2413
+ var CellScreenBuffer = class extends ScreenBuffer {
2414
+ constructor(options) {
2415
+ super(options);
2416
+ }
2417
+ };
2418
+ {
2419
+ const P = CellScreenBuffer.prototype, base = ScreenBuffer.prototype;
2420
+ P.CHAR_SIZE = CELL_TEXT_BYTES;
2421
+ P.ITEM_SIZE = base.ATTR_SIZE + CELL_TEXT_BYTES;
2422
+ const clear = Buffer.alloc(P.ITEM_SIZE);
2423
+ clear.writeInt32BE(base.CLEAR_ATTR, 0);
2424
+ clear.write(" ", base.ATTR_SIZE);
2425
+ P.CLEAR_BUFFER = clear;
2426
+ P.writeChar = function(buffer, char, at) {
2427
+ const start = at + this.ATTR_SIZE;
2428
+ buffer.fill(0, start, start + CELL_TEXT_BYTES);
2429
+ return buffer.write(char, start, CELL_TEXT_BYTES);
2430
+ };
2431
+ P.readChar = function(buffer, at) {
2432
+ const start = at + this.ATTR_SIZE;
2433
+ const b0 = buffer[start];
2434
+ if (b0 < 128 && buffer[start + 1] === 0) return ASCII_CHARS[b0];
2435
+ let end = start + 1;
2436
+ while (end < start + CELL_TEXT_BYTES && buffer[end] !== 0) end++;
2437
+ return buffer.toString("utf8", start, end);
2438
+ };
2439
+ const unflag = (mask) => function(buffer, at) {
2440
+ const attr = buffer.readInt32BE(at);
2441
+ if (!(attr & mask)) return;
2442
+ buffer.writeInt32BE(attr & ~mask, at);
2443
+ this.writeChar(buffer, " ", at);
2444
+ };
2445
+ P.removeLeadingFullWidth = unflag(base.LEADING_FULLWIDTH);
2446
+ P.removeTrailingFullWidth = unflag(base.TRAILING_FULLWIDTH);
2447
+ P.removeFullWidth = unflag(base.LEADING_FULLWIDTH | base.TRAILING_FULLWIDTH);
2448
+ }
2449
+ var CONF_DIR = accelr8Dir();
1458
2450
  var DIM_PARTNER = {
1459
2451
  39: 25,
1460
2452
  213: 132,
@@ -1468,30 +2460,13 @@ var DIM_PARTNER = {
1468
2460
  165: 90
1469
2461
  };
1470
2462
  var dimOf = (color) => DIM_PARTNER[color] ?? 243;
1471
- function loadIdentity(nameOverride) {
1472
- let saved = {};
1473
- if (existsSync(ID_FILE)) {
1474
- try {
1475
- saved = JSON.parse(readFileSync(ID_FILE, "utf8"));
1476
- } catch {
1477
- }
1478
- }
1479
- const id = saved.id || "u_" + Math.random().toString(36).slice(2, 10);
1480
- const name = (nameOverride || saved.name || process.env.USER || "guest").slice(0, 16);
1481
- let h = 0;
1482
- for (const ch of id) h = h * 31 + ch.charCodeAt(0) >>> 0;
1483
- const color = USER_COLORS[h % USER_COLORS.length];
1484
- try {
1485
- mkdirSync(CONF_DIR, { recursive: true });
1486
- writeFileSync(ID_FILE, JSON.stringify({ id, name }));
1487
- } catch {
1488
- }
1489
- return { id, name, color };
1490
- }
1491
2463
  var me = { id: "u_local", name: process.env.USER || "guest", color: 39 };
1492
2464
  var SERVER = DEFAULT_SERVER;
1493
2465
  var autoJoinCode;
1494
2466
  var mySid = "";
2467
+ var pendingRename;
2468
+ var forceRefresh = false;
2469
+ var socketToken = "";
1495
2470
  var myAccounts = [];
1496
2471
  var runningExec = /* @__PURE__ */ new Map();
1497
2472
  var acctStatus = /* @__PURE__ */ new Map();
@@ -1501,29 +2476,43 @@ var overlayFocus = 0;
1501
2476
  var updateInfo = null;
1502
2477
  var task = null;
1503
2478
  var cancelTask = null;
2479
+ var updateRunning = false;
1504
2480
  function startUpdate() {
1505
2481
  if (task && task.phase === "running") return;
1506
- task = { title: "updating accelr8", phase: "running", line: "starting npm\u2026", okLabel: "quit now \xB7 enter", onOk: () => shutdown() };
1507
- import("./update-UEPNGB6Y.js").then(({ runUpdate }) => {
1508
- cancelTask = runUpdate({
2482
+ if (updateRunning) {
2483
+ state.status = "the last update is still stopping \xB7 try again in a moment";
2484
+ return;
2485
+ }
2486
+ const t = { title: "updating accelr8", phase: "running", line: "starting npm\u2026", okLabel: "quit now \xB7 enter", onOk: () => shutdown() };
2487
+ task = t;
2488
+ updateRunning = true;
2489
+ import("./update-RGDNBI4T.js").then(({ runUpdate }) => {
2490
+ if (task !== t) {
2491
+ updateRunning = false;
2492
+ return;
2493
+ }
2494
+ const cancel = runUpdate({
1509
2495
  onLine: (l) => {
1510
- if (task) task.line = l.slice(0, 120);
2496
+ if (task === t) t.line = l.slice(0, 120);
1511
2497
  },
1512
2498
  onDone: (ok, message) => {
1513
- if (task) {
1514
- task.phase = "done";
1515
- task.ok = ok;
1516
- task.line = message;
2499
+ updateRunning = false;
2500
+ if (task === t) {
2501
+ t.phase = "done";
2502
+ t.ok = ok;
2503
+ t.line = message;
1517
2504
  }
1518
- cancelTask = null;
2505
+ if (cancelTask === cancel) cancelTask = null;
1519
2506
  if (ok) updateInfo = null;
1520
2507
  }
1521
2508
  });
2509
+ if (updateRunning) cancelTask = cancel;
1522
2510
  }).catch(() => {
1523
- if (task) {
1524
- task.phase = "done";
1525
- task.ok = false;
1526
- task.line = "could not load the updater \xB7 npm i -g accelr8@latest";
2511
+ updateRunning = false;
2512
+ if (task === t) {
2513
+ t.phase = "done";
2514
+ t.ok = false;
2515
+ t.line = "could not load the updater \xB7 npm i -g accelr8@latest";
1527
2516
  }
1528
2517
  });
1529
2518
  }
@@ -1559,7 +2548,7 @@ function startVoiceInstall() {
1559
2548
  });
1560
2549
  },
1561
2550
  onOk: () => {
1562
- voiceTools = null;
2551
+ voice.resetTools();
1563
2552
  task = null;
1564
2553
  toggleVoice();
1565
2554
  }
@@ -1569,134 +2558,29 @@ function startVoiceInstall() {
1569
2558
  state.status = "could not load the installer";
1570
2559
  });
1571
2560
  }
1572
- var voiceTools = null;
1573
- var voiceOn = false;
1574
- var voiceReady = false;
1575
- var voiceTransmit = false;
1576
- var voiceCapture = null;
1577
- var voicePlayback = null;
1578
- var voiceDevice = process.env.ACCELR8_MIC || void 0;
1579
- var voiceRetries = 0;
1580
- var voiceSent = 0;
1581
- var voiceRejoinAt = 0;
1582
- var voiceHeardWarned = false;
1583
- var voiceRecv = /* @__PURE__ */ new Map();
1584
- function autoPickMic() {
1585
- if (process.platform === "darwin") {
1586
- const mics = listMicDevices();
1587
- if (!mics.length) return void 0;
1588
- const builtin = mics.find((m) => /macbook|built.?in|internal/i.test(m.name));
1589
- if (builtin) return builtin.index;
1590
- const notPhone = mics.find((m) => !/iphone|ipad|continuity|desk view/i.test(m.name));
1591
- return notPhone?.index;
1592
- }
1593
- if (process.platform === "win32") {
1594
- const mics = listMicDevices();
1595
- return mics[0]?.index;
1596
- }
1597
- return void 0;
1598
- }
1599
- function startCapture() {
1600
- if (!voiceCapture) return;
1601
- voiceCapture.start({
1602
- device: voiceDevice,
1603
- onChunk: (b64) => {
1604
- voiceRetries = 0;
1605
- if (!voiceTransmit) return;
1606
- voiceSent++;
1607
- if (voiceSent === 1) vlog(Date.now(), "transmit", "first frame sent to server");
1608
- else if (voiceSent % 125 === 0) vlog(Date.now(), "transmit", "still sending", { frames: voiceSent });
1609
- sendMsg({ t: "voiceFrame", data: b64 });
1610
- },
1611
- onError: (m) => {
1612
- if (!voiceTransmit) return;
1613
- voiceRetries++;
1614
- if (voiceRetries <= 6) {
1615
- state.status = `mic dropped \xB7 reconnecting ${voiceRetries}\u2026`;
1616
- setTimeout(() => {
1617
- if (voiceTransmit && voiceCapture && !voiceCapture.active) startCapture();
1618
- }, 200);
1619
- } else {
1620
- voiceTransmit = false;
1621
- voiceRetries = 0;
1622
- state.status = "mic keeps dropping \xB7 /voice mics to pick another \xB7 " + m.slice(0, 30);
1623
- }
1624
- }
1625
- });
1626
- }
1627
- function toggleVoice() {
1628
- if (voiceOn) return leaveVoice();
1629
- if (!state.project) {
1630
- state.status = "join a project first";
1631
- return;
1632
- }
1633
- voiceTools = voiceTools ?? detectVoiceTools();
1634
- if (!voiceTools.capture && !voiceTools.playback) {
1635
- startVoiceInstall();
1636
- return;
1637
- }
1638
- voicePlayback = new VoicePlayback(voiceTools);
1639
- voiceCapture = new VoiceCapture(voiceTools);
1640
- voiceOn = true;
1641
- voiceReady = false;
1642
- voiceTransmit = false;
1643
- voiceSent = 0;
1644
- voiceHeardWarned = false;
1645
- voiceRecv.clear();
1646
- if (voiceDevice === void 0 && voiceTools.capture) voiceDevice = autoPickMic();
1647
- vlogSession(Date.now(), {
1648
- platform: process.platform,
1649
- version: PROTO,
1650
- name: me.name,
1651
- ffmpeg: voiceTools.ffmpegPath ?? "MISSING",
1652
- ffplay: voiceTools.ffplayPath ?? "MISSING",
1653
- capture: voiceTools.capture,
1654
- playback: voiceTools.playback,
1655
- mic: voiceDevice ?? "default"
1656
- });
1657
- sendMsg({ t: "voiceJoin" });
1658
- state.status = "voice \xB7 connecting\u2026";
2561
+ var voice = new VoiceSession({
2562
+ send: (m) => sendMsg(m),
2563
+ status: (t) => {
2564
+ state.status = t;
2565
+ },
2566
+ currentProject: () => state.project ? { id: state.project.id, name: state.project.name } : null,
2567
+ setPeers: (p) => {
2568
+ state.voicePeers = p;
2569
+ },
2570
+ offerInstall: () => startVoiceInstall(),
2571
+ meName: () => me.name,
2572
+ version: PROTO
2573
+ }, { device: process.env.ACCELR8_MIC || void 0 });
2574
+ function toggleVoice(here = false) {
2575
+ voice.toggle({ here });
1659
2576
  }
1660
2577
  function leaveVoice() {
1661
- if (!voiceOn) return;
1662
- vlog(Date.now(), "session", "leaving voice", { sent: voiceSent, recvSpeakers: voiceRecv.size, recvTotal: [...voiceRecv.values()].reduce((a, b) => a + b, 0) });
1663
- voiceCapture?.stop();
1664
- voicePlayback?.stopAll();
1665
- voiceCapture = null;
1666
- voicePlayback = null;
1667
- voiceOn = false;
1668
- voiceReady = false;
1669
- voiceTransmit = false;
1670
- state.voicePeers = [];
1671
- sendMsg({ t: "voiceLeave" });
1672
- state.status = "voice off";
1673
- }
1674
- function toggleTalk() {
1675
- if (!voiceOn || !voiceCapture) return;
1676
- if (!voiceTools?.capture) {
1677
- state.status = "no mic \xB7 install ffmpeg to talk";
1678
- return;
1679
- }
1680
- if (voiceTransmit) {
1681
- voiceTransmit = false;
1682
- voiceRetries = 0;
1683
- voiceCapture.stop();
1684
- state.status = "muted \xB7 /talk (or ctrl-g) to speak";
1685
- } else {
1686
- if (!voiceReady) {
1687
- const now = Date.now();
1688
- if (now - voiceRejoinAt > 1e3) {
1689
- voiceRejoinAt = now;
1690
- sendMsg({ t: "voiceJoin" });
1691
- }
1692
- }
1693
- voiceTransmit = true;
1694
- voiceRetries = 0;
1695
- voiceHeardWarned = false;
1696
- state.status = "\u25CF live \xB7 /mute (or ctrl-g) to stop";
1697
- startCapture();
1698
- }
2578
+ voice.leave();
2579
+ }
2580
+ function toggleTalk(on) {
2581
+ voice.talk(on);
1699
2582
  }
2583
+ var micChoices = [];
1700
2584
  function openHelp() {
1701
2585
  overlay = "help";
1702
2586
  overlayFocus = 0;
@@ -1711,7 +2595,7 @@ function openRadio() {
1711
2595
  }
1712
2596
  function goHome() {
1713
2597
  if (state.project) {
1714
- if (voiceOn) leaveVoice();
2598
+ if (voice.active) leaveVoice();
1715
2599
  overlay = null;
1716
2600
  state.screen = "home";
1717
2601
  sendMsg({ t: "tab", tab: "home" });
@@ -1737,6 +2621,13 @@ var chatLog = new ChatLog();
1737
2621
  var editor = new LineEditor();
1738
2622
  var popup = new MentionPopup();
1739
2623
  var orbEngine = new OrbEngine();
2624
+ chatLog.noticer = (e) => agentNotice(e, (handle) => {
2625
+ const ag = state.agents.find((a) => a.handle === handle);
2626
+ if (!ag) return null;
2627
+ const you = ag.ownerId === me.id;
2628
+ const cli = ag.provider === "anthropic" && !(you && myAccounts.some((a) => a.handle === handle && a.auth.kind === "api_key"));
2629
+ return { name: memberOf(ag.ownerId)?.name ?? ag.ownerName, you, cli };
2630
+ });
1740
2631
  var unread = { t0: 0, t2: 0 };
1741
2632
  var ghosts = /* @__PURE__ */ new Map();
1742
2633
  var cursors = /* @__PURE__ */ new Map();
@@ -1832,6 +2723,7 @@ function chatVisible() {
1832
2723
  }
1833
2724
  var BOOT_MIN_W = 60;
1834
2725
  var BOOT_MIN_H = 16;
2726
+ var BOOT_ACCENT = 39;
1835
2727
  var boot = null;
1836
2728
  var bootStart = 0;
1837
2729
  var bootWasFull = false;
@@ -1930,6 +2822,32 @@ var syncFrom = 0;
1930
2822
  var syncBuffer = [];
1931
2823
  function connect() {
1932
2824
  state.status = "connecting to " + SERVER + " ...";
2825
+ const force = forceRefresh;
2826
+ forceRefresh = false;
2827
+ getAccessToken(SERVER, { force }).then((tok) => {
2828
+ if (!tok) {
2829
+ signedOut();
2830
+ return;
2831
+ }
2832
+ openSocket(tok.token);
2833
+ }, () => {
2834
+ if (!reconnectTimer) reconnectTimer = setTimeout(() => {
2835
+ reconnectTimer = null;
2836
+ connect();
2837
+ }, 2e3);
2838
+ });
2839
+ }
2840
+ function authLost() {
2841
+ const saved = loadCredentials(SERVER);
2842
+ if (saved && socketToken && saved.accessToken !== socketToken) return;
2843
+ signedOut();
2844
+ }
2845
+ function signedOut() {
2846
+ deleteCredentials(SERVER, void 0, (saved) => !socketToken || saved.accessToken === socketToken);
2847
+ shutdownWithMessage(`You are signed out of ${SERVER}. Sign in again with: accelr8 login`);
2848
+ }
2849
+ function openSocket(token) {
2850
+ socketToken = token;
1933
2851
  try {
1934
2852
  ws = new WebSocket(SERVER);
1935
2853
  } catch {
@@ -1940,7 +2858,14 @@ function connect() {
1940
2858
  state.connected = true;
1941
2859
  state.status = "";
1942
2860
  lastPong = Date.now();
1943
- sendMsg({ t: "hello", v: PROTO, user: me, tz: localTz(), executes: myAccounts.map((a) => ({ handle: a.handle, provider: a.provider, model: a.model })) });
2861
+ sendMsg({
2862
+ t: "hello",
2863
+ v: PROTO,
2864
+ token,
2865
+ ...pendingRename ? { name: pendingRename } : {},
2866
+ tz: localTz(),
2867
+ executes: myAccounts.map((a) => ({ handle: a.handle, provider: a.provider, model: a.model }))
2868
+ });
1944
2869
  if (autoJoinCode && !state.project) sendMsg({ t: "joinProject", code: autoJoinCode });
1945
2870
  else if (state.project) {
1946
2871
  rejoining = true;
@@ -1976,9 +2901,19 @@ function sendMsg(msg) {
1976
2901
  }
1977
2902
  function handleServer(msg) {
1978
2903
  if (!msg || typeof msg !== "object" || typeof msg.t !== "string") return;
2904
+ if (msg.t !== "voice" && voice.handle(msg)) return;
1979
2905
  switch (msg.t) {
1980
2906
  case "welcome":
1981
2907
  mySid = msg.sid;
2908
+ if (msg.you && typeof msg.you.id === "string" && typeof msg.you.name === "string") {
2909
+ me = { id: msg.you.id, name: msg.you.name, color: Number(msg.you.color) || me.color };
2910
+ pendingRename = void 0;
2911
+ }
2912
+ break;
2913
+ case "notice":
2914
+ if (typeof msg.text !== "string") break;
2915
+ state.status = msg.text;
2916
+ if (state.project) chatLog.push({ seq: null, kind: "system", authorId: "system", authorName: "system", color: 240, text: msg.text, ts: Date.now() }, mentionEntities());
1982
2917
  break;
1983
2918
  case "projects":
1984
2919
  state.projects = Array.isArray(msg.list) ? msg.list : [];
@@ -1992,6 +2927,7 @@ function handleServer(msg) {
1992
2927
  state.sessions = Array.isArray(msg.sessions) ? msg.sessions : [];
1993
2928
  state.agents = Array.isArray(msg.agents) ? msg.agents : [];
1994
2929
  state.secrets = Array.isArray(msg.secrets) ? msg.secrets : [];
2930
+ chatLog.dropCaches();
1995
2931
  cursors.clear();
1996
2932
  ghosts.clear();
1997
2933
  if (rejoining && sameProject) {
@@ -2011,13 +2947,8 @@ function handleServer(msg) {
2011
2947
  }
2012
2948
  rejoining = false;
2013
2949
  attachMyAgents();
2014
- if (voiceOn && sameProject) {
2015
- voiceReady = false;
2016
- vlog(Date.now(), "transport", "re-sending voiceJoin after (re)connect");
2017
- sendMsg({ t: "voiceJoin" });
2018
- } else if (Array.isArray(msg.voicePeers)) {
2019
- state.voicePeers = msg.voicePeers;
2020
- }
2950
+ if (Array.isArray(msg.voicePeers)) state.voicePeers = msg.voicePeers;
2951
+ voice.onJoined(msg.project.id);
2021
2952
  break;
2022
2953
  }
2023
2954
  case "agentToken":
@@ -2055,10 +2986,22 @@ function handleServer(msg) {
2055
2986
  }
2056
2987
  break;
2057
2988
  }
2058
- case "agentEnd":
2059
- chatLog.finalize((e) => e.invId === msg.invId && !!e.streaming, { partial: msg.status === "partial" });
2060
- if (msg.status !== "done") chatLog.appendDelta((e) => e.invId === msg.invId, "");
2989
+ case "agentEnd": {
2990
+ const dropped = chatLog.dropEmptyStream((e) => e.invId === msg.invId);
2991
+ if (!dropped) chatLog.finalize((e) => e.invId === msg.invId && !!e.streaming, { partial: msg.status !== "done" });
2992
+ else if (msg.status === "done") {
2993
+ chatLog.push({
2994
+ seq: null,
2995
+ kind: "system",
2996
+ authorId: "system",
2997
+ authorName: "system",
2998
+ color: 245,
2999
+ text: `\u2726${dropped.authorName} ended without a reply`,
3000
+ ts: Date.now()
3001
+ });
3002
+ }
2061
3003
  break;
3004
+ }
2062
3005
  case "joinError":
2063
3006
  rejoining = false;
2064
3007
  syncing = false;
@@ -2074,6 +3017,7 @@ function handleServer(msg) {
2074
3017
  break;
2075
3018
  case "members":
2076
3019
  state.members = Array.isArray(msg.members) ? msg.members : [];
3020
+ chatLog.dropCaches();
2077
3021
  break;
2078
3022
  case "cursor": {
2079
3023
  if (msg.sid === mySid) break;
@@ -2144,6 +3088,7 @@ function handleServer(msg) {
2144
3088
  }
2145
3089
  case "agents":
2146
3090
  state.agents = Array.isArray(msg.agents) ? msg.agents : [];
3091
+ chatLog.dropCaches();
2147
3092
  break;
2148
3093
  case "secrets":
2149
3094
  state.secrets = Array.isArray(msg.list) ? msg.list : [];
@@ -2151,49 +3096,6 @@ function handleServer(msg) {
2151
3096
  case "voice":
2152
3097
  state.voicePeers = Array.isArray(msg.peers) ? msg.peers : [];
2153
3098
  break;
2154
- case "voiceJoined": {
2155
- const first = !voiceReady;
2156
- voiceReady = true;
2157
- state.voicePeers = Array.isArray(msg.peers) ? msg.peers : [];
2158
- vlog(Date.now(), "transport", "voiceJoined ACK", { peers: state.voicePeers.length });
2159
- if (first) {
2160
- state.status = voiceTools?.capture ? "voice on \xB7 /talk (or ctrl-g) to speak" : "voice on \xB7 listen only (no mic found)";
2161
- }
2162
- break;
2163
- }
2164
- case "voiceDrop": {
2165
- vlog(Date.now(), "transport", "voiceDrop", { reason: msg.reason });
2166
- if (msg.reason === "notInVoice") {
2167
- voiceReady = false;
2168
- const now = Date.now();
2169
- if (now - voiceRejoinAt > 1e3) {
2170
- voiceRejoinAt = now;
2171
- vlog(now, "transport", "auto re-join after notInVoice drop");
2172
- sendMsg({ t: "voiceJoin" });
2173
- }
2174
- } else if (msg.reason === "rateCapped") {
2175
- state.status = "voice \xB7 sending too fast (throttled)";
2176
- }
2177
- break;
2178
- }
2179
- case "voiceStats": {
2180
- vlog(Date.now(), "transport", "voiceStats", { heardFromYou: msg.heardFromYou });
2181
- if (voiceTransmit && msg.heardFromYou === 0 && !voiceHeardWarned) {
2182
- voiceHeardWarned = true;
2183
- state.status = "mic hot but server hears nothing \xB7 /voice mics to switch";
2184
- } else if (msg.heardFromYou > 0) {
2185
- voiceHeardWarned = false;
2186
- }
2187
- break;
2188
- }
2189
- case "voiceFrame":
2190
- if (voiceOn) {
2191
- const n = (voiceRecv.get(msg.sid) ?? 0) + 1;
2192
- voiceRecv.set(msg.sid, n);
2193
- if (n === 1) vlog(Date.now(), "receive", "first frame received from a peer", { sid: msg.sid.slice(0, 8) });
2194
- voicePlayback?.feed(msg.sid, msg.data);
2195
- }
2196
- break;
2197
3099
  case "error": {
2198
3100
  if (msg.code === "chatRate" || msg.code === "msgTooLarge") {
2199
3101
  const failed = msg.ref ? chatLog.failOptimistic(msg.ref) : null;
@@ -2201,9 +3103,24 @@ function handleServer(msg) {
2201
3103
  state.status = msg.code === "chatRate" ? "slow down" : "message too large";
2202
3104
  } else if (msg.code === "protoVersion") {
2203
3105
  shutdownWithMessage("this client is outdated. update with: npm i -g accelr8");
3106
+ } else if (msg.code === "authExpired") {
3107
+ forceRefresh = true;
3108
+ } else if (msg.code === "authInvalid" || msg.code === "authRequired" || msg.code === "authRevoked") {
3109
+ authLost();
3110
+ } else if (msg.code === "notOwner") {
3111
+ state.status = msg.message;
2204
3112
  }
2205
3113
  break;
2206
3114
  }
3115
+ case "removed":
3116
+ if (state.project?.id === msg.projectId) {
3117
+ if (voice.active) leaveVoice();
3118
+ overlay = null;
3119
+ state.project = null;
3120
+ state.screen = "home";
3121
+ }
3122
+ state.status = `${msg.by} removed you from ${msg.name}.`;
3123
+ break;
2207
3124
  }
2208
3125
  }
2209
3126
  var grid = {};
@@ -2211,7 +3128,7 @@ var screen;
2211
3128
  function makeScreen() {
2212
3129
  const width = Math.max(20, term.width || process.stdout.columns || 120);
2213
3130
  const height = Math.max(6, term.height || process.stdout.rows || 40);
2214
- screen = new ScreenBuffer({ dst: term, width, height });
3131
+ screen = new CellScreenBuffer({ dst: term, width, height });
2215
3132
  chatLog.dropCaches();
2216
3133
  if (cine) {
2217
3134
  } else if (boot && (width < BOOT_MIN_W || height < BOOT_MIN_H)) {
@@ -2225,7 +3142,7 @@ function makeScreen() {
2225
3142
  }
2226
3143
  }
2227
3144
  function __setHeadlessScreen(width, height) {
2228
- screen = new ScreenBuffer({ width, height });
3145
+ screen = new CellScreenBuffer({ width, height });
2229
3146
  chatLog.dropCaches();
2230
3147
  }
2231
3148
  function __dump() {
@@ -2237,12 +3154,26 @@ function __dumpCells() {
2237
3154
  const row = [];
2238
3155
  for (let x = 0; x < screen.width; x++) {
2239
3156
  const c = screen.get({ x, y });
2240
- row.push({ ch: c.char, fg: c.attr?.color, bg: c.attr?.bgColor, bold: c.attr?.bold });
3157
+ const at = (y * screen.width + x) * screen.ITEM_SIZE;
3158
+ row.push({
3159
+ ch: c.char,
3160
+ fg: c.attr?.color,
3161
+ bg: c.attr?.bgColor,
3162
+ bold: c.attr?.bold,
3163
+ wide: screen.hasLeadingFullWidth(screen.buffer, at),
3164
+ tail: screen.hasTrailingFullWidth(screen.buffer, at)
3165
+ });
2241
3166
  }
2242
3167
  rows.push(row);
2243
3168
  }
2244
3169
  return rows;
2245
3170
  }
3171
+ function __server(msg) {
3172
+ handleServer(msg);
3173
+ }
3174
+ function __puts(x, y, text) {
3175
+ puts(x, y, text, { color: C.text, bgColor: C.bg });
3176
+ }
2246
3177
  function __setMySid(sid) {
2247
3178
  mySid = sid;
2248
3179
  }
@@ -2267,20 +3198,82 @@ var W = () => screen.width;
2267
3198
  function puts(x, y, str, attr) {
2268
3199
  if (y < 0 || y >= H()) return;
2269
3200
  const a = attr && attr.bgColor !== void 0 ? attr : { ...attr, bgColor: C.bg };
2270
- const clean = String(str).replace(/[\x00-\x1f\x7f]/g, " ");
2271
- screen.put({ x, y, attr: a, dx: 1, dy: 0, wrap: false }, clean);
3201
+ const clean = String(str).replace(/[\x00-\x1f\x7f-\x9f]/g, " ");
3202
+ if (isPlainText(clean)) screen.put({ x, y, attr: a, dx: 1, dy: 0, wrap: false }, clean);
3203
+ else putClusters(x, y, clean, a);
2272
3204
  }
2273
3205
  function puts2(x, y, str, attr) {
2274
3206
  puts(x, y, str, attr);
2275
- return x + String(str).length;
3207
+ return x + stringWidth(String(str));
3208
+ }
3209
+ var tkWidth = tk.stringWidth;
3210
+ var plainBmp = new Uint8Array(65536);
3211
+ function isPlainCp(cp) {
3212
+ let v = plainBmp[cp];
3213
+ if (!v) {
3214
+ v = charWidth(cp) === 1 && tkWidth(String.fromCharCode(cp)) === 1 ? 1 : 2;
3215
+ plainBmp[cp] = v;
3216
+ }
3217
+ return v === 1;
3218
+ }
3219
+ function isPlainText(s) {
3220
+ for (let i = 0; i < s.length; i++) {
3221
+ const c = s.charCodeAt(i);
3222
+ if (c < 768) continue;
3223
+ if (c >= 55296 && c <= 57343) return false;
3224
+ if (!isPlainCp(c)) return false;
3225
+ }
3226
+ return true;
3227
+ }
3228
+ function putClusters(x, y, text, attr, maxCells = Infinity) {
3229
+ let col = 0;
3230
+ for (const g of clusters(text)) {
3231
+ const w = stringWidth(g);
3232
+ if (w === 0) continue;
3233
+ if (col + w > maxCells) break;
3234
+ putCluster(x + col, y, g, w, attr);
3235
+ col += w;
3236
+ }
3237
+ return col;
3238
+ }
3239
+ function putCluster(x, y, g, w, attr) {
3240
+ const cps = [...g];
3241
+ if (w === 2 && cps.length === 2 && cps.every((c) => {
3242
+ const p = c.codePointAt(0);
3243
+ return p >= 127462 && p <= 127487;
3244
+ })) {
3245
+ putPiece(x, y, cps[0], 1, attr);
3246
+ putPiece(x + 1, y, cps[1], 1, attr);
3247
+ return;
3248
+ }
3249
+ let col = x;
3250
+ for (const p of cellPieces(g)) {
3251
+ putPiece(col, y, p.emoji ? representative(p.text, p.w) : p.text, p.w, attr);
3252
+ col += p.w;
3253
+ }
3254
+ }
3255
+ var WIDE_SLOT = "\u3013";
3256
+ function putPiece(x, y, text, w, attr) {
3257
+ screen.put({ x, y, attr, dx: 1, dy: 0, wrap: false }, w === 2 ? WIDE_SLOT : " ");
3258
+ if (x >= 0 && x + w <= screen.width) screen.writeChar(screen.buffer, text, (y * screen.width + x) * screen.ITEM_SIZE);
3259
+ }
3260
+ function representative(g, w) {
3261
+ const cps = [...g];
3262
+ return cps.find((c) => charWidth(c.codePointAt(0)) === w) ?? cps.find((c) => charWidth(c.codePointAt(0)) > 0) ?? cps[0];
3263
+ }
3264
+ var fit = (s, cells) => truncateWidth(s, cells, "\u2026");
3265
+ function firstFit(variants, cells) {
3266
+ for (const v of variants) if (stringWidth(v) <= cells) return v;
3267
+ return fit(variants[variants.length - 1], cells);
2276
3268
  }
2277
3269
  var clickables = [];
2278
3270
  function button(x, y, label, attr, action) {
2279
3271
  const focused = overlay !== null && clickables.length === overlayFocus;
2280
3272
  puts(x, y, label, focused ? { ...attr, bgColor: C.popupSel, color: C.selFg, bold: true } : attr);
2281
3273
  if (focused) puts(x - 1, y, "\u276F", { color: C.accent, bold: true });
2282
- clickables.push({ x0: x, y0: y, x1: x + label.length - 1, y1: y, action });
2283
- return x + label.length;
3274
+ const lw = stringWidth(label);
3275
+ clickables.push({ x0: x, y0: y, x1: x + lw - 1, y1: y, action });
3276
+ return x + lw;
2284
3277
  }
2285
3278
  var BASE = {
2286
3279
  bg: 233,
@@ -2382,6 +3375,12 @@ function __curTheme() {
2382
3375
  function __setUpdate(latest, current = "3.0.0") {
2383
3376
  updateInfo = latest ? { latest, current } : null;
2384
3377
  }
3378
+ function __startUpdate() {
3379
+ startUpdate();
3380
+ }
3381
+ function __updateRunning() {
3382
+ return updateRunning;
3383
+ }
2385
3384
  function __taskDemo(phase, ok = true) {
2386
3385
  task = { title: "enable voice", phase, ok, line: phase === "confirm" ? "voice needs ffmpeg \xB7 brew install ffmpeg \xB7 press enter" : phase === "running" ? "installing ffmpeg\u2026" : ok ? "ffmpeg ready \xB7 voice is on" : "brew exited 1 \xB7 brew install ffmpeg", okLabel: "start voice \xB7 enter" };
2387
3386
  }
@@ -2396,6 +3395,34 @@ function __setNav(s) {
2396
3395
  function __accent() {
2397
3396
  return C.accent;
2398
3397
  }
3398
+ var voiceTest;
3399
+ function __setVoice(on, ready = on, transmit = false) {
3400
+ voiceTest = !on ? null : { label: !ready ? " \u25CC connecting\u2026" : transmit ? " \u25CF live \xB7 /mute" : ` \u25CB voice ${state.voicePeers.length} \xB7 /talk`, live: ready && transmit };
3401
+ }
3402
+ function __bootAt(tMs, short = false, rng) {
3403
+ cine = null;
3404
+ boot = new BootAnim(W(), H(), { short, rng });
3405
+ bootStart = Date.now() - tMs;
3406
+ }
3407
+ function __bootOff() {
3408
+ boot = null;
3409
+ cine = null;
3410
+ }
3411
+ function __key(name) {
3412
+ onKey(name);
3413
+ }
3414
+ function __mouse(name, x, y, extra = {}) {
3415
+ onMouse(name, { x: x + 1, y: y + 1, ...extra });
3416
+ }
3417
+ function __grid() {
3418
+ return grid;
3419
+ }
3420
+ function __canvasPan() {
3421
+ return { ...canvasPan };
3422
+ }
3423
+ function __canvasView() {
3424
+ return { ...canvasView };
3425
+ }
2399
3426
  function rungFor(w, h) {
2400
3427
  if (h <= 4) return "micro";
2401
3428
  if (w < 12) return "micro";
@@ -2452,9 +3479,14 @@ function render() {
2452
3479
  const f = boot.frame(Date.now() - bootStart);
2453
3480
  if (f === "done") finishBoot();
2454
3481
  else {
3482
+ const seat = homeLogoSeat(w, h);
3483
+ const dx = seat ? seat.left - boot.logoLeft : 0;
3484
+ const dy = seat ? seat.top - boot.logoTop : 0;
2455
3485
  for (const { x, y, cell } of f.cells) {
2456
- if (x >= 0 && x < w && y >= 0 && y < h)
2457
- screen.put({ x, y, attr: { color: cell.color, bold: cell.bold, bgColor: cell.bgColor ?? C.bg }, dx: 1, dy: 0, wrap: false }, cell.ch);
3486
+ if (x < 0 || x >= w || y < 0 || y >= h) continue;
3487
+ const sx = (x + dx + w) % w, sy = (y + dy + h) % h;
3488
+ const color = cell.color === BOOT_ACCENT ? C.accent : cell.color;
3489
+ screen.put({ x: sx, y: sy, attr: { color, bold: cell.bold, bgColor: cell.bgColor ?? C.bg }, dx: 1, dy: 0, wrap: false }, cell.ch);
2458
3490
  }
2459
3491
  if (screen.dst) screen.draw({ delta: true });
2460
3492
  return;
@@ -2532,15 +3564,17 @@ function orbInfo() {
2532
3564
  function blitOrbLine(x0, y, line) {
2533
3565
  if (!line || y < 0 || y >= H()) return;
2534
3566
  puts(x0, y, line.text, { color: C.dim, bgColor: C.bg });
3567
+ const chars = [...line.text];
3568
+ const col = columnsOf(chars);
2535
3569
  for (const sp of line.spans) {
2536
- const seg = [...line.text].slice(sp.start, sp.end).join("");
2537
- if (seg) puts(x0 + sp.start, y, seg, { color: readableFg(sp.color, C.bg, 3.2), bold: sp.bold, bgColor: C.bg });
3570
+ const seg = chars.slice(sp.start, sp.end).join("");
3571
+ if (seg) puts(x0 + col(sp.start), y, seg, { color: readableFg(sp.color, C.bg, 3.2), bold: sp.bold, bgColor: C.bg });
2538
3572
  }
2539
3573
  }
2540
3574
  function renderMicro(w, h) {
2541
3575
  const line = microLine(w, orbEngine, frame, orbInfo());
2542
3576
  const y = Math.floor((h - 1) / 2);
2543
- const x = Math.max(0, Math.floor((w - [...line.text].length) / 2));
3577
+ const x = Math.max(0, Math.floor((w - stringWidth(line.text)) / 2));
2544
3578
  blitOrbLine(x, y, line);
2545
3579
  }
2546
3580
  function renderOrb(w, h) {
@@ -2554,14 +3588,12 @@ function renderTicker(w, h) {
2554
3588
  }
2555
3589
  puts(0, 0, " ".repeat(w), { bgColor: C.panel });
2556
3590
  let x = 1;
2557
- const projName = state.project.name.slice(0, 18);
2558
- puts(x, 0, projName, { bgColor: C.panel, color: C.bright, bold: true });
2559
- x += projName.length + 3;
3591
+ const projName = fit(state.project.name, 18);
3592
+ x = puts2(x, 0, projName, { bgColor: C.panel, color: C.bright, bold: true }) + 3;
2560
3593
  if (w >= 80) {
2561
3594
  for (const m of state.members.slice(0, 5)) {
2562
3595
  const on = state.sessions.some((s) => s.userId === m.id);
2563
- puts(x, 0, (on ? "\u25CF " : "\u25CB ") + m.name.slice(0, 8), { bgColor: C.panel, color: on ? readableFg(m.color, C.panel) : C.dim });
2564
- x += m.name.slice(0, 8).length + 3;
3596
+ x = puts2(x, 0, (on ? "\u25CF " : "\u25CB ") + fit(m.name, 8), { bgColor: C.panel, color: on ? readableFg(m.color, C.panel) : C.dim }) + 1;
2565
3597
  }
2566
3598
  } else {
2567
3599
  puts(x, 0, `\xB7 ${onlineUserIds().size} on`, { bgColor: C.panel, color: C.dim });
@@ -2587,13 +3619,7 @@ function renderTicker(w, h) {
2587
3619
  drawSpannedLine(1, threadTop + i, line.text, line.spans, w - 2, line.centered);
2588
3620
  void gutters;
2589
3621
  }
2590
- if (hasGhost) {
2591
- const g = activeGhosts()[0];
2592
- const m = memberOf(g.userId);
2593
- puts(1, ghostRow, m?.name ?? "?", { color: readableFg(m?.color ?? C.dim), bold: true });
2594
- puts(1 + (m?.name ?? "?").length, ghostRow, " \u270E ", { color: C.sys });
2595
- puts(4 + (m?.name ?? "?").length, ghostRow, [...g.text].slice(0, w - (m?.name ?? "?").length - 6).join(""), { color: dimOf(m?.color ?? 245), italic: true });
2596
- }
3622
+ if (hasGhost) drawGhostRow(activeGhosts()[0], ghostRow, w);
2597
3623
  renderInputLine(inputRow, w);
2598
3624
  if (hasStatus) {
2599
3625
  puts(0, h - 1, " ".repeat(w), { bgColor: C.panel });
@@ -2611,7 +3637,7 @@ function renderSidecarRung(w, h) {
2611
3637
  return;
2612
3638
  }
2613
3639
  puts(0, 0, " ".repeat(w), { bgColor: C.panel });
2614
- puts(1, 0, state.project.name.slice(0, w - 12), { bgColor: C.panel, color: C.bright, bold: true });
3640
+ puts(1, 0, fit(state.project.name, w - 12), { bgColor: C.panel, color: C.bright, bold: true });
2615
3641
  const right = `${onlineUserIds().size} on ${state.connected ? "\u25CF" : "\u25CB"}`;
2616
3642
  puts(w - right.length - 1, 0, right, { bgColor: C.panel, color: state.connected ? C.good : C.warn });
2617
3643
  const inputRow = h - 2;
@@ -2637,15 +3663,9 @@ function renderSidecarRung(w, h) {
2637
3663
  }
2638
3664
  if (sidecarBlock) {
2639
3665
  puts(1, blockTop, `\xB7 ${sidecarBlock.title} \xB7 local view \xB7`.padStart(Math.floor((w + sidecarBlock.title.length + 16) / 2)), { color: C.sys });
2640
- sidecarBlock.lines.forEach((l, i) => puts(2, blockTop + 1 + i, l.slice(0, w - 4), { color: C.dim }));
2641
- }
2642
- if (ghostRows) {
2643
- const g = activeGhosts()[0];
2644
- const m = memberOf(g.userId);
2645
- puts(1, ghostRow, m?.name ?? "?", { color: readableFg(m?.color ?? C.dim), bold: true });
2646
- puts(1 + (m?.name ?? "?").length, ghostRow, " \u270E ", { color: C.sys });
2647
- puts(4 + (m?.name ?? "?").length, ghostRow, [...g.text].slice(0, Math.max(0, w - (m?.name ?? "?").length - 6)).join(""), { color: dimOf(m?.color ?? 245), italic: true });
3666
+ sidecarBlock.lines.forEach((l, i) => puts(2, blockTop + 1 + i, fit(l, w - 4), { color: C.dim }));
2648
3667
  }
3668
+ if (ghostRows) drawGhostRow(activeGhosts()[0], ghostRow, w);
2649
3669
  renderInputLine(inputRow, w);
2650
3670
  puts(0, h - 1, " ".repeat(w), { bgColor: C.panel });
2651
3671
  const hintLeft = Date.now() - sidecarHintAt < 3e3 ? "tabs live at 60 cols and up" : "/team /secrets";
@@ -2654,19 +3674,75 @@ function renderSidecarRung(w, h) {
2654
3674
  puts(w - hint.length, h - 1, hint, { bgColor: C.panel, color: C.dim });
2655
3675
  if (popup.open) renderPopup(inputRow, w);
2656
3676
  }
3677
+ var inputScroll = 0;
2657
3678
  function renderInputLine(inputRow, width) {
2658
3679
  puts(0, inputRow, " ".repeat(width), { bgColor: C.inputBg });
2659
3680
  puts(1, inputRow, "\u276F ", { color: C.accent, bgColor: C.inputBg, bold: true });
2660
3681
  const fieldW = width - 4;
2661
- const win = editor.window(fieldW);
2662
- puts(3, inputRow, win.view.padEnd(fieldW).slice(0, fieldW), { color: C.bright, bgColor: C.inputBg });
3682
+ const win = inputWindow(editor.text, editor.cursor, inputScroll, fieldW);
3683
+ inputScroll = win.scroll;
3684
+ puts(3, inputRow, padWidth(win.view, fieldW), { color: C.bright, bgColor: C.inputBg });
2663
3685
  if (win.clippedLeft) puts(3, inputRow, "\u2026", { color: C.dim, bgColor: C.inputBg });
2664
3686
  if (win.clippedRight) puts(3 + fieldW - 1, inputRow, "\u2026", { color: C.dim, bgColor: C.inputBg });
2665
- const cx = 3 + win.cursorCol;
2666
- if (cx < 3 + fieldW) {
2667
- const under = [...win.view][win.cursorCol] ?? " ";
2668
- puts(cx, inputRow, under, { color: onFill(C.bright), bgColor: C.bright });
3687
+ const cx = 3 + win.cursorX;
3688
+ if (fieldW > 0 && cx + Math.max(1, stringWidth(win.under)) <= 3 + fieldW)
3689
+ puts(cx, inputRow, win.under, { color: onFill(C.bright), bgColor: C.bright });
3690
+ }
3691
+ function inputWindow(text, cursor, scroll, cells) {
3692
+ const { gs, at, ws: ws2, pw } = draftClusters(text);
3693
+ const n = gs.length;
3694
+ if (cells <= 0) return { view: "", cursorX: 0, under: " ", clippedLeft: false, clippedRight: n > 0, scroll: 0 };
3695
+ const width = (a, b) => pw[b] - pw[a];
3696
+ let cc = n;
3697
+ for (let i = 0; i < n; i++) if (at[i + 1] > cursor) {
3698
+ cc = i;
3699
+ break;
3700
+ }
3701
+ let uc = cc;
3702
+ while (uc < n && ws2[uc] === 0) uc++;
3703
+ const curW = uc < n ? ws2[uc] : 1;
3704
+ const margin = cells > 2 ? 1 : 0;
3705
+ const right = uc < n ? margin : 0;
3706
+ let s = 0;
3707
+ while (s < n && at[s] < scroll) s++;
3708
+ if (cc - s < margin) s = Math.max(0, cc - margin);
3709
+ while (s < cc && width(s, cc) + curW + right > cells) s++;
3710
+ while (s > 0 && width(s - 1, n) + 1 <= cells) s--;
3711
+ let e = s, used = 0;
3712
+ while (e < n && used + ws2[e] <= cells) {
3713
+ used += ws2[e];
3714
+ e++;
3715
+ }
3716
+ return {
3717
+ view: gs.slice(s, e).join(""),
3718
+ cursorX: width(s, cc),
3719
+ under: uc < n ? gs[uc] : " ",
3720
+ clippedLeft: s > 0,
3721
+ clippedRight: e < n,
3722
+ scroll: at[s]
3723
+ };
3724
+ }
3725
+ var draftMemo = { text: "", gs: [], at: [0], ws: [], pw: [0] };
3726
+ function draftClusters(text) {
3727
+ if (draftMemo.text !== text) {
3728
+ const gs = clusters(text.replace(/\n/g, "\u21B5"));
3729
+ const at = [0], ws2 = [], pw = [0];
3730
+ for (const g of gs) {
3731
+ const w = stringWidth(g);
3732
+ at.push(at[at.length - 1] + [...g].length);
3733
+ ws2.push(w);
3734
+ pw.push(pw[pw.length - 1] + w);
3735
+ }
3736
+ draftMemo = { text, gs, at, ws: ws2, pw };
2669
3737
  }
3738
+ return draftMemo;
3739
+ }
3740
+ function drawGhostRow(g, y, width) {
3741
+ const m = memberOf(g.userId);
3742
+ const nx = puts2(1, y, fit(m?.name ?? "?", 16), { color: readableFg(m?.color ?? C.dim), bold: true });
3743
+ puts(nx, y, " \u270E ", { color: C.sys });
3744
+ const draft = [...g.text].slice(0, 120).join("");
3745
+ puts(nx + 3, y, fit(draft, Math.max(0, width - nx - 5)), { color: dimOf(m?.color ?? 245), italic: true });
2670
3746
  }
2671
3747
  function renderChrome(rung) {
2672
3748
  const w = W();
@@ -2684,8 +3760,8 @@ function renderChrome(rung) {
2684
3760
  }
2685
3761
  const whoMax = Math.max(0, w - 1 - x2);
2686
3762
  let who2 = `${me.name} \u25CF`;
2687
- if (who2.length > whoMax) who2 = whoMax >= 4 ? me.name.slice(0, whoMax - 3) + "\u2026 \u25CF" : "\u25CF".slice(0, whoMax);
2688
- const whoX = Math.max(x2, w - who2.length - 1);
3763
+ if (stringWidth(who2) > whoMax) who2 = whoMax >= 4 ? fit(me.name, whoMax - 2) + " \u25CF" : "\u25CF".slice(0, whoMax);
3764
+ const whoX = Math.max(x2, w - stringWidth(who2) - 1);
2689
3765
  puts(whoX, 0, who2, { bgColor: C.panel, color: readableFg(me.color, C.panel), bold: true });
2690
3766
  const gap = whoX - x2;
2691
3767
  const label = gap >= 10 ? " settings " : gap >= 5 ? " set " : "";
@@ -2694,11 +3770,11 @@ function renderChrome(rung) {
2694
3770
  }
2695
3771
  puts(0, 0, " ".repeat(w), { bgColor: C.panel });
2696
3772
  button(1, 0, " accelr8 ", { bgColor: C.accent, color: C.onAccent, bold: true }, goHome);
2697
- puts(11, 0, state.project?.name ?? "", { bgColor: C.panel, color: C.bright, bold: true });
2698
- const who = `${me.name} \u25CF`;
2699
- let hx = w - who.length - 1;
3773
+ const who = `${fit(me.name, 16)} \u25CF`;
3774
+ let hx = w - stringWidth(who) - 1;
2700
3775
  puts(hx, 0, who, { bgColor: C.panel, color: readableFg(me.color, C.panel), bold: true });
2701
3776
  hx -= 11;
3777
+ puts(11, 0, fit(state.project?.name ?? "", Math.max(0, hx - 12)), { bgColor: C.panel, color: C.bright, bold: true });
2702
3778
  button(hx, 0, " settings ", { bgColor: C.panel, color: C.dim }, openSettings);
2703
3779
  puts(0, 1, " ".repeat(w), { bgColor: C.bg });
2704
3780
  const style = settings.tabs;
@@ -2773,27 +3849,35 @@ function clearUnread() {
2773
3849
  unread.t0 = 0;
2774
3850
  unread.t2 = 0;
2775
3851
  }
3852
+ function homeLogoSeat(w, h) {
3853
+ if (w < LOGO_W + 4 || h < LOGO.length + 16) return null;
3854
+ return { top: 3, left: Math.max(2, Math.floor((w - LOGO_W) / 2)) };
3855
+ }
2776
3856
  function renderHome(rung) {
2777
3857
  const w = W();
2778
3858
  puts(0, 0, " ".repeat(w), { bgColor: C.panel });
2779
3859
  puts(1, 0, " accelr8 ", { bgColor: C.accent, color: C.onAccent, bold: true });
2780
3860
  puts(11, 0, "vibe-code together", { bgColor: C.panel, color: C.dim, italic: true });
2781
- const who = `${me.name} \u25CF`;
2782
- puts(w - who.length - 1, 0, who, { bgColor: C.panel, color: readableFg(me.color, C.panel), bold: true });
2783
- button(w - who.length - 11, 0, " settings ", { bgColor: C.panel, color: C.dim }, openSettings);
3861
+ const who = `${fit(me.name, 16)} \u25CF`;
3862
+ const whoW = stringWidth(who);
3863
+ puts(w - whoW - 1, 0, who, { bgColor: C.panel, color: readableFg(me.color, C.panel), bold: true });
3864
+ button(w - whoW - 11, 0, " settings ", { bgColor: C.panel, color: C.dim }, openSettings);
2784
3865
  const dot = state.connected ? "\u25CF live" : "\u25CB offline";
2785
3866
  puts(w - dot.length - 1, 1, dot, { bgColor: C.bg, color: state.connected ? C.good : C.warn });
2786
3867
  let y = 2;
2787
- if (w >= LOGO_W + 4 && H() >= LOGO.length + 16) {
2788
- y = 3;
2789
- const lx = Math.max(2, Math.floor((w - LOGO_W) / 2));
3868
+ const seat = homeLogoSeat(w, H());
3869
+ if (seat) {
3870
+ y = seat.top;
2790
3871
  for (const line of LOGO) {
2791
- puts(lx, y, line, { color: C.accent, bgColor: C.bg });
3872
+ puts(seat.left, y, line, { color: C.accent, bgColor: C.bg });
2792
3873
  y++;
2793
3874
  }
2794
3875
  y += 1;
2795
3876
  }
2796
- if (updateInfo && !task) {
3877
+ if (updateInfo && !task && updateRunning) {
3878
+ puts(2, y, "stopping the update \xB7 one moment", { color: C.dim, italic: true });
3879
+ y += 3;
3880
+ } else if (updateInfo && !task) {
2797
3881
  puts(2, y, "\u21BB", { color: C.warn, bold: true });
2798
3882
  const w2 = W();
2799
3883
  const label = ` update to v${updateInfo.latest} \xB7 press enter `;
@@ -2819,15 +3903,15 @@ function renderHome(rung) {
2819
3903
  for (const p of state.projects) {
2820
3904
  if (y >= H() - 2) break;
2821
3905
  const current = state.project?.id === p.id;
2822
- const row = `${p.code.padEnd(8)} ${p.name.slice(0, 30).padEnd(30)} ${String(p.members).padStart(5)} ${String(p.online).padStart(5)}`;
3906
+ const row = `${p.code.padEnd(8)} ${padWidth(fit(p.name, 30), 30)} ${String(p.members).padStart(5)} ${String(p.online).padStart(5)}`;
2823
3907
  puts(2, y, current ? "\u25CF" : "\u25BA", { color: current ? C.good : C.accent });
2824
- button(4, y, row, { color: C.text, bgColor: C.bg }, () => {
3908
+ const end = button(4, y, row, { color: C.text, bgColor: C.bg }, () => {
2825
3909
  if (current) {
2826
3910
  state.screen = "room";
2827
3911
  sendMsg({ t: "tab", tab: state.tab });
2828
3912
  } else sendMsg({ t: "joinProject", code: p.code });
2829
3913
  });
2830
- if (current) puts(4 + row.length + 1, y, "current", { color: C.good });
3914
+ if (current) puts(end + 1, y, "current", { color: C.good });
2831
3915
  y++;
2832
3916
  }
2833
3917
  puts(2, H() - 2, "click a row to open its room", { color: C.dim, italic: true });
@@ -2841,32 +3925,42 @@ function renderRoom(rung) {
2841
3925
  const sidebar = rung === "comfy";
2842
3926
  const mainW = sidebar ? W() - 24 : W();
2843
3927
  if (state.tab === "chat") renderChat(top, mainW, rung);
2844
- else if (state.tab === "canvas") renderCanvas(top);
3928
+ else if (state.tab === "canvas") renderCanvas(top, mainW);
2845
3929
  else if (state.tab === "secrets") renderSecrets(top);
2846
- else renderTeam(top);
3930
+ else renderTeam(top, mainW);
2847
3931
  if (sidebar) renderSidebar(mainW, top);
2848
3932
  renderStatusBar(rung);
2849
3933
  }
3934
+ var VOICE_ECHO = /^(voice · connecting|voice on · \/talk|● live · \/mute|muted · \/talk)/;
2850
3935
  function renderStatusBar(rung) {
2851
3936
  const w = W(), sy = H() - 1;
2852
3937
  puts(0, sy, " ".repeat(w), { bgColor: C.panel });
2853
3938
  const agentBit = state.agents.length ? ` \xB7 \u2726${state.agents.length} agents` : "";
2854
3939
  const left = state.project ? ` ${state.project.name} \xB7 ${state.project.code} \xB7 ${onlineUserIds().size} online${agentBit} ` : " no project open ";
2855
- const drawn = left.slice(0, w - 24);
2856
- puts(0, sy, drawn, { bgColor: C.panel, color: C.dim });
2857
- if (voiceOn) {
2858
- const live = voiceReady && voiceTransmit;
2859
- const label = !voiceReady ? " \u25CC connecting\u2026" : voiceTransmit ? " \u25CF live \xB7 /mute" : ` \u25CB voice ${state.voicePeers.length} \xB7 /talk`;
2860
- if (drawn.length + label.length < w - 24) puts(drawn.length, sy, label, { bgColor: C.panel, color: live ? C.good : C.accent, bold: live });
2861
- }
3940
+ const vi = voiceTest !== void 0 ? voiceTest : voice.indicator(state.voicePeers.length);
3941
+ const live = !!vi && vi.live;
3942
+ const voiceLabel = vi ? vi.label : "";
3943
+ let msg = state.status;
3944
+ if (vi && msg) msg = VOICE_ECHO.test(msg) ? "" : msg.replace(/^voice( on)? · /, "");
3945
+ const mid = msg ? ` ${msg} ` : bootHint ? ` ${bootHint} ` : "";
2862
3946
  const hint = state.modal ? "[enter] ok \xB7 [esc] cancel " : "[tab] tabs \xB7 [^C] quit ";
2863
- puts(w - hint.length, sy, hint, { bgColor: C.panel, color: C.dim });
2864
- if (state.status) {
2865
- const s = " " + state.status + " ";
2866
- puts(Math.max(0, Math.floor((w - s.length) / 2)), sy, s.slice(0, w), { bgColor: C.panel, color: C.warn });
2867
- } else if (bootHint) {
2868
- puts(Math.max(0, Math.floor((w - bootHint.length) / 2)), sy, bootHint, { bgColor: C.panel, color: C.dim });
2869
- }
3947
+ const voiceW = stringWidth(voiceLabel);
3948
+ let midW = stringWidth(mid);
3949
+ const hintW = stringWidth(hint);
3950
+ const showHint = voiceW + midW + 1 + hintW <= w;
3951
+ const right = showHint ? w - hintW - 1 : w;
3952
+ const leftRoom = right - voiceW - midW;
3953
+ const leftShown = leftRoom >= 8 ? fit(left, leftRoom) : "";
3954
+ let x = puts2(0, sy, leftShown, { bgColor: C.panel, color: C.dim });
3955
+ if (voiceLabel) x = puts2(x, sy, voiceLabel, { bgColor: C.panel, color: live ? C.good : C.accent, bold: live });
3956
+ if (mid) {
3957
+ const shown = fit(mid, Math.max(0, right - x));
3958
+ midW = stringWidth(shown);
3959
+ const centred = Math.floor((w - midW) / 2);
3960
+ const mx = centred >= x && centred + midW <= right ? centred : x;
3961
+ puts(mx, sy, shown, { bgColor: C.panel, color: msg ? C.warn : C.dim });
3962
+ }
3963
+ if (showHint) puts(w - hintW, sy, hint, { bgColor: C.panel, color: C.dim });
2870
3964
  }
2871
3965
  function renderChat(top, mainW, rung) {
2872
3966
  const h = H();
@@ -2895,25 +3989,19 @@ function renderChat(top, mainW, rung) {
2895
3989
  });
2896
3990
  }
2897
3991
  const gs = activeGhosts().slice(0, ghostRows);
2898
- for (let i = 0; i < gs.length; i++) {
2899
- const g = gs[i];
2900
- const m = memberOf(g.userId);
2901
- const name = m?.name ?? "?";
2902
- const y = ghostTop + i;
2903
- puts(1, y, name, { color: readableFg(m?.color ?? C.dim), bold: true });
2904
- puts(1 + name.length, y, " \u270E ", { color: C.sys });
2905
- const maxDraft = mainW - name.length - 6;
2906
- const text = [...g.text].slice(0, 120).join("");
2907
- const shown = text.length > maxDraft ? [...text].slice(0, Math.max(0, maxDraft - 1)).join("") + "\u2026" : text;
2908
- puts(1 + name.length + 3, y, shown, { color: dimOf(m?.color ?? 245), italic: true });
2909
- }
3992
+ for (let i = 0; i < gs.length; i++) drawGhostRow(gs[i], ghostTop + i, mainW);
2910
3993
  renderInputLine(inputRow, mainW);
2911
3994
  if (popup.open) renderPopup(inputRow, mainW);
2912
3995
  }
3996
+ function columnsOf(chars) {
3997
+ if (isPlainText(chars.join(""))) return (i) => i;
3998
+ return (i) => stringWidth(chars.slice(0, i).join(""));
3999
+ }
2913
4000
  function drawSpannedLine(x, y, text, spans, width, centered) {
2914
4001
  const chars = [...text];
4002
+ const col = columnsOf(chars);
2915
4003
  let ox = x;
2916
- if (centered) ox = x + Math.max(0, Math.floor((width - chars.length) / 2));
4004
+ if (centered) ox = x + Math.max(0, Math.floor((width - col(chars.length)) / 2));
2917
4005
  puts(ox, y, text, { color: C.text, bgColor: C.bg });
2918
4006
  for (const sp of spans) {
2919
4007
  const seg = chars.slice(sp.start, sp.end).join("");
@@ -2925,14 +4013,15 @@ function drawSpannedLine(x, y, text, spans, width, centered) {
2925
4013
  color = C.dim;
2926
4014
  text2 = SPINNER[frame % SPINNER.length];
2927
4015
  } else if (sp.color === -2) color = C.sys;
4016
+ else if (sp.color === NOTICE_COLOR) color = readableFg(C.warn, bg);
2928
4017
  else color = readableFg(sp.color, bg);
2929
- puts(ox + sp.start, y, text2, { color, bold: sp.bold, italic: sp.italic, bgColor: bg });
4018
+ puts(ox + col(sp.start), y, text2, { color, bold: sp.bold, italic: sp.italic, bgColor: bg });
2930
4019
  }
2931
4020
  }
2932
4021
  function renderPopup(inputRow, mainW) {
2933
4022
  const items = popup.visible();
2934
4023
  if (!items.length) return;
2935
- const boxW = Math.min(Math.max(...items.map((c) => c.name.length + (c.detail?.length ?? 0) + 8), 20), mainW - 4);
4024
+ const boxW = Math.min(Math.max(...items.map((c) => stringWidth(c.name) + stringWidth(c.detail ?? "") + 8), 20), mainW - 4);
2936
4025
  const x0 = 2;
2937
4026
  const y0 = inputRow - items.length - 2;
2938
4027
  puts(x0, y0, "\u250C mention " + "\u2500".repeat(Math.max(0, boxW - 11)) + "\u2510", { color: C.accent, bgColor: C.popupBg });
@@ -2944,8 +4033,9 @@ function renderPopup(inputRow, mainW) {
2944
4033
  puts(x0 + boxW - 1, y, "\u2502", { color: C.accent, bgColor: C.popupBg });
2945
4034
  puts(x0 + 1, y, " ".repeat(boxW - 2), { bgColor: sel ? C.popupSel : C.popupBg });
2946
4035
  puts(x0 + 2, y, c.kind === "agent" ? "\u2726" : "\u25CF", { color: nameColor, bgColor: sel ? C.popupSel : C.popupBg });
2947
- puts(x0 + 4, y, c.name, { color: nameColor, bold: true, italic: c.kind === "agent", bgColor: sel ? C.popupSel : C.popupBg });
2948
- if (c.detail) puts(x0 + boxW - 2 - c.detail.length, y, c.detail, { color: sel ? C.selFg : C.dim, bgColor: sel ? C.popupSel : C.popupBg });
4036
+ const detail = fit(c.detail ?? "", Math.max(0, boxW - 8));
4037
+ puts(x0 + 4, y, fit(c.name, Math.max(1, boxW - 7 - stringWidth(detail))), { color: nameColor, bold: true, italic: c.kind === "agent", bgColor: sel ? C.popupSel : C.popupBg });
4038
+ if (detail) puts(x0 + boxW - 2 - stringWidth(detail), y, detail, { color: sel ? C.selFg : C.dim, bgColor: sel ? C.popupSel : C.popupBg });
2949
4039
  });
2950
4040
  puts(x0, y0 + 1 + items.length, "\u2514" + "\u2500".repeat(Math.max(0, boxW - 2)) + "\u2518", { color: C.accent, bgColor: C.popupBg });
2951
4041
  }
@@ -2954,8 +4044,38 @@ function activeGhosts() {
2954
4044
  for (const [sid, g] of ghosts) if (now - g.at > 3e3) ghosts.delete(sid);
2955
4045
  return [...ghosts.values()].sort((a, b) => b.at - a.at);
2956
4046
  }
2957
- function renderCanvas(top) {
2958
- const w = W(), h = H();
4047
+ var brush = "\u2588";
4048
+ var canvasPan = { x: 0, y: 0 };
4049
+ var canvasView = { x0: IO_X, y0: IO_Y, w: 0, h: 0 };
4050
+ function canvasViewport(top, mainW, h) {
4051
+ const by0 = top + 1;
4052
+ return {
4053
+ x0: IO_X,
4054
+ y0: by0 + 1,
4055
+ w: Math.max(1, Math.min(CANVAS_W, mainW - 2)),
4056
+ h: Math.max(1, Math.min(CANVAS_H, h - 3 - by0))
4057
+ };
4058
+ }
4059
+ function clampPan(pan, v) {
4060
+ return {
4061
+ x: Math.max(0, Math.min(CANVAS_W - Math.max(1, v.w), pan.x)),
4062
+ y: Math.max(0, Math.min(CANVAS_H - Math.max(1, v.h), pan.y))
4063
+ };
4064
+ }
4065
+ function canvasCellAt(mx, my, v, pan) {
4066
+ if (mx < v.x0 || mx >= v.x0 + v.w || my < v.y0 || my >= v.y0 + v.h) return null;
4067
+ const x = mx - v.x0 + pan.x, y = my - v.y0 + pan.y;
4068
+ if (x < 0 || y < 0 || x >= CANVAS_W || y >= CANVAS_H) return null;
4069
+ return { x, y };
4070
+ }
4071
+ function panCanvas(dx, dy) {
4072
+ canvasPan = clampPan({ x: canvasPan.x + dx, y: canvasPan.y + dy }, canvasView);
4073
+ }
4074
+ function canvasGlyph(ch) {
4075
+ return ch.length > 0 && isPlainText(ch) && stringWidth(ch) === 1 ? ch : "?";
4076
+ }
4077
+ function renderCanvas(top, mainW) {
4078
+ const h = H();
2959
4079
  const ty = top;
2960
4080
  puts(1, ty, "brush", { color: C.dim });
2961
4081
  let px = 7;
@@ -2966,35 +4086,45 @@ function renderCanvas(top) {
2966
4086
  });
2967
4087
  px += 3;
2968
4088
  }
2969
- button(px + 1, ty, " \u232B erase ", { bgColor: brush === " " ? C.bright : C.panel, color: brush === " " ? onFill(C.bright) : C.text }, () => {
4089
+ const eraseEnd = button(px + 1, ty, " \u232B erase ", { bgColor: brush === " " ? C.bright : C.panel, color: brush === " " ? onFill(C.bright) : C.text }, () => {
2970
4090
  brush = " ";
2971
4091
  });
2972
- button(w - 10, ty, " clear ", { bgColor: C.warn, color: onFill(C.warn), bold: true }, () => sendMsg({ t: "clear" }));
2973
- const by0 = ty + 1, by1 = h - 2;
2974
- box(0, by0, w - 1, by1, { color: C.rule, bgColor: C.bg }, `canvas ${CANVAS_W}\xD7${CANVAS_H}`);
2975
- const innerRight = w - 2, innerBottom = by1 - 1;
2976
- const clip = (sx, sy) => sx >= IO_X && sx <= innerRight && sy >= by0 + 1 && sy <= innerBottom;
4092
+ canvasView = canvasViewport(ty, mainW, h);
4093
+ const by0 = canvasView.y0 - 1, vw = canvasView.w, vh = canvasView.h;
4094
+ const bx1 = vw + 1, by1 = by0 + vh + 1;
4095
+ panCanvas(0, 0);
4096
+ const pan = canvasPan;
4097
+ button(Math.max(eraseEnd + 2, bx1 - 9), ty, " clear ", { bgColor: C.warn, color: onFill(C.warn), bold: true }, () => sendMsg({ t: "clear" }));
4098
+ box(0, by0, bx1, by1, { color: C.rule, bgColor: C.bg }, `canvas ${CANVAS_W}\xD7${CANVAS_H}`);
4099
+ const v = canvasView;
4100
+ const clip = (sx, sy) => sx >= v.x0 && sx < v.x0 + v.w && sy >= v.y0 && sy < v.y0 + v.h;
4101
+ const toScreen = (cx, cy) => ({ sx: v.x0 + cx - pan.x, sy: v.y0 + cy - pan.y });
2977
4102
  for (const key in grid) {
2978
4103
  const [cx, cy] = key.split(",").map(Number);
2979
- const sx = IO_X + cx, sy = by0 + 1 + cy;
2980
- if (clip(sx, sy)) puts(sx, sy, grid[key][0], { color: grid[key][1], bgColor: C.bg });
4104
+ const { sx, sy } = toScreen(cx, cy);
4105
+ if (clip(sx, sy)) puts(sx, sy, canvasGlyph(grid[key][0]), { color: grid[key][1], bgColor: C.bg });
2981
4106
  }
2982
4107
  for (const [, c] of cursors) {
2983
- const sx = IO_X + Math.round(c.x), sy = by0 + 1 + Math.round(c.y);
4108
+ const { sx, sy } = toScreen(Math.round(c.x), Math.round(c.y));
2984
4109
  if (!clip(sx, sy)) continue;
2985
4110
  const m = memberOf(c.userId);
2986
4111
  puts(sx, sy, "\u25B2", { color: readableFg(m?.color ?? 7), bold: true });
2987
- const label = " " + (m?.name ?? "?");
2988
- if (sx + 1 + label.length <= innerRight) puts(sx + 1, sy, label, { bgColor: m?.color ?? 7, color: onFill(m?.color ?? 7), bold: true });
4112
+ const label = " " + fit(m?.name ?? "?", 12);
4113
+ if (sx + 1 + stringWidth(label) <= v.x0 + v.w - 1) puts(sx + 1, sy, label, { bgColor: m?.color ?? 7, color: onFill(m?.color ?? 7), bold: true });
2989
4114
  }
2990
4115
  if (myLogical) {
2991
- const sx = IO_X + myLogical.x, sy = by0 + 1 + myLogical.y;
4116
+ const { sx, sy } = toScreen(myLogical.x, myLogical.y);
2992
4117
  if (clip(sx, sy)) puts(sx, sy, brush === " " ? "\u25AB" : brush, { color: readableFg(me.color), bold: true });
2993
4118
  }
2994
- canvasTop = by0 + 1;
4119
+ if (vw < CANVAS_W || vh < CANVAS_H) {
4120
+ const midY = v.y0 + Math.floor(v.h / 2);
4121
+ if (pan.x > 0) puts(0, midY, "\u2039", { color: C.accent, bold: true, bgColor: C.bg });
4122
+ if (pan.x + vw < CANVAS_W) puts(bx1, midY, "\u203A", { color: C.accent, bold: true, bgColor: C.bg });
4123
+ const where = ` cols ${pan.x + 1}-${pan.x + vw} \xB7 rows ${pan.y + 1}-${pan.y + vh} \xB7 arrow keys move the view `;
4124
+ const label = stringWidth(where) <= vw - 2 ? where : " arrow keys move the view ";
4125
+ if (stringWidth(label) <= vw - 2) puts(bx1 - 1 - stringWidth(label), by1, label, { color: C.dim, bgColor: C.bg });
4126
+ }
2995
4127
  }
2996
- var brush = "\u2588";
2997
- var canvasTop = IO_Y;
2998
4128
  function renderSecrets(top) {
2999
4129
  puts(2, top + 1, "SECRETS", { color: C.bright, bold: true });
3000
4130
  puts(2, top + 3, "shared api keys and logins land here soon.", { color: C.dim, italic: true });
@@ -3021,15 +4151,15 @@ function blitMapLine(x0, y, line) {
3021
4151
  if (seg) puts(x0 + sp.start, y, seg, { color: readableFg(sp.color, C.bg, 2.6), bold: true, bgColor: C.bg });
3022
4152
  }
3023
4153
  }
3024
- function renderTeam(top) {
4154
+ function renderTeam(top, mainW) {
3025
4155
  if (!state.project) return;
3026
4156
  teamMap = null;
3027
4157
  let y = top + 1;
3028
4158
  const online = onlineUserIds();
3029
4159
  const availH = H() - top - 3;
3030
- if (W() >= 48 && availH >= 22) {
4160
+ if (mainW >= 48 && availH >= 22) {
3031
4161
  const dots = mapDotsFromSessions();
3032
- const mapW = Math.min(W() - 4, 130);
4162
+ const mapW = Math.min(mainW - 4, 130);
3033
4163
  const mapH = Math.min(15, Math.max(8, Math.floor(availH * 0.45)));
3034
4164
  const title = `WHERE \xB7 ${dots.length}/${online.size} placed`;
3035
4165
  puts(2, y, title, { color: C.bright, bold: true });
@@ -3047,11 +4177,11 @@ function renderTeam(top) {
3047
4177
  const sess = state.sessions.filter((s) => s.userId === m.id);
3048
4178
  const on = sess.length > 0;
3049
4179
  puts(2, y, on ? "\u25CF" : "\u25CB", { color: on ? C.good : C.dim });
3050
- puts(4, y, m.name + (m.id === me.id ? " \xB7 you" : ""), { color: readableFg(m.color), bold: true });
4180
+ puts(4, y, fit(m.name + (m.id === me.id ? " \xB7 you" : ""), 19), { color: readableFg(m.color), bold: true });
3051
4181
  const where = on ? sess.some((s) => s.tab !== "away") ? "viewing " + (sess.find((s) => s.tab !== "away")?.tab ?? "chat") : "parked" : "offline";
3052
4182
  puts(24, y, where, { color: on ? C.text : C.dim });
3053
4183
  const city = tzCity(sess.find((s) => s.tz)?.tz);
3054
- if (city && W() >= 56) puts(42, y, city.slice(0, W() - 44), { color: C.dim });
4184
+ if (city && mainW >= 56) puts(42, y, fit(city, mainW - 44), { color: C.dim });
3055
4185
  y++;
3056
4186
  }
3057
4187
  y += 1;
@@ -3076,8 +4206,8 @@ function renderTeam(top) {
3076
4206
  } else {
3077
4207
  puts(2, y, label, { color: readableFg(owner?.color ?? C.dim), bold: true, italic: true });
3078
4208
  }
3079
- puts(24, y, `${owner?.name ?? "?"} \xB7 ${a.provider} \xB7 ${status}`, { color: C.dim });
3080
- puts(52, y, access, { color: a.access?.mode === "all" ? C.good : C.dim });
4209
+ puts(24, y, fit(`${owner?.name ?? "?"} \xB7 ${a.provider} \xB7 ${status}`, 27), { color: C.dim });
4210
+ puts(52, y, fit(access, Math.max(0, mainW - 54)), { color: a.access?.mode === "all" ? C.good : C.dim });
3081
4211
  y++;
3082
4212
  }
3083
4213
  y += 1;
@@ -3095,7 +4225,10 @@ function renderTeam(top) {
3095
4225
  }
3096
4226
  function renderSidebar(x0, top) {
3097
4227
  const h = H();
3098
- for (let y2 = top; y2 < h - 1; y2++) puts(x0, y2, "\u2502", { color: C.rule, bgColor: C.bg });
4228
+ for (let y2 = top; y2 < h - 1; y2++) {
4229
+ puts(x0, y2, "\u2502", { color: C.rule, bgColor: C.bg });
4230
+ puts(x0 + 1, y2, " ".repeat(Math.max(0, W() - x0 - 1)), { bgColor: C.bg });
4231
+ }
3099
4232
  const x = x0 + 2;
3100
4233
  let y = top;
3101
4234
  const online = onlineUserIds();
@@ -3105,7 +4238,7 @@ function renderSidebar(x0, top) {
3105
4238
  const sess = state.sessions.filter((s) => s.userId === m.id);
3106
4239
  const on = sess.length > 0;
3107
4240
  puts(x, y, on ? "\u25CF" : "\u25CB", { color: on ? C.good : C.dim });
3108
- puts(x + 2, y, m.name.slice(0, 10), { color: readableFg(m.color), bold: true });
4241
+ puts(x + 2, y, fit(m.name, 10), { color: readableFg(m.color), bold: true });
3109
4242
  const where = m.id === me.id ? "you" : on ? sess.find((s) => s.tab !== "away")?.tab ?? "parked" : "";
3110
4243
  puts(x + 13, y, String(where).slice(0, 7), { color: C.dim });
3111
4244
  y++;
@@ -3142,7 +4275,14 @@ function box(x0, y0, x1, y1, attr, label) {
3142
4275
  if (label) puts(x0 + 2, y0, " " + label + " ", { ...attr, bold: true, color: C.bright });
3143
4276
  }
3144
4277
  var MODELS = {
3145
- anthropic: ["claude-opus-4-8", "claude-sonnet-5", "claude-haiku-4-5", "claude-fable-5"],
4278
+ // Only models that answer without thinking when the request sends no
4279
+ // `thinking` field. The executor sends none, no output_config.effort, and
4280
+ // max_tokens 1024 (room.ts), so a model that thinks by default (Opus 5.5
4281
+ // and Fable 5.1 always think, Sonnet 5.5, Haiku 5.5 and Sonnet 5 do unless
4282
+ // told otherwise) spends that budget thinking: the reply comes back cut off
4283
+ // or empty. Offer the 5.x line once the executor sets effort and a bigger
4284
+ // max_tokens. claude-opus-4-8 is DEFAULT_MODEL, the first pick.
4285
+ anthropic: ["claude-opus-4-8", "claude-sonnet-4-6", "claude-haiku-4-5"],
3146
4286
  openai: ["gpt-4o", "gpt-4o-mini", "o1"]
3147
4287
  };
3148
4288
  function openAccounts() {
@@ -3173,9 +4313,13 @@ function renderAccounts() {
3173
4313
  puts(0, 1, "\u2500".repeat(w), { color: C.rule, bgColor: C.bg });
3174
4314
  const ix = 2;
3175
4315
  let y = 3;
3176
- puts(ix, y, "connected accounts become named agents you can @mention. click mode to cycle:", { color: C.dim });
4316
+ puts(ix, y, "connected accounts become named agents you can @mention.", { color: C.dim });
3177
4317
  y += 1;
3178
- puts(ix, y, "plan = propose only \xB7 approve = ask before each action \xB7 auto = run freely", { color: C.dim, italic: true });
4318
+ puts(ix, y, firstFit([
4319
+ "agents chat only for now \xB7 plan, approve and auto are not enabled yet",
4320
+ "agents chat only for now \xB7 no plan, approve or auto yet",
4321
+ "agents chat only for now"
4322
+ ], w - ix - 2), { color: C.dim, italic: true });
3179
4323
  y += 2;
3180
4324
  puts(ix, y, "CONNECTED", { color: C.bright, bold: true });
3181
4325
  y += 1;
@@ -3191,12 +4335,12 @@ function renderAccounts() {
3191
4335
  const dotColor = st === "verifying" ? C.warn : st === "error" ? C.danger : online ? C.good : C.dim;
3192
4336
  const status = st === "verifying" ? "verifying" : st === "error" ? "error" : online ? "online" : "ready";
3193
4337
  puts(ix, y, dot, { color: dotColor });
3194
- puts(ix + 2, y, "\u2726" + a.handle, { color: C.accent, bold: true, italic: true });
3195
- puts(ix + 18, y, `${a.provider} \xB7 ${a.model}`, { color: C.text });
3196
- puts(ix + 46, y, a.auth.kind === "claude_code_login" ? "claude login" : "api key", { color: C.dim });
3197
- puts(ix + 60, y, status, { color: dotColor });
3198
- const modeColor = a.mode === "auto" ? C.warn : a.mode === "plan" ? C.accent : C.good;
3199
- button(w - 22, y, ` ${a.mode} `, { bgColor: C.tabOff, color: readableFg(modeColor, C.tabOff), bold: true }, () => cycleMode(a));
4338
+ puts(ix + 2, y, fit("\u2726" + a.handle, 15), { color: C.accent, bold: true, italic: true });
4339
+ const statusX = w - 32;
4340
+ const detail = firstFit(a.auth.kind === "claude_code_login" ? ["claude login \xB7 model from Claude Code", "Claude Code login + model", "claude login"] : [`${a.provider} api key \xB7 ${a.model}`, `api key \xB7 ${a.model}`, a.model], Math.max(0, statusX - ix - 19));
4341
+ puts(ix + 18, y, detail, { color: C.text });
4342
+ puts(statusX, y, status, { color: dotColor });
4343
+ puts(w - 22, y, "chat only", { color: C.dim, italic: true });
3200
4344
  button(w - 11, y, " remove ", { bgColor: C.tabOff, color: C.text }, () => removeAgentAccount(a));
3201
4345
  y += 1;
3202
4346
  }
@@ -3243,7 +4387,7 @@ function renderConnectWizard(ix, y, rx) {
3243
4387
  });
3244
4388
  } else if (d.step === "model") {
3245
4389
  if (d.authKind === "claude_code_login") {
3246
- puts(ix, y, "model follows your Claude Code setup.", { color: C.dim, bgColor: C.bg });
4390
+ puts(ix, y, "model: from your Claude Code settings.", { color: C.dim, bgColor: C.bg });
3247
4391
  y += 1;
3248
4392
  button(ix, y, " finish connecting ", { bgColor: C.good, color: onFill(C.good), bold: true }, () => finishConnect(DEFAULT_MODEL.anthropic));
3249
4393
  return;
@@ -3509,13 +4653,6 @@ function renderAgentAccess() {
3509
4653
  puts(ix, y, "no other teammates yet.", { color: C.dim, italic: true });
3510
4654
  }
3511
4655
  }
3512
- function cycleMode(a) {
3513
- const order = ["approve", "auto", "plan"];
3514
- const next = order[(order.indexOf(a.mode) + 1) % order.length];
3515
- saveAccounts(myAccounts.map((x) => x.id === a.id ? { ...x, mode: next } : x));
3516
- myAccounts = safeLoadAccounts();
3517
- state.status = `\u2726${a.handle}: ${next} mode`;
3518
- }
3519
4656
  function removeAgentAccount(a) {
3520
4657
  const agentId = state.agents.find((ag) => ag.handle === a.handle && ag.ownerId === me.id)?.agentId;
3521
4658
  if (agentId) sendMsg({ t: "agentDetach", agentId, token: loadTokens()[`${state.project?.id}:${a.handle}`] ?? "" });
@@ -3532,8 +4669,16 @@ function renderModal() {
3532
4669
  for (let y = y0; y < y0 + bh; y++) puts(x0, y, " ".repeat(bw), { bgColor: C.panel });
3533
4670
  box(x0, y0, x0 + bw - 1, y0 + bh - 1, { color: C.accent, bgColor: C.panel }, m.title);
3534
4671
  const raw = m.mask ? "\u2022".repeat([...m.value].length) : m.value;
3535
- const field = raw + "\u258F";
3536
- puts(x0 + 2, y0 + 2, field.padEnd(bw - 4).slice(-(bw - 4)), { bgColor: C.inputBg, color: readableFg(C.bright, C.inputBg) });
4672
+ const fieldW = bw - 4;
4673
+ const gs = clusters(raw + "\u258F");
4674
+ let shown = "";
4675
+ for (let i = gs.length - 1, used = 0; i >= 0; i--) {
4676
+ const gw = stringWidth(gs[i]);
4677
+ if (used + gw > fieldW) break;
4678
+ shown = gs[i] + shown;
4679
+ used += gw;
4680
+ }
4681
+ puts(x0 + 2, y0 + 2, padWidth(shown, fieldW), { bgColor: C.inputBg, color: readableFg(C.bright, C.inputBg) });
3537
4682
  }
3538
4683
  function promptCreate() {
3539
4684
  state.modal = { title: "new project name", value: "", onSubmit: (v) => {
@@ -3621,6 +4766,9 @@ function onMouse(name, data2) {
3621
4766
  chatLog.scrollDown(3, wrapW, viewH);
3622
4767
  if (chatLog.pinned) clearUnread();
3623
4768
  }
4769
+ } else if (state.screen === "room" && state.tab === "canvas") {
4770
+ panCanvas(0, name === "MOUSE_WHEEL_UP" ? -3 : 3);
4771
+ updateMyCursor(mx, my);
3624
4772
  }
3625
4773
  return;
3626
4774
  }
@@ -3643,11 +4791,7 @@ function onMouse(name, data2) {
3643
4791
  }
3644
4792
  }
3645
4793
  function toLogical(mx, my) {
3646
- const cx = mx - IO_X, cy = my - canvasTop;
3647
- if (cx < 0 || cy < 0 || cx >= CANVAS_W || cy >= CANVAS_H) return null;
3648
- if (my < canvasTop || my > H() - 3) return null;
3649
- if (mx > W() - 2) return null;
3650
- return { x: cx, y: cy };
4794
+ return canvasCellAt(mx, my, canvasView, canvasPan);
3651
4795
  }
3652
4796
  function updateMyCursor(mx, my) {
3653
4797
  if (state.screen !== "room" || state.tab !== "canvas") {
@@ -3698,34 +4842,43 @@ function slashCommand(text) {
3698
4842
  ghostsOff = cmd.endsWith("off");
3699
4843
  state.status = ghostsOff ? "draft broadcasting off" : "draft broadcasting on";
3700
4844
  draftDirty = true;
4845
+ } else if (cmd.startsWith("/remove ")) {
4846
+ const who = cmd.slice("/remove ".length).trim().replace(/^@/, "");
4847
+ const hits = state.members.filter((m) => m.id !== me.id && m.name.toLowerCase() === who);
4848
+ if (!state.project) state.status = "open a project first";
4849
+ else if (hits.length !== 1) state.status = hits.length ? `more than one member is called ${who}` : `nobody here is called ${who}`;
4850
+ else {
4851
+ sendMsg({ t: "removeMember", userId: hits[0].id });
4852
+ state.status = `removing ${hits[0].name}...`;
4853
+ }
3701
4854
  } else if (cmd === "/voice") {
3702
4855
  toggleVoice();
3703
- } else if (cmd === "/talk" || cmd === "/mute") {
3704
- toggleTalk();
4856
+ } else if (cmd === "/voice headphones") {
4857
+ if (voice.active) state.status = "already in voice \xB7 /voice to leave first";
4858
+ else toggleVoice(true);
4859
+ } else if (cmd === "/talk") {
4860
+ toggleTalk(true);
4861
+ } else if (cmd === "/mute") {
4862
+ toggleTalk(false);
3705
4863
  } else if (cmd === "/voice debug") {
3706
- const recvTotal = [...voiceRecv.values()].reduce((a, b) => a + b, 0);
3707
4864
  sidecarBlock = { title: "voice debug", at: Date.now(), lines: [
3708
- `on: ${voiceOn} \xB7 acked: ${voiceReady} \xB7 live: ${voiceTransmit}`,
3709
- `tools: ffmpeg ${voiceTools?.ffmpegPath ? "ok" : "MISSING"} \xB7 ffplay ${voiceTools?.ffplayPath ? "ok" : "MISSING"}`,
3710
- `mic: ${voiceDevice ?? "default"} \xB7 peers: ${state.voicePeers.length}`,
3711
- `sent: ${voiceSent} \xB7 received: ${recvTotal} from ${voiceRecv.size}`,
4865
+ ...voice.debugLines(state.voicePeers.length),
3712
4866
  `log: ${voiceLogPath()}`,
3713
4867
  "send me that file if voice fails"
3714
4868
  ] };
3715
4869
  state.status = "voice debug \xB7 see the panel \xB7 log at ~/.accelr8/voice.log";
3716
4870
  } else if (cmd === "/voice mics" || cmd === "/mics") {
3717
- const mics = listMicDevices();
3718
- sidecarBlock = { title: "microphones", at: Date.now(), lines: mics.length ? mics.map((m) => `${m.index} ${m.name}${voiceDevice === m.index ? " \xB7 selected" : ""}`).concat("use: /voice mic <index>") : ["none found (need ffmpeg)"] };
3719
- state.status = `${mics.length} mics \xB7 /voice mic <index> to pick`;
4871
+ void voice.listDevices().then((mics) => {
4872
+ sidecarBlock = { title: "microphones", at: Date.now(), lines: mics.length ? mics.map((m, i) => `${voice.liveKitKind === "window" ? i + 1 : m.index} ${m.name}`).concat("use: /voice mic <number>") : [voice.liveKitKind === "window" ? "none found \xB7 allow the mic in the voice window first" : "none found (need ffmpeg)"] };
4873
+ micChoices = mics.map((m) => m.index);
4874
+ state.status = `${mics.length} mics \xB7 /voice mic <number> to pick`;
4875
+ });
3720
4876
  } else if (cmd.startsWith("/voice mic ")) {
3721
- const idx = cmd.slice("/voice mic ".length).trim();
3722
- voiceDevice = idx || void 0;
3723
- state.status = `mic set to [${idx}]` + (voiceOn ? " \xB7 reconnecting" : "");
3724
- if (voiceTransmit && voiceCapture) {
3725
- voiceCapture.stop();
3726
- voiceRetries = 0;
3727
- startCapture();
3728
- }
4877
+ const pick = cmd.slice("/voice mic ".length).trim();
4878
+ const n = Number(pick);
4879
+ const id = voice.liveKitKind === "window" && Number.isInteger(n) && micChoices[n - 1] ? micChoices[n - 1] : pick;
4880
+ voice.setDevice(id || void 0);
4881
+ state.status = `mic set to [${pick}]` + (voice.active ? " \xB7 switching" : "");
3729
4882
  } else {
3730
4883
  state.status = `unknown command: ${cmd.split(" ")[0]}`;
3731
4884
  }
@@ -3752,7 +4905,7 @@ function sendChat() {
3752
4905
  }
3753
4906
  function onKey(name) {
3754
4907
  if (name === "CTRL_C") return shutdown();
3755
- if ((name === "CTRL_G" || name === "CTRL_ALT_G") && voiceOn) return toggleTalk();
4908
+ if ((name === "CTRL_G" || name === "CTRL_ALT_G") && voice.active) return toggleTalk();
3756
4909
  if (cine || boot) return skipBoot();
3757
4910
  const rung = rungFor(W(), H());
3758
4911
  if (rung === "micro" || rung === "orb") return;
@@ -3839,11 +4992,23 @@ function onKey(name) {
3839
4992
  return;
3840
4993
  }
3841
4994
  if (name === "CTRL_P") {
3842
- state.screen = "home";
3843
- sendMsg({ t: "tab", tab: "home" });
4995
+ goHome();
3844
4996
  return;
3845
4997
  }
3846
4998
  if (!onChat) {
4999
+ if (state.tab === "canvas") {
5000
+ const page = Math.max(1, canvasView.h - 1);
5001
+ if (name === "LEFT") return panCanvas(-1, 0);
5002
+ if (name === "RIGHT") return panCanvas(1, 0);
5003
+ if (name === "UP") return panCanvas(0, -1);
5004
+ if (name === "DOWN") return panCanvas(0, 1);
5005
+ if (name === "PAGE_UP") return panCanvas(0, -page);
5006
+ if (name === "PAGE_DOWN") return panCanvas(0, page);
5007
+ if (name === "HOME") {
5008
+ canvasPan = { x: 0, y: 0 };
5009
+ return;
5010
+ }
5011
+ }
3847
5012
  if (name === "1") return setTab("chat");
3848
5013
  if (name === "2") return setTab("canvas");
3849
5014
  if (name === "3") return setTab("secrets");
@@ -3996,8 +5161,7 @@ function restoreTerminal() {
3996
5161
  function shutdown() {
3997
5162
  restoreTerminal();
3998
5163
  try {
3999
- voiceCapture?.stop();
4000
- voicePlayback?.stopAll();
5164
+ voice.shutdown();
4001
5165
  } catch {
4002
5166
  }
4003
5167
  for (const h of runningExec.values()) {
@@ -4021,7 +5185,7 @@ function shutdownWithMessage(text) {
4021
5185
  }
4022
5186
  process.exit(1);
4023
5187
  }
4024
- function runClient(opts = {}) {
5188
+ async function runClient(opts = {}) {
4025
5189
  loadSettings();
4026
5190
  updateInfo = opts.update ?? null;
4027
5191
  try {
@@ -4029,7 +5193,6 @@ function runClient(opts = {}) {
4029
5193
  } catch {
4030
5194
  myAccounts = [];
4031
5195
  }
4032
- me = loadIdentity(opts.name);
4033
5196
  SERVER = opts.server || process.env.ACCELR8_SERVER || DEFAULT_SERVER;
4034
5197
  if (!/^wss?:\/\//.test(SERVER)) SERVER = "ws://" + SERVER;
4035
5198
  try {
@@ -4039,6 +5202,10 @@ function runClient(opts = {}) {
4039
5202
  process.exit(1);
4040
5203
  }
4041
5204
  autoJoinCode = opts.joinCode;
5205
+ const creds = await ensureSignedIn(SERVER, { name: opts.name });
5206
+ if (!creds) process.exit(1);
5207
+ me = { ...creds.user };
5208
+ if (opts.name && opts.name.trim() && opts.name.trim() !== creds.user.name) pendingRename = opts.name.trim();
4042
5209
  const die = (err) => {
4043
5210
  restoreTerminal();
4044
5211
  console.error("accelr8 crashed:", err instanceof Error ? err.stack || err.message : err);
@@ -4069,25 +5236,42 @@ export {
4069
5236
  THEMES,
4070
5237
  __accent,
4071
5238
  __applyTheme,
5239
+ __bootAt,
5240
+ __bootOff,
5241
+ __canvasPan,
5242
+ __canvasView,
4072
5243
  __click,
4073
5244
  __closeOverlay,
4074
5245
  __curTheme,
4075
5246
  __dump,
4076
5247
  __dumpCells,
5248
+ __grid,
5249
+ homeLogoSeat as __homeLogoSeat,
5250
+ __key,
5251
+ __mouse,
4077
5252
  __openAccounts,
4078
5253
  __openAgentAccess,
4079
5254
  __openSettings,
5255
+ __puts,
5256
+ __server,
4080
5257
  __setAccounts,
4081
5258
  __setHeadlessScreen,
4082
5259
  __setMe,
4083
5260
  __setMySid,
4084
5261
  __setNav,
4085
5262
  __setUpdate,
5263
+ __setVoice,
5264
+ __startUpdate,
4086
5265
  __taskDemo,
5266
+ __updateRunning,
5267
+ canvasCellAt,
5268
+ canvasViewport,
4087
5269
  chatLog,
5270
+ clampPan,
4088
5271
  cursors,
4089
5272
  editor,
4090
5273
  ghosts,
5274
+ inputWindow,
4091
5275
  popup,
4092
5276
  render,
4093
5277
  runClient,