accelr8 3.1.1 → 3.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chunks/app-VQ33JVDI.js +793 -0
- package/dist/chunks/chunk-HY6ULYGH.js +559 -0
- package/dist/chunks/{chunk-AY3UXM6A.js → chunk-TW4QNNNT.js} +1 -1
- package/dist/chunks/client-6SYGGJ7L.js +4035 -0
- package/dist/chunks/{server-EFKIKFUG.js → server-VWQ2NYRN.js} +62 -14
- package/dist/cli.js +17 -4
- package/package.json +5 -3
- package/dist/chunks/client-J62VQCBE.js +0 -4331
|
@@ -1,4331 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
CANVAS_H,
|
|
4
|
-
CANVAS_W,
|
|
5
|
-
DEFAULT_SERVER,
|
|
6
|
-
PROTO,
|
|
7
|
-
VERSION,
|
|
8
|
-
decode,
|
|
9
|
-
encode
|
|
10
|
-
} from "./chunk-AY3UXM6A.js";
|
|
11
|
-
import {
|
|
12
|
-
DEFAULT_MODEL,
|
|
13
|
-
addAccount,
|
|
14
|
-
defaultHandle,
|
|
15
|
-
detectClaudeCode,
|
|
16
|
-
execute,
|
|
17
|
-
loadAccounts,
|
|
18
|
-
loadTokens,
|
|
19
|
-
removeAccount,
|
|
20
|
-
saveAccounts,
|
|
21
|
-
saveToken,
|
|
22
|
-
verifyAccount
|
|
23
|
-
} from "./chunk-3JNUWI22.js";
|
|
24
|
-
|
|
25
|
-
// src/client.ts
|
|
26
|
-
import tk from "terminal-kit";
|
|
27
|
-
import { WebSocket } from "ws";
|
|
28
|
-
import { spawn as spawn2 } from "node:child_process";
|
|
29
|
-
import { readFileSync, writeFileSync, mkdirSync, existsSync } from "node:fs";
|
|
30
|
-
import { homedir } from "node:os";
|
|
31
|
-
import { join } from "node:path";
|
|
32
|
-
|
|
33
|
-
// src/logo.ts
|
|
34
|
-
var LOGO = [
|
|
35
|
-
" ('-. ('-. _ .-')",
|
|
36
|
-
" ( OO ).-. _( OO) ( \\( -O )",
|
|
37
|
-
" / . --. / .-----. .-----. (,------. ,--. ,------. .-----.",
|
|
38
|
-
" | \\-. \\ ' .--./ ' .--./ | .---' | |.-') | /`. ' / .-. \\",
|
|
39
|
-
".-'-' | | | |('-. | |('-. | | | | OO ) | / | | | \\_.' /",
|
|
40
|
-
" \\| |_.' | /_) |OO ) /_) |OO ) (| '--. | |`-' | | |_.' | / .-. '.",
|
|
41
|
-
" | .-. | || |`-'| || |`-'| | .--' (| '---.' | . '.' | | | |",
|
|
42
|
-
" | | | | (_' '--'\\ (_' '--'\\ | `---. | | | |\\ \\ \\ '-' /",
|
|
43
|
-
" `--' `--' `-----' `-----' `------' `------' `--' '--' `----''"
|
|
44
|
-
];
|
|
45
|
-
var LOGO_W = Math.max(...LOGO.map((l) => l.length));
|
|
46
|
-
|
|
47
|
-
// src/chat.ts
|
|
48
|
-
var CHAT_MAX = 1e3;
|
|
49
|
-
var SPINNER_COLOR = -1;
|
|
50
|
-
var SYSTEM_COLOR = -2;
|
|
51
|
-
var DIM = 245;
|
|
52
|
-
var FAINT = 245;
|
|
53
|
-
var SELF_MENTION_BG = -3;
|
|
54
|
-
var SELF_BG = SELF_MENTION_BG;
|
|
55
|
-
var INDENT_MAX = 12;
|
|
56
|
-
var CUTOFF = " \u2026(cut off)";
|
|
57
|
-
var GUTTER_GAP_MS = 5 * 6e4;
|
|
58
|
-
function scanMentions(text, entities) {
|
|
59
|
-
const out = [];
|
|
60
|
-
const re = /@[a-z0-9_-]+/gi;
|
|
61
|
-
let cu = 0, cp = 0;
|
|
62
|
-
for (let m = re.exec(text); m; m = re.exec(text)) {
|
|
63
|
-
const token = m[0];
|
|
64
|
-
const ent = entities.find((x) => x.name.toLowerCase() === token.slice(1).toLowerCase());
|
|
65
|
-
if (!ent) continue;
|
|
66
|
-
cp += [...text.slice(cu, m.index)].length;
|
|
67
|
-
cu = m.index;
|
|
68
|
-
const span = { start: cp, end: cp + token.length, color: ent.color, bold: true };
|
|
69
|
-
if (ent.self) span.bgColor = SELF_BG;
|
|
70
|
-
out.push(span);
|
|
71
|
-
}
|
|
72
|
-
return out;
|
|
73
|
-
}
|
|
74
|
-
function gutterFor(prev, e) {
|
|
75
|
-
if (e.kind === "system") return null;
|
|
76
|
-
if (prev && prev.authorId === e.authorId && e.ts - prev.ts < GUTTER_GAP_MS) return null;
|
|
77
|
-
const d = new Date(e.ts);
|
|
78
|
-
return `${String(d.getHours()).padStart(2, "0")}:${String(d.getMinutes()).padStart(2, "0")}`;
|
|
79
|
-
}
|
|
80
|
-
function wrapRanges(cps, firstW, contW) {
|
|
81
|
-
const out = [];
|
|
82
|
-
let first = true;
|
|
83
|
-
let segStart = 0;
|
|
84
|
-
for (let j = 0; j <= cps.length; j++) {
|
|
85
|
-
if (j < cps.length && cps[j] !== "\n") continue;
|
|
86
|
-
let i = segStart;
|
|
87
|
-
if (i === j) {
|
|
88
|
-
out.push({ s: i, e: i });
|
|
89
|
-
first = false;
|
|
90
|
-
}
|
|
91
|
-
while (i < j) {
|
|
92
|
-
const avail = Math.max(1, first ? firstW : contW);
|
|
93
|
-
first = false;
|
|
94
|
-
if (j - i <= avail) {
|
|
95
|
-
out.push({ s: i, e: j });
|
|
96
|
-
break;
|
|
97
|
-
}
|
|
98
|
-
let brk = -1;
|
|
99
|
-
for (let k = i + avail; k > i; k--) if (cps[k] === " ") {
|
|
100
|
-
brk = k;
|
|
101
|
-
break;
|
|
102
|
-
}
|
|
103
|
-
if (brk >= 0) {
|
|
104
|
-
out.push({ s: i, e: brk });
|
|
105
|
-
i = brk + 1;
|
|
106
|
-
} else {
|
|
107
|
-
out.push({ s: i, e: i + avail });
|
|
108
|
-
i += avail;
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
segStart = j + 1;
|
|
112
|
-
}
|
|
113
|
-
return out;
|
|
114
|
-
}
|
|
115
|
-
function clampLine(line, width) {
|
|
116
|
-
const cps = [...line.text];
|
|
117
|
-
if (cps.length <= width) return line;
|
|
118
|
-
return {
|
|
119
|
-
...line,
|
|
120
|
-
text: cps.slice(0, width).join(""),
|
|
121
|
-
spans: line.spans.filter((s) => s.start < width).map((s) => ({ ...s, end: Math.min(s.end, width) }))
|
|
122
|
-
};
|
|
123
|
-
}
|
|
124
|
-
function systemLines(text, width) {
|
|
125
|
-
const cps = [...`\xB7 ${text} \xB7`];
|
|
126
|
-
return wrapRanges(cps, width, width).map((r) => clampLine({
|
|
127
|
-
text: cps.slice(r.s, r.e).join(""),
|
|
128
|
-
spans: r.e > r.s ? [{ start: 0, end: r.e - r.s, color: SYSTEM_COLOR }] : [],
|
|
129
|
-
centered: true
|
|
130
|
-
}, width));
|
|
131
|
-
}
|
|
132
|
-
function entryLines(e, mentions, width) {
|
|
133
|
-
if (e.kind === "system") return systemLines(e.text, width);
|
|
134
|
-
const name = (e.kind === "agent" ? "\u2726" : "") + e.authorName;
|
|
135
|
-
const nameLen = [...name].length;
|
|
136
|
-
const pre = name + (e.streaming ? " \u283F" : "") + " \u25B8 ";
|
|
137
|
-
const preLen = [...pre].length;
|
|
138
|
-
const nameSpan = { start: 0, end: nameLen, color: e.color };
|
|
139
|
-
if (!e.pending) nameSpan.bold = true;
|
|
140
|
-
if (e.kind === "agent") nameSpan.italic = true;
|
|
141
|
-
const preSpans = [nameSpan];
|
|
142
|
-
if (e.streaming) preSpans.push({ start: nameLen + 1, end: nameLen + 2, color: SPINNER_COLOR });
|
|
143
|
-
preSpans.push({ start: preLen - 2, end: preLen - 1, color: DIM });
|
|
144
|
-
const cps = [...e.partial ? e.text + CUTOFF : e.text];
|
|
145
|
-
const textLen = [...e.text].length;
|
|
146
|
-
const bodySpans = e.pending ? [{ start: 0, end: cps.length, color: DIM }] : [...mentions];
|
|
147
|
-
if (e.partial) bodySpans.push({ start: textLen, end: cps.length, color: FAINT });
|
|
148
|
-
const indent = Math.min(preLen, INDENT_MAX);
|
|
149
|
-
const ranges = wrapRanges(cps, Math.max(1, width - preLen), Math.max(1, width - indent));
|
|
150
|
-
return ranges.map((r, k) => {
|
|
151
|
-
const col = k === 0 ? preLen : indent;
|
|
152
|
-
const spans = k === 0 ? [...preSpans] : [];
|
|
153
|
-
for (const m of bodySpans) {
|
|
154
|
-
const s = Math.max(m.start, r.s), t = Math.min(m.end, r.e);
|
|
155
|
-
if (s < t) spans.push({ ...m, start: s - r.s + col, end: t - r.s + col });
|
|
156
|
-
}
|
|
157
|
-
return clampLine({ text: (k === 0 ? pre : " ".repeat(indent)) + cps.slice(r.s, r.e).join(""), spans }, width);
|
|
158
|
-
});
|
|
159
|
-
}
|
|
160
|
-
var makeCell = (e, ents) => ({
|
|
161
|
-
e,
|
|
162
|
-
ents,
|
|
163
|
-
mentions: ents ? scanMentions(e.text, ents) : [],
|
|
164
|
-
cache: null
|
|
165
|
-
});
|
|
166
|
-
var emptyLine = () => ({ text: "", spans: [] });
|
|
167
|
-
var ChatLog = class {
|
|
168
|
-
cells = [];
|
|
169
|
-
/** Wrapped line pinned to the TOP of the viewport while unpinned —
|
|
170
|
-
* top-anchored so a streaming tail growing below never shifts what
|
|
171
|
-
* you are reading. */
|
|
172
|
-
anchor = null;
|
|
173
|
-
lastGutters = [];
|
|
174
|
-
pinned = true;
|
|
175
|
-
unseen = 0;
|
|
176
|
-
/** Older pages may exist above; assume yes until a history page says no. */
|
|
177
|
-
hasMoreHistory = true;
|
|
178
|
-
get size() {
|
|
179
|
-
return this.cells.length;
|
|
180
|
-
}
|
|
181
|
-
push(e, mentionEntities2) {
|
|
182
|
-
this.cells.push(makeCell(e, mentionEntities2));
|
|
183
|
-
if (this.cells.length > CHAT_MAX) {
|
|
184
|
-
this.cells.shift();
|
|
185
|
-
if (this.anchor) {
|
|
186
|
-
if (this.anchor.index > 0) this.anchor.index--;
|
|
187
|
-
else this.anchor.offset = 0;
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
if (!this.pinned) this.unseen++;
|
|
191
|
-
}
|
|
192
|
-
/** Replace the whole log (fresh join / resync); re-pins to the tail. */
|
|
193
|
-
reset(entries, mentionEntities2) {
|
|
194
|
-
this.cells = entries.slice(-CHAT_MAX).map((e) => makeCell(e, mentionEntities2));
|
|
195
|
-
this.hasMoreHistory = true;
|
|
196
|
-
this.pinToTail();
|
|
197
|
-
}
|
|
198
|
-
/** Older history pages in ABOVE existing content; the anchored read
|
|
199
|
-
* position must not move. Only what the ring can still hold is kept
|
|
200
|
-
* (trimmed rows still count as "more above"). */
|
|
201
|
-
prepend(entries, mentionEntities2, hasMore = false) {
|
|
202
|
-
const take = entries.slice(Math.max(0, entries.length - (CHAT_MAX - this.cells.length)));
|
|
203
|
-
this.hasMoreHistory = hasMore || take.length < entries.length;
|
|
204
|
-
if (!take.length) return;
|
|
205
|
-
this.cells.unshift(...take.map((e) => makeCell(e, mentionEntities2)));
|
|
206
|
-
if (this.anchor) this.anchor.index += take.length;
|
|
207
|
-
}
|
|
208
|
-
/** Swap the acked server entry in where the optimistic one sat. */
|
|
209
|
-
ackOptimistic(cmid, real) {
|
|
210
|
-
const i = this.cells.findIndex((c) => c.e.pending && c.e.cmid === cmid);
|
|
211
|
-
if (i < 0) return false;
|
|
212
|
-
this.cells[i] = makeCell({ ...real, pending: false }, this.cells[i].ents);
|
|
213
|
-
return true;
|
|
214
|
-
}
|
|
215
|
-
/** Remove a failed optimistic send; caller restores it as the draft. */
|
|
216
|
-
failOptimistic(cmid) {
|
|
217
|
-
const i = this.cells.findIndex((c) => c.e.pending && c.e.cmid === cmid);
|
|
218
|
-
if (i < 0) return null;
|
|
219
|
-
const [cell] = this.cells.splice(i, 1);
|
|
220
|
-
if (this.anchor) {
|
|
221
|
-
if (this.anchor.index > i) this.anchor.index--;
|
|
222
|
-
else if (this.anchor.index === i) {
|
|
223
|
-
if (!this.cells.length) this.pinToTail();
|
|
224
|
-
else this.anchor = { index: Math.min(i, this.cells.length - 1), offset: 0 };
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
return cell.e;
|
|
228
|
-
}
|
|
229
|
-
/** Append stream text to the matching streaming entry; only that
|
|
230
|
-
* entry's wrap cache is invalidated. Returns false if none matched
|
|
231
|
-
* (caller can then push a fresh entry). */
|
|
232
|
-
appendDelta(match, delta) {
|
|
233
|
-
for (let i = this.cells.length - 1; i >= 0; i--) {
|
|
234
|
-
const c = this.cells[i];
|
|
235
|
-
if (!c.e.streaming || !match(c.e)) continue;
|
|
236
|
-
c.e.text += delta;
|
|
237
|
-
if (c.ents) c.mentions = scanMentions(c.e.text, c.ents);
|
|
238
|
-
c.cache = null;
|
|
239
|
-
return true;
|
|
240
|
-
}
|
|
241
|
-
return false;
|
|
242
|
-
}
|
|
243
|
-
/** Replace a streaming entry in place with the server's final message
|
|
244
|
-
* (real seq + text), keeping its position. Returns false if none matched. */
|
|
245
|
-
replaceStreaming(match, real, mentionEntities2) {
|
|
246
|
-
for (let i = this.cells.length - 1; i >= 0; i--) {
|
|
247
|
-
if (!match(this.cells[i].e)) continue;
|
|
248
|
-
this.cells[i] = makeCell({ ...real, streaming: false }, mentionEntities2);
|
|
249
|
-
return true;
|
|
250
|
-
}
|
|
251
|
-
return false;
|
|
252
|
-
}
|
|
253
|
-
/** End a stream; opts.partial marks it as cut off mid-generation. */
|
|
254
|
-
finalize(match, opts) {
|
|
255
|
-
for (let i = this.cells.length - 1; i >= 0; i--) {
|
|
256
|
-
const c = this.cells[i];
|
|
257
|
-
if (!c.e.streaming || !match(c.e)) continue;
|
|
258
|
-
c.e.streaming = false;
|
|
259
|
-
if (opts?.partial) c.e.partial = true;
|
|
260
|
-
c.cache = null;
|
|
261
|
-
return true;
|
|
262
|
-
}
|
|
263
|
-
return false;
|
|
264
|
-
}
|
|
265
|
-
// ---------- scrolling ----------
|
|
266
|
-
scrollUp(nLines, width, viewH) {
|
|
267
|
-
const a = this.pinned ? this.topAnchor(width, viewH) : this.clampedAnchor(width);
|
|
268
|
-
if (!a) return;
|
|
269
|
-
for (let n = nLines; n > 0; n--) {
|
|
270
|
-
if (a.offset > 0) a.offset--;
|
|
271
|
-
else if (a.index > 0) {
|
|
272
|
-
a.index--;
|
|
273
|
-
a.offset = this.wrapOf(a.index, width).length - 1;
|
|
274
|
-
} else break;
|
|
275
|
-
}
|
|
276
|
-
this.pinned = false;
|
|
277
|
-
this.anchor = a;
|
|
278
|
-
}
|
|
279
|
-
scrollDown(nLines, width, viewH) {
|
|
280
|
-
if (this.pinned) return;
|
|
281
|
-
const a = this.clampedAnchor(width);
|
|
282
|
-
if (!a) {
|
|
283
|
-
this.pinToTail();
|
|
284
|
-
return;
|
|
285
|
-
}
|
|
286
|
-
for (let n = nLines; n > 0; n--) {
|
|
287
|
-
if (a.offset < this.wrapOf(a.index, width).length - 1) a.offset++;
|
|
288
|
-
else if (a.index < this.cells.length - 1) {
|
|
289
|
-
a.index++;
|
|
290
|
-
a.offset = 0;
|
|
291
|
-
} else break;
|
|
292
|
-
}
|
|
293
|
-
let rem = 0;
|
|
294
|
-
for (let i = a.index; i < this.cells.length && rem <= viewH; i++)
|
|
295
|
-
rem += this.wrapOf(i, width).length - (i === a.index ? a.offset : 0);
|
|
296
|
-
if (rem <= viewH) this.pinToTail();
|
|
297
|
-
else this.anchor = a;
|
|
298
|
-
}
|
|
299
|
-
pinToTail() {
|
|
300
|
-
this.pinned = true;
|
|
301
|
-
this.anchor = null;
|
|
302
|
-
this.unseen = 0;
|
|
303
|
-
}
|
|
304
|
-
scrollToTop(width, viewH) {
|
|
305
|
-
if (!this.topAnchor(width, viewH)) {
|
|
306
|
-
this.pinToTail();
|
|
307
|
-
return;
|
|
308
|
-
}
|
|
309
|
-
this.pinned = false;
|
|
310
|
-
this.anchor = { index: 0, offset: 0 };
|
|
311
|
-
}
|
|
312
|
-
// ---------- rendering ----------
|
|
313
|
-
/** Exactly viewH lines. Pinned: bottom-up from the tail, padded on top.
|
|
314
|
-
* Unpinned: top-down from the anchor, padded at the bottom only in the
|
|
315
|
-
* degenerate case (width shrank / evictions ran past the anchor).
|
|
316
|
-
* Also records the row-aligned gutter column for visibleGutters(). */
|
|
317
|
-
visible(width, viewH) {
|
|
318
|
-
const rows = [];
|
|
319
|
-
const guts = [];
|
|
320
|
-
if (this.pinned || !this.anchor || !this.cells.length) {
|
|
321
|
-
for (let i = this.cells.length - 1; i >= 0 && rows.length < viewH; i--) {
|
|
322
|
-
const lines = this.wrapOf(i, width);
|
|
323
|
-
for (let k = lines.length - 1; k >= 0 && rows.length < viewH; k--) {
|
|
324
|
-
rows.push(lines[k]);
|
|
325
|
-
guts.push(k === 0 ? this.gutterAt(i) : null);
|
|
326
|
-
}
|
|
327
|
-
}
|
|
328
|
-
while (rows.length < viewH) {
|
|
329
|
-
rows.push(emptyLine());
|
|
330
|
-
guts.push(null);
|
|
331
|
-
}
|
|
332
|
-
rows.reverse();
|
|
333
|
-
guts.reverse();
|
|
334
|
-
} else {
|
|
335
|
-
const a = this.clampedAnchor(width);
|
|
336
|
-
let off = a.offset;
|
|
337
|
-
for (let i = a.index; i < this.cells.length && rows.length < viewH; i++) {
|
|
338
|
-
const lines = this.wrapOf(i, width);
|
|
339
|
-
for (let k = off; k < lines.length && rows.length < viewH; k++) {
|
|
340
|
-
rows.push(lines[k]);
|
|
341
|
-
guts.push(k === 0 ? this.gutterAt(i) : null);
|
|
342
|
-
}
|
|
343
|
-
off = 0;
|
|
344
|
-
}
|
|
345
|
-
while (rows.length < viewH) {
|
|
346
|
-
rows.push(emptyLine());
|
|
347
|
-
guts.push(null);
|
|
348
|
-
}
|
|
349
|
-
}
|
|
350
|
-
this.lastGutters = guts;
|
|
351
|
-
return rows;
|
|
352
|
-
}
|
|
353
|
-
/** Gutter column ('HH:MM' | null per row) for the LAST visible() call:
|
|
354
|
-
* set on an entry's first wrapped line when gutterFor() wants one. */
|
|
355
|
-
visibleGutters() {
|
|
356
|
-
return this.lastGutters;
|
|
357
|
-
}
|
|
358
|
-
/** On terminal resize; caches also self-invalidate lazily per width. */
|
|
359
|
-
dropCaches() {
|
|
360
|
-
for (const c of this.cells) c.cache = null;
|
|
361
|
-
}
|
|
362
|
-
// ---------- internals ----------
|
|
363
|
-
wrapOf(i, width) {
|
|
364
|
-
const c = this.cells[i];
|
|
365
|
-
if (!c.cache || c.cache.width !== width) c.cache = { width, lines: entryLines(c.e, c.mentions, width) };
|
|
366
|
-
return c.cache.lines;
|
|
367
|
-
}
|
|
368
|
-
gutterAt(i) {
|
|
369
|
-
return gutterFor(i > 0 ? this.cells[i - 1].e : null, this.cells[i].e);
|
|
370
|
-
}
|
|
371
|
-
/** Anchor equivalent of the current pinned viewport top, or null when
|
|
372
|
-
* all content fits within viewH. */
|
|
373
|
-
topAnchor(width, viewH) {
|
|
374
|
-
let acc = 0;
|
|
375
|
-
for (let i = this.cells.length - 1; i >= 0; i--) {
|
|
376
|
-
acc += this.wrapOf(i, width).length;
|
|
377
|
-
if (acc >= viewH) return { index: i, offset: acc - viewH };
|
|
378
|
-
}
|
|
379
|
-
return null;
|
|
380
|
-
}
|
|
381
|
-
/** Copy of the anchor, clamped in case width changes shrank an entry. */
|
|
382
|
-
clampedAnchor(width) {
|
|
383
|
-
if (!this.anchor || !this.cells.length) return null;
|
|
384
|
-
const index = Math.min(this.anchor.index, this.cells.length - 1);
|
|
385
|
-
const offset = Math.max(0, Math.min(this.anchor.offset, this.wrapOf(index, width).length - 1));
|
|
386
|
-
return { index, offset };
|
|
387
|
-
}
|
|
388
|
-
};
|
|
389
|
-
|
|
390
|
-
// src/input.ts
|
|
391
|
-
var MAX_HISTORY = 50;
|
|
392
|
-
var isSpace = (ch) => /\s/.test(ch);
|
|
393
|
-
var isTokenChar = (ch) => /[a-z0-9_-]/i.test(ch);
|
|
394
|
-
var LineEditor = class {
|
|
395
|
-
text = "";
|
|
396
|
-
cursor = 0;
|
|
397
|
-
history = [];
|
|
398
|
-
histIdx = null;
|
|
399
|
-
// null = editing the live buffer
|
|
400
|
-
draft = null;
|
|
401
|
-
// live buffer stashed during a history walk
|
|
402
|
-
offset = 0;
|
|
403
|
-
// window() horizontal scroll, self-correcting
|
|
404
|
-
/** Returns true if the event was consumed / state changed (renderer marks dirty). */
|
|
405
|
-
key(name) {
|
|
406
|
-
switch (name) {
|
|
407
|
-
case "BACKSPACE":
|
|
408
|
-
return this.remove(this.cursor - 1, this.cursor);
|
|
409
|
-
case "DELETE":
|
|
410
|
-
return this.remove(this.cursor, this.cursor + 1);
|
|
411
|
-
case "LEFT":
|
|
412
|
-
return this.moveTo(this.cursor - 1);
|
|
413
|
-
case "RIGHT":
|
|
414
|
-
return this.moveTo(this.cursor + 1);
|
|
415
|
-
case "CTRL_LEFT":
|
|
416
|
-
case "ALT_B":
|
|
417
|
-
return this.moveTo(this.wordLeft());
|
|
418
|
-
case "CTRL_RIGHT":
|
|
419
|
-
case "ALT_F":
|
|
420
|
-
return this.moveTo(this.wordRight());
|
|
421
|
-
case "HOME":
|
|
422
|
-
case "CTRL_A":
|
|
423
|
-
return this.moveTo(0);
|
|
424
|
-
case "END":
|
|
425
|
-
case "CTRL_E":
|
|
426
|
-
return this.moveTo(this.len());
|
|
427
|
-
case "CTRL_W":
|
|
428
|
-
return this.remove(this.wordLeft(), this.cursor);
|
|
429
|
-
case "CTRL_U":
|
|
430
|
-
return this.remove(0, this.cursor);
|
|
431
|
-
case "CTRL_K":
|
|
432
|
-
return this.remove(this.cursor, this.len());
|
|
433
|
-
case "UP":
|
|
434
|
-
return this.histBack();
|
|
435
|
-
case "DOWN":
|
|
436
|
-
return this.histForward();
|
|
437
|
-
case "SPACE":
|
|
438
|
-
this.insert(" ");
|
|
439
|
-
return true;
|
|
440
|
-
default: {
|
|
441
|
-
if ([...name].length !== 1) return false;
|
|
442
|
-
const cp = name.codePointAt(0);
|
|
443
|
-
if (cp < 32 || cp === 127) return false;
|
|
444
|
-
this.insert(name);
|
|
445
|
-
return true;
|
|
446
|
-
}
|
|
447
|
-
}
|
|
448
|
-
}
|
|
449
|
-
/** Trimmed text or null if empty; pushes to history and resets the buffer. */
|
|
450
|
-
submit() {
|
|
451
|
-
const t = this.text.trim();
|
|
452
|
-
this.text = "";
|
|
453
|
-
this.cursor = 0;
|
|
454
|
-
this.offset = 0;
|
|
455
|
-
this.histIdx = null;
|
|
456
|
-
this.draft = null;
|
|
457
|
-
if (!t) return null;
|
|
458
|
-
this.history.push(t);
|
|
459
|
-
if (this.history.length > MAX_HISTORY) this.history.shift();
|
|
460
|
-
return t;
|
|
461
|
-
}
|
|
462
|
-
/** Replace the buffer (e.g. restoring a failed optimistic send). */
|
|
463
|
-
setText(t) {
|
|
464
|
-
this.text = t;
|
|
465
|
-
this.cursor = this.len();
|
|
466
|
-
this.histIdx = null;
|
|
467
|
-
this.draft = null;
|
|
468
|
-
}
|
|
469
|
-
clear() {
|
|
470
|
-
this.text = "";
|
|
471
|
-
this.cursor = 0;
|
|
472
|
-
this.offset = 0;
|
|
473
|
-
this.histIdx = null;
|
|
474
|
-
this.draft = null;
|
|
475
|
-
}
|
|
476
|
-
/** Odd run of trailing backslashes: 'a\' continues, 'a\\' is an escaped '\'. */
|
|
477
|
-
endsWithContinuation() {
|
|
478
|
-
let n = 0;
|
|
479
|
-
for (let i = this.text.length - 1; i >= 0 && this.text[i] === "\\"; i--) n++;
|
|
480
|
-
return n % 2 === 1;
|
|
481
|
-
}
|
|
482
|
-
/** Renderer calls this instead of sending: swap the trailing '\' for '\n'. */
|
|
483
|
-
newline() {
|
|
484
|
-
if (!this.endsWithContinuation()) return;
|
|
485
|
-
this.text = this.text.slice(0, -1) + "\n";
|
|
486
|
-
this.cursor = this.len();
|
|
487
|
-
}
|
|
488
|
-
/** Viewport into the buffer: scrolls to keep the cursor visible with a
|
|
489
|
-
* 1-col margin; '\n' renders as '↵' (text keeps the real newline). */
|
|
490
|
-
window(width) {
|
|
491
|
-
const cps = [...this.text].map((c) => c === "\n" ? "\u21B5" : c);
|
|
492
|
-
const len = cps.length;
|
|
493
|
-
if (width <= 0) return { view: "", cursorCol: 0, clippedLeft: false, clippedRight: len > 0 };
|
|
494
|
-
const margin = width > 2 ? 1 : 0;
|
|
495
|
-
if (this.cursor - this.offset < margin) this.offset = this.cursor - margin;
|
|
496
|
-
if (this.cursor - this.offset > width - 1 - margin) this.offset = this.cursor - (width - 1 - margin);
|
|
497
|
-
this.offset = Math.max(0, Math.min(this.offset, Math.max(0, len - width + 1)));
|
|
498
|
-
return {
|
|
499
|
-
view: cps.slice(this.offset, this.offset + width).join(""),
|
|
500
|
-
cursorCol: this.cursor - this.offset,
|
|
501
|
-
clippedLeft: this.offset > 0,
|
|
502
|
-
clippedRight: this.offset + width < len
|
|
503
|
-
};
|
|
504
|
-
}
|
|
505
|
-
// ---------- internals ----------
|
|
506
|
-
len() {
|
|
507
|
-
return [...this.text].length;
|
|
508
|
-
}
|
|
509
|
-
insert(s) {
|
|
510
|
-
const cps = [...this.text];
|
|
511
|
-
const add = [...s];
|
|
512
|
-
cps.splice(this.cursor, 0, ...add);
|
|
513
|
-
this.text = cps.join("");
|
|
514
|
-
this.cursor += add.length;
|
|
515
|
-
}
|
|
516
|
-
moveTo(i) {
|
|
517
|
-
const c = Math.max(0, Math.min(this.len(), i));
|
|
518
|
-
if (c === this.cursor) return false;
|
|
519
|
-
this.cursor = c;
|
|
520
|
-
return true;
|
|
521
|
-
}
|
|
522
|
-
remove(from, to) {
|
|
523
|
-
const cps = [...this.text];
|
|
524
|
-
const a = Math.max(0, from), b = Math.min(cps.length, to);
|
|
525
|
-
if (a >= b) return false;
|
|
526
|
-
cps.splice(a, b - a);
|
|
527
|
-
this.text = cps.join("");
|
|
528
|
-
if (this.cursor > b) this.cursor -= b - a;
|
|
529
|
-
else if (this.cursor > a) this.cursor = a;
|
|
530
|
-
return true;
|
|
531
|
-
}
|
|
532
|
-
// words = runs of non-space
|
|
533
|
-
wordLeft() {
|
|
534
|
-
const cps = [...this.text];
|
|
535
|
-
let i = this.cursor;
|
|
536
|
-
while (i > 0 && isSpace(cps[i - 1])) i--;
|
|
537
|
-
while (i > 0 && !isSpace(cps[i - 1])) i--;
|
|
538
|
-
return i;
|
|
539
|
-
}
|
|
540
|
-
wordRight() {
|
|
541
|
-
const cps = [...this.text];
|
|
542
|
-
let i = this.cursor;
|
|
543
|
-
while (i < cps.length && isSpace(cps[i])) i++;
|
|
544
|
-
while (i < cps.length && !isSpace(cps[i])) i++;
|
|
545
|
-
return i;
|
|
546
|
-
}
|
|
547
|
-
histBack() {
|
|
548
|
-
if (this.history.length === 0) return false;
|
|
549
|
-
if (this.histIdx === null) {
|
|
550
|
-
this.draft = this.text;
|
|
551
|
-
this.histIdx = this.history.length - 1;
|
|
552
|
-
} else if (this.histIdx > 0) this.histIdx--;
|
|
553
|
-
else return false;
|
|
554
|
-
this.text = this.history[this.histIdx];
|
|
555
|
-
this.cursor = this.len();
|
|
556
|
-
return true;
|
|
557
|
-
}
|
|
558
|
-
histForward() {
|
|
559
|
-
if (this.histIdx === null) return false;
|
|
560
|
-
if (this.histIdx < this.history.length - 1) this.text = this.history[++this.histIdx];
|
|
561
|
-
else {
|
|
562
|
-
this.histIdx = null;
|
|
563
|
-
this.text = this.draft ?? "";
|
|
564
|
-
this.draft = null;
|
|
565
|
-
}
|
|
566
|
-
this.cursor = this.len();
|
|
567
|
-
return true;
|
|
568
|
-
}
|
|
569
|
-
};
|
|
570
|
-
var MentionPopup = class {
|
|
571
|
-
open = false;
|
|
572
|
-
filter = "";
|
|
573
|
-
selIdx = 0;
|
|
574
|
-
matches = [];
|
|
575
|
-
tokenStart = 0;
|
|
576
|
-
// code-point index of the '@'
|
|
577
|
-
tokenEnd = 0;
|
|
578
|
-
// code-point index of the cursor (token end)
|
|
579
|
-
/** Call after every editor mutation. Opens when the text up to the cursor
|
|
580
|
-
* ends with an @-token ('@' at 0 or after whitespace + [a-z0-9_-]* chars). */
|
|
581
|
-
update(editor2, candidates2) {
|
|
582
|
-
const cps = [...editor2.text];
|
|
583
|
-
const end = Math.max(0, Math.min(editor2.cursor, cps.length));
|
|
584
|
-
let i = end;
|
|
585
|
-
while (i > 0 && isTokenChar(cps[i - 1])) i--;
|
|
586
|
-
if (i === 0 || cps[i - 1] !== "@" || i >= 2 && !isSpace(cps[i - 2])) return this.close();
|
|
587
|
-
const filter = cps.slice(i, end).join("");
|
|
588
|
-
const f = filter.toLowerCase();
|
|
589
|
-
const ms = candidates2().filter((c) => c.name.toLowerCase().startsWith(f)).sort((a, b) => a.kind === b.kind ? a.name.localeCompare(b.name) : a.kind === "agent" ? -1 : 1).slice(0, 6);
|
|
590
|
-
if (ms.length === 0) return this.close();
|
|
591
|
-
if (!this.open || filter !== this.filter) this.selIdx = 0;
|
|
592
|
-
this.selIdx = Math.min(this.selIdx, ms.length - 1);
|
|
593
|
-
this.open = true;
|
|
594
|
-
this.filter = filter;
|
|
595
|
-
this.matches = ms;
|
|
596
|
-
this.tokenStart = i - 1;
|
|
597
|
-
this.tokenEnd = end;
|
|
598
|
-
}
|
|
599
|
-
/** Filtered candidates: agents first then humans, each alphabetical, max 6. */
|
|
600
|
-
visible() {
|
|
601
|
-
return this.matches;
|
|
602
|
-
}
|
|
603
|
-
/** Key routing while open; renderer calls this BEFORE the editor. On accept
|
|
604
|
-
* the renderer splices [start, end) with `insert` and moves the cursor. */
|
|
605
|
-
key(name) {
|
|
606
|
-
if (!this.open) return { kind: "pass" };
|
|
607
|
-
switch (name) {
|
|
608
|
-
case "UP":
|
|
609
|
-
this.selIdx = Math.max(0, this.selIdx - 1);
|
|
610
|
-
return { kind: "consumed" };
|
|
611
|
-
case "DOWN":
|
|
612
|
-
this.selIdx = Math.min(this.matches.length - 1, this.selIdx + 1);
|
|
613
|
-
return { kind: "consumed" };
|
|
614
|
-
case "TAB":
|
|
615
|
-
case "ENTER": {
|
|
616
|
-
const c = this.matches[this.selIdx];
|
|
617
|
-
this.close();
|
|
618
|
-
return { kind: "accept", start: this.tokenStart, end: this.tokenEnd, insert: "@" + c.name + " " };
|
|
619
|
-
}
|
|
620
|
-
case "ESC":
|
|
621
|
-
case "ESCAPE":
|
|
622
|
-
this.close();
|
|
623
|
-
return { kind: "closed" };
|
|
624
|
-
default:
|
|
625
|
-
return { kind: "pass" };
|
|
626
|
-
}
|
|
627
|
-
}
|
|
628
|
-
close() {
|
|
629
|
-
this.open = false;
|
|
630
|
-
this.filter = "";
|
|
631
|
-
this.selIdx = 0;
|
|
632
|
-
this.matches = [];
|
|
633
|
-
}
|
|
634
|
-
};
|
|
635
|
-
|
|
636
|
-
// src/boot.ts
|
|
637
|
-
var NOISE_CHARS = " .:-=+";
|
|
638
|
-
var NOISE_GREYS = [233, 234, 236, 238];
|
|
639
|
-
var SPARK_P = 0.02;
|
|
640
|
-
var CHURN = 0.3;
|
|
641
|
-
var CLEAR_P = 0.2;
|
|
642
|
-
var ACCENT = 39;
|
|
643
|
-
var SCAN_FG = 231;
|
|
644
|
-
var SCAN_BG = 236;
|
|
645
|
-
var TAG_COLOR = 245;
|
|
646
|
-
var VER_COLOR = 240;
|
|
647
|
-
var TAGLINE = "vibe-code together";
|
|
648
|
-
var TAG_SUFFIX = ` \xB7 v${VERSION}`;
|
|
649
|
-
var TYPE_MS = 25;
|
|
650
|
-
var FALLBACK = "A C C E L R 8";
|
|
651
|
-
var T = { logo: 350, scan: 1500, tag: 1900, handoff: 2400, done: 2900 };
|
|
652
|
-
var S = { scan: 100, scanEnd: 500, tag: 500, done: 700 };
|
|
653
|
-
var lerp = (a, b, p) => a + (b - a) * p;
|
|
654
|
-
var BootAnim = class {
|
|
655
|
-
/** Where the logo block landed, so the caller can align HOME. For the
|
|
656
|
-
* narrow-terminal text fallback these point at the fallback row. */
|
|
657
|
-
logoTop;
|
|
658
|
-
logoLeft;
|
|
659
|
-
w;
|
|
660
|
-
h;
|
|
661
|
-
short;
|
|
662
|
-
rng;
|
|
663
|
-
blockW;
|
|
664
|
-
logoH;
|
|
665
|
-
tagRow;
|
|
666
|
-
tagX;
|
|
667
|
-
glyphs = [];
|
|
668
|
-
glyphAt = /* @__PURE__ */ new Map();
|
|
669
|
-
// y*w+x -> glyph
|
|
670
|
-
noise = /* @__PURE__ */ new Map();
|
|
671
|
-
// y*w+x -> flicker cell
|
|
672
|
-
noiseSeeded = false;
|
|
673
|
-
constructor(w, h, opts = {}) {
|
|
674
|
-
this.w = w;
|
|
675
|
-
this.h = h;
|
|
676
|
-
this.short = !!opts.short;
|
|
677
|
-
this.rng = opts.rng ?? Math.random;
|
|
678
|
-
const rows = w < LOGO_W + 2 ? [FALLBACK] : LOGO;
|
|
679
|
-
this.blockW = Math.max(...rows.map((r) => r.length));
|
|
680
|
-
this.logoH = rows.length;
|
|
681
|
-
this.logoLeft = Math.max(0, Math.floor((w - this.blockW) / 2));
|
|
682
|
-
this.logoTop = Math.max(0, Math.min(h - this.logoH, Math.floor(h / 3) - Math.floor(this.logoH / 2)));
|
|
683
|
-
this.tagRow = this.logoTop + this.logoH + 1;
|
|
684
|
-
this.tagX = Math.floor((w - (TAGLINE.length + TAG_SUFFIX.length)) / 2);
|
|
685
|
-
const span = Math.max(1, this.blockW - 1);
|
|
686
|
-
for (let r = 0; r < rows.length; r++) {
|
|
687
|
-
const row = rows[r];
|
|
688
|
-
for (let c = 0; c < row.length; c++) {
|
|
689
|
-
if (row[c] === " ") continue;
|
|
690
|
-
const at = this.short ? 0 : T.logo + c / span * 600 + this.rng() * 450;
|
|
691
|
-
const g = { x: this.logoLeft + c, y: this.logoTop + r, ch: row[c], at };
|
|
692
|
-
this.glyphs.push(g);
|
|
693
|
-
this.glyphAt.set(g.y * w + g.x, g);
|
|
694
|
-
}
|
|
695
|
-
}
|
|
696
|
-
}
|
|
697
|
-
/** Cells to draw this frame, sparse: only lit cells. 'done' when finished. */
|
|
698
|
-
frame(tMs) {
|
|
699
|
-
if (tMs >= (this.short ? S.done : T.done)) return "done";
|
|
700
|
-
const cells = [];
|
|
701
|
-
const scanFrom = this.short ? S.scan : T.scan;
|
|
702
|
-
const scanTo = this.short ? S.scanEnd : T.tag;
|
|
703
|
-
const band = tMs >= scanFrom && tMs < scanTo ? this.logoTop + Math.min(this.logoH - 1, Math.floor((tMs - scanFrom) / (scanTo - scanFrom) * this.logoH)) : -1;
|
|
704
|
-
const typed = this.short ? tMs >= S.tag ? TAGLINE.length : 0 : Math.max(0, Math.min(TAGLINE.length, Math.floor((tMs - T.tag) / TYPE_MS)));
|
|
705
|
-
const withVersion = typed === TAGLINE.length;
|
|
706
|
-
const tagLen = typed + (withVersion ? TAG_SUFFIX.length : 0);
|
|
707
|
-
if (!this.short) this.stepNoise(tMs, band, tagLen, cells);
|
|
708
|
-
for (const g of this.glyphs) {
|
|
709
|
-
if (tMs < g.at) continue;
|
|
710
|
-
if (g.y === band) this.push(cells, g.x, g.y, { ch: g.ch, color: SCAN_FG, bold: true, bgColor: SCAN_BG });
|
|
711
|
-
else this.push(cells, g.x, g.y, { ch: g.ch, color: ACCENT, bold: true });
|
|
712
|
-
}
|
|
713
|
-
if (band >= 0) {
|
|
714
|
-
for (let x = this.logoLeft; x < this.logoLeft + this.blockW; x++) {
|
|
715
|
-
const g = this.glyphAt.get(band * this.w + x);
|
|
716
|
-
if (g && tMs >= g.at) continue;
|
|
717
|
-
this.push(cells, x, band, { ch: " ", color: SCAN_FG, bgColor: SCAN_BG });
|
|
718
|
-
}
|
|
719
|
-
}
|
|
720
|
-
for (let i = 0; i < typed; i++)
|
|
721
|
-
this.push(cells, this.tagX + i, this.tagRow, { ch: TAGLINE[i], color: TAG_COLOR });
|
|
722
|
-
if (withVersion)
|
|
723
|
-
for (let i = 0; i < TAG_SUFFIX.length; i++)
|
|
724
|
-
this.push(cells, this.tagX + TAGLINE.length + i, this.tagRow, { ch: TAG_SUFFIX[i], color: VER_COLOR });
|
|
725
|
-
return { cells };
|
|
726
|
-
}
|
|
727
|
-
// ---------- noise field ----------
|
|
728
|
-
/** Mutate the persistent noise field for this call, then emit it —
|
|
729
|
-
* skipping any cell the logo, scanline, or tagline owns this frame. */
|
|
730
|
-
stepNoise(tMs, band, tagLen, cells) {
|
|
731
|
-
const total = this.w * this.h;
|
|
732
|
-
if (tMs >= T.handoff) {
|
|
733
|
-
for (const idx of this.noise.keys()) if (this.rng() < CLEAR_P) this.noise.delete(idx);
|
|
734
|
-
} else {
|
|
735
|
-
const density = this.noiseDensity(tMs);
|
|
736
|
-
if (!this.noiseSeeded) {
|
|
737
|
-
this.noiseSeeded = true;
|
|
738
|
-
for (let idx = 0; idx < total; idx++) if (this.rng() < density) this.noise.set(idx, this.noiseCell());
|
|
739
|
-
}
|
|
740
|
-
const churn = Math.max(1, Math.round(total * CHURN));
|
|
741
|
-
for (let i = 0; i < churn; i++) {
|
|
742
|
-
const idx = Math.floor(this.rng() * total);
|
|
743
|
-
if (this.rng() < density) this.noise.set(idx, this.noiseCell());
|
|
744
|
-
else this.noise.delete(idx);
|
|
745
|
-
}
|
|
746
|
-
}
|
|
747
|
-
for (const [idx, cell] of this.noise) {
|
|
748
|
-
const x = idx % this.w;
|
|
749
|
-
const y = Math.floor(idx / this.w);
|
|
750
|
-
const g = this.glyphAt.get(idx);
|
|
751
|
-
if (g && tMs >= g.at) continue;
|
|
752
|
-
if (y === band && x >= this.logoLeft && x < this.logoLeft + this.blockW) continue;
|
|
753
|
-
if (tagLen > 0 && y === this.tagRow && x >= this.tagX && x < this.tagX + tagLen) continue;
|
|
754
|
-
cells.push({ x, y, cell });
|
|
755
|
-
}
|
|
756
|
-
}
|
|
757
|
-
/** Storyboard density ramp: 30% static -> 8% by scanline -> 4% by tagline. */
|
|
758
|
-
noiseDensity(t) {
|
|
759
|
-
if (t < T.logo) return 0.3;
|
|
760
|
-
if (t < T.scan) return lerp(0.3, 0.08, (t - T.logo) / (T.scan - T.logo));
|
|
761
|
-
if (t < T.tag) return lerp(0.08, 0.04, (t - T.scan) / (T.tag - T.scan));
|
|
762
|
-
return 0.04;
|
|
763
|
-
}
|
|
764
|
-
noiseCell() {
|
|
765
|
-
const ch = NOISE_CHARS[Math.floor(this.rng() * NOISE_CHARS.length)];
|
|
766
|
-
const color = this.rng() < SPARK_P ? ACCENT : NOISE_GREYS[Math.floor(this.rng() * NOISE_GREYS.length)];
|
|
767
|
-
return { ch, color };
|
|
768
|
-
}
|
|
769
|
-
// ---------- helpers ----------
|
|
770
|
-
/** Bounds-guarded emit: tiny terminals clip instead of overflowing. */
|
|
771
|
-
push(cells, x, y, cell) {
|
|
772
|
-
if (x >= 0 && x < this.w && y >= 0 && y < this.h) cells.push({ x, y, cell });
|
|
773
|
-
}
|
|
774
|
-
};
|
|
775
|
-
|
|
776
|
-
// src/orb.ts
|
|
777
|
-
var ACCENT2 = 39;
|
|
778
|
-
var BRIGHT = 231;
|
|
779
|
-
var BALL = 245;
|
|
780
|
-
var SHADOW = 238;
|
|
781
|
-
var FAINT2 = 240;
|
|
782
|
-
var WARN = 214;
|
|
783
|
-
var IDLE_HOLD = 10;
|
|
784
|
-
var GLINT_F = 2;
|
|
785
|
-
var STATIC_GLINT_EVERY = 90;
|
|
786
|
-
var SHAKE_F = 24;
|
|
787
|
-
var R1_F = 3;
|
|
788
|
-
var R2_F = 45;
|
|
789
|
-
var STREAM_STEP = 5;
|
|
790
|
-
var FLASH_F = 30;
|
|
791
|
-
var MENTION_SHAKE_F = 18;
|
|
792
|
-
var URGENT_ALT_F = 4;
|
|
793
|
-
var PULSE_F = 8;
|
|
794
|
-
var EGG_SHAKE_F = 12;
|
|
795
|
-
var EGG_HOLD_F = 180;
|
|
796
|
-
var EGG_IDLE_MIN = 5 * 60 * 30;
|
|
797
|
-
var EGG_CHANCE = 0.02;
|
|
798
|
-
var STREAM_GLYPHS = ["~", "\u2248", "~", "\u2726"];
|
|
799
|
-
var K_SHIFTS = [-1, 1, 0];
|
|
800
|
-
var EGG_ANSWERS = [
|
|
801
|
-
"ask again later",
|
|
802
|
-
"outlook good",
|
|
803
|
-
"signs point to yes",
|
|
804
|
-
"reply hazy",
|
|
805
|
-
"it is decidedly so",
|
|
806
|
-
"cannot predict now"
|
|
807
|
-
];
|
|
808
|
-
function art(o) {
|
|
809
|
-
const rows = ["", "", "", ""];
|
|
810
|
-
const spans = [];
|
|
811
|
-
const base = o.low ? 1 : 0;
|
|
812
|
-
const put = (row, start, end, role) => spans.push({ row, start, end, role });
|
|
813
|
-
if (o.glint) {
|
|
814
|
-
rows[base] = " .\xB7--. ";
|
|
815
|
-
put(base, 2, 3, "ring");
|
|
816
|
-
put(base, 3, 4, "die");
|
|
817
|
-
put(base, 4, 7, "ring");
|
|
818
|
-
} else {
|
|
819
|
-
rows[base] = " .---. ";
|
|
820
|
-
put(base, 2, 7, "ring");
|
|
821
|
-
}
|
|
822
|
-
if (o.window) {
|
|
823
|
-
rows[base + 1] = ` ( |${o.die}| ) `;
|
|
824
|
-
put(base + 1, 1, 2, "ring");
|
|
825
|
-
put(base + 1, 3, 4, "bars");
|
|
826
|
-
put(base + 1, 4, 5, "die");
|
|
827
|
-
put(base + 1, 5, 6, "bars");
|
|
828
|
-
put(base + 1, 7, 8, "ring");
|
|
829
|
-
} else {
|
|
830
|
-
rows[base + 1] = ` ( (${o.die}) ) `;
|
|
831
|
-
put(base + 1, 1, 2, "ring");
|
|
832
|
-
put(base + 1, 3, 4, "ring");
|
|
833
|
-
put(base + 1, 4, 5, "die");
|
|
834
|
-
put(base + 1, 5, 6, "ring");
|
|
835
|
-
put(base + 1, 7, 8, "ring");
|
|
836
|
-
}
|
|
837
|
-
rows[base + 2] = " `---' ";
|
|
838
|
-
put(base + 2, 2, 7, "ring");
|
|
839
|
-
if (!o.low && o.shadow !== false) {
|
|
840
|
-
rows[3] = " \xB7\xB7\xB7 ";
|
|
841
|
-
put(3, 3, 6, "shadow");
|
|
842
|
-
}
|
|
843
|
-
for (let r = 0; r < 4; r++) if (!rows[r]) rows[r] = " ".repeat(9);
|
|
844
|
-
if (o.glow) {
|
|
845
|
-
for (const r of [base, base + 2]) {
|
|
846
|
-
rows[r] = o.glow + rows[r].slice(1, 8) + o.glow;
|
|
847
|
-
put(r, 0, 1, "glow");
|
|
848
|
-
put(r, 8, 9, "glow");
|
|
849
|
-
}
|
|
850
|
-
}
|
|
851
|
-
const shift = o.shift ?? 0;
|
|
852
|
-
if (shift) {
|
|
853
|
-
for (let r = 0; r < 4; r++) rows[r] = shift < 0 ? rows[r].slice(1) + " " : " " + rows[r].slice(0, 8);
|
|
854
|
-
for (const s of spans) {
|
|
855
|
-
s.start = Math.max(0, Math.min(9, s.start + shift));
|
|
856
|
-
s.end = Math.max(0, Math.min(9, s.end + shift));
|
|
857
|
-
}
|
|
858
|
-
}
|
|
859
|
-
return { rows, spans };
|
|
860
|
-
}
|
|
861
|
-
var kFrame = (i) => art({ die: "\xB7", shift: K_SHIFTS[i] });
|
|
862
|
-
var tone = (color, bold) => bold ? { color, bold } : { color };
|
|
863
|
-
var PAL_IDLE = { ring: tone(BALL), die: tone(BRIGHT, true), bars: tone(BALL), glow: tone(BALL), shadow: tone(SHADOW) };
|
|
864
|
-
var PAL_SHAKE = { ring: tone(BALL), die: tone(BALL), bars: tone(BALL), glow: tone(BALL), shadow: tone(SHADOW) };
|
|
865
|
-
var PAL_OFFLINE = { ring: tone(SHADOW), die: tone(SHADOW), bars: tone(SHADOW), glow: tone(SHADOW), shadow: tone(SHADOW) };
|
|
866
|
-
var palReveal = (c) => ({ ring: tone(BALL), die: tone(c, true), bars: tone(c), glow: tone(c), shadow: tone(SHADOW) });
|
|
867
|
-
var palR3 = (c) => ({ ring: tone(BALL), die: tone(BRIGHT, true), bars: tone(c), glow: tone(c), shadow: tone(SHADOW) });
|
|
868
|
-
var palStream = (c) => ({ ring: tone(BALL), die: tone(c, true), bars: tone(BALL), glow: tone(BALL), shadow: tone(SHADOW) });
|
|
869
|
-
var palUrgent = (c) => ({ ring: tone(BRIGHT), die: tone(BRIGHT, true), bars: tone(BRIGHT), glow: tone(c), shadow: tone(SHADOW) });
|
|
870
|
-
function paint(a, pal, cols, rows) {
|
|
871
|
-
const off = cols === 9 ? 0 : 1;
|
|
872
|
-
const out = [];
|
|
873
|
-
for (let r = 0; r < rows; r++) {
|
|
874
|
-
const text = cols === 9 ? a.rows[r] : a.rows[r].slice(1, 8);
|
|
875
|
-
const spans = [];
|
|
876
|
-
for (const s of a.spans) {
|
|
877
|
-
if (s.row !== r) continue;
|
|
878
|
-
const start = Math.max(0, s.start - off);
|
|
879
|
-
const end = Math.min(cols, s.end - off);
|
|
880
|
-
if (start >= end) continue;
|
|
881
|
-
const t = pal[s.role];
|
|
882
|
-
spans.push(t.bold ? { start, end, color: t.color, bold: true } : { start, end, color: t.color });
|
|
883
|
-
}
|
|
884
|
-
spans.sort((x, y) => x.start - y.start);
|
|
885
|
-
out.push({ text, spans });
|
|
886
|
-
}
|
|
887
|
-
return out;
|
|
888
|
-
}
|
|
889
|
-
var OrbEngine = class {
|
|
890
|
-
rng;
|
|
891
|
-
lastFrame = 0;
|
|
892
|
-
offline = false;
|
|
893
|
-
t0 = 0;
|
|
894
|
-
t2 = 0;
|
|
895
|
-
lastColor = ACCENT2;
|
|
896
|
-
// last speaker's color (R3 glow, micro blink)
|
|
897
|
-
lastInitial = "\xB7";
|
|
898
|
-
activity = null;
|
|
899
|
-
urgent = null;
|
|
900
|
-
streamOn = false;
|
|
901
|
-
streamColor = ACCENT2;
|
|
902
|
-
streamStart = 0;
|
|
903
|
-
flashUntil = -1;
|
|
904
|
-
// done-flash deadline; -1 when inactive
|
|
905
|
-
eggState = null;
|
|
906
|
-
idleSince = 0;
|
|
907
|
-
// frame of the last event / animation end
|
|
908
|
-
constructor(opts) {
|
|
909
|
-
this.rng = opts?.rng ?? Math.random;
|
|
910
|
-
}
|
|
911
|
-
noteActivity(authorInitial, authorColor) {
|
|
912
|
-
this.lastInitial = ([...authorInitial.trim()][0] || "\xB7").toLowerCase();
|
|
913
|
-
this.lastColor = authorColor;
|
|
914
|
-
this.activity = { start: this.lastFrame + 1, color: authorColor };
|
|
915
|
-
this.eggState = null;
|
|
916
|
-
this.idleSince = this.lastFrame;
|
|
917
|
-
}
|
|
918
|
-
noteMention(authorColor) {
|
|
919
|
-
this.lastColor = authorColor;
|
|
920
|
-
this.urgent = { start: this.lastFrame + 1, color: authorColor };
|
|
921
|
-
this.eggState = null;
|
|
922
|
-
this.idleSince = this.lastFrame;
|
|
923
|
-
}
|
|
924
|
-
setStreaming(on, agentColor) {
|
|
925
|
-
if (on) {
|
|
926
|
-
if (agentColor !== void 0) this.streamColor = agentColor;
|
|
927
|
-
if (!this.streamOn) this.streamStart = this.lastFrame + 1;
|
|
928
|
-
this.streamOn = true;
|
|
929
|
-
this.flashUntil = -1;
|
|
930
|
-
this.eggState = null;
|
|
931
|
-
this.idleSince = this.lastFrame;
|
|
932
|
-
} else if (this.streamOn) {
|
|
933
|
-
this.streamOn = false;
|
|
934
|
-
this.flashUntil = this.lastFrame + 1 + FLASH_F;
|
|
935
|
-
this.idleSince = this.lastFrame;
|
|
936
|
-
}
|
|
937
|
-
}
|
|
938
|
-
setCounts(c) {
|
|
939
|
-
const t0 = Math.max(0, Math.floor(c.t0));
|
|
940
|
-
const t2 = Math.max(0, Math.floor(c.t2));
|
|
941
|
-
if (t0 === this.t0 && t2 === this.t2) return;
|
|
942
|
-
this.t0 = t0;
|
|
943
|
-
this.t2 = t2;
|
|
944
|
-
this.idleSince = this.lastFrame;
|
|
945
|
-
if (t0 + t2 === 0) this.urgent = null;
|
|
946
|
-
}
|
|
947
|
-
setOffline(off) {
|
|
948
|
-
if (off === this.offline) return;
|
|
949
|
-
this.offline = off;
|
|
950
|
-
this.idleSince = this.lastFrame;
|
|
951
|
-
if (off) {
|
|
952
|
-
this.activity = null;
|
|
953
|
-
this.urgent = null;
|
|
954
|
-
this.eggState = null;
|
|
955
|
-
this.streamOn = false;
|
|
956
|
-
this.flashUntil = -1;
|
|
957
|
-
}
|
|
958
|
-
}
|
|
959
|
-
/** Caller invokes once per minute of idle; 2% chance (injectable rng),
|
|
960
|
-
* only when idle 5+ min with zero unreads. Consumes rng twice on the
|
|
961
|
-
* gate pass: fire roll, then answer pick. */
|
|
962
|
-
maybeEgg(frame2) {
|
|
963
|
-
this.lastFrame = Math.max(this.lastFrame, frame2);
|
|
964
|
-
this.expire(frame2);
|
|
965
|
-
if (this.offline || this.streamOn || this.flashUntil >= 0) return false;
|
|
966
|
-
if (this.urgent || this.activity || this.eggState) return false;
|
|
967
|
-
if (this.t0 + this.t2 > 0) return false;
|
|
968
|
-
if (frame2 - this.idleSince < EGG_IDLE_MIN) return false;
|
|
969
|
-
if (this.rng() >= EGG_CHANCE) return false;
|
|
970
|
-
const pick = Math.max(0, Math.min(EGG_ANSWERS.length - 1, Math.floor(this.rng() * EGG_ANSWERS.length)));
|
|
971
|
-
this.eggState = { start: frame2, text: EGG_ANSWERS[pick] };
|
|
972
|
-
return true;
|
|
973
|
-
}
|
|
974
|
-
/** The egg's answer while the '?' frame shows (caller's ticker slot,
|
|
975
|
-
* render it FAINT/240); null otherwise. Valid after this frame's
|
|
976
|
-
* ball()/renderOrbRung() call. */
|
|
977
|
-
eggText() {
|
|
978
|
-
if (!this.eggState) return null;
|
|
979
|
-
const ph = this.lastFrame - this.eggState.start;
|
|
980
|
-
return ph >= EGG_SHAKE_F && ph < EGG_SHAKE_F + EGG_HOLD_F ? this.eggState.text : null;
|
|
981
|
-
}
|
|
982
|
-
/** Ball art for this frame: exactly `rows` lines, `cols` wide. */
|
|
983
|
-
ball(frame2, cols, rows) {
|
|
984
|
-
this.lastFrame = frame2;
|
|
985
|
-
this.expire(frame2);
|
|
986
|
-
const [a, pal] = this.artFor(frame2, rows === 4);
|
|
987
|
-
return paint(a, pal, cols, rows);
|
|
988
|
-
}
|
|
989
|
-
// status accessors for the rung renderers
|
|
990
|
-
isOffline() {
|
|
991
|
-
return this.offline;
|
|
992
|
-
}
|
|
993
|
-
isStreaming() {
|
|
994
|
-
return this.streamOn;
|
|
995
|
-
}
|
|
996
|
-
streamingColor() {
|
|
997
|
-
return this.streamColor;
|
|
998
|
-
}
|
|
999
|
-
lastAuthorColor() {
|
|
1000
|
-
return this.lastColor;
|
|
1001
|
-
}
|
|
1002
|
-
// ---------- internals ----------
|
|
1003
|
-
expire(frame2) {
|
|
1004
|
-
if (this.activity && frame2 >= this.activity.start + SHAKE_F + R1_F + R2_F) {
|
|
1005
|
-
this.activity = null;
|
|
1006
|
-
this.idleSince = frame2;
|
|
1007
|
-
}
|
|
1008
|
-
if (this.eggState && frame2 >= this.eggState.start + EGG_SHAKE_F + EGG_HOLD_F) {
|
|
1009
|
-
this.eggState = null;
|
|
1010
|
-
this.idleSince = frame2;
|
|
1011
|
-
}
|
|
1012
|
-
if (this.flashUntil >= 0 && frame2 >= this.flashUntil) {
|
|
1013
|
-
this.flashUntil = -1;
|
|
1014
|
-
this.idleSince = frame2;
|
|
1015
|
-
}
|
|
1016
|
-
}
|
|
1017
|
-
artFor(frame2, animated) {
|
|
1018
|
-
if (this.offline) return [art({ die: "8", shadow: false }), PAL_OFFLINE];
|
|
1019
|
-
if (this.urgent) {
|
|
1020
|
-
const ph = Math.max(0, frame2 - this.urgent.start);
|
|
1021
|
-
if (ph < MENTION_SHAKE_F) return [kFrame(ph % 3), PAL_SHAKE];
|
|
1022
|
-
const glyph = Math.floor((ph - MENTION_SHAKE_F) / URGENT_ALT_F) % 2 === 0 ? "!" : "\xB7";
|
|
1023
|
-
return [art({ die: "@", window: true, glow: glyph }), palUrgent(this.urgent.color)];
|
|
1024
|
-
}
|
|
1025
|
-
if (this.streamOn) {
|
|
1026
|
-
const ph = Math.max(0, frame2 - this.streamStart);
|
|
1027
|
-
const glyph = STREAM_GLYPHS[Math.floor(ph % (STREAM_STEP * 4) / STREAM_STEP)];
|
|
1028
|
-
return [art({ die: glyph, window: true }), palStream(this.streamColor)];
|
|
1029
|
-
}
|
|
1030
|
-
if (this.flashUntil >= 0) return [art({ die: "\u2726", window: true }), palStream(this.streamColor)];
|
|
1031
|
-
if (this.activity) {
|
|
1032
|
-
const ph = Math.max(0, frame2 - this.activity.start);
|
|
1033
|
-
if (ph < SHAKE_F) return [kFrame(Math.floor(ph / 2) % 3), PAL_SHAKE];
|
|
1034
|
-
if (ph < SHAKE_F + R1_F) return [art({ die: "\xB7", window: true }), palReveal(this.activity.color)];
|
|
1035
|
-
return [art({ die: this.lastInitial, window: true, glow: "\xB7" }), palReveal(this.activity.color)];
|
|
1036
|
-
}
|
|
1037
|
-
if (this.eggState) {
|
|
1038
|
-
const ph = Math.max(0, frame2 - this.eggState.start);
|
|
1039
|
-
if (ph < EGG_SHAKE_F) return [kFrame(Math.floor(ph / 2) % 3), PAL_SHAKE];
|
|
1040
|
-
return [art({ die: "?", window: true }), PAL_IDLE];
|
|
1041
|
-
}
|
|
1042
|
-
const unread2 = this.t0 + this.t2;
|
|
1043
|
-
if (unread2 > 0) {
|
|
1044
|
-
const pulseOn = Math.floor(frame2 / PULSE_F) % 2 === 0;
|
|
1045
|
-
return [art({ die: unread2 > 9 ? "+" : String(unread2), window: true, glow: pulseOn ? "\xB7" : null }), palR3(this.lastColor)];
|
|
1046
|
-
}
|
|
1047
|
-
if (animated) {
|
|
1048
|
-
const t = frame2 % (IDLE_HOLD * 2);
|
|
1049
|
-
if (t >= IDLE_HOLD) return [art({ die: "8", low: true }), PAL_IDLE];
|
|
1050
|
-
const glint = Math.floor(frame2 / (IDLE_HOLD * 2)) % 3 === 2 && t < GLINT_F;
|
|
1051
|
-
return [art({ die: "8", glint }), PAL_IDLE];
|
|
1052
|
-
}
|
|
1053
|
-
return [art({ die: "8", glint: frame2 % STATIC_GLINT_EVERY >= STATIC_GLINT_EVERY - GLINT_F }), PAL_IDLE];
|
|
1054
|
-
}
|
|
1055
|
-
};
|
|
1056
|
-
var emptyLine2 = () => ({ text: "", spans: [] });
|
|
1057
|
-
function trunc(text, max) {
|
|
1058
|
-
if (max <= 0) return "";
|
|
1059
|
-
const cps = [...text];
|
|
1060
|
-
if (cps.length <= max) return text;
|
|
1061
|
-
return cps.slice(0, max - 1).join("") + "\u2026";
|
|
1062
|
-
}
|
|
1063
|
-
function centerIn(line, w) {
|
|
1064
|
-
const pad = Math.max(0, Math.floor((w - [...line.text].length) / 2));
|
|
1065
|
-
if (!pad) return line;
|
|
1066
|
-
return { text: " ".repeat(pad) + line.text, spans: line.spans.map((s) => ({ ...s, start: s.start + pad, end: s.end + pad })) };
|
|
1067
|
-
}
|
|
1068
|
-
function clampTo(line, w) {
|
|
1069
|
-
const cps = [...line.text];
|
|
1070
|
-
if (cps.length <= w) return line;
|
|
1071
|
-
return {
|
|
1072
|
-
text: cps.slice(0, w).join(""),
|
|
1073
|
-
spans: line.spans.filter((s) => s.start < w).map((s) => ({ ...s, end: Math.min(s.end, w) }))
|
|
1074
|
-
};
|
|
1075
|
-
}
|
|
1076
|
-
function orbMeta(engine, info) {
|
|
1077
|
-
const bits = [];
|
|
1078
|
-
if (info.t0 > 0) bits.push(`${info.t0} new`);
|
|
1079
|
-
if (info.t2 > 0) bits.push(`@${info.t2}`);
|
|
1080
|
-
return {
|
|
1081
|
-
counts: bits.length ? bits.join(" \xB7 ") : null,
|
|
1082
|
-
ticker: engine.eggText() ?? info.lastEvent,
|
|
1083
|
-
project: info.project,
|
|
1084
|
-
connection: info.connected ? `\u25CF ${info.online} online` : "\u25CB offline",
|
|
1085
|
-
hint: info.hint
|
|
1086
|
-
};
|
|
1087
|
-
}
|
|
1088
|
-
function renderOrbRung(w, h, engine, frame2, info) {
|
|
1089
|
-
const cols = w >= 16 ? 9 : 7;
|
|
1090
|
-
const ballRows = h >= 9 ? 4 : h >= 6 ? 3 : 2;
|
|
1091
|
-
const out = engine.ball(frame2, cols, ballRows).slice(0, Math.max(0, h)).map((l) => clampTo(centerIn(l, w), w));
|
|
1092
|
-
const meta = orbMeta(engine, info);
|
|
1093
|
-
const entries = [];
|
|
1094
|
-
if (meta.counts) entries.push({ text: meta.counts, color: BRIGHT, bold: true });
|
|
1095
|
-
if (meta.ticker) entries.push({ text: meta.ticker, color: engine.eggText() ? FAINT2 : info.lastEventColor });
|
|
1096
|
-
if (meta.project) entries.push({ text: meta.project, color: BALL });
|
|
1097
|
-
entries.push({ text: meta.connection, color: info.connected ? ACCENT2 : WARN });
|
|
1098
|
-
if (meta.hint) entries.push({ text: meta.hint, color: FAINT2 });
|
|
1099
|
-
const room = h - out.length;
|
|
1100
|
-
const spacer = room > entries.length ? 1 : 0;
|
|
1101
|
-
if (spacer) out.push(emptyLine2());
|
|
1102
|
-
for (const e of entries.slice(0, Math.max(0, room - spacer))) {
|
|
1103
|
-
const text = trunc(e.text, Math.max(0, w - 2));
|
|
1104
|
-
const len = [...text].length;
|
|
1105
|
-
const span = e.bold ? { start: 0, end: len, color: e.color, bold: true } : { start: 0, end: len, color: e.color };
|
|
1106
|
-
out.push(centerIn({ text, spans: len ? [span] : [] }, w));
|
|
1107
|
-
}
|
|
1108
|
-
while (out.length < h) out.push(emptyLine2());
|
|
1109
|
-
return out.slice(0, h);
|
|
1110
|
-
}
|
|
1111
|
-
var sepPart = () => ({ text: " \xB7 ", color: SHADOW });
|
|
1112
|
-
var widthOf = (parts) => parts.reduce((n, p) => n + [...p.text].length, 0);
|
|
1113
|
-
function buildLine(parts) {
|
|
1114
|
-
let text = "";
|
|
1115
|
-
let at = 0;
|
|
1116
|
-
const spans = [];
|
|
1117
|
-
for (const p of parts) {
|
|
1118
|
-
const len = [...p.text].length;
|
|
1119
|
-
if (p.color !== void 0 && len) spans.push(p.bold ? { start: at, end: at + len, color: p.color, bold: true } : { start: at, end: at + len, color: p.color });
|
|
1120
|
-
text += p.text;
|
|
1121
|
-
at += len;
|
|
1122
|
-
}
|
|
1123
|
-
return { text, spans };
|
|
1124
|
-
}
|
|
1125
|
-
function statusColor(engine, frame2, info) {
|
|
1126
|
-
if (engine.isOffline() || !info.connected) return WARN;
|
|
1127
|
-
if (engine.isStreaming() || info.agentStreaming) return engine.streamingColor();
|
|
1128
|
-
if (info.t0 + info.t2 > 0) return Math.floor(frame2 / (PULSE_F / 2)) % 2 === 0 ? engine.lastAuthorColor() : SHADOW;
|
|
1129
|
-
return ACCENT2;
|
|
1130
|
-
}
|
|
1131
|
-
function microLine(w, engine, frame2, info) {
|
|
1132
|
-
const dot = { text: "\u25C9", color: statusColor(engine, frame2, info) };
|
|
1133
|
-
const project2 = info.project ? { text: info.project, color: BALL } : null;
|
|
1134
|
-
const counts = info.t0 > 0 ? { text: `${info.t0} new`, color: BRIGHT, bold: true } : null;
|
|
1135
|
-
const ticker = info.lastEvent ? { text: info.lastEvent, color: info.lastEventColor } : null;
|
|
1136
|
-
const onlineFull = { text: `${info.online} online`, color: BALL };
|
|
1137
|
-
const onlineNum = { text: `${info.online}`, color: BALL };
|
|
1138
|
-
const ladder = [
|
|
1139
|
-
{ brand: true, fields: [project2, onlineFull, counts], ticker: "full" },
|
|
1140
|
-
{ fields: [project2, onlineFull, counts], ticker: "trunc" },
|
|
1141
|
-
{ fields: [project2, onlineFull, counts] },
|
|
1142
|
-
{ fields: [project2, onlineNum, counts] },
|
|
1143
|
-
{ fields: [onlineNum, counts] }
|
|
1144
|
-
];
|
|
1145
|
-
for (const rung of ladder) {
|
|
1146
|
-
const parts = [];
|
|
1147
|
-
if (rung.brand) parts.push({ text: "accelr8", color: BALL, bold: true }, { text: " " });
|
|
1148
|
-
parts.push(dot);
|
|
1149
|
-
let first = true;
|
|
1150
|
-
for (const f of rung.fields) {
|
|
1151
|
-
if (!f) continue;
|
|
1152
|
-
parts.push(first ? { text: " " } : sepPart(), f);
|
|
1153
|
-
first = false;
|
|
1154
|
-
}
|
|
1155
|
-
const baseLen = widthOf(parts);
|
|
1156
|
-
if (rung.ticker && ticker) {
|
|
1157
|
-
const avail = w - baseLen - 3;
|
|
1158
|
-
const fits = rung.ticker === "full" ? [...ticker.text].length <= avail : avail >= 2;
|
|
1159
|
-
if (fits) {
|
|
1160
|
-
parts.push(sepPart(), { ...ticker, text: trunc(ticker.text, avail) });
|
|
1161
|
-
return buildLine(parts);
|
|
1162
|
-
}
|
|
1163
|
-
continue;
|
|
1164
|
-
}
|
|
1165
|
-
if (baseLen <= w) return buildLine(parts);
|
|
1166
|
-
}
|
|
1167
|
-
const bits = [];
|
|
1168
|
-
if (info.t0 > 0) bits.push(String(info.t0));
|
|
1169
|
-
if (info.t2 > 0) bits.push(`@${info.t2}`);
|
|
1170
|
-
const compact = buildLine(bits.length ? [dot, { text: " " }, { text: bits.join("\xB7"), color: BRIGHT, bold: true }] : [dot]);
|
|
1171
|
-
if ([...compact.text].length <= w) return compact;
|
|
1172
|
-
const tiny = buildLine(info.t0 > 0 ? [dot, { text: String(info.t0), color: BRIGHT, bold: true }] : [dot]);
|
|
1173
|
-
return clampTo(tiny, Math.max(0, w));
|
|
1174
|
-
}
|
|
1175
|
-
|
|
1176
|
-
// src/voice.ts
|
|
1177
|
-
import { spawn, spawnSync } from "node:child_process";
|
|
1178
|
-
import fs from "node:fs";
|
|
1179
|
-
import path from "node:path";
|
|
1180
|
-
var SAMPLE_RATE = 16e3;
|
|
1181
|
-
var CHUNK_BYTES = 1280;
|
|
1182
|
-
var KILL_GRACE_MS = 300;
|
|
1183
|
-
var SPEAKER_IDLE_MS = 2e3;
|
|
1184
|
-
var PREBUFFER_BYTES = 4800;
|
|
1185
|
-
var DRAIN_KILL_MS = 2e3;
|
|
1186
|
-
var MAX_PLAYERS = 8;
|
|
1187
|
-
var STDERR_TAIL = 400;
|
|
1188
|
-
var VERIFY_MIN_BYTES = SAMPLE_RATE * 2 / 10;
|
|
1189
|
-
function playbackArgs(ffplayPath) {
|
|
1190
|
-
const ch = ffplayChannelFlag(ffplayPath);
|
|
1191
|
-
return [
|
|
1192
|
-
"-hide_banner",
|
|
1193
|
-
"-loglevel",
|
|
1194
|
-
"error",
|
|
1195
|
-
"-nodisp",
|
|
1196
|
-
"-sync",
|
|
1197
|
-
"audio",
|
|
1198
|
-
"-infbuf",
|
|
1199
|
-
"-f",
|
|
1200
|
-
"s16le",
|
|
1201
|
-
"-ar",
|
|
1202
|
-
String(SAMPLE_RATE),
|
|
1203
|
-
...ch,
|
|
1204
|
-
"-i",
|
|
1205
|
-
"-"
|
|
1206
|
-
];
|
|
1207
|
-
}
|
|
1208
|
-
var chFlagCache = null;
|
|
1209
|
-
function ffplayChannelFlag(ffplayPath) {
|
|
1210
|
-
if (chFlagCache) return chFlagCache;
|
|
1211
|
-
let modern = true;
|
|
1212
|
-
try {
|
|
1213
|
-
const out = spawnSync(ffplayPath, ["-hide_banner", "-version"], { encoding: "utf8", timeout: 3e3 });
|
|
1214
|
-
const m = ((out.stdout || "") + (out.stderr || "")).match(/version\s+n?(\d+)\.(\d+)/i);
|
|
1215
|
-
if (m) {
|
|
1216
|
-
const major = +m[1], minor = +m[2];
|
|
1217
|
-
modern = major > 5 || major === 5 && minor >= 1;
|
|
1218
|
-
}
|
|
1219
|
-
} catch {
|
|
1220
|
-
}
|
|
1221
|
-
chFlagCache = modern ? ["-ch_layout", "mono"] : ["-ac", "1"];
|
|
1222
|
-
return chFlagCache;
|
|
1223
|
-
}
|
|
1224
|
-
function detectVoiceTools() {
|
|
1225
|
-
const ffmpegPath = which("ffmpeg");
|
|
1226
|
-
const ffplayPath = which("ffplay");
|
|
1227
|
-
const out = { capture: !!ffmpegPath, playback: !!ffplayPath };
|
|
1228
|
-
if (ffmpegPath) out.ffmpegPath = ffmpegPath;
|
|
1229
|
-
if (ffplayPath) out.ffplayPath = ffplayPath;
|
|
1230
|
-
if (!ffmpegPath || !ffplayPath) {
|
|
1231
|
-
const missing = [ffmpegPath ? "" : "ffmpeg", ffplayPath ? "" : "ffplay"].filter(Boolean).join(" + ");
|
|
1232
|
-
out.reason = `${missing} not found on PATH (voice needs ffmpeg; brew/apt install ffmpeg)`;
|
|
1233
|
-
}
|
|
1234
|
-
return out;
|
|
1235
|
-
}
|
|
1236
|
-
function which(bin) {
|
|
1237
|
-
const exts = process.platform === "win32" ? [".exe", ".cmd", ".bat", ""] : [""];
|
|
1238
|
-
for (const dir of (process.env.PATH ?? "").split(path.delimiter).filter(Boolean)) {
|
|
1239
|
-
for (const ext of exts) {
|
|
1240
|
-
const candidate = path.join(dir, bin + ext);
|
|
1241
|
-
try {
|
|
1242
|
-
if (!fs.existsSync(candidate)) continue;
|
|
1243
|
-
fs.accessSync(candidate, fs.constants.X_OK);
|
|
1244
|
-
if (fs.statSync(candidate).isFile()) return candidate;
|
|
1245
|
-
} catch {
|
|
1246
|
-
}
|
|
1247
|
-
}
|
|
1248
|
-
}
|
|
1249
|
-
return void 0;
|
|
1250
|
-
}
|
|
1251
|
-
function captureArgs(platform, device) {
|
|
1252
|
-
const base = ["-hide_banner", "-loglevel", "error"];
|
|
1253
|
-
const output = ["-ac", "1", "-ar", String(SAMPLE_RATE), "-f", "s16le", "-"];
|
|
1254
|
-
if (platform === "darwin") return [...base, "-f", "avfoundation", "-i", ":" + (device ?? "default"), ...output];
|
|
1255
|
-
if (platform === "win32") return [...base, "-f", "dshow", "-i", "audio=" + (device ?? "default"), ...output];
|
|
1256
|
-
return [...base, "-f", "alsa", "-i", device ?? "default", ...output];
|
|
1257
|
-
}
|
|
1258
|
-
function chunkPcm(buf, chunkBytes) {
|
|
1259
|
-
if (!Number.isInteger(chunkBytes) || chunkBytes <= 0) throw new RangeError("chunkBytes must be a positive integer");
|
|
1260
|
-
const b = Buffer.isBuffer(buf) ? buf : Buffer.from(buf.buffer, buf.byteOffset, buf.byteLength);
|
|
1261
|
-
const out = [];
|
|
1262
|
-
for (let i = 0; i < b.length; i += chunkBytes) out.push(b.subarray(i, i + chunkBytes).toString("base64"));
|
|
1263
|
-
return out;
|
|
1264
|
-
}
|
|
1265
|
-
var VoiceCapture = class {
|
|
1266
|
-
constructor(tools) {
|
|
1267
|
-
this.tools = tools;
|
|
1268
|
-
}
|
|
1269
|
-
tools;
|
|
1270
|
-
session = null;
|
|
1271
|
-
get active() {
|
|
1272
|
-
return this.session !== null;
|
|
1273
|
-
}
|
|
1274
|
-
/** Spawn ffmpeg mic->PCM and stream ~40ms base64 chunks to onChunk.
|
|
1275
|
-
* No-op while a capture is already active. */
|
|
1276
|
-
start(opts) {
|
|
1277
|
-
if (this.session) return;
|
|
1278
|
-
if (!this.tools.capture || !this.tools.ffmpegPath) {
|
|
1279
|
-
const why = this.tools.reason ?? "ffmpeg not found on PATH";
|
|
1280
|
-
queueMicrotask(() => opts.onError(why));
|
|
1281
|
-
return;
|
|
1282
|
-
}
|
|
1283
|
-
this.session = this.spawnSession(opts, captureArgs(process.platform, opts.device), "primary");
|
|
1284
|
-
}
|
|
1285
|
-
/** Flush the sub-chunk remainder, then SIGINT -> SIGKILL after 300ms. */
|
|
1286
|
-
stop() {
|
|
1287
|
-
const s = this.session;
|
|
1288
|
-
if (!s) return;
|
|
1289
|
-
this.session = null;
|
|
1290
|
-
s.stopped = true;
|
|
1291
|
-
if (s.buf.length) {
|
|
1292
|
-
s.onChunk(s.buf.toString("base64"));
|
|
1293
|
-
s.buf = Buffer.alloc(0);
|
|
1294
|
-
}
|
|
1295
|
-
try {
|
|
1296
|
-
s.child.kill("SIGINT");
|
|
1297
|
-
} catch {
|
|
1298
|
-
}
|
|
1299
|
-
const t = setTimeout(() => {
|
|
1300
|
-
try {
|
|
1301
|
-
s.child.kill("SIGKILL");
|
|
1302
|
-
} catch {
|
|
1303
|
-
}
|
|
1304
|
-
}, KILL_GRACE_MS);
|
|
1305
|
-
t.unref();
|
|
1306
|
-
}
|
|
1307
|
-
spawnSession(opts, args, backend) {
|
|
1308
|
-
let child;
|
|
1309
|
-
try {
|
|
1310
|
-
child = spawn(this.tools.ffmpegPath, args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
1311
|
-
} catch (e) {
|
|
1312
|
-
queueMicrotask(() => opts.onError("mic capture failed to start: " + errMsg(e)));
|
|
1313
|
-
return null;
|
|
1314
|
-
}
|
|
1315
|
-
const s = { child, onChunk: opts.onChunk, buf: Buffer.alloc(0), stderrTail: "", produced: false, stopped: false };
|
|
1316
|
-
child.stdout?.on("data", (chunk) => {
|
|
1317
|
-
if (s.stopped) return;
|
|
1318
|
-
s.produced = true;
|
|
1319
|
-
s.buf = s.buf.length ? Buffer.concat([s.buf, chunk]) : chunk;
|
|
1320
|
-
const whole = Math.floor(s.buf.length / CHUNK_BYTES) * CHUNK_BYTES;
|
|
1321
|
-
if (whole === 0) return;
|
|
1322
|
-
for (const b64 of chunkPcm(s.buf.subarray(0, whole), CHUNK_BYTES)) opts.onChunk(b64);
|
|
1323
|
-
s.buf = s.buf.subarray(whole);
|
|
1324
|
-
});
|
|
1325
|
-
child.stderr?.setEncoding("utf8");
|
|
1326
|
-
child.stderr?.on("data", (c) => {
|
|
1327
|
-
s.stderrTail = (s.stderrTail + c).slice(-STDERR_TAIL);
|
|
1328
|
-
});
|
|
1329
|
-
child.on("error", (e) => {
|
|
1330
|
-
if (s.stopped || this.session !== s) return;
|
|
1331
|
-
this.session = null;
|
|
1332
|
-
opts.onError(e.code === "ENOENT" ? "ffmpeg not found" : "mic capture error: " + errMsg(e));
|
|
1333
|
-
});
|
|
1334
|
-
child.on("close", (code) => {
|
|
1335
|
-
if (s.stopped || this.session !== s) return;
|
|
1336
|
-
if (backend === "primary" && process.platform === "linux" && !s.produced && code !== 0) {
|
|
1337
|
-
const pulse = captureArgs("linux", opts.device).map((a) => a === "alsa" ? "pulse" : a);
|
|
1338
|
-
this.session = this.spawnSession(opts, pulse, "pulse");
|
|
1339
|
-
return;
|
|
1340
|
-
}
|
|
1341
|
-
this.session = null;
|
|
1342
|
-
const tail = s.stderrTail.trim();
|
|
1343
|
-
opts.onError(code === 0 ? "mic capture ended unexpectedly" : "mic capture failed: " + (tail || `ffmpeg exited with code ${code}`));
|
|
1344
|
-
});
|
|
1345
|
-
return s;
|
|
1346
|
-
}
|
|
1347
|
-
};
|
|
1348
|
-
var VoicePlayback = class {
|
|
1349
|
-
constructor(tools) {
|
|
1350
|
-
this.tools = tools;
|
|
1351
|
-
}
|
|
1352
|
-
tools;
|
|
1353
|
-
players = /* @__PURE__ */ new Map();
|
|
1354
|
-
/** ids with an active player */
|
|
1355
|
-
get speakers() {
|
|
1356
|
-
return [...this.players.keys()];
|
|
1357
|
-
}
|
|
1358
|
-
/** Feed a received chunk; lazily spawns one ffplay per speaker. Chunks
|
|
1359
|
-
* are dropped silently when playback is unavailable, the speaker cap
|
|
1360
|
-
* is hit, or a player cannot be (re)spawned. */
|
|
1361
|
-
feed(speakerId, b64) {
|
|
1362
|
-
if (!this.tools.playback || !this.tools.ffplayPath) return;
|
|
1363
|
-
const bytes = Buffer.from(b64, "base64");
|
|
1364
|
-
if (bytes.length === 0) return;
|
|
1365
|
-
let p = this.players.get(speakerId);
|
|
1366
|
-
if (p && !p.alive) {
|
|
1367
|
-
this.reap(speakerId, p);
|
|
1368
|
-
p = void 0;
|
|
1369
|
-
}
|
|
1370
|
-
if (!p) {
|
|
1371
|
-
if (this.players.size >= MAX_PLAYERS) return;
|
|
1372
|
-
const spawned = this.spawnPlayer(speakerId);
|
|
1373
|
-
if (!spawned) return;
|
|
1374
|
-
p = spawned;
|
|
1375
|
-
}
|
|
1376
|
-
if (!p.flowing) {
|
|
1377
|
-
p.pre.push(bytes);
|
|
1378
|
-
if (p.pre.reduce((n, b) => n + b.length, 0) >= PREBUFFER_BYTES) {
|
|
1379
|
-
const cushion = Buffer.concat(p.pre);
|
|
1380
|
-
p.pre = [];
|
|
1381
|
-
p.flowing = true;
|
|
1382
|
-
const np2 = this.writeOrRespawn(speakerId, p, cushion);
|
|
1383
|
-
if (np2) this.touch(speakerId, np2);
|
|
1384
|
-
} else {
|
|
1385
|
-
this.touch(speakerId, p);
|
|
1386
|
-
}
|
|
1387
|
-
return;
|
|
1388
|
-
}
|
|
1389
|
-
const np = this.writeOrRespawn(speakerId, p, bytes);
|
|
1390
|
-
if (np) this.touch(speakerId, np);
|
|
1391
|
-
}
|
|
1392
|
-
/** Write to a player; on EPIPE (it died) respawn once and write there. Returns
|
|
1393
|
-
* the live player, or null if it couldn't be (re)spawned. */
|
|
1394
|
-
writeOrRespawn(speakerId, p, buf) {
|
|
1395
|
-
if (this.write(p, buf)) return p;
|
|
1396
|
-
this.reap(speakerId, p);
|
|
1397
|
-
const fresh = this.players.size < MAX_PLAYERS ? this.spawnPlayer(speakerId) : null;
|
|
1398
|
-
if (!fresh || !this.write(fresh, buf)) return null;
|
|
1399
|
-
fresh.flowing = true;
|
|
1400
|
-
return fresh;
|
|
1401
|
-
}
|
|
1402
|
-
/** Graceful close for one speaker (voiceStop): end stdin so ffplay drains
|
|
1403
|
-
* its buffered audio, then a SIGKILL backstop after 2s (we dropped -autoexit,
|
|
1404
|
-
* so the backstop is what actually reaps the idle player). */
|
|
1405
|
-
end(speakerId) {
|
|
1406
|
-
const p = this.players.get(speakerId);
|
|
1407
|
-
if (!p) return;
|
|
1408
|
-
this.players.delete(speakerId);
|
|
1409
|
-
if (p.idleTimer) {
|
|
1410
|
-
clearTimeout(p.idleTimer);
|
|
1411
|
-
p.idleTimer = null;
|
|
1412
|
-
}
|
|
1413
|
-
try {
|
|
1414
|
-
p.child.stdin?.end();
|
|
1415
|
-
} catch {
|
|
1416
|
-
}
|
|
1417
|
-
const t = setTimeout(() => {
|
|
1418
|
-
try {
|
|
1419
|
-
p.child.kill("SIGKILL");
|
|
1420
|
-
} catch {
|
|
1421
|
-
}
|
|
1422
|
-
}, DRAIN_KILL_MS);
|
|
1423
|
-
t.unref();
|
|
1424
|
-
}
|
|
1425
|
-
/** Hard cleanup on quit: kill every player immediately. */
|
|
1426
|
-
stopAll() {
|
|
1427
|
-
for (const [, p] of this.players) {
|
|
1428
|
-
if (p.idleTimer) {
|
|
1429
|
-
clearTimeout(p.idleTimer);
|
|
1430
|
-
p.idleTimer = null;
|
|
1431
|
-
}
|
|
1432
|
-
p.alive = false;
|
|
1433
|
-
try {
|
|
1434
|
-
p.child.kill("SIGKILL");
|
|
1435
|
-
} catch {
|
|
1436
|
-
}
|
|
1437
|
-
}
|
|
1438
|
-
this.players.clear();
|
|
1439
|
-
}
|
|
1440
|
-
spawnPlayer(speakerId) {
|
|
1441
|
-
let child;
|
|
1442
|
-
try {
|
|
1443
|
-
child = spawn(this.tools.ffplayPath, playbackArgs(this.tools.ffplayPath), { stdio: ["pipe", "ignore", "ignore"] });
|
|
1444
|
-
} catch {
|
|
1445
|
-
return null;
|
|
1446
|
-
}
|
|
1447
|
-
const p = { child, alive: true, idleTimer: null, pre: [], flowing: false };
|
|
1448
|
-
child.stdin?.on("error", () => {
|
|
1449
|
-
p.alive = false;
|
|
1450
|
-
});
|
|
1451
|
-
child.on("error", () => {
|
|
1452
|
-
p.alive = false;
|
|
1453
|
-
if (this.players.get(speakerId) === p) this.reap(speakerId, p);
|
|
1454
|
-
});
|
|
1455
|
-
child.on("close", () => {
|
|
1456
|
-
p.alive = false;
|
|
1457
|
-
if (this.players.get(speakerId) === p) {
|
|
1458
|
-
if (p.idleTimer) {
|
|
1459
|
-
clearTimeout(p.idleTimer);
|
|
1460
|
-
p.idleTimer = null;
|
|
1461
|
-
}
|
|
1462
|
-
this.players.delete(speakerId);
|
|
1463
|
-
}
|
|
1464
|
-
});
|
|
1465
|
-
this.players.set(speakerId, p);
|
|
1466
|
-
return p;
|
|
1467
|
-
}
|
|
1468
|
-
write(p, bytes) {
|
|
1469
|
-
const sin = p.child.stdin;
|
|
1470
|
-
if (!p.alive || !sin || sin.destroyed || !sin.writable) return false;
|
|
1471
|
-
try {
|
|
1472
|
-
sin.write(bytes);
|
|
1473
|
-
return true;
|
|
1474
|
-
} catch {
|
|
1475
|
-
p.alive = false;
|
|
1476
|
-
return false;
|
|
1477
|
-
}
|
|
1478
|
-
}
|
|
1479
|
-
touch(speakerId, p) {
|
|
1480
|
-
if (p.idleTimer) clearTimeout(p.idleTimer);
|
|
1481
|
-
p.idleTimer = setTimeout(() => this.end(speakerId), SPEAKER_IDLE_MS);
|
|
1482
|
-
p.idleTimer.unref();
|
|
1483
|
-
}
|
|
1484
|
-
reap(speakerId, p) {
|
|
1485
|
-
if (p.idleTimer) {
|
|
1486
|
-
clearTimeout(p.idleTimer);
|
|
1487
|
-
p.idleTimer = null;
|
|
1488
|
-
}
|
|
1489
|
-
p.alive = false;
|
|
1490
|
-
try {
|
|
1491
|
-
p.child.kill("SIGKILL");
|
|
1492
|
-
} catch {
|
|
1493
|
-
}
|
|
1494
|
-
if (this.players.get(speakerId) === p) this.players.delete(speakerId);
|
|
1495
|
-
}
|
|
1496
|
-
};
|
|
1497
|
-
function listMicDevices() {
|
|
1498
|
-
try {
|
|
1499
|
-
if (process.platform === "darwin") {
|
|
1500
|
-
const text = runForOutput(["-hide_banner", "-f", "avfoundation", "-list_devices", "true", "-i", ""]);
|
|
1501
|
-
const out = [];
|
|
1502
|
-
let inAudio = false;
|
|
1503
|
-
for (const line of text.split("\n")) {
|
|
1504
|
-
if (/audio devices/i.test(line)) {
|
|
1505
|
-
inAudio = true;
|
|
1506
|
-
continue;
|
|
1507
|
-
}
|
|
1508
|
-
if (/video devices/i.test(line)) {
|
|
1509
|
-
inAudio = false;
|
|
1510
|
-
continue;
|
|
1511
|
-
}
|
|
1512
|
-
if (!inAudio) continue;
|
|
1513
|
-
const m = line.match(/\[(\d+)\]\s+(.+?)\s*$/);
|
|
1514
|
-
if (m) out.push({ index: m[1], name: m[2] });
|
|
1515
|
-
}
|
|
1516
|
-
return out;
|
|
1517
|
-
}
|
|
1518
|
-
if (process.platform === "linux") {
|
|
1519
|
-
const text = runForOutput(["-hide_banner", "-sources", "pulse"]);
|
|
1520
|
-
const out = [];
|
|
1521
|
-
for (const line of text.split("\n")) {
|
|
1522
|
-
if (/auto-detected/i.test(line)) continue;
|
|
1523
|
-
const m = line.match(/^\s*\*?\s*(\S+)\s+\[(.+)\]\s*$/);
|
|
1524
|
-
if (m) out.push({ index: m[1], name: m[2] });
|
|
1525
|
-
}
|
|
1526
|
-
return out;
|
|
1527
|
-
}
|
|
1528
|
-
if (process.platform === "win32") {
|
|
1529
|
-
const text = runForOutput(["-hide_banner", "-list_devices", "true", "-f", "dshow", "-i", "dummy"]);
|
|
1530
|
-
const out = [];
|
|
1531
|
-
for (const line of text.split("\n")) {
|
|
1532
|
-
const m = line.match(/"(.+)"\s+\(audio\)/);
|
|
1533
|
-
if (m) out.push({ index: m[1], name: m[1] });
|
|
1534
|
-
}
|
|
1535
|
-
return out;
|
|
1536
|
-
}
|
|
1537
|
-
return [];
|
|
1538
|
-
} catch {
|
|
1539
|
-
return [];
|
|
1540
|
-
}
|
|
1541
|
-
}
|
|
1542
|
-
function runForOutput(args) {
|
|
1543
|
-
const res = spawnSync("ffmpeg", args, { encoding: "utf8", timeout: 5e3 });
|
|
1544
|
-
return (res.stderr ?? "") + (res.stdout ?? "");
|
|
1545
|
-
}
|
|
1546
|
-
function errMsg(e) {
|
|
1547
|
-
return e instanceof Error ? e.message : String(e);
|
|
1548
|
-
}
|
|
1549
|
-
|
|
1550
|
-
// src/world-data.ts
|
|
1551
|
-
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`;
|
|
1552
|
-
var POLYLINES = PACKED.split(";").map(
|
|
1553
|
-
(pl) => pl.split(" ").filter(Boolean).map((pt) => {
|
|
1554
|
-
const c = pt.split(",");
|
|
1555
|
-
return [parseFloat(c[0]), parseFloat(c[1])];
|
|
1556
|
-
}).filter((p) => Number.isFinite(p[0]) && Number.isFinite(p[1]))
|
|
1557
|
-
).filter((pl) => pl.length > 0);
|
|
1558
|
-
|
|
1559
|
-
// src/map.ts
|
|
1560
|
-
var LAND = 240;
|
|
1561
|
-
var MIXED = 231;
|
|
1562
|
-
var DOT_CHAR = "\u25CF";
|
|
1563
|
-
function project(lat, lon, w, h) {
|
|
1564
|
-
const la = Math.max(-90, Math.min(90, Number.isFinite(lat) ? lat : 0));
|
|
1565
|
-
const lo = Math.max(-180, Math.min(180, Number.isFinite(lon) ? lon : 0));
|
|
1566
|
-
const x = w > 1 ? Math.round((lo + 180) / 360 * (w - 1)) : 0;
|
|
1567
|
-
const y = h > 1 ? Math.round((90 - la) / 180 * (h - 1)) : 0;
|
|
1568
|
-
return { x, y };
|
|
1569
|
-
}
|
|
1570
|
-
var BRAILLE = [[1, 8], [2, 16], [4, 32], [64, 128]];
|
|
1571
|
-
var brailleCache = null;
|
|
1572
|
-
function brailleWorld(W2, H2) {
|
|
1573
|
-
if (brailleCache && brailleCache.w === W2 && brailleCache.h === H2) return brailleCache.grid;
|
|
1574
|
-
const pw = W2 * 2, ph = H2 * 4;
|
|
1575
|
-
const grid2 = new Uint8Array(W2 * H2);
|
|
1576
|
-
const toX = (lon) => {
|
|
1577
|
-
let l = lon;
|
|
1578
|
-
if (l > 180) l -= 360;
|
|
1579
|
-
if (l < -180) l += 360;
|
|
1580
|
-
return Math.round((l + 180) / 360 * (pw - 1));
|
|
1581
|
-
};
|
|
1582
|
-
const toY = (lat) => Math.round((90 - Math.max(-90, Math.min(90, lat))) / 180 * (ph - 1));
|
|
1583
|
-
const set = (x, y) => {
|
|
1584
|
-
if (x < 0 || x >= pw || y < 0 || y >= ph) return;
|
|
1585
|
-
grid2[(y >> 2) * W2 + (x >> 1)] |= BRAILLE[y & 3][x & 1];
|
|
1586
|
-
};
|
|
1587
|
-
const line = (x0, y0, x1, y1) => {
|
|
1588
|
-
const dx = Math.abs(x1 - x0), dy = Math.abs(y1 - y0), sx = x0 < x1 ? 1 : -1, sy = y0 < y1 ? 1 : -1;
|
|
1589
|
-
let err = dx - dy, guard = 0;
|
|
1590
|
-
for (; ; ) {
|
|
1591
|
-
set(x0, y0);
|
|
1592
|
-
if (x0 === x1 && y0 === y1 || guard++ > pw + ph) break;
|
|
1593
|
-
const e2 = 2 * err;
|
|
1594
|
-
if (e2 > -dy) {
|
|
1595
|
-
err -= dy;
|
|
1596
|
-
x0 += sx;
|
|
1597
|
-
}
|
|
1598
|
-
if (e2 < dx) {
|
|
1599
|
-
err += dx;
|
|
1600
|
-
y0 += sy;
|
|
1601
|
-
}
|
|
1602
|
-
}
|
|
1603
|
-
};
|
|
1604
|
-
for (const pl of POLYLINES) {
|
|
1605
|
-
if (pl.length === 1) {
|
|
1606
|
-
set(toX(pl[0][0]), toY(pl[0][1]));
|
|
1607
|
-
continue;
|
|
1608
|
-
}
|
|
1609
|
-
for (let i = 1; i < pl.length; i++) {
|
|
1610
|
-
const ax = toX(pl[i - 1][0]), ay = toY(pl[i - 1][1]), bx = toX(pl[i][0]), by = toY(pl[i][1]);
|
|
1611
|
-
if (Math.abs(ax - bx) > pw * 0.6) {
|
|
1612
|
-
set(ax, ay);
|
|
1613
|
-
set(bx, by);
|
|
1614
|
-
continue;
|
|
1615
|
-
}
|
|
1616
|
-
line(ax, ay, bx, by);
|
|
1617
|
-
}
|
|
1618
|
-
}
|
|
1619
|
-
brailleCache = { w: W2, h: H2, grid: grid2 };
|
|
1620
|
-
return grid2;
|
|
1621
|
-
}
|
|
1622
|
-
function renderMap(w, h, dots) {
|
|
1623
|
-
const W2 = Math.max(0, Math.floor(w));
|
|
1624
|
-
const H2 = Math.max(0, Math.floor(h));
|
|
1625
|
-
const lines = [];
|
|
1626
|
-
const hits = [];
|
|
1627
|
-
if (W2 < 1) {
|
|
1628
|
-
while (lines.length < H2) lines.push({ text: "", spans: [] });
|
|
1629
|
-
return { lines, hits };
|
|
1630
|
-
}
|
|
1631
|
-
if (H2 >= 1) {
|
|
1632
|
-
const byCell = /* @__PURE__ */ new Map();
|
|
1633
|
-
for (const d of dots) {
|
|
1634
|
-
const { x, y } = project(d.lat, d.lon, W2, H2);
|
|
1635
|
-
const hit = byCell.get(y * W2 + x);
|
|
1636
|
-
if (!hit) {
|
|
1637
|
-
byCell.set(y * W2 + x, { x, y, names: [d.name], color: d.color });
|
|
1638
|
-
} else {
|
|
1639
|
-
hit.names.push(d.name);
|
|
1640
|
-
if (hit.color !== d.color) hit.color = MIXED;
|
|
1641
|
-
}
|
|
1642
|
-
}
|
|
1643
|
-
hits.push(...byCell.values());
|
|
1644
|
-
}
|
|
1645
|
-
const hitRows = /* @__PURE__ */ new Map();
|
|
1646
|
-
for (const hit of hits) {
|
|
1647
|
-
const row = hitRows.get(hit.y);
|
|
1648
|
-
if (row) row.push(hit);
|
|
1649
|
-
else hitRows.set(hit.y, [hit]);
|
|
1650
|
-
}
|
|
1651
|
-
const grid2 = brailleWorld(W2, H2);
|
|
1652
|
-
for (let y = 0; y < H2; y++) {
|
|
1653
|
-
const chars = new Array(W2).fill(" ");
|
|
1654
|
-
const colors = new Array(W2).fill(-1);
|
|
1655
|
-
const isDot = new Array(W2).fill(false);
|
|
1656
|
-
for (let x = 0; x < W2; x++) {
|
|
1657
|
-
const b = grid2[y * W2 + x];
|
|
1658
|
-
if (b) {
|
|
1659
|
-
chars[x] = String.fromCharCode(10240 + b);
|
|
1660
|
-
colors[x] = LAND;
|
|
1661
|
-
}
|
|
1662
|
-
}
|
|
1663
|
-
for (const hit of hitRows.get(y) ?? []) {
|
|
1664
|
-
chars[hit.x] = DOT_CHAR;
|
|
1665
|
-
colors[hit.x] = hit.color;
|
|
1666
|
-
isDot[hit.x] = true;
|
|
1667
|
-
}
|
|
1668
|
-
const spans = [];
|
|
1669
|
-
for (let x = 0; x < W2; ) {
|
|
1670
|
-
if (colors[x] < 0) {
|
|
1671
|
-
x++;
|
|
1672
|
-
continue;
|
|
1673
|
-
}
|
|
1674
|
-
let end = x + 1;
|
|
1675
|
-
while (end < W2 && colors[end] === colors[x] && isDot[end] === isDot[x]) end++;
|
|
1676
|
-
spans.push(isDot[x] ? { start: x, end, color: colors[x], bold: true } : { start: x, end, color: colors[x] });
|
|
1677
|
-
x = end;
|
|
1678
|
-
}
|
|
1679
|
-
lines.push({ text: chars.join("").replace(/ +$/, ""), spans });
|
|
1680
|
-
}
|
|
1681
|
-
return { lines, hits };
|
|
1682
|
-
}
|
|
1683
|
-
function hover(rendered, x, y) {
|
|
1684
|
-
let best = null;
|
|
1685
|
-
let bestDist = 2;
|
|
1686
|
-
for (const hit of rendered.hits) {
|
|
1687
|
-
const dist = Math.max(Math.abs(hit.x - x), Math.abs(hit.y - y));
|
|
1688
|
-
if (dist <= 1 && dist < bestDist) {
|
|
1689
|
-
best = hit;
|
|
1690
|
-
bestDist = dist;
|
|
1691
|
-
}
|
|
1692
|
-
}
|
|
1693
|
-
return best ? { names: best.names } : null;
|
|
1694
|
-
}
|
|
1695
|
-
|
|
1696
|
-
// src/geo.ts
|
|
1697
|
-
function localTz() {
|
|
1698
|
-
try {
|
|
1699
|
-
return Intl.DateTimeFormat().resolvedOptions().timeZone || "";
|
|
1700
|
-
} catch {
|
|
1701
|
-
return "";
|
|
1702
|
-
}
|
|
1703
|
-
}
|
|
1704
|
-
var TZ = {
|
|
1705
|
-
// north america
|
|
1706
|
-
"America/New_York": [40.7, -74],
|
|
1707
|
-
"America/Detroit": [42.3, -83],
|
|
1708
|
-
"America/Toronto": [43.7, -79.4],
|
|
1709
|
-
"America/Chicago": [41.9, -87.6],
|
|
1710
|
-
"America/Winnipeg": [49.9, -97.1],
|
|
1711
|
-
"America/Mexico_City": [19.4, -99.1],
|
|
1712
|
-
"America/Denver": [39.7, -104.99],
|
|
1713
|
-
"America/Edmonton": [53.5, -113.5],
|
|
1714
|
-
"America/Phoenix": [33.4, -112.1],
|
|
1715
|
-
"America/Los_Angeles": [34.05, -118.24],
|
|
1716
|
-
"America/Vancouver": [49.3, -123.1],
|
|
1717
|
-
"America/Anchorage": [61.2, -149.9],
|
|
1718
|
-
"Pacific/Honolulu": [21.3, -157.8],
|
|
1719
|
-
"America/Halifax": [44.6, -63.6],
|
|
1720
|
-
"America/St_Johns": [47.6, -52.7],
|
|
1721
|
-
// south + central america
|
|
1722
|
-
"America/Sao_Paulo": [-23.5, -46.6],
|
|
1723
|
-
"America/Argentina/Buenos_Aires": [-34.6, -58.4],
|
|
1724
|
-
"America/Santiago": [-33.5, -70.7],
|
|
1725
|
-
"America/Lima": [-12, -77],
|
|
1726
|
-
"America/Bogota": [4.7, -74.1],
|
|
1727
|
-
"America/Caracas": [10.5, -66.9],
|
|
1728
|
-
"America/Guatemala": [14.6, -90.5],
|
|
1729
|
-
// europe
|
|
1730
|
-
"Europe/London": [51.5, -0.13],
|
|
1731
|
-
"Europe/Dublin": [53.3, -6.3],
|
|
1732
|
-
"Europe/Lisbon": [38.7, -9.1],
|
|
1733
|
-
"Europe/Madrid": [40.4, -3.7],
|
|
1734
|
-
"Europe/Paris": [48.86, 2.35],
|
|
1735
|
-
"Europe/Amsterdam": [52.4, 4.9],
|
|
1736
|
-
"Europe/Brussels": [50.8, 4.4],
|
|
1737
|
-
"Europe/Zurich": [47.4, 8.5],
|
|
1738
|
-
"Europe/Berlin": [52.5, 13.4],
|
|
1739
|
-
"Europe/Rome": [41.9, 12.5],
|
|
1740
|
-
"Europe/Vienna": [48.2, 16.4],
|
|
1741
|
-
"Europe/Prague": [50.1, 14.4],
|
|
1742
|
-
"Europe/Copenhagen": [55.7, 12.6],
|
|
1743
|
-
"Europe/Stockholm": [59.3, 18.1],
|
|
1744
|
-
"Europe/Oslo": [59.9, 10.7],
|
|
1745
|
-
"Europe/Warsaw": [52.2, 21],
|
|
1746
|
-
"Europe/Athens": [38, 23.7],
|
|
1747
|
-
"Europe/Bucharest": [44.4, 26.1],
|
|
1748
|
-
"Europe/Kyiv": [50.5, 30.5],
|
|
1749
|
-
"Europe/Kiev": [50.5, 30.5],
|
|
1750
|
-
"Europe/Istanbul": [41, 28.98],
|
|
1751
|
-
"Europe/Moscow": [55.75, 37.6],
|
|
1752
|
-
"Europe/Helsinki": [60.2, 24.9],
|
|
1753
|
-
// africa + middle east
|
|
1754
|
-
"Africa/Casablanca": [33.6, -7.6],
|
|
1755
|
-
"Africa/Lagos": [6.5, 3.4],
|
|
1756
|
-
"Africa/Cairo": [30, 31.2],
|
|
1757
|
-
"Africa/Nairobi": [-1.3, 36.8],
|
|
1758
|
-
"Africa/Johannesburg": [-26.2, 28],
|
|
1759
|
-
"Asia/Jerusalem": [31.8, 35.2],
|
|
1760
|
-
"Asia/Dubai": [25.2, 55.3],
|
|
1761
|
-
"Asia/Tehran": [35.7, 51.4],
|
|
1762
|
-
"Asia/Riyadh": [24.7, 46.7],
|
|
1763
|
-
"Asia/Baghdad": [33.3, 44.4],
|
|
1764
|
-
// asia
|
|
1765
|
-
"Asia/Karachi": [24.9, 67],
|
|
1766
|
-
"Asia/Kolkata": [22.6, 88.4],
|
|
1767
|
-
"Asia/Calcutta": [22.6, 88.4],
|
|
1768
|
-
"Asia/Colombo": [6.9, 79.9],
|
|
1769
|
-
"Asia/Dhaka": [23.8, 90.4],
|
|
1770
|
-
"Asia/Bangkok": [13.8, 100.5],
|
|
1771
|
-
"Asia/Jakarta": [-6.2, 106.8],
|
|
1772
|
-
"Asia/Singapore": [1.35, 103.8],
|
|
1773
|
-
"Asia/Kuala_Lumpur": [3.1, 101.7],
|
|
1774
|
-
"Asia/Manila": [14.6, 121],
|
|
1775
|
-
"Asia/Hong_Kong": [22.3, 114.2],
|
|
1776
|
-
"Asia/Shanghai": [31.2, 121.5],
|
|
1777
|
-
"Asia/Taipei": [25, 121.5],
|
|
1778
|
-
"Asia/Seoul": [37.6, 127],
|
|
1779
|
-
"Asia/Tokyo": [35.7, 139.7],
|
|
1780
|
-
// oceania
|
|
1781
|
-
"Australia/Perth": [-31.95, 115.9],
|
|
1782
|
-
"Australia/Adelaide": [-34.9, 138.6],
|
|
1783
|
-
"Australia/Brisbane": [-27.5, 153],
|
|
1784
|
-
"Australia/Sydney": [-33.87, 151.2],
|
|
1785
|
-
"Australia/Melbourne": [-37.8, 145],
|
|
1786
|
-
"Pacific/Auckland": [-36.85, 174.76]
|
|
1787
|
-
};
|
|
1788
|
-
var REGION = {
|
|
1789
|
-
America: [15, -90],
|
|
1790
|
-
Europe: [50, 15],
|
|
1791
|
-
Africa: [3, 22],
|
|
1792
|
-
Asia: [35, 95],
|
|
1793
|
-
Australia: [-25, 135],
|
|
1794
|
-
Pacific: [-10, -160],
|
|
1795
|
-
Atlantic: [35, -35],
|
|
1796
|
-
Indian: [-15, 75],
|
|
1797
|
-
Antarctica: [-80, 0]
|
|
1798
|
-
};
|
|
1799
|
-
function tzToCoords(tz) {
|
|
1800
|
-
if (!tz) return null;
|
|
1801
|
-
const exact = TZ[tz];
|
|
1802
|
-
if (exact) return { lat: exact[0], lon: exact[1] };
|
|
1803
|
-
const region = REGION[tz.split("/")[0]];
|
|
1804
|
-
if (region) return { lat: region[0], lon: region[1] };
|
|
1805
|
-
return null;
|
|
1806
|
-
}
|
|
1807
|
-
function tzCity(tz) {
|
|
1808
|
-
if (!tz) return "";
|
|
1809
|
-
const parts = tz.split("/");
|
|
1810
|
-
return (parts[parts.length - 1] || tz).replace(/_/g, " ");
|
|
1811
|
-
}
|
|
1812
|
-
|
|
1813
|
-
// src/client.ts
|
|
1814
|
-
var term = tk.terminal;
|
|
1815
|
-
var ScreenBuffer = tk.ScreenBuffer;
|
|
1816
|
-
var CONF_DIR = join(homedir(), ".accelr8");
|
|
1817
|
-
var ID_FILE = join(CONF_DIR, "identity.json");
|
|
1818
|
-
var USER_COLORS = [39, 213, 220, 46, 208, 141, 51, 203, 118, 165];
|
|
1819
|
-
var DIM_PARTNER = {
|
|
1820
|
-
39: 25,
|
|
1821
|
-
213: 132,
|
|
1822
|
-
220: 136,
|
|
1823
|
-
46: 28,
|
|
1824
|
-
208: 130,
|
|
1825
|
-
141: 60,
|
|
1826
|
-
51: 30,
|
|
1827
|
-
203: 131,
|
|
1828
|
-
118: 64,
|
|
1829
|
-
165: 90
|
|
1830
|
-
};
|
|
1831
|
-
var dimOf = (color) => DIM_PARTNER[color] ?? 243;
|
|
1832
|
-
function loadIdentity(nameOverride) {
|
|
1833
|
-
let saved = {};
|
|
1834
|
-
if (existsSync(ID_FILE)) {
|
|
1835
|
-
try {
|
|
1836
|
-
saved = JSON.parse(readFileSync(ID_FILE, "utf8"));
|
|
1837
|
-
} catch {
|
|
1838
|
-
}
|
|
1839
|
-
}
|
|
1840
|
-
const id = saved.id || "u_" + Math.random().toString(36).slice(2, 10);
|
|
1841
|
-
const name = (nameOverride || saved.name || process.env.USER || "guest").slice(0, 16);
|
|
1842
|
-
let h = 0;
|
|
1843
|
-
for (const ch of id) h = h * 31 + ch.charCodeAt(0) >>> 0;
|
|
1844
|
-
const color = USER_COLORS[h % USER_COLORS.length];
|
|
1845
|
-
try {
|
|
1846
|
-
mkdirSync(CONF_DIR, { recursive: true });
|
|
1847
|
-
writeFileSync(ID_FILE, JSON.stringify({ id, name }));
|
|
1848
|
-
} catch {
|
|
1849
|
-
}
|
|
1850
|
-
return { id, name, color };
|
|
1851
|
-
}
|
|
1852
|
-
var me = { id: "u_local", name: process.env.USER || "guest", color: 39 };
|
|
1853
|
-
var SERVER = DEFAULT_SERVER;
|
|
1854
|
-
var autoJoinCode;
|
|
1855
|
-
var mySid = "";
|
|
1856
|
-
var myAccounts = [];
|
|
1857
|
-
var runningExec = /* @__PURE__ */ new Map();
|
|
1858
|
-
var acctStatus = /* @__PURE__ */ new Map();
|
|
1859
|
-
var overlay = null;
|
|
1860
|
-
var accessAgentId = null;
|
|
1861
|
-
var overlayFocus = 0;
|
|
1862
|
-
var updateInfo = null;
|
|
1863
|
-
var task = null;
|
|
1864
|
-
var cancelTask = null;
|
|
1865
|
-
function startUpdate() {
|
|
1866
|
-
if (task && task.phase === "running") return;
|
|
1867
|
-
task = { title: "updating accelr8", phase: "running", line: "starting npm\u2026", okLabel: "quit now \xB7 enter", onOk: () => shutdown() };
|
|
1868
|
-
import("./update-UEPNGB6Y.js").then(({ runUpdate }) => {
|
|
1869
|
-
cancelTask = runUpdate({
|
|
1870
|
-
onLine: (l) => {
|
|
1871
|
-
if (task) task.line = l.slice(0, 120);
|
|
1872
|
-
},
|
|
1873
|
-
onDone: (ok, message) => {
|
|
1874
|
-
if (task) {
|
|
1875
|
-
task.phase = "done";
|
|
1876
|
-
task.ok = ok;
|
|
1877
|
-
task.line = message;
|
|
1878
|
-
}
|
|
1879
|
-
cancelTask = null;
|
|
1880
|
-
if (ok) updateInfo = null;
|
|
1881
|
-
}
|
|
1882
|
-
});
|
|
1883
|
-
}).catch(() => {
|
|
1884
|
-
if (task) {
|
|
1885
|
-
task.phase = "done";
|
|
1886
|
-
task.ok = false;
|
|
1887
|
-
task.line = "could not load the updater \xB7 npm i -g accelr8@latest";
|
|
1888
|
-
}
|
|
1889
|
-
});
|
|
1890
|
-
}
|
|
1891
|
-
function startVoiceInstall() {
|
|
1892
|
-
import("./install-DL5WLVXN.js").then(({ ffmpegInstallPlan, runCommand }) => {
|
|
1893
|
-
const plan = ffmpegInstallPlan();
|
|
1894
|
-
if (!plan.auto) {
|
|
1895
|
-
state.status = `voice needs ffmpeg \xB7 ${plan.manual}`;
|
|
1896
|
-
return;
|
|
1897
|
-
}
|
|
1898
|
-
const auto = plan.auto;
|
|
1899
|
-
task = {
|
|
1900
|
-
title: "enable voice",
|
|
1901
|
-
phase: "confirm",
|
|
1902
|
-
line: `voice needs ffmpeg \xB7 ${auto.label} \xB7 press enter`,
|
|
1903
|
-
run: () => {
|
|
1904
|
-
if (!task) return;
|
|
1905
|
-
task.phase = "running";
|
|
1906
|
-
task.line = "installing ffmpeg\u2026";
|
|
1907
|
-
cancelTask = runCommand(auto.cmd, auto.args, {
|
|
1908
|
-
onLine: (l) => {
|
|
1909
|
-
if (task) task.line = l.slice(0, 120);
|
|
1910
|
-
},
|
|
1911
|
-
onDone: (ok, message) => {
|
|
1912
|
-
if (task) {
|
|
1913
|
-
task.phase = "done";
|
|
1914
|
-
task.ok = ok;
|
|
1915
|
-
task.line = ok ? "ffmpeg ready \xB7 voice is on" : message + " \xB7 " + auto.label;
|
|
1916
|
-
task.okLabel = ok ? "start voice \xB7 enter" : void 0;
|
|
1917
|
-
}
|
|
1918
|
-
cancelTask = null;
|
|
1919
|
-
}
|
|
1920
|
-
});
|
|
1921
|
-
},
|
|
1922
|
-
onOk: () => {
|
|
1923
|
-
voiceTools = null;
|
|
1924
|
-
task = null;
|
|
1925
|
-
toggleVoice();
|
|
1926
|
-
}
|
|
1927
|
-
// re-detect + join
|
|
1928
|
-
};
|
|
1929
|
-
}).catch(() => {
|
|
1930
|
-
state.status = "could not load the installer";
|
|
1931
|
-
});
|
|
1932
|
-
}
|
|
1933
|
-
var voiceTools = null;
|
|
1934
|
-
var voiceOn = false;
|
|
1935
|
-
var voiceTransmit = false;
|
|
1936
|
-
var voiceCapture = null;
|
|
1937
|
-
var voicePlayback = null;
|
|
1938
|
-
var voiceDevice = process.env.ACCELR8_MIC || void 0;
|
|
1939
|
-
var voiceRetries = 0;
|
|
1940
|
-
function autoPickMic() {
|
|
1941
|
-
if (process.platform === "darwin") {
|
|
1942
|
-
const mics = listMicDevices();
|
|
1943
|
-
if (!mics.length) return void 0;
|
|
1944
|
-
const builtin = mics.find((m) => /macbook|built.?in|internal/i.test(m.name));
|
|
1945
|
-
if (builtin) return builtin.index;
|
|
1946
|
-
const notPhone = mics.find((m) => !/iphone|ipad|continuity|desk view/i.test(m.name));
|
|
1947
|
-
return notPhone?.index;
|
|
1948
|
-
}
|
|
1949
|
-
if (process.platform === "win32") {
|
|
1950
|
-
const mics = listMicDevices();
|
|
1951
|
-
return mics[0]?.index;
|
|
1952
|
-
}
|
|
1953
|
-
return void 0;
|
|
1954
|
-
}
|
|
1955
|
-
function startCapture() {
|
|
1956
|
-
if (!voiceCapture) return;
|
|
1957
|
-
voiceCapture.start({
|
|
1958
|
-
device: voiceDevice,
|
|
1959
|
-
onChunk: (b64) => {
|
|
1960
|
-
voiceRetries = 0;
|
|
1961
|
-
if (voiceTransmit) sendMsg({ t: "voiceFrame", data: b64 });
|
|
1962
|
-
},
|
|
1963
|
-
onError: (m) => {
|
|
1964
|
-
if (!voiceTransmit) return;
|
|
1965
|
-
voiceRetries++;
|
|
1966
|
-
if (voiceRetries <= 6) {
|
|
1967
|
-
state.status = `mic dropped \xB7 reconnecting ${voiceRetries}\u2026`;
|
|
1968
|
-
setTimeout(() => {
|
|
1969
|
-
if (voiceTransmit && voiceCapture && !voiceCapture.active) startCapture();
|
|
1970
|
-
}, 200);
|
|
1971
|
-
} else {
|
|
1972
|
-
voiceTransmit = false;
|
|
1973
|
-
voiceRetries = 0;
|
|
1974
|
-
state.status = "mic keeps dropping \xB7 /voice mics to pick another \xB7 " + m.slice(0, 30);
|
|
1975
|
-
}
|
|
1976
|
-
}
|
|
1977
|
-
});
|
|
1978
|
-
}
|
|
1979
|
-
function toggleVoice() {
|
|
1980
|
-
if (voiceOn) return leaveVoice();
|
|
1981
|
-
if (!state.project) {
|
|
1982
|
-
state.status = "join a project first";
|
|
1983
|
-
return;
|
|
1984
|
-
}
|
|
1985
|
-
voiceTools = voiceTools ?? detectVoiceTools();
|
|
1986
|
-
if (!voiceTools.capture && !voiceTools.playback) {
|
|
1987
|
-
startVoiceInstall();
|
|
1988
|
-
return;
|
|
1989
|
-
}
|
|
1990
|
-
voicePlayback = new VoicePlayback(voiceTools);
|
|
1991
|
-
voiceCapture = new VoiceCapture(voiceTools);
|
|
1992
|
-
voiceOn = true;
|
|
1993
|
-
voiceTransmit = false;
|
|
1994
|
-
if (voiceDevice === void 0 && voiceTools.capture) voiceDevice = autoPickMic();
|
|
1995
|
-
sendMsg({ t: "voiceJoin" });
|
|
1996
|
-
state.status = voiceTools.capture ? "voice on \xB7 /talk (or ctrl-g) to speak" : "voice on \xB7 listen only (no mic found)";
|
|
1997
|
-
}
|
|
1998
|
-
function leaveVoice() {
|
|
1999
|
-
if (!voiceOn) return;
|
|
2000
|
-
voiceCapture?.stop();
|
|
2001
|
-
voicePlayback?.stopAll();
|
|
2002
|
-
voiceCapture = null;
|
|
2003
|
-
voicePlayback = null;
|
|
2004
|
-
voiceOn = false;
|
|
2005
|
-
voiceTransmit = false;
|
|
2006
|
-
state.voicePeers = [];
|
|
2007
|
-
sendMsg({ t: "voiceLeave" });
|
|
2008
|
-
state.status = "voice off";
|
|
2009
|
-
}
|
|
2010
|
-
function toggleTalk() {
|
|
2011
|
-
if (!voiceOn || !voiceCapture) return;
|
|
2012
|
-
if (!voiceTools?.capture) {
|
|
2013
|
-
state.status = "no mic \xB7 install ffmpeg to talk";
|
|
2014
|
-
return;
|
|
2015
|
-
}
|
|
2016
|
-
if (voiceTransmit) {
|
|
2017
|
-
voiceTransmit = false;
|
|
2018
|
-
voiceRetries = 0;
|
|
2019
|
-
voiceCapture.stop();
|
|
2020
|
-
state.status = "muted \xB7 /talk (or ctrl-g) to speak";
|
|
2021
|
-
} else {
|
|
2022
|
-
voiceTransmit = true;
|
|
2023
|
-
voiceRetries = 0;
|
|
2024
|
-
state.status = "\u25CF live \xB7 /mute (or ctrl-g) to stop";
|
|
2025
|
-
startCapture();
|
|
2026
|
-
}
|
|
2027
|
-
}
|
|
2028
|
-
function openHelp() {
|
|
2029
|
-
overlay = "help";
|
|
2030
|
-
overlayFocus = 0;
|
|
2031
|
-
}
|
|
2032
|
-
function openSettings() {
|
|
2033
|
-
overlay = "settings";
|
|
2034
|
-
overlayFocus = 0;
|
|
2035
|
-
}
|
|
2036
|
-
function goHome() {
|
|
2037
|
-
if (state.project) {
|
|
2038
|
-
if (voiceOn) leaveVoice();
|
|
2039
|
-
overlay = null;
|
|
2040
|
-
state.screen = "home";
|
|
2041
|
-
sendMsg({ t: "tab", tab: "home" });
|
|
2042
|
-
}
|
|
2043
|
-
}
|
|
2044
|
-
var connectDraft = null;
|
|
2045
|
-
var ROOM_TABS = ["chat", "canvas", "secrets", "team"];
|
|
2046
|
-
var state = {
|
|
2047
|
-
screen: "home",
|
|
2048
|
-
tab: "chat",
|
|
2049
|
-
connected: false,
|
|
2050
|
-
projects: [],
|
|
2051
|
-
project: null,
|
|
2052
|
-
members: [],
|
|
2053
|
-
sessions: [],
|
|
2054
|
-
agents: [],
|
|
2055
|
-
secrets: [],
|
|
2056
|
-
voicePeers: [],
|
|
2057
|
-
status: "",
|
|
2058
|
-
modal: null
|
|
2059
|
-
};
|
|
2060
|
-
var chatLog = new ChatLog();
|
|
2061
|
-
var editor = new LineEditor();
|
|
2062
|
-
var popup = new MentionPopup();
|
|
2063
|
-
var orbEngine = new OrbEngine();
|
|
2064
|
-
var unread = { t0: 0, t2: 0 };
|
|
2065
|
-
var ghosts = /* @__PURE__ */ new Map();
|
|
2066
|
-
var cursors = /* @__PURE__ */ new Map();
|
|
2067
|
-
var myLogical = null;
|
|
2068
|
-
var painting = false;
|
|
2069
|
-
var lastPaintKey = "";
|
|
2070
|
-
var cursorDirty = false;
|
|
2071
|
-
var draftDirty = false;
|
|
2072
|
-
var lastSentDraft = "";
|
|
2073
|
-
var cmidCounter = 0;
|
|
2074
|
-
var PALETTE = ["\u2588", "\u2593", "\u2592", "\u2591", "\u25CF", "\u25C6", "\u2605", "\u25B2", "\u25A0", "\u2550", "\u2551", "\xB7", "#", "@", "*", "+"];
|
|
2075
|
-
var IO_X = 1;
|
|
2076
|
-
var IO_Y = 5;
|
|
2077
|
-
var SPINNER = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
|
|
2078
|
-
var frame = 0;
|
|
2079
|
-
var memberOf = (userId) => state.members.find((m) => m.id === userId);
|
|
2080
|
-
var onlineUserIds = () => new Set(state.sessions.map((s) => s.userId));
|
|
2081
|
-
function mentionEntities() {
|
|
2082
|
-
return [
|
|
2083
|
-
...state.agents.map((a) => ({ name: a.handle, color: memberOf(a.ownerId)?.color ?? 245 })),
|
|
2084
|
-
...state.members.map((m) => ({ name: m.name, color: m.color, self: m.id === me.id }))
|
|
2085
|
-
];
|
|
2086
|
-
}
|
|
2087
|
-
function candidates() {
|
|
2088
|
-
const online = onlineUserIds();
|
|
2089
|
-
return [
|
|
2090
|
-
...state.agents.map((a) => ({
|
|
2091
|
-
name: a.handle,
|
|
2092
|
-
color: memberOf(a.ownerId)?.color ?? 245,
|
|
2093
|
-
kind: "agent",
|
|
2094
|
-
detail: `agent \xB7 ${memberOf(a.ownerId)?.name ?? "?"}`
|
|
2095
|
-
})),
|
|
2096
|
-
...state.members.filter((m) => m.id !== me.id).map((m) => ({
|
|
2097
|
-
name: m.name,
|
|
2098
|
-
color: m.color,
|
|
2099
|
-
kind: "human",
|
|
2100
|
-
detail: online.has(m.id) ? "human" : "human \xB7 offline"
|
|
2101
|
-
}))
|
|
2102
|
-
];
|
|
2103
|
-
}
|
|
2104
|
-
function entryFrom(msg) {
|
|
2105
|
-
return {
|
|
2106
|
-
seq: msg.seq,
|
|
2107
|
-
kind: msg.kind,
|
|
2108
|
-
authorId: msg.authorId,
|
|
2109
|
-
authorName: msg.authorName,
|
|
2110
|
-
color: msg.color,
|
|
2111
|
-
text: msg.text,
|
|
2112
|
-
ts: msg.ts,
|
|
2113
|
-
partial: msg.agent?.partial,
|
|
2114
|
-
invId: msg.agent?.invId
|
|
2115
|
-
};
|
|
2116
|
-
}
|
|
2117
|
-
function attachMyAgents() {
|
|
2118
|
-
if (!state.project) return;
|
|
2119
|
-
myAccounts = safeLoadAccounts();
|
|
2120
|
-
const tokens = loadTokens();
|
|
2121
|
-
for (const acct of myAccounts) {
|
|
2122
|
-
const token = tokens[`${state.project.id}:${acct.handle}`];
|
|
2123
|
-
sendMsg({ t: "agentAttach", handle: acct.handle, provider: acct.provider, model: acct.model, token });
|
|
2124
|
-
}
|
|
2125
|
-
}
|
|
2126
|
-
function runAgentInvocation(invId, agentId, handle, context) {
|
|
2127
|
-
const acct = myAccounts.find((a) => a.handle === handle);
|
|
2128
|
-
if (!acct) {
|
|
2129
|
-
sendMsg({ t: "agentError", invId, code: "internal", message: "no local account for @" + handle });
|
|
2130
|
-
return;
|
|
2131
|
-
}
|
|
2132
|
-
sendMsg({ t: "agentAccept", invId });
|
|
2133
|
-
try {
|
|
2134
|
-
const h = execute(acct, context, {
|
|
2135
|
-
onDelta: (text) => sendMsg({ t: "agentDelta", invId, text }),
|
|
2136
|
-
onDone: (finish) => {
|
|
2137
|
-
runningExec.delete(invId);
|
|
2138
|
-
sendMsg({ t: "agentDone", invId, finish });
|
|
2139
|
-
},
|
|
2140
|
-
onError: (code, message) => {
|
|
2141
|
-
runningExec.delete(invId);
|
|
2142
|
-
sendMsg({ t: "agentError", invId, code, message });
|
|
2143
|
-
}
|
|
2144
|
-
});
|
|
2145
|
-
runningExec.set(invId, h);
|
|
2146
|
-
} catch (e) {
|
|
2147
|
-
sendMsg({ t: "agentError", invId, code: "internal", message: e instanceof Error ? e.message : "exec failed" });
|
|
2148
|
-
}
|
|
2149
|
-
}
|
|
2150
|
-
function chatVisible() {
|
|
2151
|
-
if (state.screen !== "room" || !state.project) return false;
|
|
2152
|
-
const rung = rungFor(W(), H());
|
|
2153
|
-
if (rung === "ticker" || rung === "sidecar") return true;
|
|
2154
|
-
if (rung === "micro" || rung === "orb") return false;
|
|
2155
|
-
return state.tab === "chat";
|
|
2156
|
-
}
|
|
2157
|
-
var BOOT_MIN_W = 60;
|
|
2158
|
-
var BOOT_MIN_H = 16;
|
|
2159
|
-
var boot = null;
|
|
2160
|
-
var bootStart = 0;
|
|
2161
|
-
var bootWasFull = false;
|
|
2162
|
-
var pendingBoot = false;
|
|
2163
|
-
var bootHint = "";
|
|
2164
|
-
function startBoot(full) {
|
|
2165
|
-
bootWasFull = full;
|
|
2166
|
-
boot = new BootAnim(W(), H(), full ? void 0 : { short: true });
|
|
2167
|
-
bootStart = Date.now();
|
|
2168
|
-
}
|
|
2169
|
-
function skipBoot() {
|
|
2170
|
-
if (!boot) return;
|
|
2171
|
-
boot = null;
|
|
2172
|
-
if (bootWasFull) {
|
|
2173
|
-
settings.bootSkips++;
|
|
2174
|
-
if (settings.bootSkips === 3) bootHint = "accelr8 --intro brings back the full show";
|
|
2175
|
-
saveSettings();
|
|
2176
|
-
}
|
|
2177
|
-
}
|
|
2178
|
-
function finishBoot() {
|
|
2179
|
-
boot = null;
|
|
2180
|
-
if (bootWasFull && settings.bootSkips > 0) {
|
|
2181
|
-
settings.bootSkips = 0;
|
|
2182
|
-
saveSettings();
|
|
2183
|
-
}
|
|
2184
|
-
}
|
|
2185
|
-
var SETTINGS_FILE = join(CONF_DIR, "settings.json");
|
|
2186
|
-
var NAV_STYLES = ["underline", "brackets", "blocks", "pipes"];
|
|
2187
|
-
var settings = { bell: "mention", bootSkips: 0, theme: "midnight", tabs: "underline" };
|
|
2188
|
-
function loadSettings() {
|
|
2189
|
-
try {
|
|
2190
|
-
settings = { ...settings, ...JSON.parse(readFileSync(SETTINGS_FILE, "utf8")) };
|
|
2191
|
-
} catch {
|
|
2192
|
-
}
|
|
2193
|
-
if (!THEMES[settings.theme]) settings.theme = "midnight";
|
|
2194
|
-
if (!NAV_STYLES.includes(settings.tabs)) settings.tabs = "underline";
|
|
2195
|
-
applyTheme(settings.theme);
|
|
2196
|
-
}
|
|
2197
|
-
function saveSettings() {
|
|
2198
|
-
try {
|
|
2199
|
-
mkdirSync(CONF_DIR, { recursive: true });
|
|
2200
|
-
writeFileSync(SETTINGS_FILE, JSON.stringify(settings));
|
|
2201
|
-
} catch {
|
|
2202
|
-
}
|
|
2203
|
-
}
|
|
2204
|
-
function noteUnread(msg) {
|
|
2205
|
-
if (chatVisible() && chatLog.pinned) return;
|
|
2206
|
-
if (msg.authorId === me.id) return;
|
|
2207
|
-
unread.t0++;
|
|
2208
|
-
lastEvent = { text: `${msg.authorName}: ${msg.text}`, color: msg.color, initial: msg.authorName[0] ?? "?" };
|
|
2209
|
-
const mentioned = scanMentions(msg.text, [{ name: me.name, color: me.color, self: true }]).length > 0;
|
|
2210
|
-
if (mentioned) {
|
|
2211
|
-
unread.t2++;
|
|
2212
|
-
orbEngine.noteMention(msg.color);
|
|
2213
|
-
} else orbEngine.noteActivity(msg.authorName[0] ?? "?", msg.color);
|
|
2214
|
-
bell(mentioned ? "mention" : "activity");
|
|
2215
|
-
}
|
|
2216
|
-
var lastEvent = null;
|
|
2217
|
-
var lastBell = 0;
|
|
2218
|
-
function bell(tier) {
|
|
2219
|
-
if (chatVisible()) return;
|
|
2220
|
-
if (settings.bell === "off") return;
|
|
2221
|
-
if (settings.bell === "mention" && tier !== "mention") return;
|
|
2222
|
-
const now = Date.now();
|
|
2223
|
-
if (now - lastBell < 3e4) return;
|
|
2224
|
-
lastBell = now;
|
|
2225
|
-
try {
|
|
2226
|
-
process.stdout.write("\x07");
|
|
2227
|
-
} catch {
|
|
2228
|
-
}
|
|
2229
|
-
}
|
|
2230
|
-
var ws = null;
|
|
2231
|
-
var reconnectTimer = null;
|
|
2232
|
-
var rejoining = false;
|
|
2233
|
-
var lastPong = 0;
|
|
2234
|
-
var lastSeenSeq = 0;
|
|
2235
|
-
var syncing = false;
|
|
2236
|
-
var syncFrom = 0;
|
|
2237
|
-
var syncBuffer = [];
|
|
2238
|
-
function connect() {
|
|
2239
|
-
state.status = "connecting to " + SERVER + " ...";
|
|
2240
|
-
try {
|
|
2241
|
-
ws = new WebSocket(SERVER);
|
|
2242
|
-
} catch {
|
|
2243
|
-
state.status = "invalid server url: " + SERVER;
|
|
2244
|
-
return;
|
|
2245
|
-
}
|
|
2246
|
-
ws.on("open", () => {
|
|
2247
|
-
state.connected = true;
|
|
2248
|
-
state.status = "";
|
|
2249
|
-
lastPong = Date.now();
|
|
2250
|
-
sendMsg({ t: "hello", v: PROTO, user: me, tz: localTz(), executes: myAccounts.map((a) => ({ handle: a.handle, provider: a.provider, model: a.model })) });
|
|
2251
|
-
if (autoJoinCode && !state.project) sendMsg({ t: "joinProject", code: autoJoinCode });
|
|
2252
|
-
else if (state.project) {
|
|
2253
|
-
rejoining = true;
|
|
2254
|
-
sendMsg({ t: "joinProject", code: state.project.code });
|
|
2255
|
-
}
|
|
2256
|
-
});
|
|
2257
|
-
ws.on("message", (raw) => {
|
|
2258
|
-
lastPong = Date.now();
|
|
2259
|
-
try {
|
|
2260
|
-
handleServer(decode(raw.toString()));
|
|
2261
|
-
} catch {
|
|
2262
|
-
}
|
|
2263
|
-
});
|
|
2264
|
-
ws.on("pong", () => {
|
|
2265
|
-
lastPong = Date.now();
|
|
2266
|
-
});
|
|
2267
|
-
ws.on("close", () => {
|
|
2268
|
-
state.connected = false;
|
|
2269
|
-
state.status = "disconnected, retrying...";
|
|
2270
|
-
ghosts.clear();
|
|
2271
|
-
syncing = false;
|
|
2272
|
-
syncBuffer.length = 0;
|
|
2273
|
-
if (!reconnectTimer) reconnectTimer = setTimeout(() => {
|
|
2274
|
-
reconnectTimer = null;
|
|
2275
|
-
connect();
|
|
2276
|
-
}, 2e3);
|
|
2277
|
-
});
|
|
2278
|
-
ws.on("error", () => {
|
|
2279
|
-
});
|
|
2280
|
-
}
|
|
2281
|
-
function sendMsg(msg) {
|
|
2282
|
-
if (ws && ws.readyState === WebSocket.OPEN) ws.send(encode(msg));
|
|
2283
|
-
}
|
|
2284
|
-
function handleServer(msg) {
|
|
2285
|
-
if (!msg || typeof msg !== "object" || typeof msg.t !== "string") return;
|
|
2286
|
-
switch (msg.t) {
|
|
2287
|
-
case "welcome":
|
|
2288
|
-
mySid = msg.sid;
|
|
2289
|
-
break;
|
|
2290
|
-
case "projects":
|
|
2291
|
-
state.projects = Array.isArray(msg.list) ? msg.list : [];
|
|
2292
|
-
break;
|
|
2293
|
-
case "joined": {
|
|
2294
|
-
if (!msg.project || typeof msg.project.code !== "string" || typeof msg.project.name !== "string") break;
|
|
2295
|
-
const sameProject = state.project?.id === msg.project.id;
|
|
2296
|
-
state.project = msg.project;
|
|
2297
|
-
grid = msg.grid || {};
|
|
2298
|
-
state.members = Array.isArray(msg.members) ? msg.members : [];
|
|
2299
|
-
state.sessions = Array.isArray(msg.sessions) ? msg.sessions : [];
|
|
2300
|
-
state.agents = Array.isArray(msg.agents) ? msg.agents : [];
|
|
2301
|
-
state.secrets = Array.isArray(msg.secrets) ? msg.secrets : [];
|
|
2302
|
-
cursors.clear();
|
|
2303
|
-
ghosts.clear();
|
|
2304
|
-
if (rejoining && sameProject) {
|
|
2305
|
-
if (msg.chatLatestSeq > lastSeenSeq) {
|
|
2306
|
-
syncing = true;
|
|
2307
|
-
syncFrom = lastSeenSeq;
|
|
2308
|
-
syncBuffer.length = 0;
|
|
2309
|
-
sendMsg({ t: "chatSync", sinceSeq: lastSeenSeq });
|
|
2310
|
-
}
|
|
2311
|
-
} else {
|
|
2312
|
-
chatLog.reset(msg.chatTail.map(entryFrom), mentionEntities());
|
|
2313
|
-
lastSeenSeq = msg.chatLatestSeq;
|
|
2314
|
-
state.screen = "room";
|
|
2315
|
-
state.tab = "chat";
|
|
2316
|
-
state.status = `joined "${msg.project.name}"`;
|
|
2317
|
-
sendMsg({ t: "tab", tab: "chat" });
|
|
2318
|
-
}
|
|
2319
|
-
rejoining = false;
|
|
2320
|
-
attachMyAgents();
|
|
2321
|
-
break;
|
|
2322
|
-
}
|
|
2323
|
-
case "agentToken":
|
|
2324
|
-
if (state.project) saveToken(state.project.id, msg.handle, msg.token);
|
|
2325
|
-
break;
|
|
2326
|
-
case "agentInvoke":
|
|
2327
|
-
runAgentInvocation(msg.invId, msg.agentId, msg.handle, msg.context);
|
|
2328
|
-
break;
|
|
2329
|
-
case "agentStart": {
|
|
2330
|
-
const color = memberOf(state.agents.find((a) => a.agentId === msg.agentId)?.ownerId ?? "")?.color ?? C.accent;
|
|
2331
|
-
chatLog.push({
|
|
2332
|
-
seq: null,
|
|
2333
|
-
invId: msg.invId,
|
|
2334
|
-
kind: "agent",
|
|
2335
|
-
authorId: msg.agentId,
|
|
2336
|
-
authorName: msg.handle,
|
|
2337
|
-
color,
|
|
2338
|
-
text: "",
|
|
2339
|
-
ts: Date.now(),
|
|
2340
|
-
streaming: true
|
|
2341
|
-
}, mentionEntities());
|
|
2342
|
-
break;
|
|
2343
|
-
}
|
|
2344
|
-
case "agentStream":
|
|
2345
|
-
chatLog.appendDelta((e) => e.invId === msg.invId && !!e.streaming, msg.text);
|
|
2346
|
-
break;
|
|
2347
|
-
case "agentCancel": {
|
|
2348
|
-
const h = runningExec.get(msg.invId);
|
|
2349
|
-
if (h) {
|
|
2350
|
-
try {
|
|
2351
|
-
h.abort();
|
|
2352
|
-
} catch {
|
|
2353
|
-
}
|
|
2354
|
-
runningExec.delete(msg.invId);
|
|
2355
|
-
}
|
|
2356
|
-
break;
|
|
2357
|
-
}
|
|
2358
|
-
case "agentEnd":
|
|
2359
|
-
chatLog.finalize((e) => e.invId === msg.invId && !!e.streaming, { partial: msg.status === "partial" });
|
|
2360
|
-
if (msg.status !== "done") chatLog.appendDelta((e) => e.invId === msg.invId, "");
|
|
2361
|
-
break;
|
|
2362
|
-
case "joinError":
|
|
2363
|
-
rejoining = false;
|
|
2364
|
-
syncing = false;
|
|
2365
|
-
syncBuffer.length = 0;
|
|
2366
|
-
state.status = msg.message;
|
|
2367
|
-
break;
|
|
2368
|
-
case "codeChanged":
|
|
2369
|
-
if (state.project) state.project = { ...state.project, code: msg.code };
|
|
2370
|
-
break;
|
|
2371
|
-
case "sessions":
|
|
2372
|
-
state.sessions = Array.isArray(msg.sessions) ? msg.sessions : [];
|
|
2373
|
-
for (const sid of [...ghosts.keys()]) if (!state.sessions.some((s) => s.sid === sid)) ghosts.delete(sid);
|
|
2374
|
-
break;
|
|
2375
|
-
case "members":
|
|
2376
|
-
state.members = Array.isArray(msg.members) ? msg.members : [];
|
|
2377
|
-
break;
|
|
2378
|
-
case "cursor": {
|
|
2379
|
-
if (msg.sid === mySid) break;
|
|
2380
|
-
let c = cursors.get(msg.sid);
|
|
2381
|
-
if (!c) {
|
|
2382
|
-
c = { tx: msg.x, ty: msg.y, x: msg.x, y: msg.y, userId: msg.userId, seen: Date.now() };
|
|
2383
|
-
cursors.set(msg.sid, c);
|
|
2384
|
-
}
|
|
2385
|
-
c.tx = msg.x;
|
|
2386
|
-
c.ty = msg.y;
|
|
2387
|
-
c.userId = msg.userId;
|
|
2388
|
-
c.seen = Date.now();
|
|
2389
|
-
break;
|
|
2390
|
-
}
|
|
2391
|
-
case "cursorGone":
|
|
2392
|
-
cursors.delete(msg.sid);
|
|
2393
|
-
break;
|
|
2394
|
-
case "draft":
|
|
2395
|
-
if (msg.sid === mySid) break;
|
|
2396
|
-
if (msg.text === "") ghosts.delete(msg.sid);
|
|
2397
|
-
else ghosts.set(msg.sid, { userId: msg.userId, text: msg.text, at: Date.now() });
|
|
2398
|
-
break;
|
|
2399
|
-
case "paint":
|
|
2400
|
-
if (msg.char === " ") delete grid[`${msg.x},${msg.y}`];
|
|
2401
|
-
else grid[`${msg.x},${msg.y}`] = [msg.char, msg.color];
|
|
2402
|
-
break;
|
|
2403
|
-
case "cleared":
|
|
2404
|
-
grid = {};
|
|
2405
|
-
break;
|
|
2406
|
-
case "chat": {
|
|
2407
|
-
const m = msg.msg;
|
|
2408
|
-
if (!m || typeof m.text !== "string") break;
|
|
2409
|
-
lastSeenSeq = Math.max(lastSeenSeq, m.seq);
|
|
2410
|
-
if (msg.echoOf && chatLog.ackOptimistic(msg.echoOf, entryFrom(m))) break;
|
|
2411
|
-
if (m.agent?.invId && chatLog.replaceStreaming((e) => e.invId === m.agent.invId, entryFrom(m), mentionEntities())) {
|
|
2412
|
-
noteUnread(m);
|
|
2413
|
-
break;
|
|
2414
|
-
}
|
|
2415
|
-
if (syncing) {
|
|
2416
|
-
syncBuffer.push(m);
|
|
2417
|
-
noteUnread(m);
|
|
2418
|
-
break;
|
|
2419
|
-
}
|
|
2420
|
-
chatLog.push(entryFrom(m), mentionEntities());
|
|
2421
|
-
if (m.authorId !== me.id) ghosts.delete([...ghosts.entries()].find(([, g]) => g.userId === m.authorId)?.[0] ?? "");
|
|
2422
|
-
noteUnread(m);
|
|
2423
|
-
break;
|
|
2424
|
-
}
|
|
2425
|
-
case "chatHistoryPage": {
|
|
2426
|
-
if (!Array.isArray(msg.msgs)) break;
|
|
2427
|
-
chatLog.prepend(msg.msgs.map(entryFrom), mentionEntities(), msg.hasMore);
|
|
2428
|
-
break;
|
|
2429
|
-
}
|
|
2430
|
-
case "chatSyncResult": {
|
|
2431
|
-
if (!Array.isArray(msg.msgs)) break;
|
|
2432
|
-
if (msg.resync) {
|
|
2433
|
-
chatLog.reset(msg.msgs.map(entryFrom), mentionEntities());
|
|
2434
|
-
} else {
|
|
2435
|
-
const bySeq = /* @__PURE__ */ new Map();
|
|
2436
|
-
for (const m of [...msg.msgs, ...syncBuffer]) if (m.seq > syncFrom) bySeq.set(m.seq, m);
|
|
2437
|
-
for (const m of [...bySeq.values()].sort((a, b) => a.seq - b.seq)) chatLog.push(entryFrom(m), mentionEntities());
|
|
2438
|
-
}
|
|
2439
|
-
if (msg.msgs.length) lastSeenSeq = Math.max(lastSeenSeq, msg.msgs.at(-1).seq);
|
|
2440
|
-
for (const m of syncBuffer) lastSeenSeq = Math.max(lastSeenSeq, m.seq);
|
|
2441
|
-
syncBuffer.length = 0;
|
|
2442
|
-
syncing = false;
|
|
2443
|
-
break;
|
|
2444
|
-
}
|
|
2445
|
-
case "agents":
|
|
2446
|
-
state.agents = Array.isArray(msg.agents) ? msg.agents : [];
|
|
2447
|
-
break;
|
|
2448
|
-
case "secrets":
|
|
2449
|
-
state.secrets = Array.isArray(msg.list) ? msg.list : [];
|
|
2450
|
-
break;
|
|
2451
|
-
case "voice":
|
|
2452
|
-
state.voicePeers = Array.isArray(msg.peers) ? msg.peers : [];
|
|
2453
|
-
break;
|
|
2454
|
-
case "voiceFrame":
|
|
2455
|
-
if (voiceOn) voicePlayback?.feed(msg.sid, msg.data);
|
|
2456
|
-
break;
|
|
2457
|
-
case "error": {
|
|
2458
|
-
if (msg.code === "chatRate" || msg.code === "msgTooLarge") {
|
|
2459
|
-
const failed = msg.ref ? chatLog.failOptimistic(msg.ref) : null;
|
|
2460
|
-
if (failed) editor.setText(failed.text);
|
|
2461
|
-
state.status = msg.code === "chatRate" ? "slow down" : "message too large";
|
|
2462
|
-
} else if (msg.code === "protoVersion") {
|
|
2463
|
-
shutdownWithMessage("this client is outdated. update with: npm i -g accelr8");
|
|
2464
|
-
}
|
|
2465
|
-
break;
|
|
2466
|
-
}
|
|
2467
|
-
}
|
|
2468
|
-
}
|
|
2469
|
-
var grid = {};
|
|
2470
|
-
var screen;
|
|
2471
|
-
function makeScreen() {
|
|
2472
|
-
const width = Math.max(20, term.width || process.stdout.columns || 120);
|
|
2473
|
-
const height = Math.max(6, term.height || process.stdout.rows || 40);
|
|
2474
|
-
screen = new ScreenBuffer({ dst: term, width, height });
|
|
2475
|
-
chatLog.dropCaches();
|
|
2476
|
-
if (boot && (width < BOOT_MIN_W || height < BOOT_MIN_H)) {
|
|
2477
|
-
boot = null;
|
|
2478
|
-
pendingBoot = true;
|
|
2479
|
-
} else if (boot) {
|
|
2480
|
-
startBoot(bootWasFull);
|
|
2481
|
-
} else if (pendingBoot && width >= BOOT_MIN_W && height >= BOOT_MIN_H) {
|
|
2482
|
-
pendingBoot = false;
|
|
2483
|
-
startBoot(settings.bootSkips < 3);
|
|
2484
|
-
}
|
|
2485
|
-
}
|
|
2486
|
-
function __setHeadlessScreen(width, height) {
|
|
2487
|
-
screen = new ScreenBuffer({ width, height });
|
|
2488
|
-
chatLog.dropCaches();
|
|
2489
|
-
}
|
|
2490
|
-
function __dump() {
|
|
2491
|
-
return screen.dumpChars();
|
|
2492
|
-
}
|
|
2493
|
-
function __dumpCells() {
|
|
2494
|
-
const rows = [];
|
|
2495
|
-
for (let y = 0; y < screen.height; y++) {
|
|
2496
|
-
const row = [];
|
|
2497
|
-
for (let x = 0; x < screen.width; x++) {
|
|
2498
|
-
const c = screen.get({ x, y });
|
|
2499
|
-
row.push({ ch: c.char, fg: c.attr?.color, bg: c.attr?.bgColor, bold: c.attr?.bold });
|
|
2500
|
-
}
|
|
2501
|
-
rows.push(row);
|
|
2502
|
-
}
|
|
2503
|
-
return rows;
|
|
2504
|
-
}
|
|
2505
|
-
function __setMySid(sid) {
|
|
2506
|
-
mySid = sid;
|
|
2507
|
-
}
|
|
2508
|
-
function __openAccounts() {
|
|
2509
|
-
openAccounts();
|
|
2510
|
-
}
|
|
2511
|
-
function __setAccounts(a) {
|
|
2512
|
-
myAccounts = a;
|
|
2513
|
-
}
|
|
2514
|
-
function __setMe(u) {
|
|
2515
|
-
me = { ...me, ...u };
|
|
2516
|
-
}
|
|
2517
|
-
function __openAgentAccess(id, focus = 0) {
|
|
2518
|
-
openAgentAccess(id);
|
|
2519
|
-
overlayFocus = focus;
|
|
2520
|
-
}
|
|
2521
|
-
function __openSettings() {
|
|
2522
|
-
openSettings();
|
|
2523
|
-
}
|
|
2524
|
-
var H = () => screen.height;
|
|
2525
|
-
var W = () => screen.width;
|
|
2526
|
-
function puts(x, y, str, attr) {
|
|
2527
|
-
if (y < 0 || y >= H()) return;
|
|
2528
|
-
const a = attr && attr.bgColor !== void 0 ? attr : { ...attr, bgColor: C.bg };
|
|
2529
|
-
const clean = String(str).replace(/[\x00-\x1f\x7f]/g, " ");
|
|
2530
|
-
screen.put({ x, y, attr: a, dx: 1, dy: 0, wrap: false }, clean);
|
|
2531
|
-
}
|
|
2532
|
-
function puts2(x, y, str, attr) {
|
|
2533
|
-
puts(x, y, str, attr);
|
|
2534
|
-
return x + String(str).length;
|
|
2535
|
-
}
|
|
2536
|
-
var clickables = [];
|
|
2537
|
-
function button(x, y, label, attr, action) {
|
|
2538
|
-
const focused = overlay !== null && clickables.length === overlayFocus;
|
|
2539
|
-
puts(x, y, label, focused ? { ...attr, bgColor: C.popupSel, color: C.selFg, bold: true } : attr);
|
|
2540
|
-
if (focused) puts(x - 1, y, "\u276F", { color: C.accent, bold: true });
|
|
2541
|
-
clickables.push({ x0: x, y0: y, x1: x + label.length - 1, y1: y, action });
|
|
2542
|
-
return x + label.length;
|
|
2543
|
-
}
|
|
2544
|
-
var BASE = {
|
|
2545
|
-
bg: 233,
|
|
2546
|
-
panel: 234,
|
|
2547
|
-
rule: 238,
|
|
2548
|
-
dim: 245,
|
|
2549
|
-
text: 252,
|
|
2550
|
-
bright: 231,
|
|
2551
|
-
accent: 39,
|
|
2552
|
-
good: 46,
|
|
2553
|
-
warn: 214,
|
|
2554
|
-
tabOff: 240,
|
|
2555
|
-
sys: 245,
|
|
2556
|
-
mentionSelfBg: 237,
|
|
2557
|
-
pillBg: 25,
|
|
2558
|
-
pillFg: 231,
|
|
2559
|
-
inputBg: 235,
|
|
2560
|
-
popupBg: 236,
|
|
2561
|
-
popupSel: 24,
|
|
2562
|
-
selFg: 231,
|
|
2563
|
-
danger: 196,
|
|
2564
|
-
onAccent: 16
|
|
2565
|
-
};
|
|
2566
|
-
var THEMES = {
|
|
2567
|
-
midnight: { name: "midnight", over: {} },
|
|
2568
|
-
amber: { name: "amber", over: { accent: 214, bright: 223, text: 180, dim: 172, good: 190, warn: 208, rule: 58, panel: 235, tabOff: 237, popupSel: 130, selFg: 230, pillBg: 130, pillFg: 230, mentionSelfBg: 58, inputBg: 236, onAccent: 232 } },
|
|
2569
|
-
matrix: { name: "matrix", over: { accent: 46, bright: 46, text: 42, dim: 34, good: 46, warn: 226, rule: 22, tabOff: 22, popupSel: 28, selFg: 231, pillBg: 28, pillFg: 231, mentionSelfBg: 22, inputBg: 234, onAccent: 232 } },
|
|
2570
|
-
mono: { name: "mono", over: { accent: 252, bright: 255, text: 250, dim: 243, good: 252, warn: 246, rule: 240, tabOff: 237, popupSel: 245, selFg: 232, pillBg: 250, pillFg: 232, mentionSelfBg: 238, onAccent: 232 } },
|
|
2571
|
-
paper: { name: "paper", over: { bg: 255, panel: 253, rule: 250, dim: 243, text: 235, bright: 232, accent: 25, good: 28, warn: 130, tabOff: 252, sys: 240, mentionSelfBg: 194, pillBg: 25, pillFg: 231, inputBg: 254, popupBg: 254, popupSel: 153, selFg: 232, danger: 124, onAccent: 231 } }
|
|
2572
|
-
};
|
|
2573
|
-
var C = { ...BASE };
|
|
2574
|
-
function applyTheme(name) {
|
|
2575
|
-
C = { ...BASE, ...THEMES[name]?.over ?? {} };
|
|
2576
|
-
}
|
|
2577
|
-
function xtermRgb(n) {
|
|
2578
|
-
if (n < 16) {
|
|
2579
|
-
const t = { 0: [0, 0, 0], 7: [192, 192, 192], 8: [128, 128, 128], 15: [255, 255, 255] };
|
|
2580
|
-
return t[n] ?? [128, 128, 128];
|
|
2581
|
-
}
|
|
2582
|
-
if (n <= 231) {
|
|
2583
|
-
const c = n - 16, v = (i) => i === 0 ? 0 : 55 + 40 * i;
|
|
2584
|
-
return [v(Math.floor(c / 36)), v(Math.floor(c % 36 / 6)), v(c % 6)];
|
|
2585
|
-
}
|
|
2586
|
-
const g = 8 + 10 * (n - 232);
|
|
2587
|
-
return [g, g, g];
|
|
2588
|
-
}
|
|
2589
|
-
function lum(n) {
|
|
2590
|
-
const f = (c) => {
|
|
2591
|
-
const s = c / 255;
|
|
2592
|
-
return s <= 0.03928 ? s / 12.92 : Math.pow((s + 0.055) / 1.055, 2.4);
|
|
2593
|
-
};
|
|
2594
|
-
const [r, g, b] = xtermRgb(n);
|
|
2595
|
-
return 0.2126 * f(r) + 0.7152 * f(g) + 0.0722 * f(b);
|
|
2596
|
-
}
|
|
2597
|
-
function ratio(a, b) {
|
|
2598
|
-
const la = lum(a), lb = lum(b);
|
|
2599
|
-
return (Math.max(la, lb) + 0.05) / (Math.min(la, lb) + 0.05);
|
|
2600
|
-
}
|
|
2601
|
-
function onFill(bg) {
|
|
2602
|
-
return ratio(16, bg) >= ratio(231, bg) ? 16 : 231;
|
|
2603
|
-
}
|
|
2604
|
-
function readableFg(color, bg = C.bg, min = 3.2) {
|
|
2605
|
-
if (ratio(color, bg) >= min) return color;
|
|
2606
|
-
const lightBg = lum(bg) > 0.5;
|
|
2607
|
-
if (color >= 16 && color <= 231) {
|
|
2608
|
-
const c = color - 16;
|
|
2609
|
-
let r6 = Math.floor(c / 36), g6 = Math.floor(c % 36 / 6), b6 = c % 6;
|
|
2610
|
-
for (let step = 0; step < 5; step++) {
|
|
2611
|
-
if (lightBg) {
|
|
2612
|
-
r6 = Math.max(0, r6 - 1);
|
|
2613
|
-
g6 = Math.max(0, g6 - 1);
|
|
2614
|
-
b6 = Math.max(0, b6 - 1);
|
|
2615
|
-
} else {
|
|
2616
|
-
r6 = Math.min(5, r6 + 1);
|
|
2617
|
-
g6 = Math.min(5, g6 + 1);
|
|
2618
|
-
b6 = Math.min(5, b6 + 1);
|
|
2619
|
-
}
|
|
2620
|
-
const cand = 16 + 36 * r6 + 6 * g6 + b6;
|
|
2621
|
-
if (ratio(cand, bg) >= min) return cand;
|
|
2622
|
-
}
|
|
2623
|
-
}
|
|
2624
|
-
const ramp = lightBg ? Array.from({ length: 12 }, (_, i) => 243 - i) : Array.from({ length: 12 }, (_, i) => 244 + i);
|
|
2625
|
-
for (const g of ramp) if (ratio(g, bg) >= min) return g;
|
|
2626
|
-
return lightBg ? 16 : 231;
|
|
2627
|
-
}
|
|
2628
|
-
function __applyTheme(name) {
|
|
2629
|
-
applyTheme(name);
|
|
2630
|
-
}
|
|
2631
|
-
function __click(x, y) {
|
|
2632
|
-
for (const r of clickables) if (x >= r.x0 && x <= r.x1 && y >= r.y0 && y <= r.y1) {
|
|
2633
|
-
r.action();
|
|
2634
|
-
return true;
|
|
2635
|
-
}
|
|
2636
|
-
return false;
|
|
2637
|
-
}
|
|
2638
|
-
function __curTheme() {
|
|
2639
|
-
return settings.theme;
|
|
2640
|
-
}
|
|
2641
|
-
function __setUpdate(latest, current = "3.0.0") {
|
|
2642
|
-
updateInfo = latest ? { latest, current } : null;
|
|
2643
|
-
}
|
|
2644
|
-
function __taskDemo(phase, ok = true) {
|
|
2645
|
-
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" };
|
|
2646
|
-
}
|
|
2647
|
-
function __closeOverlay() {
|
|
2648
|
-
overlay = null;
|
|
2649
|
-
}
|
|
2650
|
-
function __setNav(s) {
|
|
2651
|
-
if (NAV_STYLES.includes(s)) {
|
|
2652
|
-
settings.tabs = s;
|
|
2653
|
-
}
|
|
2654
|
-
}
|
|
2655
|
-
function __accent() {
|
|
2656
|
-
return C.accent;
|
|
2657
|
-
}
|
|
2658
|
-
function rungFor(w, h) {
|
|
2659
|
-
if (h <= 4) return "micro";
|
|
2660
|
-
if (w < 12) return "micro";
|
|
2661
|
-
if (w < 32) return "orb";
|
|
2662
|
-
if (h <= 15) return "ticker";
|
|
2663
|
-
if (w < 60) return "sidecar";
|
|
2664
|
-
if (w < 80 || h < 22) return "compact";
|
|
2665
|
-
if (w < 120) return "standard";
|
|
2666
|
-
return "comfy";
|
|
2667
|
-
}
|
|
2668
|
-
var prevRung = null;
|
|
2669
|
-
var parkedTab = "chat";
|
|
2670
|
-
function onRungChange(rung) {
|
|
2671
|
-
const wasParked = prevRung === "orb" || prevRung === "micro";
|
|
2672
|
-
const isParked = rung === "orb" || rung === "micro";
|
|
2673
|
-
if (isParked && !wasParked) {
|
|
2674
|
-
parkedTab = state.tab;
|
|
2675
|
-
popup.open = false;
|
|
2676
|
-
if (state.project) {
|
|
2677
|
-
sendMsg({ t: "draft", text: "" });
|
|
2678
|
-
lastSentDraft = "";
|
|
2679
|
-
sendMsg({ t: "tab", tab: "away" });
|
|
2680
|
-
}
|
|
2681
|
-
} else if (!isParked && wasParked) {
|
|
2682
|
-
state.tab = parkedTab;
|
|
2683
|
-
if (state.project && state.screen === "room") {
|
|
2684
|
-
const wire = rung === "ticker" || rung === "sidecar" ? "chat" : parkedTab;
|
|
2685
|
-
sendMsg({ t: "tab", tab: wire });
|
|
2686
|
-
}
|
|
2687
|
-
} else if (!isParked && prevRung !== null && prevRung !== rung && state.project && state.screen === "room") {
|
|
2688
|
-
const wire = rung === "ticker" || rung === "sidecar" ? "chat" : state.tab;
|
|
2689
|
-
sendMsg({ t: "tab", tab: wire });
|
|
2690
|
-
}
|
|
2691
|
-
prevRung = rung;
|
|
2692
|
-
}
|
|
2693
|
-
function render() {
|
|
2694
|
-
const w = W(), h = H();
|
|
2695
|
-
frame++;
|
|
2696
|
-
screen.fill({ attr: { bgColor: C.bg, color: C.text } });
|
|
2697
|
-
clickables = [];
|
|
2698
|
-
if (boot) {
|
|
2699
|
-
const f = boot.frame(Date.now() - bootStart);
|
|
2700
|
-
if (f === "done") finishBoot();
|
|
2701
|
-
else {
|
|
2702
|
-
for (const { x, y, cell } of f.cells) {
|
|
2703
|
-
if (x >= 0 && x < w && y >= 0 && y < h)
|
|
2704
|
-
screen.put({ x, y, attr: { color: cell.color, bold: cell.bold, bgColor: cell.bgColor ?? C.bg }, dx: 1, dy: 0, wrap: false }, cell.ch);
|
|
2705
|
-
}
|
|
2706
|
-
if (screen.dst) screen.draw({ delta: true });
|
|
2707
|
-
return;
|
|
2708
|
-
}
|
|
2709
|
-
}
|
|
2710
|
-
const rung = rungFor(w, h);
|
|
2711
|
-
if (rung !== prevRung) onRungChange(rung);
|
|
2712
|
-
orbEngine.setCounts({ t0: unread.t0, t2: unread.t2 });
|
|
2713
|
-
orbEngine.setOffline(!state.connected);
|
|
2714
|
-
orbEngine.setStreaming(state.agents.some((a) => a.busy), state.agents.find((a) => a.busy) ? memberOf(state.agents.find((a) => a.busy).ownerId)?.color ?? C.accent : void 0);
|
|
2715
|
-
if (frame % 1800 === 0) orbEngine.maybeEgg(frame);
|
|
2716
|
-
if (rung === "micro") renderMicro(w, h);
|
|
2717
|
-
else if (rung === "orb") renderOrb(w, h);
|
|
2718
|
-
else if (rung === "ticker") renderTicker(w, h);
|
|
2719
|
-
else if (rung === "sidecar") renderSidecarRung(w, h);
|
|
2720
|
-
else if (state.screen === "home") renderHome(rung);
|
|
2721
|
-
else renderRoom(rung);
|
|
2722
|
-
const bigEnough = rung !== "micro" && rung !== "orb" && rung !== "ticker";
|
|
2723
|
-
if (overlay && bigEnough) {
|
|
2724
|
-
clickables = [];
|
|
2725
|
-
if (overlay === "accounts") renderAccounts();
|
|
2726
|
-
else if (overlay === "agentAccess") renderAgentAccess();
|
|
2727
|
-
else if (overlay === "help") renderHelp();
|
|
2728
|
-
else if (overlay === "settings") renderSettings();
|
|
2729
|
-
overlayFocus = clickables.length ? Math.max(0, Math.min(overlayFocus, clickables.length - 1)) : 0;
|
|
2730
|
-
}
|
|
2731
|
-
if (state.modal && bigEnough) renderModal();
|
|
2732
|
-
if (task && bigEnough) renderTask();
|
|
2733
|
-
if (screen.dst) screen.draw({ delta: true });
|
|
2734
|
-
}
|
|
2735
|
-
function renderTask() {
|
|
2736
|
-
clickables = [];
|
|
2737
|
-
const w = W(), h = H();
|
|
2738
|
-
const bw = Math.min(66, w - 6), bh = 7;
|
|
2739
|
-
const x0 = Math.floor((w - bw) / 2), y0 = Math.floor((h - bh) / 2);
|
|
2740
|
-
for (let yy = y0; yy < y0 + bh; yy++) puts(x0, yy, " ".repeat(bw), { bgColor: C.panel });
|
|
2741
|
-
box(x0, y0, x0 + bw - 1, y0 + bh - 1, { color: C.accent, bgColor: C.panel }, task.title);
|
|
2742
|
-
const t = task;
|
|
2743
|
-
if (t.phase === "confirm") {
|
|
2744
|
-
puts(x0 + 2, y0 + 2, t.line.slice(0, bw - 4), { color: C.bright, bgColor: C.panel, bold: true });
|
|
2745
|
-
button(x0 + 2, y0 + 4, " install \xB7 enter ", { bgColor: C.accent, color: onFill(C.accent), bold: true }, () => t.run?.());
|
|
2746
|
-
button(x0 + 20, y0 + 4, " not now \xB7 esc ", { bgColor: C.tabOff, color: C.text }, () => {
|
|
2747
|
-
task = null;
|
|
2748
|
-
});
|
|
2749
|
-
} else if (t.phase === "running") {
|
|
2750
|
-
puts(x0 + 2, y0 + 2, `${SPINNER[frame % SPINNER.length]} ${t.title}\u2026`, { color: C.bright, bgColor: C.panel, bold: true });
|
|
2751
|
-
puts(x0 + 2, y0 + 4, ("\xB7 " + t.line).slice(0, bw - 4), { color: C.dim, bgColor: C.panel });
|
|
2752
|
-
} else if (t.ok) {
|
|
2753
|
-
puts(x0 + 2, y0 + 2, "\u2713 " + t.line.slice(0, bw - 6), { color: C.good, bgColor: C.panel, bold: true });
|
|
2754
|
-
if (t.okLabel) button(x0 + 2, y0 + 4, ` ${t.okLabel} `, { bgColor: C.good, color: onFill(C.good), bold: true }, () => t.onOk?.());
|
|
2755
|
-
else button(x0 + 2, y0 + 4, " close \xB7 enter ", { bgColor: C.good, color: onFill(C.good), bold: true }, () => {
|
|
2756
|
-
task = null;
|
|
2757
|
-
});
|
|
2758
|
-
} else {
|
|
2759
|
-
puts(x0 + 2, y0 + 2, "\u2715 " + t.line.slice(0, bw - 6), { color: C.danger, bgColor: C.panel, bold: true });
|
|
2760
|
-
button(x0 + 2, y0 + 4, " close \xB7 esc ", { bgColor: C.tabOff, color: C.text }, () => {
|
|
2761
|
-
task = null;
|
|
2762
|
-
});
|
|
2763
|
-
}
|
|
2764
|
-
}
|
|
2765
|
-
function orbInfo() {
|
|
2766
|
-
const streamAgent = state.agents.find((a) => a.busy);
|
|
2767
|
-
return {
|
|
2768
|
-
project: state.project?.name ?? null,
|
|
2769
|
-
online: onlineUserIds().size,
|
|
2770
|
-
t0: unread.t0,
|
|
2771
|
-
t2: unread.t2,
|
|
2772
|
-
lastEvent: lastEvent?.text ?? null,
|
|
2773
|
-
lastEventColor: lastEvent?.color ?? C.dim,
|
|
2774
|
-
connected: state.connected,
|
|
2775
|
-
agentStreaming: !!streamAgent,
|
|
2776
|
-
hint: bootHint || (pendingBoot ? "resize for full app" : null)
|
|
2777
|
-
};
|
|
2778
|
-
}
|
|
2779
|
-
function blitOrbLine(x0, y, line) {
|
|
2780
|
-
if (!line || y < 0 || y >= H()) return;
|
|
2781
|
-
puts(x0, y, line.text, { color: C.dim, bgColor: C.bg });
|
|
2782
|
-
for (const sp of line.spans) {
|
|
2783
|
-
const seg = [...line.text].slice(sp.start, sp.end).join("");
|
|
2784
|
-
if (seg) puts(x0 + sp.start, y, seg, { color: readableFg(sp.color, C.bg, 3.2), bold: sp.bold, bgColor: C.bg });
|
|
2785
|
-
}
|
|
2786
|
-
}
|
|
2787
|
-
function renderMicro(w, h) {
|
|
2788
|
-
const line = microLine(w, orbEngine, frame, orbInfo());
|
|
2789
|
-
const y = Math.floor((h - 1) / 2);
|
|
2790
|
-
const x = Math.max(0, Math.floor((w - [...line.text].length) / 2));
|
|
2791
|
-
blitOrbLine(x, y, line);
|
|
2792
|
-
}
|
|
2793
|
-
function renderOrb(w, h) {
|
|
2794
|
-
const lines = renderOrbRung(w, h, orbEngine, frame, orbInfo());
|
|
2795
|
-
for (let i = 0; i < lines.length && i < h; i++) blitOrbLine(0, i, lines[i]);
|
|
2796
|
-
}
|
|
2797
|
-
function renderTicker(w, h) {
|
|
2798
|
-
if (!state.project) {
|
|
2799
|
-
renderMicro(w, h);
|
|
2800
|
-
return;
|
|
2801
|
-
}
|
|
2802
|
-
puts(0, 0, " ".repeat(w), { bgColor: C.panel });
|
|
2803
|
-
let x = 1;
|
|
2804
|
-
const projName = state.project.name.slice(0, 18);
|
|
2805
|
-
puts(x, 0, projName, { bgColor: C.panel, color: C.bright, bold: true });
|
|
2806
|
-
x += projName.length + 3;
|
|
2807
|
-
if (w >= 80) {
|
|
2808
|
-
for (const m of state.members.slice(0, 5)) {
|
|
2809
|
-
const on = state.sessions.some((s) => s.userId === m.id);
|
|
2810
|
-
puts(x, 0, (on ? "\u25CF " : "\u25CB ") + m.name.slice(0, 8), { bgColor: C.panel, color: on ? readableFg(m.color, C.panel) : C.dim });
|
|
2811
|
-
x += m.name.slice(0, 8).length + 3;
|
|
2812
|
-
}
|
|
2813
|
-
} else {
|
|
2814
|
-
puts(x, 0, `\xB7 ${onlineUserIds().size} on`, { bgColor: C.panel, color: C.dim });
|
|
2815
|
-
x += 8;
|
|
2816
|
-
}
|
|
2817
|
-
if (state.agents.length) {
|
|
2818
|
-
puts(x, 0, `\xB7 \u2726${state.agents.length}`, { bgColor: C.panel, color: C.dim });
|
|
2819
|
-
}
|
|
2820
|
-
const dot = state.connected ? "\u25CF live" : "\u25CB offline";
|
|
2821
|
-
puts(w - dot.length - 1, 0, dot, { bgColor: C.panel, color: state.connected ? C.good : C.warn });
|
|
2822
|
-
const hasStatus = h >= 8;
|
|
2823
|
-
const hasGhost = h >= 7 && activeGhosts().length > 0;
|
|
2824
|
-
const inputRow = h - (hasStatus ? 2 : 1);
|
|
2825
|
-
const ghostRow = hasGhost ? inputRow - 1 : -1;
|
|
2826
|
-
const threadTop = 1;
|
|
2827
|
-
const threadH = (hasGhost ? ghostRow : inputRow) - threadTop;
|
|
2828
|
-
chatLog.pinToTail();
|
|
2829
|
-
const lines = chatLog.visible(w - 2, threadH);
|
|
2830
|
-
const gutters = chatLog.visibleGutters();
|
|
2831
|
-
for (let i = 0; i < lines.length; i++) {
|
|
2832
|
-
const line = lines[i];
|
|
2833
|
-
if (!line) continue;
|
|
2834
|
-
drawSpannedLine(1, threadTop + i, line.text, line.spans, w - 2, line.centered);
|
|
2835
|
-
void gutters;
|
|
2836
|
-
}
|
|
2837
|
-
if (hasGhost) {
|
|
2838
|
-
const g = activeGhosts()[0];
|
|
2839
|
-
const m = memberOf(g.userId);
|
|
2840
|
-
puts(1, ghostRow, m?.name ?? "?", { color: readableFg(m?.color ?? C.dim), bold: true });
|
|
2841
|
-
puts(1 + (m?.name ?? "?").length, ghostRow, " \u270E ", { color: C.sys });
|
|
2842
|
-
puts(4 + (m?.name ?? "?").length, ghostRow, [...g.text].slice(0, w - (m?.name ?? "?").length - 6).join(""), { color: dimOf(m?.color ?? 245), italic: true });
|
|
2843
|
-
}
|
|
2844
|
-
renderInputLine(inputRow, w);
|
|
2845
|
-
if (hasStatus) {
|
|
2846
|
-
puts(0, h - 1, " ".repeat(w), { bgColor: C.panel });
|
|
2847
|
-
puts(1, h - 1, `${state.project.code} \xB7 ${state.members.length} members`, { bgColor: C.panel, color: C.dim });
|
|
2848
|
-
const hint = "[enter] send \xB7 [^C] quit ";
|
|
2849
|
-
puts(w - hint.length, h - 1, hint, { bgColor: C.panel, color: C.dim });
|
|
2850
|
-
}
|
|
2851
|
-
if (popup.open) renderPopup(inputRow, w);
|
|
2852
|
-
}
|
|
2853
|
-
var sidecarBlock = null;
|
|
2854
|
-
var sidecarHintAt = 0;
|
|
2855
|
-
function renderSidecarRung(w, h) {
|
|
2856
|
-
if (!state.project) {
|
|
2857
|
-
renderHome("compact");
|
|
2858
|
-
return;
|
|
2859
|
-
}
|
|
2860
|
-
puts(0, 0, " ".repeat(w), { bgColor: C.panel });
|
|
2861
|
-
puts(1, 0, state.project.name.slice(0, w - 12), { bgColor: C.panel, color: C.bright, bold: true });
|
|
2862
|
-
const right = `${onlineUserIds().size} on ${state.connected ? "\u25CF" : "\u25CB"}`;
|
|
2863
|
-
puts(w - right.length - 1, 0, right, { bgColor: C.panel, color: state.connected ? C.good : C.warn });
|
|
2864
|
-
const inputRow = h - 2;
|
|
2865
|
-
const ghostRows = Math.min(activeGhosts().length, 1);
|
|
2866
|
-
const blockLines = sidecarBlock && Date.now() - sidecarBlock.at < 6e4 ? sidecarBlock.lines.length + 1 : 0;
|
|
2867
|
-
if (!blockLines) sidecarBlock = null;
|
|
2868
|
-
const ghostRow = inputRow - ghostRows;
|
|
2869
|
-
const blockTop = ghostRow - blockLines;
|
|
2870
|
-
const threadTop = 1;
|
|
2871
|
-
const threadH = blockTop - threadTop;
|
|
2872
|
-
const lines = chatLog.visible(w - 2, threadH);
|
|
2873
|
-
for (let i = 0; i < lines.length; i++) {
|
|
2874
|
-
const line = lines[i];
|
|
2875
|
-
if (!line) continue;
|
|
2876
|
-
drawSpannedLine(1, threadTop + i, line.text, line.spans, w - 2, line.centered);
|
|
2877
|
-
}
|
|
2878
|
-
if (!chatLog.pinned && chatLog.unseen > 0) {
|
|
2879
|
-
const pill = ` \u2193 ${chatLog.unseen} new `;
|
|
2880
|
-
button(Math.floor((w - pill.length) / 2), blockTop - 1, pill, { bgColor: C.pillBg, color: C.pillFg, bold: true }, () => {
|
|
2881
|
-
chatLog.pinToTail();
|
|
2882
|
-
clearUnread();
|
|
2883
|
-
});
|
|
2884
|
-
}
|
|
2885
|
-
if (sidecarBlock) {
|
|
2886
|
-
puts(1, blockTop, `\xB7 ${sidecarBlock.title} \xB7 local view \xB7`.padStart(Math.floor((w + sidecarBlock.title.length + 16) / 2)), { color: C.sys });
|
|
2887
|
-
sidecarBlock.lines.forEach((l, i) => puts(2, blockTop + 1 + i, l.slice(0, w - 4), { color: C.dim }));
|
|
2888
|
-
}
|
|
2889
|
-
if (ghostRows) {
|
|
2890
|
-
const g = activeGhosts()[0];
|
|
2891
|
-
const m = memberOf(g.userId);
|
|
2892
|
-
puts(1, ghostRow, m?.name ?? "?", { color: readableFg(m?.color ?? C.dim), bold: true });
|
|
2893
|
-
puts(1 + (m?.name ?? "?").length, ghostRow, " \u270E ", { color: C.sys });
|
|
2894
|
-
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 });
|
|
2895
|
-
}
|
|
2896
|
-
renderInputLine(inputRow, w);
|
|
2897
|
-
puts(0, h - 1, " ".repeat(w), { bgColor: C.panel });
|
|
2898
|
-
const hintLeft = Date.now() - sidecarHintAt < 3e3 ? "tabs live at 60 cols and up" : "/team /secrets";
|
|
2899
|
-
puts(1, h - 1, hintLeft, { bgColor: C.panel, color: C.dim });
|
|
2900
|
-
const hint = "[^C] quit ";
|
|
2901
|
-
puts(w - hint.length, h - 1, hint, { bgColor: C.panel, color: C.dim });
|
|
2902
|
-
if (popup.open) renderPopup(inputRow, w);
|
|
2903
|
-
}
|
|
2904
|
-
function renderInputLine(inputRow, width) {
|
|
2905
|
-
puts(0, inputRow, " ".repeat(width), { bgColor: C.inputBg });
|
|
2906
|
-
puts(1, inputRow, "\u276F ", { color: C.accent, bgColor: C.inputBg, bold: true });
|
|
2907
|
-
const fieldW = width - 4;
|
|
2908
|
-
const win = editor.window(fieldW);
|
|
2909
|
-
puts(3, inputRow, win.view.padEnd(fieldW).slice(0, fieldW), { color: C.bright, bgColor: C.inputBg });
|
|
2910
|
-
if (win.clippedLeft) puts(3, inputRow, "\u2026", { color: C.dim, bgColor: C.inputBg });
|
|
2911
|
-
if (win.clippedRight) puts(3 + fieldW - 1, inputRow, "\u2026", { color: C.dim, bgColor: C.inputBg });
|
|
2912
|
-
const cx = 3 + win.cursorCol;
|
|
2913
|
-
if (cx < 3 + fieldW) {
|
|
2914
|
-
const under = [...win.view][win.cursorCol] ?? " ";
|
|
2915
|
-
puts(cx, inputRow, under, { color: onFill(C.bright), bgColor: C.bright });
|
|
2916
|
-
}
|
|
2917
|
-
}
|
|
2918
|
-
function renderChrome(rung) {
|
|
2919
|
-
const w = W();
|
|
2920
|
-
if (rung === "compact") {
|
|
2921
|
-
puts(0, 0, " ".repeat(w), { bgColor: C.panel });
|
|
2922
|
-
let x2 = 1;
|
|
2923
|
-
button(x2, 0, " a8 ", { bgColor: C.accent, color: C.onAccent, bold: true }, goHome);
|
|
2924
|
-
x2 += 5;
|
|
2925
|
-
const labels2 = [["1\xB7Chat", "chat"], ["2\xB7Cvs", "canvas"], ["3\xB7Sec", "secrets"], ["4\xB7Team", "team"]];
|
|
2926
|
-
for (const [label2, tab] of labels2) {
|
|
2927
|
-
const on = state.tab === tab;
|
|
2928
|
-
x2 = button(x2, 0, ` ${label2} `, { bgColor: C.panel, color: on ? C.bright : C.dim, bold: on, underline: on }, () => setTab(tab));
|
|
2929
|
-
x2 = drawTabDecor(x2, 0, tab, 1);
|
|
2930
|
-
x2 += 1;
|
|
2931
|
-
}
|
|
2932
|
-
const whoMax = Math.max(0, w - 1 - x2);
|
|
2933
|
-
let who2 = `${me.name} \u25CF`;
|
|
2934
|
-
if (who2.length > whoMax) who2 = whoMax >= 4 ? me.name.slice(0, whoMax - 3) + "\u2026 \u25CF" : "\u25CF".slice(0, whoMax);
|
|
2935
|
-
const whoX = Math.max(x2, w - who2.length - 1);
|
|
2936
|
-
puts(whoX, 0, who2, { bgColor: C.panel, color: readableFg(me.color, C.panel), bold: true });
|
|
2937
|
-
const gap = whoX - x2;
|
|
2938
|
-
const label = gap >= 10 ? " settings " : gap >= 5 ? " set " : "";
|
|
2939
|
-
if (label) button(whoX - label.length, 0, label, { bgColor: C.panel, color: C.dim }, openSettings);
|
|
2940
|
-
return 1;
|
|
2941
|
-
}
|
|
2942
|
-
puts(0, 0, " ".repeat(w), { bgColor: C.panel });
|
|
2943
|
-
button(1, 0, " accelr8 ", { bgColor: C.accent, color: C.onAccent, bold: true }, goHome);
|
|
2944
|
-
puts(11, 0, state.project?.name ?? "", { bgColor: C.panel, color: C.bright, bold: true });
|
|
2945
|
-
const who = `${me.name} \u25CF`;
|
|
2946
|
-
let hx = w - who.length - 1;
|
|
2947
|
-
puts(hx, 0, who, { bgColor: C.panel, color: readableFg(me.color, C.panel), bold: true });
|
|
2948
|
-
hx -= 11;
|
|
2949
|
-
button(hx, 0, " settings ", { bgColor: C.panel, color: C.dim }, openSettings);
|
|
2950
|
-
puts(0, 1, " ".repeat(w), { bgColor: C.bg });
|
|
2951
|
-
const style = settings.tabs;
|
|
2952
|
-
const labels = [["Chat", "chat"], ["Canvas", "canvas"], ["Secrets", "secrets"], ["Team", "team"]];
|
|
2953
|
-
const ranges = [];
|
|
2954
|
-
let x = 2;
|
|
2955
|
-
if (style === "pipes") {
|
|
2956
|
-
puts(x, 1, "\u2502", { color: C.rule, bgColor: C.bg });
|
|
2957
|
-
x += 2;
|
|
2958
|
-
}
|
|
2959
|
-
for (const [label, tab] of labels) {
|
|
2960
|
-
const on = state.tab === tab;
|
|
2961
|
-
let txt;
|
|
2962
|
-
let attr;
|
|
2963
|
-
if (style === "blocks") {
|
|
2964
|
-
txt = ` ${label} `;
|
|
2965
|
-
attr = { bgColor: on ? C.accent : C.tabOff, color: on ? C.onAccent : C.text, bold: on };
|
|
2966
|
-
} else if (style === "brackets") {
|
|
2967
|
-
txt = on ? `[ ${label} ]` : ` ${label} `;
|
|
2968
|
-
attr = { bgColor: C.bg, color: on ? C.accent : C.dim, bold: on };
|
|
2969
|
-
} else {
|
|
2970
|
-
txt = ` ${label} `;
|
|
2971
|
-
attr = { bgColor: C.bg, color: on ? C.bright : C.dim, bold: on };
|
|
2972
|
-
}
|
|
2973
|
-
const nx = button(x, 1, txt, attr, () => setTab(tab));
|
|
2974
|
-
ranges.push({ x0: x, x1: nx - 1, on });
|
|
2975
|
-
x = drawTabDecor(nx, 1, tab, 3);
|
|
2976
|
-
if (style === "pipes") {
|
|
2977
|
-
x += 1;
|
|
2978
|
-
puts(x, 1, "\u2502", { color: C.rule, bgColor: C.bg });
|
|
2979
|
-
x += 2;
|
|
2980
|
-
} else x += 2;
|
|
2981
|
-
}
|
|
2982
|
-
const dot = state.connected ? "\u25CF live" : "\u25CB offline";
|
|
2983
|
-
puts(W() - dot.length - 1, 1, dot, { bgColor: C.bg, color: state.connected ? C.good : C.warn });
|
|
2984
|
-
puts(0, 2, "\u2500".repeat(w), { color: C.rule, bgColor: C.bg });
|
|
2985
|
-
if (style === "underline") {
|
|
2986
|
-
const active = ranges.find((r) => r.on);
|
|
2987
|
-
if (active) for (let cx = active.x0; cx <= active.x1; cx++) puts(cx, 2, "\u2501", { color: C.accent, bold: true, bgColor: C.bg });
|
|
2988
|
-
}
|
|
2989
|
-
return 3;
|
|
2990
|
-
}
|
|
2991
|
-
function drawTabDecor(x, y, tab, maxDots) {
|
|
2992
|
-
const others = state.sessions.filter((s) => s.sid !== mySid && s.tab === tab);
|
|
2993
|
-
let shown = 0;
|
|
2994
|
-
for (const s of others) {
|
|
2995
|
-
if (shown >= maxDots) {
|
|
2996
|
-
puts(x, y, `+${others.length - shown}`, { color: C.dim, bgColor: y === 0 ? C.panel : C.bg });
|
|
2997
|
-
x += 2;
|
|
2998
|
-
break;
|
|
2999
|
-
}
|
|
3000
|
-
puts(x, y, "\u25CF", { color: readableFg(memberOf(s.userId)?.color ?? C.dim, y === 0 ? C.panel : C.bg, 2.2), bgColor: y === 0 ? C.panel : C.bg });
|
|
3001
|
-
x += 1;
|
|
3002
|
-
shown++;
|
|
3003
|
-
}
|
|
3004
|
-
if (tab === "chat" && !chatVisible() && unread.t0 > 0) {
|
|
3005
|
-
const badge = `\xB7${Math.min(unread.t0, 99)}`;
|
|
3006
|
-
puts(x, y, badge, { color: C.accent, bgColor: y === 0 ? C.panel : C.bg });
|
|
3007
|
-
x += badge.length;
|
|
3008
|
-
}
|
|
3009
|
-
return x;
|
|
3010
|
-
}
|
|
3011
|
-
function setTab(tab) {
|
|
3012
|
-
if (state.screen !== "room") return;
|
|
3013
|
-
state.tab = tab;
|
|
3014
|
-
popup.open = false;
|
|
3015
|
-
sendMsg({ t: "tab", tab });
|
|
3016
|
-
if (tab === "chat") clearUnread();
|
|
3017
|
-
if (tab === "team") attachMyAgents();
|
|
3018
|
-
}
|
|
3019
|
-
function clearUnread() {
|
|
3020
|
-
unread.t0 = 0;
|
|
3021
|
-
unread.t2 = 0;
|
|
3022
|
-
}
|
|
3023
|
-
function renderHome(rung) {
|
|
3024
|
-
const w = W();
|
|
3025
|
-
puts(0, 0, " ".repeat(w), { bgColor: C.panel });
|
|
3026
|
-
puts(1, 0, " accelr8 ", { bgColor: C.accent, color: C.onAccent, bold: true });
|
|
3027
|
-
puts(11, 0, "vibe-code together", { bgColor: C.panel, color: C.dim, italic: true });
|
|
3028
|
-
const who = `${me.name} \u25CF`;
|
|
3029
|
-
puts(w - who.length - 1, 0, who, { bgColor: C.panel, color: readableFg(me.color, C.panel), bold: true });
|
|
3030
|
-
button(w - who.length - 11, 0, " settings ", { bgColor: C.panel, color: C.dim }, openSettings);
|
|
3031
|
-
const dot = state.connected ? "\u25CF live" : "\u25CB offline";
|
|
3032
|
-
puts(w - dot.length - 1, 1, dot, { bgColor: C.bg, color: state.connected ? C.good : C.warn });
|
|
3033
|
-
let y = 2;
|
|
3034
|
-
if (w >= LOGO_W + 4 && H() >= LOGO.length + 16) {
|
|
3035
|
-
y = 3;
|
|
3036
|
-
const lx = Math.max(2, Math.floor((w - LOGO_W) / 2));
|
|
3037
|
-
for (const line of LOGO) {
|
|
3038
|
-
puts(lx, y, line, { color: C.accent, bgColor: C.bg });
|
|
3039
|
-
y++;
|
|
3040
|
-
}
|
|
3041
|
-
y += 1;
|
|
3042
|
-
}
|
|
3043
|
-
if (updateInfo && !task) {
|
|
3044
|
-
puts(2, y, "\u21BB", { color: C.warn, bold: true });
|
|
3045
|
-
const w2 = W();
|
|
3046
|
-
const label = ` update to v${updateInfo.latest} \xB7 press enter `;
|
|
3047
|
-
button(4, y, label.slice(0, Math.max(0, w2 - 6)), { bgColor: C.warn, color: onFill(C.warn), bold: true }, startUpdate);
|
|
3048
|
-
puts(2, y + 1, `you're on v${updateInfo.current} \xB7 your friends' agents need everyone on the latest`, { color: C.dim, italic: true });
|
|
3049
|
-
y += 3;
|
|
3050
|
-
}
|
|
3051
|
-
puts(2, y, "PROJECTS", { color: C.bright, bold: true });
|
|
3052
|
-
y += 1;
|
|
3053
|
-
let bx = 2;
|
|
3054
|
-
bx = button(bx, y, " + new project ", { bgColor: C.good, color: onFill(C.good), bold: true }, promptCreate) + 2;
|
|
3055
|
-
bx = button(bx, y, " join by code ", { bgColor: C.accent, color: onFill(C.accent), bold: true }, promptJoin) + 2;
|
|
3056
|
-
button(bx, y, " accounts ", { bgColor: C.tabOff, color: C.text }, openAccounts);
|
|
3057
|
-
y += 2;
|
|
3058
|
-
if (bootHint) puts(2, H() - 1, bootHint, { color: C.dim });
|
|
3059
|
-
if (state.projects.length === 0) {
|
|
3060
|
-
puts(2, y, "no projects yet. create one to start a room.", { color: C.dim, italic: true });
|
|
3061
|
-
return;
|
|
3062
|
-
}
|
|
3063
|
-
puts(2, y, "CODE PROJECT MEMBERS ONLINE", { color: C.dim });
|
|
3064
|
-
y += 1;
|
|
3065
|
-
for (const p of state.projects) {
|
|
3066
|
-
if (y >= H() - 2) break;
|
|
3067
|
-
const current = state.project?.id === p.id;
|
|
3068
|
-
const row = `${p.code.padEnd(8)} ${p.name.slice(0, 30).padEnd(30)} ${String(p.members).padStart(5)} ${String(p.online).padStart(5)}`;
|
|
3069
|
-
puts(2, y, current ? "\u25CF" : "\u25BA", { color: current ? C.good : C.accent });
|
|
3070
|
-
button(4, y, row, { color: C.text, bgColor: C.bg }, () => {
|
|
3071
|
-
if (current) {
|
|
3072
|
-
state.screen = "room";
|
|
3073
|
-
sendMsg({ t: "tab", tab: state.tab });
|
|
3074
|
-
} else sendMsg({ t: "joinProject", code: p.code });
|
|
3075
|
-
});
|
|
3076
|
-
if (current) puts(4 + row.length + 1, y, "current", { color: C.good });
|
|
3077
|
-
y++;
|
|
3078
|
-
}
|
|
3079
|
-
puts(2, H() - 2, "click a row to open its room", { color: C.dim, italic: true });
|
|
3080
|
-
}
|
|
3081
|
-
function renderRoom(rung) {
|
|
3082
|
-
const top = renderChrome(rung);
|
|
3083
|
-
if (!state.project) {
|
|
3084
|
-
state.screen = "home";
|
|
3085
|
-
return;
|
|
3086
|
-
}
|
|
3087
|
-
const sidebar = rung === "comfy";
|
|
3088
|
-
const mainW = sidebar ? W() - 24 : W();
|
|
3089
|
-
if (state.tab === "chat") renderChat(top, mainW, rung);
|
|
3090
|
-
else if (state.tab === "canvas") renderCanvas(top);
|
|
3091
|
-
else if (state.tab === "secrets") renderSecrets(top);
|
|
3092
|
-
else renderTeam(top);
|
|
3093
|
-
if (sidebar) renderSidebar(mainW, top);
|
|
3094
|
-
renderStatusBar(rung);
|
|
3095
|
-
}
|
|
3096
|
-
function renderStatusBar(rung) {
|
|
3097
|
-
const w = W(), sy = H() - 1;
|
|
3098
|
-
puts(0, sy, " ".repeat(w), { bgColor: C.panel });
|
|
3099
|
-
const agentBit = state.agents.length ? ` \xB7 \u2726${state.agents.length} agents` : "";
|
|
3100
|
-
const left = state.project ? ` ${state.project.name} \xB7 ${state.project.code} \xB7 ${onlineUserIds().size} online${agentBit} ` : " no project open ";
|
|
3101
|
-
const drawn = left.slice(0, w - 24);
|
|
3102
|
-
puts(0, sy, drawn, { bgColor: C.panel, color: C.dim });
|
|
3103
|
-
if (voiceOn) {
|
|
3104
|
-
const label = voiceTransmit ? " \u25CF live \xB7 /mute" : ` \u25CB voice ${state.voicePeers.length} \xB7 /talk`;
|
|
3105
|
-
if (drawn.length + label.length < w - 24) puts(drawn.length, sy, label, { bgColor: C.panel, color: voiceTransmit ? C.good : C.accent, bold: voiceTransmit });
|
|
3106
|
-
}
|
|
3107
|
-
const hint = state.modal ? "[enter] ok \xB7 [esc] cancel " : "[tab] tabs \xB7 [^C] quit ";
|
|
3108
|
-
puts(w - hint.length, sy, hint, { bgColor: C.panel, color: C.dim });
|
|
3109
|
-
if (state.status) {
|
|
3110
|
-
const s = " " + state.status + " ";
|
|
3111
|
-
puts(Math.max(0, Math.floor((w - s.length) / 2)), sy, s.slice(0, w), { bgColor: C.panel, color: C.warn });
|
|
3112
|
-
} else if (bootHint) {
|
|
3113
|
-
puts(Math.max(0, Math.floor((w - bootHint.length) / 2)), sy, bootHint, { bgColor: C.panel, color: C.dim });
|
|
3114
|
-
}
|
|
3115
|
-
}
|
|
3116
|
-
function renderChat(top, mainW, rung) {
|
|
3117
|
-
const h = H();
|
|
3118
|
-
const gutterW = mainW >= 80 ? 6 : 0;
|
|
3119
|
-
const ghostRows = Math.min(activeGhosts().length, h >= 32 ? 2 : 1);
|
|
3120
|
-
const inputRow = h - 2;
|
|
3121
|
-
const ghostTop = inputRow - ghostRows;
|
|
3122
|
-
const threadTop = top;
|
|
3123
|
-
const threadH = ghostTop - threadTop;
|
|
3124
|
-
const wrapW = mainW - gutterW - 1;
|
|
3125
|
-
const lines = chatLog.visible(wrapW, threadH);
|
|
3126
|
-
const gutters = chatLog.visibleGutters();
|
|
3127
|
-
for (let i = 0; i < lines.length; i++) {
|
|
3128
|
-
const y = threadTop + i;
|
|
3129
|
-
const line = lines[i];
|
|
3130
|
-
if (!line) continue;
|
|
3131
|
-
if (gutterW && gutters[i]) puts(1, y, gutters[i], { color: C.dim });
|
|
3132
|
-
drawSpannedLine(gutterW + 1, y, line.text, line.spans, wrapW, line.centered);
|
|
3133
|
-
}
|
|
3134
|
-
if (!chatLog.pinned && chatLog.unseen > 0) {
|
|
3135
|
-
const pill = ` \u2193 ${chatLog.unseen} new message${chatLog.unseen > 1 ? "s" : ""} `;
|
|
3136
|
-
const px = Math.floor((mainW - pill.length) / 2);
|
|
3137
|
-
button(px, ghostTop - 1, pill, { bgColor: C.pillBg, color: C.pillFg, bold: true }, () => {
|
|
3138
|
-
chatLog.pinToTail();
|
|
3139
|
-
clearUnread();
|
|
3140
|
-
});
|
|
3141
|
-
}
|
|
3142
|
-
const gs = activeGhosts().slice(0, ghostRows);
|
|
3143
|
-
for (let i = 0; i < gs.length; i++) {
|
|
3144
|
-
const g = gs[i];
|
|
3145
|
-
const m = memberOf(g.userId);
|
|
3146
|
-
const name = m?.name ?? "?";
|
|
3147
|
-
const y = ghostTop + i;
|
|
3148
|
-
puts(1, y, name, { color: readableFg(m?.color ?? C.dim), bold: true });
|
|
3149
|
-
puts(1 + name.length, y, " \u270E ", { color: C.sys });
|
|
3150
|
-
const maxDraft = mainW - name.length - 6;
|
|
3151
|
-
const text = [...g.text].slice(0, 120).join("");
|
|
3152
|
-
const shown = text.length > maxDraft ? [...text].slice(0, Math.max(0, maxDraft - 1)).join("") + "\u2026" : text;
|
|
3153
|
-
puts(1 + name.length + 3, y, shown, { color: dimOf(m?.color ?? 245), italic: true });
|
|
3154
|
-
}
|
|
3155
|
-
renderInputLine(inputRow, mainW);
|
|
3156
|
-
if (popup.open) renderPopup(inputRow, mainW);
|
|
3157
|
-
}
|
|
3158
|
-
function drawSpannedLine(x, y, text, spans, width, centered) {
|
|
3159
|
-
const chars = [...text];
|
|
3160
|
-
let ox = x;
|
|
3161
|
-
if (centered) ox = x + Math.max(0, Math.floor((width - chars.length) / 2));
|
|
3162
|
-
puts(ox, y, text, { color: C.text, bgColor: C.bg });
|
|
3163
|
-
for (const sp of spans) {
|
|
3164
|
-
const seg = chars.slice(sp.start, sp.end).join("");
|
|
3165
|
-
if (!seg) continue;
|
|
3166
|
-
const bg = sp.bgColor === -3 ? C.mentionSelfBg : sp.bgColor ?? C.bg;
|
|
3167
|
-
let color;
|
|
3168
|
-
let text2 = seg;
|
|
3169
|
-
if (sp.color === -1) {
|
|
3170
|
-
color = C.dim;
|
|
3171
|
-
text2 = SPINNER[frame % SPINNER.length];
|
|
3172
|
-
} else if (sp.color === -2) color = C.sys;
|
|
3173
|
-
else color = readableFg(sp.color, bg);
|
|
3174
|
-
puts(ox + sp.start, y, text2, { color, bold: sp.bold, italic: sp.italic, bgColor: bg });
|
|
3175
|
-
}
|
|
3176
|
-
}
|
|
3177
|
-
function renderPopup(inputRow, mainW) {
|
|
3178
|
-
const items = popup.visible();
|
|
3179
|
-
if (!items.length) return;
|
|
3180
|
-
const boxW = Math.min(Math.max(...items.map((c) => c.name.length + (c.detail?.length ?? 0) + 8), 20), mainW - 4);
|
|
3181
|
-
const x0 = 2;
|
|
3182
|
-
const y0 = inputRow - items.length - 2;
|
|
3183
|
-
puts(x0, y0, "\u250C mention " + "\u2500".repeat(Math.max(0, boxW - 11)) + "\u2510", { color: C.accent, bgColor: C.popupBg });
|
|
3184
|
-
items.forEach((c, i) => {
|
|
3185
|
-
const y = y0 + 1 + i;
|
|
3186
|
-
const sel = i === popup.selIdx;
|
|
3187
|
-
const nameColor = sel ? C.selFg : readableFg(c.color, C.popupBg);
|
|
3188
|
-
puts(x0, y, "\u2502", { color: C.accent, bgColor: C.popupBg });
|
|
3189
|
-
puts(x0 + boxW - 1, y, "\u2502", { color: C.accent, bgColor: C.popupBg });
|
|
3190
|
-
puts(x0 + 1, y, " ".repeat(boxW - 2), { bgColor: sel ? C.popupSel : C.popupBg });
|
|
3191
|
-
puts(x0 + 2, y, c.kind === "agent" ? "\u2726" : "\u25CF", { color: nameColor, bgColor: sel ? C.popupSel : C.popupBg });
|
|
3192
|
-
puts(x0 + 4, y, c.name, { color: nameColor, bold: true, italic: c.kind === "agent", bgColor: sel ? C.popupSel : C.popupBg });
|
|
3193
|
-
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 });
|
|
3194
|
-
});
|
|
3195
|
-
puts(x0, y0 + 1 + items.length, "\u2514" + "\u2500".repeat(Math.max(0, boxW - 2)) + "\u2518", { color: C.accent, bgColor: C.popupBg });
|
|
3196
|
-
}
|
|
3197
|
-
function activeGhosts() {
|
|
3198
|
-
const now = Date.now();
|
|
3199
|
-
for (const [sid, g] of ghosts) if (now - g.at > 3e3) ghosts.delete(sid);
|
|
3200
|
-
return [...ghosts.values()].sort((a, b) => b.at - a.at);
|
|
3201
|
-
}
|
|
3202
|
-
function renderCanvas(top) {
|
|
3203
|
-
const w = W(), h = H();
|
|
3204
|
-
const ty = top;
|
|
3205
|
-
puts(1, ty, "brush", { color: C.dim });
|
|
3206
|
-
let px = 7;
|
|
3207
|
-
for (const ch of PALETTE) {
|
|
3208
|
-
const on = brush === ch;
|
|
3209
|
-
button(px, ty, ` ${ch} `, { bgColor: on ? C.bright : C.panel, color: on ? onFill(C.bright) : C.text, bold: on }, () => {
|
|
3210
|
-
brush = ch;
|
|
3211
|
-
});
|
|
3212
|
-
px += 3;
|
|
3213
|
-
}
|
|
3214
|
-
button(px + 1, ty, " \u232B erase ", { bgColor: brush === " " ? C.bright : C.panel, color: brush === " " ? onFill(C.bright) : C.text }, () => {
|
|
3215
|
-
brush = " ";
|
|
3216
|
-
});
|
|
3217
|
-
button(w - 10, ty, " clear ", { bgColor: C.warn, color: onFill(C.warn), bold: true }, () => sendMsg({ t: "clear" }));
|
|
3218
|
-
const by0 = ty + 1, by1 = h - 2;
|
|
3219
|
-
box(0, by0, w - 1, by1, { color: C.rule, bgColor: C.bg }, `canvas ${CANVAS_W}\xD7${CANVAS_H}`);
|
|
3220
|
-
const innerRight = w - 2, innerBottom = by1 - 1;
|
|
3221
|
-
const clip = (sx, sy) => sx >= IO_X && sx <= innerRight && sy >= by0 + 1 && sy <= innerBottom;
|
|
3222
|
-
for (const key in grid) {
|
|
3223
|
-
const [cx, cy] = key.split(",").map(Number);
|
|
3224
|
-
const sx = IO_X + cx, sy = by0 + 1 + cy;
|
|
3225
|
-
if (clip(sx, sy)) puts(sx, sy, grid[key][0], { color: grid[key][1], bgColor: C.bg });
|
|
3226
|
-
}
|
|
3227
|
-
for (const [, c] of cursors) {
|
|
3228
|
-
const sx = IO_X + Math.round(c.x), sy = by0 + 1 + Math.round(c.y);
|
|
3229
|
-
if (!clip(sx, sy)) continue;
|
|
3230
|
-
const m = memberOf(c.userId);
|
|
3231
|
-
puts(sx, sy, "\u25B2", { color: readableFg(m?.color ?? 7), bold: true });
|
|
3232
|
-
const label = " " + (m?.name ?? "?");
|
|
3233
|
-
if (sx + 1 + label.length <= innerRight) puts(sx + 1, sy, label, { bgColor: m?.color ?? 7, color: onFill(m?.color ?? 7), bold: true });
|
|
3234
|
-
}
|
|
3235
|
-
if (myLogical) {
|
|
3236
|
-
const sx = IO_X + myLogical.x, sy = by0 + 1 + myLogical.y;
|
|
3237
|
-
if (clip(sx, sy)) puts(sx, sy, brush === " " ? "\u25AB" : brush, { color: readableFg(me.color), bold: true });
|
|
3238
|
-
}
|
|
3239
|
-
canvasTop = by0 + 1;
|
|
3240
|
-
}
|
|
3241
|
-
var brush = "\u2588";
|
|
3242
|
-
var canvasTop = IO_Y;
|
|
3243
|
-
function renderSecrets(top) {
|
|
3244
|
-
puts(2, top + 1, "SECRETS", { color: C.bright, bold: true });
|
|
3245
|
-
puts(2, top + 3, "shared api keys and logins land here soon.", { color: C.dim, italic: true });
|
|
3246
|
-
puts(2, top + 4, "values will be masked, reveals audited, agents only ever see names.", { color: C.dim, italic: true });
|
|
3247
|
-
}
|
|
3248
|
-
function mapDotsFromSessions() {
|
|
3249
|
-
const seen = /* @__PURE__ */ new Set();
|
|
3250
|
-
const dots = [];
|
|
3251
|
-
for (const s of state.sessions) {
|
|
3252
|
-
if (seen.has(s.userId)) continue;
|
|
3253
|
-
const coords = tzToCoords(s.tz);
|
|
3254
|
-
if (!coords) continue;
|
|
3255
|
-
seen.add(s.userId);
|
|
3256
|
-
dots.push({ lat: coords.lat, lon: coords.lon, name: s.name, color: s.color });
|
|
3257
|
-
}
|
|
3258
|
-
return dots;
|
|
3259
|
-
}
|
|
3260
|
-
var teamMap = null;
|
|
3261
|
-
function blitMapLine(x0, y, line) {
|
|
3262
|
-
puts(x0, y, line.text, { color: readableFg(C.rule, C.bg, 3.05), bgColor: C.bg });
|
|
3263
|
-
for (const sp of line.spans) {
|
|
3264
|
-
if (!sp.bold) continue;
|
|
3265
|
-
const seg = [...line.text].slice(sp.start, sp.end).join("");
|
|
3266
|
-
if (seg) puts(x0 + sp.start, y, seg, { color: readableFg(sp.color, C.bg, 2.6), bold: true, bgColor: C.bg });
|
|
3267
|
-
}
|
|
3268
|
-
}
|
|
3269
|
-
function renderTeam(top) {
|
|
3270
|
-
if (!state.project) return;
|
|
3271
|
-
teamMap = null;
|
|
3272
|
-
let y = top + 1;
|
|
3273
|
-
const online = onlineUserIds();
|
|
3274
|
-
const availH = H() - top - 3;
|
|
3275
|
-
if (W() >= 48 && availH >= 22) {
|
|
3276
|
-
const dots = mapDotsFromSessions();
|
|
3277
|
-
const mapW = Math.min(W() - 4, 130);
|
|
3278
|
-
const mapH = Math.min(15, Math.max(8, Math.floor(availH * 0.45)));
|
|
3279
|
-
const title = `WHERE \xB7 ${dots.length}/${online.size} placed`;
|
|
3280
|
-
puts(2, y, title, { color: C.bright, bold: true });
|
|
3281
|
-
puts(2 + title.length + 2, y, dots.length ? "hover a dot" : "timezones load as people connect", { color: C.dim, italic: true });
|
|
3282
|
-
y += 1;
|
|
3283
|
-
const rendered = renderMap(mapW, mapH, dots);
|
|
3284
|
-
for (let i = 0; i < rendered.lines.length; i++) blitMapLine(2, y + i, rendered.lines[i]);
|
|
3285
|
-
teamMap = { rendered, x0: 2, y0: y };
|
|
3286
|
-
y += rendered.lines.length + 1;
|
|
3287
|
-
}
|
|
3288
|
-
puts(2, y, `HUMANS \xB7 ${online.size} online`, { color: C.bright, bold: true });
|
|
3289
|
-
y += 1;
|
|
3290
|
-
for (const m of state.members) {
|
|
3291
|
-
if (y >= H() - 8) break;
|
|
3292
|
-
const sess = state.sessions.filter((s) => s.userId === m.id);
|
|
3293
|
-
const on = sess.length > 0;
|
|
3294
|
-
puts(2, y, on ? "\u25CF" : "\u25CB", { color: on ? C.good : C.dim });
|
|
3295
|
-
puts(4, y, m.name + (m.id === me.id ? " \xB7 you" : ""), { color: readableFg(m.color), bold: true });
|
|
3296
|
-
const where = on ? sess.some((s) => s.tab !== "away") ? "viewing " + (sess.find((s) => s.tab !== "away")?.tab ?? "chat") : "parked" : "offline";
|
|
3297
|
-
puts(24, y, where, { color: on ? C.text : C.dim });
|
|
3298
|
-
const city = tzCity(sess.find((s) => s.tz)?.tz);
|
|
3299
|
-
if (city && W() >= 56) puts(42, y, city.slice(0, W() - 44), { color: C.dim });
|
|
3300
|
-
y++;
|
|
3301
|
-
}
|
|
3302
|
-
y += 1;
|
|
3303
|
-
const agentsTitle = `AGENTS \xB7 ${state.agents.length}`;
|
|
3304
|
-
puts(2, y, agentsTitle, { color: C.bright, bold: true });
|
|
3305
|
-
if (state.agents.some((a) => a.ownerId === me.id))
|
|
3306
|
-
puts(2 + agentsTitle.length + 3, y, "\xB7 click your agent to set who can use it", { color: C.dim, italic: true });
|
|
3307
|
-
y += 1;
|
|
3308
|
-
if (state.agents.length === 0) {
|
|
3309
|
-
puts(2, y, "none yet. press F2 to connect one on your Claude account.", { color: C.dim, italic: true });
|
|
3310
|
-
y += 1;
|
|
3311
|
-
}
|
|
3312
|
-
for (const a of state.agents) {
|
|
3313
|
-
if (y >= H() - 5) break;
|
|
3314
|
-
const mine = a.ownerId === me.id;
|
|
3315
|
-
const owner = memberOf(a.ownerId);
|
|
3316
|
-
const status = a.busy ? "streaming" : a.online ? "idle" : "offline";
|
|
3317
|
-
const access = a.access?.mode === "all" ? "open" : a.access?.mode === "list" ? `${a.access.users.length} allowed` : mine ? "only you" : "private";
|
|
3318
|
-
const label = `\u2726${a.handle}`;
|
|
3319
|
-
if (mine) {
|
|
3320
|
-
button(2, y, label, { color: readableFg(owner?.color ?? C.dim), bold: true, italic: true }, () => openAgentAccess(a.agentId));
|
|
3321
|
-
} else {
|
|
3322
|
-
puts(2, y, label, { color: readableFg(owner?.color ?? C.dim), bold: true, italic: true });
|
|
3323
|
-
}
|
|
3324
|
-
puts(24, y, `${owner?.name ?? "?"} \xB7 ${a.provider} \xB7 ${status}`, { color: C.dim });
|
|
3325
|
-
puts(52, y, access, { color: a.access?.mode === "all" ? C.good : C.dim });
|
|
3326
|
-
y++;
|
|
3327
|
-
}
|
|
3328
|
-
y += 1;
|
|
3329
|
-
puts(2, y, "INVITE", { color: C.bright, bold: true });
|
|
3330
|
-
y += 1;
|
|
3331
|
-
puts(2, y, "friends join with this command:", { color: C.dim });
|
|
3332
|
-
y += 1;
|
|
3333
|
-
const cmd = `accelr8 join ${state.project.code}`;
|
|
3334
|
-
puts(4, y, cmd, { color: C.good, bold: true });
|
|
3335
|
-
button(4 + cmd.length + 2, y, " copy ", { bgColor: C.accent, color: onFill(C.accent), bold: true }, () => copyToClipboard(cmd));
|
|
3336
|
-
y += 2;
|
|
3337
|
-
const codeBig = ` ${state.project.code.split("").join(" ")} `;
|
|
3338
|
-
puts(2, y, "code", { color: C.dim });
|
|
3339
|
-
puts(8, y, codeBig, { bgColor: C.bright, color: onFill(C.bright), bold: true });
|
|
3340
|
-
}
|
|
3341
|
-
function renderSidebar(x0, top) {
|
|
3342
|
-
const h = H();
|
|
3343
|
-
for (let y2 = top; y2 < h - 1; y2++) puts(x0, y2, "\u2502", { color: C.rule, bgColor: C.bg });
|
|
3344
|
-
const x = x0 + 2;
|
|
3345
|
-
let y = top;
|
|
3346
|
-
const online = onlineUserIds();
|
|
3347
|
-
puts(x, y, `ONLINE \xB7 ${online.size}`, { color: C.bright, bold: true });
|
|
3348
|
-
y += 1;
|
|
3349
|
-
for (const m of state.members.slice(0, 6)) {
|
|
3350
|
-
const sess = state.sessions.filter((s) => s.userId === m.id);
|
|
3351
|
-
const on = sess.length > 0;
|
|
3352
|
-
puts(x, y, on ? "\u25CF" : "\u25CB", { color: on ? C.good : C.dim });
|
|
3353
|
-
puts(x + 2, y, m.name.slice(0, 10), { color: readableFg(m.color), bold: true });
|
|
3354
|
-
const where = m.id === me.id ? "you" : on ? sess.find((s) => s.tab !== "away")?.tab ?? "parked" : "";
|
|
3355
|
-
puts(x + 13, y, String(where).slice(0, 7), { color: C.dim });
|
|
3356
|
-
y++;
|
|
3357
|
-
}
|
|
3358
|
-
y += 1;
|
|
3359
|
-
puts(x, y, `AGENTS \xB7 ${state.agents.length}`, { color: C.bright, bold: true });
|
|
3360
|
-
y += 1;
|
|
3361
|
-
for (const a of state.agents.slice(0, 4)) {
|
|
3362
|
-
puts(x, y, "\u2726", { color: readableFg(memberOf(a.ownerId)?.color ?? C.dim) });
|
|
3363
|
-
puts(x + 2, y, a.handle.slice(0, 12), { color: readableFg(memberOf(a.ownerId)?.color ?? C.dim), italic: true });
|
|
3364
|
-
puts(x + 15, y, a.busy ? SPINNER[frame % SPINNER.length] : "\xB7", { color: C.dim });
|
|
3365
|
-
y++;
|
|
3366
|
-
}
|
|
3367
|
-
if (state.agents.length === 0) {
|
|
3368
|
-
puts(x, y, "none yet", { color: C.dim, italic: true });
|
|
3369
|
-
y++;
|
|
3370
|
-
}
|
|
3371
|
-
y += 1;
|
|
3372
|
-
puts(x, y, "INVITE", { color: C.bright, bold: true });
|
|
3373
|
-
y += 1;
|
|
3374
|
-
if (state.project) {
|
|
3375
|
-
puts(x, y, `accelr8 join ${state.project.code}`.slice(0, 21), { color: C.good });
|
|
3376
|
-
y += 1;
|
|
3377
|
-
button(x, y, "[ copy ]", { color: C.accent }, () => copyToClipboard(`accelr8 join ${state.project.code}`));
|
|
3378
|
-
}
|
|
3379
|
-
}
|
|
3380
|
-
function box(x0, y0, x1, y1, attr, label) {
|
|
3381
|
-
puts(x0, y0, "\u250C" + "\u2500".repeat(Math.max(0, x1 - x0 - 1)) + "\u2510", attr);
|
|
3382
|
-
puts(x0, y1, "\u2514" + "\u2500".repeat(Math.max(0, x1 - x0 - 1)) + "\u2518", attr);
|
|
3383
|
-
for (let y = y0 + 1; y < y1; y++) {
|
|
3384
|
-
puts(x0, y, "\u2502", attr);
|
|
3385
|
-
puts(x1, y, "\u2502", attr);
|
|
3386
|
-
}
|
|
3387
|
-
if (label) puts(x0 + 2, y0, " " + label + " ", { ...attr, bold: true, color: C.bright });
|
|
3388
|
-
}
|
|
3389
|
-
var MODELS = {
|
|
3390
|
-
anthropic: ["claude-opus-4-8", "claude-sonnet-5", "claude-haiku-4-5", "claude-fable-5"],
|
|
3391
|
-
openai: ["gpt-4o", "gpt-4o-mini", "o1"]
|
|
3392
|
-
};
|
|
3393
|
-
function openAccounts() {
|
|
3394
|
-
overlay = "accounts";
|
|
3395
|
-
connectDraft = null;
|
|
3396
|
-
myAccounts = safeLoadAccounts();
|
|
3397
|
-
}
|
|
3398
|
-
function safeLoadAccounts() {
|
|
3399
|
-
try {
|
|
3400
|
-
return loadAccounts();
|
|
3401
|
-
} catch {
|
|
3402
|
-
return [];
|
|
3403
|
-
}
|
|
3404
|
-
}
|
|
3405
|
-
function agentOnline(handle) {
|
|
3406
|
-
return state.agents.some((a) => a.handle === handle && a.online);
|
|
3407
|
-
}
|
|
3408
|
-
function renderAccounts() {
|
|
3409
|
-
const w = W(), h = H();
|
|
3410
|
-
screen.fill({ attr: { bgColor: C.bg, color: C.text } });
|
|
3411
|
-
puts(0, 0, " ".repeat(w), { bgColor: C.panel });
|
|
3412
|
-
puts(1, 0, " accounts ", { bgColor: C.accent, color: onFill(C.accent), bold: true });
|
|
3413
|
-
puts(12, 0, "this machine \xB7 keys stay in ~/.accelr8 (0600)", { bgColor: C.panel, color: C.dim, italic: true });
|
|
3414
|
-
button(w - 12, 0, " esc / close ", { bgColor: C.panel, color: C.dim }, () => {
|
|
3415
|
-
overlay = null;
|
|
3416
|
-
connectDraft = null;
|
|
3417
|
-
});
|
|
3418
|
-
puts(0, 1, "\u2500".repeat(w), { color: C.rule, bgColor: C.bg });
|
|
3419
|
-
const ix = 2;
|
|
3420
|
-
let y = 3;
|
|
3421
|
-
puts(ix, y, "connected accounts become named agents you can @mention. click mode to cycle:", { color: C.dim });
|
|
3422
|
-
y += 1;
|
|
3423
|
-
puts(ix, y, "plan = propose only \xB7 approve = ask before each action \xB7 auto = run freely", { color: C.dim, italic: true });
|
|
3424
|
-
y += 2;
|
|
3425
|
-
puts(ix, y, "CONNECTED", { color: C.bright, bold: true });
|
|
3426
|
-
y += 1;
|
|
3427
|
-
if (myAccounts.length === 0) {
|
|
3428
|
-
puts(ix, y, "none yet.", { color: C.dim, italic: true });
|
|
3429
|
-
y += 1;
|
|
3430
|
-
}
|
|
3431
|
-
for (const a of myAccounts) {
|
|
3432
|
-
if (y >= h - 10) break;
|
|
3433
|
-
const st = acctStatus.get(a.id);
|
|
3434
|
-
const online = agentOnline(a.handle);
|
|
3435
|
-
const dot = st === "verifying" ? SPINNER[frame % SPINNER.length] : st === "error" ? "\u2715" : "\u25CF";
|
|
3436
|
-
const dotColor = st === "verifying" ? C.warn : st === "error" ? C.danger : online ? C.good : C.dim;
|
|
3437
|
-
const status = st === "verifying" ? "verifying" : st === "error" ? "error" : online ? "online" : "ready";
|
|
3438
|
-
puts(ix, y, dot, { color: dotColor });
|
|
3439
|
-
puts(ix + 2, y, "\u2726" + a.handle, { color: C.accent, bold: true, italic: true });
|
|
3440
|
-
puts(ix + 18, y, `${a.provider} \xB7 ${a.model}`, { color: C.text });
|
|
3441
|
-
puts(ix + 46, y, a.auth.kind === "claude_code_login" ? "claude login" : "api key", { color: C.dim });
|
|
3442
|
-
puts(ix + 60, y, status, { color: dotColor });
|
|
3443
|
-
const modeColor = a.mode === "auto" ? C.warn : a.mode === "plan" ? C.accent : C.good;
|
|
3444
|
-
button(w - 22, y, ` ${a.mode} `, { bgColor: C.tabOff, color: readableFg(modeColor, C.tabOff), bold: true }, () => cycleMode(a));
|
|
3445
|
-
button(w - 11, y, " remove ", { bgColor: C.tabOff, color: C.text }, () => removeAgentAccount(a));
|
|
3446
|
-
y += 1;
|
|
3447
|
-
}
|
|
3448
|
-
y += 1;
|
|
3449
|
-
puts(ix, y, "CONNECT A NEW AGENT", { color: C.bright, bold: true });
|
|
3450
|
-
y += 1;
|
|
3451
|
-
if (!connectDraft) {
|
|
3452
|
-
button(ix, y, " + connect a provider ", { bgColor: C.good, color: onFill(C.good), bold: true }, () => {
|
|
3453
|
-
connectDraft = { step: "provider" };
|
|
3454
|
-
});
|
|
3455
|
-
puts(ix, h - 1, "you can also run: accelr8 agent add", { color: C.dim, italic: true });
|
|
3456
|
-
return;
|
|
3457
|
-
}
|
|
3458
|
-
renderConnectWizard(ix, y, w - 2);
|
|
3459
|
-
}
|
|
3460
|
-
function renderConnectWizard(ix, y, rx) {
|
|
3461
|
-
const d = connectDraft;
|
|
3462
|
-
const cc = detectClaudeCode();
|
|
3463
|
-
if (d.step === "provider") {
|
|
3464
|
-
puts(ix, y, "provider:", { color: C.text, bgColor: C.bg });
|
|
3465
|
-
let bx = ix + 10;
|
|
3466
|
-
bx = button(bx, y, " Anthropic ", { bgColor: C.accent, color: onFill(C.accent), bold: true }, () => pickProvider("anthropic")) + 2;
|
|
3467
|
-
button(bx, y, " OpenAI ", { bgColor: C.accent, color: onFill(C.accent), bold: true }, () => pickProvider("openai"));
|
|
3468
|
-
} else if (d.step === "auth") {
|
|
3469
|
-
puts(ix, y, `${d.provider} \xB7 how to authenticate:`, { color: C.text, bgColor: C.bg });
|
|
3470
|
-
y += 1;
|
|
3471
|
-
if (d.provider === "anthropic" && cc.available) {
|
|
3472
|
-
button(ix, y, " use my Claude Code login ", { bgColor: C.good, color: onFill(C.good), bold: true }, () => pickAuth("claude_code_login"));
|
|
3473
|
-
puts(ix + 28, y, "runs on your subscription", { color: C.dim, bgColor: C.bg });
|
|
3474
|
-
y += 1;
|
|
3475
|
-
}
|
|
3476
|
-
button(ix, y, " paste an API key ", { bgColor: C.accent, color: onFill(C.accent), bold: true }, () => promptKey());
|
|
3477
|
-
puts(ix + 20, y, "billed per token", { color: C.dim, bgColor: C.bg });
|
|
3478
|
-
if (d.provider === "anthropic" && !cc.available) {
|
|
3479
|
-
y += 2;
|
|
3480
|
-
puts(ix, y, "tip: log into Claude Code (claude) to use your subscription instead.", { color: C.dim, italic: true, bgColor: C.bg });
|
|
3481
|
-
}
|
|
3482
|
-
} else if (d.step === "name") {
|
|
3483
|
-
puts(ix, y, `agent name: @${d.handle ?? ""}`, { color: C.text, bgColor: C.bg });
|
|
3484
|
-
y += 1;
|
|
3485
|
-
button(ix, y, " rename ", { bgColor: C.tabOff, color: C.text }, promptName);
|
|
3486
|
-
button(ix + 10, y, " next \u2192 ", { bgColor: C.good, color: onFill(C.good), bold: true }, () => {
|
|
3487
|
-
d.step = "model";
|
|
3488
|
-
});
|
|
3489
|
-
} else if (d.step === "model") {
|
|
3490
|
-
if (d.authKind === "claude_code_login") {
|
|
3491
|
-
puts(ix, y, "model follows your Claude Code setup.", { color: C.dim, bgColor: C.bg });
|
|
3492
|
-
y += 1;
|
|
3493
|
-
button(ix, y, " finish connecting ", { bgColor: C.good, color: onFill(C.good), bold: true }, () => finishConnect(DEFAULT_MODEL.anthropic));
|
|
3494
|
-
return;
|
|
3495
|
-
}
|
|
3496
|
-
puts(ix, y, `model for @${d.handle}:`, { color: C.text, bgColor: C.bg });
|
|
3497
|
-
y += 1;
|
|
3498
|
-
for (const m of MODELS[d.provider]) {
|
|
3499
|
-
if (y >= H()) break;
|
|
3500
|
-
button(ix, y, ` ${m} `, { bgColor: C.tabOff, color: C.text }, () => finishConnect(m));
|
|
3501
|
-
y += 1;
|
|
3502
|
-
}
|
|
3503
|
-
}
|
|
3504
|
-
}
|
|
3505
|
-
function pickProvider(p) {
|
|
3506
|
-
connectDraft.provider = p;
|
|
3507
|
-
connectDraft.step = "auth";
|
|
3508
|
-
}
|
|
3509
|
-
function pickAuth(kind) {
|
|
3510
|
-
const d = connectDraft;
|
|
3511
|
-
d.authKind = kind;
|
|
3512
|
-
d.handle = defaultHandle(me.name, d.provider);
|
|
3513
|
-
d.step = "name";
|
|
3514
|
-
}
|
|
3515
|
-
function promptKey() {
|
|
3516
|
-
const d = connectDraft;
|
|
3517
|
-
state.modal = { title: `paste your ${d.provider} api key`, value: "", mask: true, onSubmit: (v) => {
|
|
3518
|
-
if (!v.trim()) return;
|
|
3519
|
-
d.authKind = "api_key";
|
|
3520
|
-
d.apiKey = v.trim();
|
|
3521
|
-
d.handle = defaultHandle(me.name, d.provider);
|
|
3522
|
-
d.step = "name";
|
|
3523
|
-
} };
|
|
3524
|
-
}
|
|
3525
|
-
function promptName() {
|
|
3526
|
-
const d = connectDraft;
|
|
3527
|
-
state.modal = { title: "name your agent", value: d.handle ?? "", onSubmit: (v) => {
|
|
3528
|
-
const h = v.trim().toLowerCase();
|
|
3529
|
-
if (/^[a-z0-9][a-z0-9-]{2,19}$/.test(h)) d.handle = h;
|
|
3530
|
-
else state.status = "handle must be 3-20 chars, a-z 0-9 -";
|
|
3531
|
-
} };
|
|
3532
|
-
}
|
|
3533
|
-
function finishConnect(model) {
|
|
3534
|
-
const d = connectDraft;
|
|
3535
|
-
try {
|
|
3536
|
-
const acct = addAccount({
|
|
3537
|
-
provider: d.provider,
|
|
3538
|
-
auth: d.authKind === "claude_code_login" ? { kind: "claude_code_login" } : { kind: "api_key", apiKey: d.apiKey },
|
|
3539
|
-
handle: d.handle,
|
|
3540
|
-
model
|
|
3541
|
-
});
|
|
3542
|
-
myAccounts = safeLoadAccounts();
|
|
3543
|
-
connectDraft = null;
|
|
3544
|
-
state.status = `\u2726${acct.handle} connected`;
|
|
3545
|
-
acctStatus.set(acct.id, "verifying");
|
|
3546
|
-
verifyAccount(acct).then((v) => {
|
|
3547
|
-
acctStatus.set(acct.id, v.ok ? "ready" : "error");
|
|
3548
|
-
if (!v.ok) state.status = `\u2726${acct.handle}: ${v.message}`;
|
|
3549
|
-
}).catch(() => acctStatus.set(acct.id, "error"));
|
|
3550
|
-
if (ws && ws.readyState === WebSocket.OPEN && state.project) {
|
|
3551
|
-
const tokens = loadTokens();
|
|
3552
|
-
sendMsg({ t: "agentAttach", handle: acct.handle, provider: acct.provider, model: acct.model, token: tokens[`${state.project.id}:${acct.handle}`] });
|
|
3553
|
-
}
|
|
3554
|
-
} catch (e) {
|
|
3555
|
-
state.status = "could not connect: " + (e instanceof Error ? e.message : "error");
|
|
3556
|
-
}
|
|
3557
|
-
}
|
|
3558
|
-
function openAgentAccess(agentId) {
|
|
3559
|
-
accessAgentId = agentId;
|
|
3560
|
-
overlay = "agentAccess";
|
|
3561
|
-
overlayFocus = 0;
|
|
3562
|
-
}
|
|
3563
|
-
function selectTheme(key) {
|
|
3564
|
-
settings.theme = key;
|
|
3565
|
-
applyTheme(key);
|
|
3566
|
-
saveSettings();
|
|
3567
|
-
}
|
|
3568
|
-
function setTabsStyle(v) {
|
|
3569
|
-
settings.tabs = v;
|
|
3570
|
-
saveSettings();
|
|
3571
|
-
}
|
|
3572
|
-
function setBell(v) {
|
|
3573
|
-
settings.bell = v;
|
|
3574
|
-
saveSettings();
|
|
3575
|
-
}
|
|
3576
|
-
function renderSettings() {
|
|
3577
|
-
const w = W(), h = H();
|
|
3578
|
-
screen.fill({ attr: { bgColor: C.bg, color: C.text } });
|
|
3579
|
-
puts(0, 0, " ".repeat(w), { bgColor: C.panel });
|
|
3580
|
-
puts(1, 0, " accelr8 \xB7 settings ", { bgColor: C.accent, color: C.onAccent, bold: true });
|
|
3581
|
-
button(w - 12, 0, " esc / close ", { bgColor: C.panel, color: C.dim }, () => {
|
|
3582
|
-
overlay = null;
|
|
3583
|
-
});
|
|
3584
|
-
puts(0, 1, "\u2500".repeat(w), { color: C.rule, bgColor: C.bg });
|
|
3585
|
-
const ix = 2;
|
|
3586
|
-
let y = 3;
|
|
3587
|
-
const radio = (x, yy, label, on, action) => {
|
|
3588
|
-
puts(x, yy, on ? "\u25C9" : "\u25CB", { color: on ? C.good : C.dim, bgColor: C.bg });
|
|
3589
|
-
return button(x + 2, yy, label, { color: on ? C.bright : C.text, bold: on, bgColor: C.bg }, action) + 2;
|
|
3590
|
-
};
|
|
3591
|
-
const canFit = (need) => y + need <= h - 1;
|
|
3592
|
-
let dropped = false;
|
|
3593
|
-
puts(ix, y, "THEME", { color: C.bright, bold: true, bgColor: C.bg });
|
|
3594
|
-
y += 1;
|
|
3595
|
-
for (const key of Object.keys(THEMES)) {
|
|
3596
|
-
if (y >= h - 9) {
|
|
3597
|
-
dropped = true;
|
|
3598
|
-
break;
|
|
3599
|
-
}
|
|
3600
|
-
const sel = settings.theme === key;
|
|
3601
|
-
const p = { ...BASE, ...THEMES[key].over };
|
|
3602
|
-
puts(ix, y, sel ? "\u25C9" : "\u25CB", { color: sel ? C.good : C.dim, bgColor: C.bg });
|
|
3603
|
-
button(ix + 2, y, key.padEnd(9), { color: sel ? C.bright : C.text, bold: sel, bgColor: C.bg }, () => selectTheme(key));
|
|
3604
|
-
const sx = ix + 13;
|
|
3605
|
-
puts(sx, y, " ", { bgColor: p.bg });
|
|
3606
|
-
puts(sx + 2, y, "\u2588\u2588", { color: p.accent, bgColor: p.bg });
|
|
3607
|
-
puts(sx + 4, y, "\u2588\u2588", { color: p.good, bgColor: p.bg });
|
|
3608
|
-
puts(sx + 6, y, "\u2588\u2588", { color: p.warn, bgColor: p.bg });
|
|
3609
|
-
puts(sx + 8, y, " Ab ", { color: p.text, bgColor: p.bg });
|
|
3610
|
-
y += 1;
|
|
3611
|
-
}
|
|
3612
|
-
y += 1;
|
|
3613
|
-
const navPreview = (px, py, s) => {
|
|
3614
|
-
if (s === "blocks") {
|
|
3615
|
-
let q = puts2(px, py, " Chat ", { bgColor: C.accent, color: C.onAccent, bold: true });
|
|
3616
|
-
puts2(q + 1, py, " Team ", { bgColor: C.tabOff, color: C.text });
|
|
3617
|
-
} else if (s === "brackets") {
|
|
3618
|
-
let q = puts2(px, py, "[ Chat ]", { color: C.accent, bold: true, bgColor: C.bg });
|
|
3619
|
-
puts2(q + 1, py, "Team", { color: C.dim, bgColor: C.bg });
|
|
3620
|
-
} else if (s === "pipes") {
|
|
3621
|
-
let q = puts2(px, py, "\u2502 ", { color: C.rule, bgColor: C.bg });
|
|
3622
|
-
q = puts2(q, py, "Chat", { color: C.bright, bold: true, bgColor: C.bg });
|
|
3623
|
-
q = puts2(q, py, " \u2502 ", { color: C.rule, bgColor: C.bg });
|
|
3624
|
-
q = puts2(q, py, "Team", { color: C.dim, bgColor: C.bg });
|
|
3625
|
-
puts2(q, py, " \u2502", { color: C.rule, bgColor: C.bg });
|
|
3626
|
-
} else {
|
|
3627
|
-
const q = puts2(px, py, "Chat", { color: C.accent, bold: true, underline: true, bgColor: C.bg });
|
|
3628
|
-
puts2(q + 1, py, "Team", { color: C.dim, bgColor: C.bg });
|
|
3629
|
-
}
|
|
3630
|
-
};
|
|
3631
|
-
if (canFit(2)) {
|
|
3632
|
-
puts(ix, y, "TOP NAV", { color: C.bright, bold: true, bgColor: C.bg });
|
|
3633
|
-
y += 1;
|
|
3634
|
-
for (const s of NAV_STYLES) {
|
|
3635
|
-
if (!canFit(1)) {
|
|
3636
|
-
dropped = true;
|
|
3637
|
-
break;
|
|
3638
|
-
}
|
|
3639
|
-
const sel = settings.tabs === s;
|
|
3640
|
-
puts(ix, y, sel ? "\u25C9" : "\u25CB", { color: sel ? C.good : C.dim, bgColor: C.bg });
|
|
3641
|
-
button(ix + 2, y, s.padEnd(10), { color: sel ? C.bright : C.text, bold: sel, bgColor: C.bg }, () => setTabsStyle(s));
|
|
3642
|
-
navPreview(ix + 14, y, s);
|
|
3643
|
-
y += 1;
|
|
3644
|
-
}
|
|
3645
|
-
y += 1;
|
|
3646
|
-
} else dropped = true;
|
|
3647
|
-
if (canFit(2)) {
|
|
3648
|
-
puts(ix, y, "NOTIFY ON", { color: C.bright, bold: true, bgColor: C.bg });
|
|
3649
|
-
y += 1;
|
|
3650
|
-
let x = ix;
|
|
3651
|
-
x = radio(x, y, "mentions", settings.bell === "mention", () => setBell("mention"));
|
|
3652
|
-
x = radio(x, y, "all messages", settings.bell === "all", () => setBell("all"));
|
|
3653
|
-
radio(x, y, "off", settings.bell === "off", () => setBell("off"));
|
|
3654
|
-
y += 2;
|
|
3655
|
-
} else dropped = true;
|
|
3656
|
-
if (canFit(1)) {
|
|
3657
|
-
button(ix, y, " manage AI accounts \u2192 ", { bgColor: C.tabOff, color: C.text }, openAccounts);
|
|
3658
|
-
button(ix + 24, y, " keyboard help \u2192 ", { bgColor: C.tabOff, color: C.text }, openHelp);
|
|
3659
|
-
} else dropped = true;
|
|
3660
|
-
puts(ix, h - 1, dropped ? "\xB7 resize taller for more settings \xB7 esc to close \xB7" : "changes save automatically to ~/.accelr8/settings.json", { color: C.dim, italic: true, bgColor: C.bg });
|
|
3661
|
-
}
|
|
3662
|
-
function renderHelp() {
|
|
3663
|
-
const w = W(), h = H();
|
|
3664
|
-
screen.fill({ attr: { bgColor: C.bg, color: C.text } });
|
|
3665
|
-
puts(0, 0, " ".repeat(w), { bgColor: C.panel });
|
|
3666
|
-
puts(1, 0, " accelr8 \xB7 help ", { bgColor: C.accent, color: onFill(C.accent), bold: true });
|
|
3667
|
-
button(w - 12, 0, " esc / close ", { bgColor: C.panel, color: C.dim }, () => {
|
|
3668
|
-
overlay = null;
|
|
3669
|
-
});
|
|
3670
|
-
puts(0, 1, "\u2500".repeat(w), { color: C.rule, bgColor: C.bg });
|
|
3671
|
-
const rows = [
|
|
3672
|
-
["tab / 1 2 3 4", "switch tabs (chat \xB7 canvas \xB7 secrets \xB7 team)"],
|
|
3673
|
-
["type @", "mention a teammate or agent, then Tab to accept"],
|
|
3674
|
-
["enter", "send a message \xB7 trailing \\ then enter = newline"],
|
|
3675
|
-
["pgup / pgdn / wheel", "scroll chat \xB7 end = jump to latest"],
|
|
3676
|
-
["click + drag (canvas)", "paint \xB7 right-click erases"],
|
|
3677
|
-
["F2 or click accounts", "connect an AI account as a named agent"],
|
|
3678
|
-
["team tab \u2192 click agent", "set who can @mention it (default: only you)"],
|
|
3679
|
-
["ctrl+p", "back to the project list (stays joined)"],
|
|
3680
|
-
["ctrl+c", "quit"]
|
|
3681
|
-
];
|
|
3682
|
-
let y = 3;
|
|
3683
|
-
for (const [k, v] of rows) {
|
|
3684
|
-
puts(2, y, k.padEnd(22), { color: C.accent, bold: true });
|
|
3685
|
-
puts(24, y, v, { color: C.text });
|
|
3686
|
-
y += 1;
|
|
3687
|
-
}
|
|
3688
|
-
y += 1;
|
|
3689
|
-
puts(2, y, "resize the window down small to park it as an ambient 8-ball.", { color: C.dim, italic: true });
|
|
3690
|
-
}
|
|
3691
|
-
function accessSummary(access) {
|
|
3692
|
-
if (!access || access.mode === "owner") return "only you";
|
|
3693
|
-
if (access.mode === "all") return "everyone";
|
|
3694
|
-
return `${access.users.length} ${access.users.length === 1 ? "person" : "people"}`;
|
|
3695
|
-
}
|
|
3696
|
-
function setAccess(access) {
|
|
3697
|
-
const ag = state.agents.find((a) => a.agentId === accessAgentId);
|
|
3698
|
-
if (!ag || ag.ownerId !== me.id || !state.project) return;
|
|
3699
|
-
const token = loadTokens()[`${state.project.id}:${ag.handle}`] ?? "";
|
|
3700
|
-
sendMsg({ t: "agentPolicy", agentId: ag.agentId, token, access });
|
|
3701
|
-
}
|
|
3702
|
-
function renderAgentAccess() {
|
|
3703
|
-
const w = W(), h = H();
|
|
3704
|
-
screen.fill({ attr: { bgColor: C.bg, color: C.text } });
|
|
3705
|
-
const ag = state.agents.find((a) => a.agentId === accessAgentId);
|
|
3706
|
-
if (!ag) {
|
|
3707
|
-
overlay = null;
|
|
3708
|
-
return;
|
|
3709
|
-
}
|
|
3710
|
-
const access = ag.access ?? { mode: "owner", users: [] };
|
|
3711
|
-
const mine = ag.ownerId === me.id;
|
|
3712
|
-
puts(0, 0, " ".repeat(w), { bgColor: C.panel });
|
|
3713
|
-
puts(1, 0, ` \u2726${ag.handle} `, { bgColor: C.accent, color: onFill(C.accent), bold: true, italic: true });
|
|
3714
|
-
puts(ag.handle.length + 4, 0, "who can @mention this agent", { bgColor: C.panel, color: C.dim });
|
|
3715
|
-
button(w - 12, 0, " esc / back ", { bgColor: C.panel, color: C.dim }, () => {
|
|
3716
|
-
overlay = null;
|
|
3717
|
-
accessAgentId = null;
|
|
3718
|
-
});
|
|
3719
|
-
puts(0, 1, "\u2500".repeat(w), { color: C.rule, bgColor: C.bg });
|
|
3720
|
-
const ix = 2;
|
|
3721
|
-
let y = 3;
|
|
3722
|
-
if (!mine) {
|
|
3723
|
-
puts(ix, y, `owned by ${ag.ownerName}. only ${ag.ownerName} can change who uses it.`, { color: C.dim, italic: true });
|
|
3724
|
-
y += 2;
|
|
3725
|
-
puts(ix, y, `current access: ${accessSummary(access)}`, { color: C.text });
|
|
3726
|
-
return;
|
|
3727
|
-
}
|
|
3728
|
-
puts(ix, y, "anyone allowed here can spend your credits by messaging this agent.", { color: C.dim, italic: true });
|
|
3729
|
-
y += 2;
|
|
3730
|
-
const opt = (label, on, action) => {
|
|
3731
|
-
puts(ix, y, on ? "\u25C9" : "\u25CB", { color: on ? C.good : C.dim });
|
|
3732
|
-
button(ix + 2, y, label, { color: on ? C.bright : C.text, bold: on }, action);
|
|
3733
|
-
y += 1;
|
|
3734
|
-
};
|
|
3735
|
-
opt("only me", access.mode === "owner", () => setAccess({ mode: "owner", users: [] }));
|
|
3736
|
-
opt("everyone in the project", access.mode === "all", () => setAccess({ mode: "all", users: [] }));
|
|
3737
|
-
opt("choose people\u2026", access.mode === "list", () => setAccess({ mode: "list", users: access.users }));
|
|
3738
|
-
if (access.mode === "list") {
|
|
3739
|
-
y += 1;
|
|
3740
|
-
puts(ix, y, "allowed teammates:", { color: C.dim });
|
|
3741
|
-
y += 1;
|
|
3742
|
-
for (const m of state.members) {
|
|
3743
|
-
if (m.id === me.id) continue;
|
|
3744
|
-
if (y >= h - 1) break;
|
|
3745
|
-
const checked = access.users.includes(m.id);
|
|
3746
|
-
puts(ix, y, checked ? "\u2611" : "\u2610", { color: checked ? C.good : C.dim });
|
|
3747
|
-
button(ix + 2, y, m.name, { color: readableFg(m.color), bold: true }, () => {
|
|
3748
|
-
const users = checked ? access.users.filter((u) => u !== m.id) : [...access.users, m.id];
|
|
3749
|
-
setAccess({ mode: "list", users });
|
|
3750
|
-
});
|
|
3751
|
-
y += 1;
|
|
3752
|
-
}
|
|
3753
|
-
if (state.members.filter((m) => m.id !== me.id).length === 0)
|
|
3754
|
-
puts(ix, y, "no other teammates yet.", { color: C.dim, italic: true });
|
|
3755
|
-
}
|
|
3756
|
-
}
|
|
3757
|
-
function cycleMode(a) {
|
|
3758
|
-
const order = ["approve", "auto", "plan"];
|
|
3759
|
-
const next = order[(order.indexOf(a.mode) + 1) % order.length];
|
|
3760
|
-
saveAccounts(myAccounts.map((x) => x.id === a.id ? { ...x, mode: next } : x));
|
|
3761
|
-
myAccounts = safeLoadAccounts();
|
|
3762
|
-
state.status = `\u2726${a.handle}: ${next} mode`;
|
|
3763
|
-
}
|
|
3764
|
-
function removeAgentAccount(a) {
|
|
3765
|
-
const agentId = state.agents.find((ag) => ag.handle === a.handle && ag.ownerId === me.id)?.agentId;
|
|
3766
|
-
if (agentId) sendMsg({ t: "agentDetach", agentId, token: loadTokens()[`${state.project?.id}:${a.handle}`] ?? "" });
|
|
3767
|
-
removeAccount(a.id);
|
|
3768
|
-
acctStatus.delete(a.id);
|
|
3769
|
-
myAccounts = safeLoadAccounts();
|
|
3770
|
-
state.status = `removed \u2726${a.handle}`;
|
|
3771
|
-
}
|
|
3772
|
-
function renderModal() {
|
|
3773
|
-
const m = state.modal;
|
|
3774
|
-
const w = W(), h = H();
|
|
3775
|
-
const bw = Math.min(60, w - 6), bh = 5;
|
|
3776
|
-
const x0 = Math.floor((w - bw) / 2), y0 = Math.floor((h - bh) / 2);
|
|
3777
|
-
for (let y = y0; y < y0 + bh; y++) puts(x0, y, " ".repeat(bw), { bgColor: C.panel });
|
|
3778
|
-
box(x0, y0, x0 + bw - 1, y0 + bh - 1, { color: C.accent, bgColor: C.panel }, m.title);
|
|
3779
|
-
const raw = m.mask ? "\u2022".repeat([...m.value].length) : m.value;
|
|
3780
|
-
const field = raw + "\u258F";
|
|
3781
|
-
puts(x0 + 2, y0 + 2, field.padEnd(bw - 4).slice(-(bw - 4)), { bgColor: C.inputBg, color: readableFg(C.bright, C.inputBg) });
|
|
3782
|
-
}
|
|
3783
|
-
function promptCreate() {
|
|
3784
|
-
state.modal = { title: "new project name", value: "", onSubmit: (v) => {
|
|
3785
|
-
if (v.trim()) sendMsg({ t: "createProject", name: v.trim() });
|
|
3786
|
-
} };
|
|
3787
|
-
}
|
|
3788
|
-
function promptJoin() {
|
|
3789
|
-
state.modal = { title: "enter project code", value: "", onSubmit: (v) => {
|
|
3790
|
-
if (v.trim()) sendMsg({ t: "joinProject", code: v.trim() });
|
|
3791
|
-
} };
|
|
3792
|
-
}
|
|
3793
|
-
function copyToClipboard(text, tools) {
|
|
3794
|
-
const candidatesList = tools ?? (process.platform === "darwin" ? [["pbcopy", []]] : process.platform === "win32" ? [["clip", []]] : [["wl-copy", []], ["xclip", ["-selection", "clipboard"]], ["xsel", ["--clipboard", "--input"]]]);
|
|
3795
|
-
const [tool, args] = candidatesList[0] ?? [null, []];
|
|
3796
|
-
if (!tool) {
|
|
3797
|
-
state.status = "copy failed (no clipboard tool)";
|
|
3798
|
-
return;
|
|
3799
|
-
}
|
|
3800
|
-
try {
|
|
3801
|
-
const p = spawn2(tool, args, { stdio: ["pipe", "ignore", "ignore"] });
|
|
3802
|
-
p.on("error", () => copyToClipboard(text, candidatesList.slice(1)));
|
|
3803
|
-
p.on("close", (code) => {
|
|
3804
|
-
state.status = code === 0 ? "copied join command" : "copy failed";
|
|
3805
|
-
if (code !== 0 && candidatesList.length > 1) copyToClipboard(text, candidatesList.slice(1));
|
|
3806
|
-
});
|
|
3807
|
-
p.stdin.end(text);
|
|
3808
|
-
} catch {
|
|
3809
|
-
copyToClipboard(text, candidatesList.slice(1));
|
|
3810
|
-
}
|
|
3811
|
-
}
|
|
3812
|
-
function onMouse(name, data) {
|
|
3813
|
-
if (boot) {
|
|
3814
|
-
if (name.endsWith("_PRESSED")) skipBoot();
|
|
3815
|
-
return;
|
|
3816
|
-
}
|
|
3817
|
-
const mx = (data?.x ?? 1) - 1, my = (data?.y ?? 1) - 1;
|
|
3818
|
-
if (name === "MOUSE_LEFT_BUTTON_RELEASED") {
|
|
3819
|
-
painting = false;
|
|
3820
|
-
return;
|
|
3821
|
-
}
|
|
3822
|
-
if (state.modal) return;
|
|
3823
|
-
if (task) {
|
|
3824
|
-
if (name === "MOUSE_LEFT_BUTTON_PRESSED") {
|
|
3825
|
-
for (const r of clickables) if (mx >= r.x0 && mx <= r.x1 && my >= r.y0 && my <= r.y1) {
|
|
3826
|
-
r.action();
|
|
3827
|
-
return;
|
|
3828
|
-
}
|
|
3829
|
-
}
|
|
3830
|
-
return;
|
|
3831
|
-
}
|
|
3832
|
-
const rung = rungFor(W(), H());
|
|
3833
|
-
if (rung === "orb" || rung === "micro") return;
|
|
3834
|
-
if (overlay) {
|
|
3835
|
-
if (name === "MOUSE_LEFT_BUTTON_PRESSED") {
|
|
3836
|
-
for (const r of clickables) if (mx >= r.x0 && mx <= r.x1 && my >= r.y0 && my <= r.y1) {
|
|
3837
|
-
r.action();
|
|
3838
|
-
return;
|
|
3839
|
-
}
|
|
3840
|
-
}
|
|
3841
|
-
return;
|
|
3842
|
-
}
|
|
3843
|
-
if (name === "MOUSE_MOTION" || name === "MOUSE_DRAG") {
|
|
3844
|
-
updateMyCursor(mx, my);
|
|
3845
|
-
if (state.screen === "room" && state.tab === "canvas" && state.project) {
|
|
3846
|
-
if (name === "MOUSE_DRAG" ? data?.left : painting) paintAt(mx, my);
|
|
3847
|
-
if (name === "MOUSE_DRAG" && data?.right) eraseAt(mx, my);
|
|
3848
|
-
}
|
|
3849
|
-
if (state.tab === "team" && teamMap) {
|
|
3850
|
-
const hit = hover(teamMap.rendered, mx - teamMap.x0, my - teamMap.y0);
|
|
3851
|
-
if (hit) {
|
|
3852
|
-
const city = tzCity(state.sessions.find((s) => hit.names.includes(s.name))?.tz);
|
|
3853
|
-
state.status = hit.names.join(" \xB7 ") + (city ? " \xB7 " + city : "");
|
|
3854
|
-
}
|
|
3855
|
-
}
|
|
3856
|
-
return;
|
|
3857
|
-
}
|
|
3858
|
-
if (name === "MOUSE_WHEEL_UP" || name === "MOUSE_WHEEL_DOWN") {
|
|
3859
|
-
if (chatVisible()) {
|
|
3860
|
-
if (rung === "ticker") return;
|
|
3861
|
-
const w = rung === "comfy" ? W() - 24 : W();
|
|
3862
|
-
const wrapW = rung === "sidecar" ? W() - 2 : w - (w >= 80 ? 6 : 0) - 1;
|
|
3863
|
-
const viewH = rung === "sidecar" ? H() - 4 : H() - 5;
|
|
3864
|
-
if (name === "MOUSE_WHEEL_UP") chatLog.scrollUp(3, wrapW, viewH);
|
|
3865
|
-
else {
|
|
3866
|
-
chatLog.scrollDown(3, wrapW, viewH);
|
|
3867
|
-
if (chatLog.pinned) clearUnread();
|
|
3868
|
-
}
|
|
3869
|
-
}
|
|
3870
|
-
return;
|
|
3871
|
-
}
|
|
3872
|
-
if (name === "MOUSE_LEFT_BUTTON_PRESSED") {
|
|
3873
|
-
for (const r of clickables) {
|
|
3874
|
-
if (mx >= r.x0 && mx <= r.x1 && my >= r.y0 && my <= r.y1) {
|
|
3875
|
-
r.action();
|
|
3876
|
-
return;
|
|
3877
|
-
}
|
|
3878
|
-
}
|
|
3879
|
-
if (state.screen === "room" && state.tab === "canvas" && state.project) {
|
|
3880
|
-
painting = true;
|
|
3881
|
-
lastPaintKey = "";
|
|
3882
|
-
paintAt(mx, my);
|
|
3883
|
-
}
|
|
3884
|
-
return;
|
|
3885
|
-
}
|
|
3886
|
-
if (name === "MOUSE_RIGHT_BUTTON_PRESSED") {
|
|
3887
|
-
if (state.screen === "room" && state.tab === "canvas" && state.project) eraseAt(mx, my);
|
|
3888
|
-
}
|
|
3889
|
-
}
|
|
3890
|
-
function toLogical(mx, my) {
|
|
3891
|
-
const cx = mx - IO_X, cy = my - canvasTop;
|
|
3892
|
-
if (cx < 0 || cy < 0 || cx >= CANVAS_W || cy >= CANVAS_H) return null;
|
|
3893
|
-
if (my < canvasTop || my > H() - 3) return null;
|
|
3894
|
-
if (mx > W() - 2) return null;
|
|
3895
|
-
return { x: cx, y: cy };
|
|
3896
|
-
}
|
|
3897
|
-
function updateMyCursor(mx, my) {
|
|
3898
|
-
if (state.screen !== "room" || state.tab !== "canvas") {
|
|
3899
|
-
myLogical = null;
|
|
3900
|
-
return;
|
|
3901
|
-
}
|
|
3902
|
-
const lp = toLogical(mx, my);
|
|
3903
|
-
myLogical = lp;
|
|
3904
|
-
if (lp) cursorDirty = true;
|
|
3905
|
-
}
|
|
3906
|
-
function paintAt(mx, my) {
|
|
3907
|
-
const lp = toLogical(mx, my);
|
|
3908
|
-
if (!lp) return;
|
|
3909
|
-
const key = `${lp.x},${lp.y}`;
|
|
3910
|
-
if (key === lastPaintKey) return;
|
|
3911
|
-
lastPaintKey = key;
|
|
3912
|
-
if (brush === " ") delete grid[key];
|
|
3913
|
-
else grid[key] = [brush, me.color];
|
|
3914
|
-
sendMsg({ t: "paint", x: lp.x, y: lp.y, char: brush });
|
|
3915
|
-
}
|
|
3916
|
-
function eraseAt(mx, my) {
|
|
3917
|
-
const lp = toLogical(mx, my);
|
|
3918
|
-
if (!lp) return;
|
|
3919
|
-
delete grid[`${lp.x},${lp.y}`];
|
|
3920
|
-
sendMsg({ t: "paint", x: lp.x, y: lp.y, char: " " });
|
|
3921
|
-
}
|
|
3922
|
-
var ghostsOff = false;
|
|
3923
|
-
function slashCommand(text) {
|
|
3924
|
-
const cmd = text.trim().toLowerCase();
|
|
3925
|
-
if (!cmd.startsWith("/")) return false;
|
|
3926
|
-
if (cmd === "/team") {
|
|
3927
|
-
const online = onlineUserIds();
|
|
3928
|
-
sidecarBlock = {
|
|
3929
|
-
title: "team",
|
|
3930
|
-
at: Date.now(),
|
|
3931
|
-
lines: [
|
|
3932
|
-
...state.members.map((m) => `${online.has(m.id) ? "\u25CF" : "\u25CB"} ${m.name}${m.id === me.id ? " \xB7 you" : ""}`),
|
|
3933
|
-
...state.agents.map((a) => `\u2726 ${a.handle} \xB7 ${a.busy ? "streaming" : a.online ? "idle" : "offline"}`)
|
|
3934
|
-
].slice(0, 6)
|
|
3935
|
-
};
|
|
3936
|
-
} else if (cmd === "/secrets") {
|
|
3937
|
-
sidecarBlock = {
|
|
3938
|
-
title: "secrets",
|
|
3939
|
-
at: Date.now(),
|
|
3940
|
-
lines: state.secrets.length ? state.secrets.slice(0, 6).map((s) => `${s.name} \xB7\xB7\xB7\xB7\xB7\xB7\xB7\xB7`) : ["none yet"]
|
|
3941
|
-
};
|
|
3942
|
-
} else if (cmd === "/ghost off" || cmd === "/ghost on") {
|
|
3943
|
-
ghostsOff = cmd.endsWith("off");
|
|
3944
|
-
state.status = ghostsOff ? "draft broadcasting off" : "draft broadcasting on";
|
|
3945
|
-
draftDirty = true;
|
|
3946
|
-
} else if (cmd === "/voice") {
|
|
3947
|
-
toggleVoice();
|
|
3948
|
-
} else if (cmd === "/talk" || cmd === "/mute") {
|
|
3949
|
-
toggleTalk();
|
|
3950
|
-
} else if (cmd === "/voice mics" || cmd === "/mics") {
|
|
3951
|
-
const mics = listMicDevices();
|
|
3952
|
-
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)"] };
|
|
3953
|
-
state.status = `${mics.length} mics \xB7 /voice mic <index> to pick`;
|
|
3954
|
-
} else if (cmd.startsWith("/voice mic ")) {
|
|
3955
|
-
const idx = cmd.slice("/voice mic ".length).trim();
|
|
3956
|
-
voiceDevice = idx || void 0;
|
|
3957
|
-
state.status = `mic set to [${idx}]` + (voiceOn ? " \xB7 reconnecting" : "");
|
|
3958
|
-
if (voiceTransmit && voiceCapture) {
|
|
3959
|
-
voiceCapture.stop();
|
|
3960
|
-
voiceRetries = 0;
|
|
3961
|
-
startCapture();
|
|
3962
|
-
}
|
|
3963
|
-
} else {
|
|
3964
|
-
state.status = `unknown command: ${cmd.split(" ")[0]}`;
|
|
3965
|
-
}
|
|
3966
|
-
return true;
|
|
3967
|
-
}
|
|
3968
|
-
function sendChat() {
|
|
3969
|
-
const text = editor.submit();
|
|
3970
|
-
if (!text) return;
|
|
3971
|
-
if (slashCommand(text)) return;
|
|
3972
|
-
const cmid = "cm_" + ++cmidCounter + "_" + Math.random().toString(36).slice(2, 6);
|
|
3973
|
-
chatLog.push({
|
|
3974
|
-
seq: null,
|
|
3975
|
-
cmid,
|
|
3976
|
-
kind: "user",
|
|
3977
|
-
authorId: me.id,
|
|
3978
|
-
authorName: me.name,
|
|
3979
|
-
color: me.color,
|
|
3980
|
-
text,
|
|
3981
|
-
ts: Date.now(),
|
|
3982
|
-
pending: true
|
|
3983
|
-
}, mentionEntities());
|
|
3984
|
-
sendMsg({ t: "chatSend", cmid, text });
|
|
3985
|
-
draftDirty = true;
|
|
3986
|
-
}
|
|
3987
|
-
function onKey(name) {
|
|
3988
|
-
if (name === "CTRL_C") return shutdown();
|
|
3989
|
-
if ((name === "CTRL_G" || name === "CTRL_ALT_G") && voiceOn) return toggleTalk();
|
|
3990
|
-
if (boot) return skipBoot();
|
|
3991
|
-
const rung = rungFor(W(), H());
|
|
3992
|
-
if (rung === "micro" || rung === "orb") return;
|
|
3993
|
-
const paused = rung === "ticker";
|
|
3994
|
-
if (task && !paused) {
|
|
3995
|
-
const enter = name === "ENTER" || name === "KP_ENTER";
|
|
3996
|
-
if (task.phase === "confirm") {
|
|
3997
|
-
if (enter) task.run?.();
|
|
3998
|
-
else if (name === "ESCAPE") task = null;
|
|
3999
|
-
return;
|
|
4000
|
-
}
|
|
4001
|
-
if (task.phase === "running") {
|
|
4002
|
-
if (name === "ESCAPE") {
|
|
4003
|
-
cancelTask?.();
|
|
4004
|
-
task = null;
|
|
4005
|
-
}
|
|
4006
|
-
return;
|
|
4007
|
-
}
|
|
4008
|
-
if (enter && task.ok && task.onOk) return task.onOk();
|
|
4009
|
-
if (enter || name === "ESCAPE") task = null;
|
|
4010
|
-
return;
|
|
4011
|
-
}
|
|
4012
|
-
if (state.modal && !paused) {
|
|
4013
|
-
const m = state.modal;
|
|
4014
|
-
if (name === "ENTER") {
|
|
4015
|
-
const v = m.value;
|
|
4016
|
-
state.modal = null;
|
|
4017
|
-
m.onSubmit(v);
|
|
4018
|
-
} else if (name === "ESCAPE") state.modal = null;
|
|
4019
|
-
else if (name === "BACKSPACE") m.value = m.value.slice(0, -1);
|
|
4020
|
-
else if (name === "SPACE") m.value += " ";
|
|
4021
|
-
else if (name.length === 1 && name >= " ") m.value += name;
|
|
4022
|
-
return;
|
|
4023
|
-
}
|
|
4024
|
-
if (name === "F2" && !paused) {
|
|
4025
|
-
if (overlay) {
|
|
4026
|
-
overlay = null;
|
|
4027
|
-
connectDraft = null;
|
|
4028
|
-
accessAgentId = null;
|
|
4029
|
-
} else openAccounts();
|
|
4030
|
-
return;
|
|
4031
|
-
}
|
|
4032
|
-
if (name === "F1" && !paused) {
|
|
4033
|
-
openHelp();
|
|
4034
|
-
return;
|
|
4035
|
-
}
|
|
4036
|
-
if (overlay && !paused) {
|
|
4037
|
-
if (name === "ESCAPE") {
|
|
4038
|
-
if (overlay === "agentAccess") {
|
|
4039
|
-
overlay = null;
|
|
4040
|
-
accessAgentId = null;
|
|
4041
|
-
} else if (connectDraft) connectDraft = null;
|
|
4042
|
-
else overlay = null;
|
|
4043
|
-
} else if (name === "UP" || name === "k" || name === "SHIFT_TAB") {
|
|
4044
|
-
overlayFocus = Math.max(0, overlayFocus - 1);
|
|
4045
|
-
} else if (name === "DOWN" || name === "j" || name === "TAB") {
|
|
4046
|
-
overlayFocus = Math.min(clickables.length - 1, overlayFocus + 1);
|
|
4047
|
-
} else if (name === "ENTER" || name === "KP_ENTER" || name === "SPACE") {
|
|
4048
|
-
clickables[overlayFocus]?.action();
|
|
4049
|
-
}
|
|
4050
|
-
return;
|
|
4051
|
-
}
|
|
4052
|
-
if (rung === "ticker" || rung === "sidecar") return onChatSurfaceKey(name, rung);
|
|
4053
|
-
if (state.screen === "home") {
|
|
4054
|
-
if (updateInfo && (name === "ENTER" || name === "KP_ENTER" || name === "u")) return startUpdate();
|
|
4055
|
-
if (name === "n") return promptCreate();
|
|
4056
|
-
if (name === "j") return promptJoin();
|
|
4057
|
-
return;
|
|
4058
|
-
}
|
|
4059
|
-
const onChat = state.tab === "chat";
|
|
4060
|
-
if (onChat && popup.open) {
|
|
4061
|
-
const res = popup.key(name);
|
|
4062
|
-
if (res.kind === "accept") {
|
|
4063
|
-
editor.text = [...editor.text].slice(0, res.start).join("") + res.insert + [...editor.text].slice(res.end).join("");
|
|
4064
|
-
editor.cursor = res.start + [...res.insert].length;
|
|
4065
|
-
popup.update({ text: editor.text, cursor: editor.cursor }, candidates);
|
|
4066
|
-
draftDirty = true;
|
|
4067
|
-
return;
|
|
4068
|
-
}
|
|
4069
|
-
if (res.kind === "consumed" || res.kind === "closed") return;
|
|
4070
|
-
}
|
|
4071
|
-
if (name === "TAB") {
|
|
4072
|
-
setTab(ROOM_TABS[(ROOM_TABS.indexOf(state.tab) + 1) % ROOM_TABS.length]);
|
|
4073
|
-
return;
|
|
4074
|
-
}
|
|
4075
|
-
if (name === "CTRL_P") {
|
|
4076
|
-
state.screen = "home";
|
|
4077
|
-
sendMsg({ t: "tab", tab: "home" });
|
|
4078
|
-
return;
|
|
4079
|
-
}
|
|
4080
|
-
if (!onChat) {
|
|
4081
|
-
if (name === "1") return setTab("chat");
|
|
4082
|
-
if (name === "2") return setTab("canvas");
|
|
4083
|
-
if (name === "3") return setTab("secrets");
|
|
4084
|
-
if (name === "4") return setTab("team");
|
|
4085
|
-
return;
|
|
4086
|
-
}
|
|
4087
|
-
if (name === "ENTER") {
|
|
4088
|
-
if (editor.endsWithContinuation()) {
|
|
4089
|
-
editor.newline();
|
|
4090
|
-
draftDirty = true;
|
|
4091
|
-
return;
|
|
4092
|
-
}
|
|
4093
|
-
sendChat();
|
|
4094
|
-
popup.update({ text: editor.text, cursor: editor.cursor }, candidates);
|
|
4095
|
-
return;
|
|
4096
|
-
}
|
|
4097
|
-
if (name === "ESCAPE") {
|
|
4098
|
-
if (editor.text) {
|
|
4099
|
-
editor.clear();
|
|
4100
|
-
draftDirty = true;
|
|
4101
|
-
}
|
|
4102
|
-
popup.update({ text: editor.text, cursor: editor.cursor }, candidates);
|
|
4103
|
-
return;
|
|
4104
|
-
}
|
|
4105
|
-
if (name === "PAGE_UP" || name === "PAGE_DOWN") {
|
|
4106
|
-
const w = rungFor(W(), H()) === "comfy" ? W() - 24 : W();
|
|
4107
|
-
const wrapW = w - (w >= 80 ? 6 : 0) - 1;
|
|
4108
|
-
const viewH = H() - 5;
|
|
4109
|
-
if (name === "PAGE_UP") chatLog.scrollUp(viewH - 2, wrapW, viewH);
|
|
4110
|
-
else {
|
|
4111
|
-
chatLog.scrollDown(viewH - 2, wrapW, viewH);
|
|
4112
|
-
if (chatLog.pinned) clearUnread();
|
|
4113
|
-
}
|
|
4114
|
-
return;
|
|
4115
|
-
}
|
|
4116
|
-
if (name === "END" && !editor.text) {
|
|
4117
|
-
chatLog.pinToTail();
|
|
4118
|
-
clearUnread();
|
|
4119
|
-
return;
|
|
4120
|
-
}
|
|
4121
|
-
const consumed = editor.key(name);
|
|
4122
|
-
if (consumed) {
|
|
4123
|
-
draftDirty = true;
|
|
4124
|
-
popup.update({ text: editor.text, cursor: editor.cursor }, candidates);
|
|
4125
|
-
}
|
|
4126
|
-
}
|
|
4127
|
-
function onChatSurfaceKey(name, rung) {
|
|
4128
|
-
if (!state.project) return;
|
|
4129
|
-
if (popup.open) {
|
|
4130
|
-
const res = popup.key(name);
|
|
4131
|
-
if (res.kind === "accept") {
|
|
4132
|
-
editor.text = [...editor.text].slice(0, res.start).join("") + res.insert + [...editor.text].slice(res.end).join("");
|
|
4133
|
-
editor.cursor = res.start + [...res.insert].length;
|
|
4134
|
-
popup.update({ text: editor.text, cursor: editor.cursor }, candidates);
|
|
4135
|
-
draftDirty = true;
|
|
4136
|
-
return;
|
|
4137
|
-
}
|
|
4138
|
-
if (res.kind === "consumed" || res.kind === "closed") return;
|
|
4139
|
-
}
|
|
4140
|
-
if (name === "TAB") {
|
|
4141
|
-
if (rung === "sidecar") sidecarHintAt = Date.now();
|
|
4142
|
-
return;
|
|
4143
|
-
}
|
|
4144
|
-
if (name === "ENTER") {
|
|
4145
|
-
if (editor.endsWithContinuation()) {
|
|
4146
|
-
editor.newline();
|
|
4147
|
-
draftDirty = true;
|
|
4148
|
-
return;
|
|
4149
|
-
}
|
|
4150
|
-
sendChat();
|
|
4151
|
-
popup.update({ text: editor.text, cursor: editor.cursor }, candidates);
|
|
4152
|
-
return;
|
|
4153
|
-
}
|
|
4154
|
-
if (name === "ESCAPE") {
|
|
4155
|
-
if (editor.text) {
|
|
4156
|
-
editor.clear();
|
|
4157
|
-
draftDirty = true;
|
|
4158
|
-
}
|
|
4159
|
-
popup.update({ text: editor.text, cursor: editor.cursor }, candidates);
|
|
4160
|
-
return;
|
|
4161
|
-
}
|
|
4162
|
-
if (rung === "sidecar" && (name === "PAGE_UP" || name === "PAGE_DOWN")) {
|
|
4163
|
-
const viewH = H() - 4;
|
|
4164
|
-
if (name === "PAGE_UP") chatLog.scrollUp(viewH - 2, W() - 2, viewH);
|
|
4165
|
-
else {
|
|
4166
|
-
chatLog.scrollDown(viewH - 2, W() - 2, viewH);
|
|
4167
|
-
if (chatLog.pinned) clearUnread();
|
|
4168
|
-
}
|
|
4169
|
-
return;
|
|
4170
|
-
}
|
|
4171
|
-
if (name === "END" && !editor.text) {
|
|
4172
|
-
chatLog.pinToTail();
|
|
4173
|
-
clearUnread();
|
|
4174
|
-
return;
|
|
4175
|
-
}
|
|
4176
|
-
const consumed = editor.key(name);
|
|
4177
|
-
if (consumed) {
|
|
4178
|
-
draftDirty = true;
|
|
4179
|
-
popup.update({ text: editor.text, cursor: editor.cursor }, candidates);
|
|
4180
|
-
}
|
|
4181
|
-
}
|
|
4182
|
-
function startLoops() {
|
|
4183
|
-
setInterval(() => {
|
|
4184
|
-
for (const [sid, c] of cursors) {
|
|
4185
|
-
c.x += (c.tx - c.x) * 0.35;
|
|
4186
|
-
c.y += (c.ty - c.y) * 0.35;
|
|
4187
|
-
if (Date.now() - c.seen > 8e3) cursors.delete(sid);
|
|
4188
|
-
}
|
|
4189
|
-
if (chatVisible() && chatLog.pinned && unread.t0 > 0) clearUnread();
|
|
4190
|
-
render();
|
|
4191
|
-
}, 33);
|
|
4192
|
-
setInterval(() => {
|
|
4193
|
-
if (cursorDirty && myLogical && state.project) {
|
|
4194
|
-
sendMsg({ t: "cursor", x: myLogical.x, y: myLogical.y });
|
|
4195
|
-
cursorDirty = false;
|
|
4196
|
-
}
|
|
4197
|
-
}, 40);
|
|
4198
|
-
setInterval(() => {
|
|
4199
|
-
if (!draftDirty || !state.project) return;
|
|
4200
|
-
draftDirty = false;
|
|
4201
|
-
const text = !ghostsOff && chatVisible() ? [...editor.text].slice(0, 200).join("") : "";
|
|
4202
|
-
if (text === lastSentDraft) return;
|
|
4203
|
-
lastSentDraft = text;
|
|
4204
|
-
sendMsg({ t: "draft", text });
|
|
4205
|
-
}, 250);
|
|
4206
|
-
setInterval(() => {
|
|
4207
|
-
if (!ws || ws.readyState !== WebSocket.OPEN) return;
|
|
4208
|
-
if (Date.now() - lastPong > 75e3) {
|
|
4209
|
-
try {
|
|
4210
|
-
ws.terminate();
|
|
4211
|
-
} catch {
|
|
4212
|
-
}
|
|
4213
|
-
return;
|
|
4214
|
-
}
|
|
4215
|
-
try {
|
|
4216
|
-
ws.ping();
|
|
4217
|
-
} catch {
|
|
4218
|
-
}
|
|
4219
|
-
}, 3e4);
|
|
4220
|
-
}
|
|
4221
|
-
function restoreTerminal() {
|
|
4222
|
-
try {
|
|
4223
|
-
term.grabInput(false);
|
|
4224
|
-
term.hideCursor(false);
|
|
4225
|
-
term.fullscreen(false);
|
|
4226
|
-
term.styleReset();
|
|
4227
|
-
} catch {
|
|
4228
|
-
}
|
|
4229
|
-
}
|
|
4230
|
-
function shutdown() {
|
|
4231
|
-
restoreTerminal();
|
|
4232
|
-
try {
|
|
4233
|
-
voiceCapture?.stop();
|
|
4234
|
-
voicePlayback?.stopAll();
|
|
4235
|
-
} catch {
|
|
4236
|
-
}
|
|
4237
|
-
for (const h of runningExec.values()) {
|
|
4238
|
-
try {
|
|
4239
|
-
h.abort();
|
|
4240
|
-
} catch {
|
|
4241
|
-
}
|
|
4242
|
-
}
|
|
4243
|
-
try {
|
|
4244
|
-
ws?.close();
|
|
4245
|
-
} catch {
|
|
4246
|
-
}
|
|
4247
|
-
process.exit(0);
|
|
4248
|
-
}
|
|
4249
|
-
function shutdownWithMessage(text) {
|
|
4250
|
-
restoreTerminal();
|
|
4251
|
-
console.error(text);
|
|
4252
|
-
try {
|
|
4253
|
-
ws?.close();
|
|
4254
|
-
} catch {
|
|
4255
|
-
}
|
|
4256
|
-
process.exit(1);
|
|
4257
|
-
}
|
|
4258
|
-
function runClient(opts = {}) {
|
|
4259
|
-
loadSettings();
|
|
4260
|
-
updateInfo = opts.update ?? null;
|
|
4261
|
-
try {
|
|
4262
|
-
myAccounts = loadAccounts();
|
|
4263
|
-
} catch {
|
|
4264
|
-
myAccounts = [];
|
|
4265
|
-
}
|
|
4266
|
-
me = loadIdentity(opts.name);
|
|
4267
|
-
SERVER = opts.server || process.env.ACCELR8_SERVER || DEFAULT_SERVER;
|
|
4268
|
-
if (!/^wss?:\/\//.test(SERVER)) SERVER = "ws://" + SERVER;
|
|
4269
|
-
try {
|
|
4270
|
-
new URL(SERVER);
|
|
4271
|
-
} catch {
|
|
4272
|
-
console.error(`invalid server url: ${SERVER}`);
|
|
4273
|
-
process.exit(1);
|
|
4274
|
-
}
|
|
4275
|
-
autoJoinCode = opts.joinCode;
|
|
4276
|
-
const die = (err) => {
|
|
4277
|
-
restoreTerminal();
|
|
4278
|
-
console.error("accelr8 crashed:", err instanceof Error ? err.stack || err.message : err);
|
|
4279
|
-
process.exit(1);
|
|
4280
|
-
};
|
|
4281
|
-
process.on("uncaughtException", die);
|
|
4282
|
-
process.on("unhandledRejection", die);
|
|
4283
|
-
term.fullscreen(true);
|
|
4284
|
-
term.hideCursor();
|
|
4285
|
-
term.grabInput({ mouse: "motion" });
|
|
4286
|
-
makeScreen();
|
|
4287
|
-
if (W() >= BOOT_MIN_W && H() >= BOOT_MIN_H) {
|
|
4288
|
-
startBoot(opts.intro || settings.bootSkips < 3);
|
|
4289
|
-
} else {
|
|
4290
|
-
pendingBoot = true;
|
|
4291
|
-
}
|
|
4292
|
-
if (settings.bootSkips >= 3 && !opts.intro) bootHint = "accelr8 --intro brings back the full show";
|
|
4293
|
-
term.on("mouse", onMouse);
|
|
4294
|
-
term.on("key", onKey);
|
|
4295
|
-
term.on("resize", () => makeScreen());
|
|
4296
|
-
process.on("SIGINT", shutdown);
|
|
4297
|
-
process.on("SIGTERM", shutdown);
|
|
4298
|
-
connect();
|
|
4299
|
-
startLoops();
|
|
4300
|
-
}
|
|
4301
|
-
export {
|
|
4302
|
-
THEMES,
|
|
4303
|
-
__accent,
|
|
4304
|
-
__applyTheme,
|
|
4305
|
-
__click,
|
|
4306
|
-
__closeOverlay,
|
|
4307
|
-
__curTheme,
|
|
4308
|
-
__dump,
|
|
4309
|
-
__dumpCells,
|
|
4310
|
-
__openAccounts,
|
|
4311
|
-
__openAgentAccess,
|
|
4312
|
-
__openSettings,
|
|
4313
|
-
__setAccounts,
|
|
4314
|
-
__setHeadlessScreen,
|
|
4315
|
-
__setMe,
|
|
4316
|
-
__setMySid,
|
|
4317
|
-
__setNav,
|
|
4318
|
-
__setUpdate,
|
|
4319
|
-
__taskDemo,
|
|
4320
|
-
chatLog,
|
|
4321
|
-
cursors,
|
|
4322
|
-
editor,
|
|
4323
|
-
ghosts,
|
|
4324
|
-
popup,
|
|
4325
|
-
render,
|
|
4326
|
-
runClient,
|
|
4327
|
-
rungFor,
|
|
4328
|
-
saveSettings,
|
|
4329
|
-
state,
|
|
4330
|
-
unread
|
|
4331
|
-
};
|