coban 0.0.0-stage → 0.2.2

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/tui.js ADDED
@@ -0,0 +1,1382 @@
1
+ import { pickOption } from './prompt.js';
2
+ const PREFIX = '\x02'; // ctrl+b
3
+ const out = process.stdout;
4
+ const ESC = '\x1b[';
5
+ const MOUSE_RE = /\x1b\[<(\d+);(\d+);(\d+)([Mm])/g;
6
+ const STATUS_STYLE = {
7
+ // glyph, color, label
8
+ starting: ['◌', '38;5;244', 'açılıyor'],
9
+ working: ['●', '38;5;214', 'çalışıyor'],
10
+ blocked: ['!', '1;38;5;203', 'izin bekliyor'],
11
+ limited: ['⧗', '1;38;5;199', 'limit doldu'],
12
+ idle: ['○', '38;5;248', 'boşta'],
13
+ exited: ['×', '38;5;240', 'kapandı'],
14
+ };
15
+ // Visual language borrowed from Sokratic's bubble graph: gold root, gold heavy root edges,
16
+ // nodes outlined in their own (profile) colour, centred labels.
17
+ const GOLD = '38;2;203;163;67'; // #cba343 root
18
+ const GOLD_DEEP = '38;2;184;134;42'; // #b8862a root edges
19
+ const DIM = '38;5;244';
20
+ const MARK = '38;5;114';
21
+ const SEP = '0;38;5;238';
22
+ const GOLD_RGB = [203, 163, 67];
23
+ const BG_RGB = [32, 32, 32];
24
+ const WHITE = [255, 255, 255];
25
+ const RED = [255, 95, 95];
26
+ const rgbOf = (h) => {
27
+ const m = h?.match(/^#?([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$/i);
28
+ return m ? [parseInt(m[1], 16), parseInt(m[2], 16), parseInt(m[3], 16)] : null;
29
+ };
30
+ const mix = (a, b, t) => a.map((v, i) => v + (b[i] - v) * clamp(t, 0, 1));
31
+ const fg = (c, f = 1) => `38;2;${c.map((v) => clamp(Math.round(v * f), 0, 255)).join(';')}`;
32
+ const hex = (h) => { const c = rgbOf(h); return c ? fg(c) : '39'; };
33
+ const fit = (s, w) => (w <= 0 ? '' : s.length > w ? s.slice(0, Math.max(0, w - 1)) + '…' : s + ' '.repeat(w - s.length));
34
+ const flat = (s) => s.replace(/\s+/g, ' ').trim();
35
+ const clamp = (v, lo, hi) => Math.max(lo, Math.min(hi, v));
36
+ const hhmm = (ts) => { const t = new Date(ts); return `${String(t.getHours()).padStart(2, '0')}:${String(t.getMinutes()).padStart(2, '0')}`; };
37
+ const hash = (s) => [...s].reduce((h, c) => (h * 31 + c.charCodeAt(0)) >>> 0, 7);
38
+ const SPINNER = '⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏';
39
+ const FRAME_MS = 80;
40
+ // ── per-account usage bars (cached; refreshed every 30 s, never in the render path) ──
41
+ const USAGE_MS = 30_000;
42
+ const usageBarSegs = (u, room) => {
43
+ const segs = [];
44
+ let len = 0;
45
+ const bw = room >= u.length * 20 ? 6 : 4;
46
+ for (const a of u) {
47
+ const pct = Math.max(0, a.pct5h || 0);
48
+ const n = Math.round(clamp(pct, 0, 1) * bw);
49
+ const col = a.limited ? '1;38;5;203' : pct > 0.8 ? '1;38;5;214' : GOLD;
50
+ const known = a.limit5h > 0; // non-Claude accounts have no usage logs
51
+ const label = `${a.profileId} `, tail = ` ${known ? Math.round(pct * 100) + '%' : '?'}${a.limited ? '⧗' : ''}`;
52
+ const w = label.length + bw + tail.length + 2;
53
+ if (len + w > room)
54
+ break;
55
+ segs.push([label, DIM], ['▰'.repeat(n), col], ['▱'.repeat(bw - n), '38;5;238'], [tail + ' ', a.limited ? col : '38;5;250']);
56
+ len += w;
57
+ }
58
+ return { segs, len };
59
+ };
60
+ const segLen = (segs) => segs.reduce((n, [t]) => n + [...t].length, 0);
61
+ const N = 1, E = 2, S = 4, W_ = 8;
62
+ const HEAVY = {
63
+ [N]: '┃', [S]: '┃', [N | S]: '┃', [E]: '━', [W_]: '━', [E | W_]: '━',
64
+ [S | E]: '┏', [S | W_]: '┓', [N | E]: '┗', [N | W_]: '┛',
65
+ [N | S | E]: '┣', [N | S | W_]: '┫', [E | W_ | S]: '┳', [E | W_ | N]: '┻', [N | E | S | W_]: '╋',
66
+ };
67
+ class Canvas {
68
+ w;
69
+ h;
70
+ ch;
71
+ sgr;
72
+ set;
73
+ mask;
74
+ lineSgr = `0;${GOLD_DEEP}`;
75
+ constructor(w, h) {
76
+ this.w = Math.max(0, w);
77
+ this.h = Math.max(0, h);
78
+ const n = this.w * this.h;
79
+ this.ch = Array(n).fill(' ');
80
+ this.sgr = Array(n).fill('0');
81
+ this.set = Array(n).fill(false);
82
+ this.mask = new Uint8Array(n);
83
+ }
84
+ put(x, y, text, sgr) {
85
+ if (y < 0 || y >= this.h)
86
+ return;
87
+ for (const c of text) {
88
+ if (x >= 0 && x < this.w) {
89
+ const i = y * this.w + x;
90
+ this.ch[i] = c;
91
+ this.sgr[i] = sgr;
92
+ this.set[i] = true;
93
+ }
94
+ x++;
95
+ }
96
+ }
97
+ segs(x, y, w, segs, bg, center = true) {
98
+ if (w <= 0)
99
+ return;
100
+ let cells = segs.flatMap(([t, s]) => [...t].map((c) => [c, s]));
101
+ if (cells.length > w)
102
+ cells = [...cells.slice(0, w - 1), ['…', cells[w - 1][1]]];
103
+ const pad = center ? Math.floor((w - cells.length) / 2) : 0;
104
+ this.put(x, y, ' '.repeat(w), `0${bg}`);
105
+ cells.forEach(([c, s], i) => this.put(x + pad + i, y, c, `0${s ? ';' + s : ''}${bg}`));
106
+ }
107
+ bit(x, y, b) {
108
+ if (x >= 0 && x < this.w && y >= 0 && y < this.h)
109
+ this.mask[y * this.w + x] |= b;
110
+ }
111
+ hline(y, x1, x2) {
112
+ for (let x = x1; x <= x2; x++) {
113
+ if (x > x1)
114
+ this.bit(x, y, W_);
115
+ if (x < x2)
116
+ this.bit(x, y, E);
117
+ }
118
+ }
119
+ vline(x, y1, y2) {
120
+ for (let y = y1; y <= y2; y++) {
121
+ if (y > y1)
122
+ this.bit(x, y, N);
123
+ if (y < y2)
124
+ this.bit(x, y, S);
125
+ }
126
+ }
127
+ row(y, x0 = 0, x1 = this.w) {
128
+ let s = '', last = '';
129
+ for (let x = x0; x < x1; x++) {
130
+ const i = y * this.w + x;
131
+ let c = this.ch[i] ?? ' ', g = this.sgr[i] ?? '0';
132
+ if (!this.set[i] && this.mask[i]) {
133
+ c = HEAVY[this.mask[i]] ?? '╋';
134
+ g = this.lineSgr;
135
+ }
136
+ if (g !== last) {
137
+ s += `\x1b[${g}m`;
138
+ last = g;
139
+ }
140
+ s += c;
141
+ }
142
+ return s + '\x1b[0m';
143
+ }
144
+ }
145
+ export class Tui {
146
+ hub;
147
+ prev = [];
148
+ scheduled = false;
149
+ animPending = false;
150
+ prefix = false;
151
+ mode = 'normal';
152
+ pickRole = 'manual';
153
+ pickIdx = 0;
154
+ notice = '';
155
+ showBoard = false;
156
+ feedHidden = false;
157
+ selectMode = false; // mouse released to the terminal so text can be selected/copied
158
+ composing = false; // keystrokes go to coban's own input line instead of the focused agent
159
+ draft = '';
160
+ pending = [];
161
+ approvalSel = 0;
162
+ showApprovals = false; // stacked layout only: the panel is opened on demand
163
+ approvalsBox;
164
+ approvalHits = [];
165
+ feedScroll = 0; // lines scrolled up from the newest feed entry
166
+ boardBox;
167
+ noticeTimer;
168
+ outerAppCursor = false;
169
+ outerMouse = '';
170
+ now = Date.now();
171
+ hits = [];
172
+ pickHits = [];
173
+ pickBox;
174
+ geo;
175
+ // animation state
176
+ nodes = new Map();
177
+ orchJoint = [0, 0];
178
+ packets = [];
179
+ bornAt = new Map();
180
+ arrivedAt = new Map();
181
+ events = [];
182
+ lastStatus = new Map();
183
+ workingSince = new Map();
184
+ doneCandidates = new Map(); // id → when it went idle after working
185
+ lastLogId = 0;
186
+ constructor(hub) {
187
+ this.hub = hub;
188
+ hub.on('render', (a) => { if (a.id === hub.focus)
189
+ this.schedule(); });
190
+ hub.on('change', () => { this.observe(); this.schedule(); });
191
+ hub.on('board', () => this.schedule());
192
+ hub.on('handoff', (h) => {
193
+ if (h.to)
194
+ this.event(h.to, '⇄', STATUS_STYLE.limited[1], `devir: ${h.from} (${h.fromProfile}) → ${h.toProfile}`);
195
+ else
196
+ this.event(h.from, '⇄', STATUS_STYLE.limited[1], `devir yapılamadı${h.toProfile ? ` (${h.toProfile})` : ': uygun hesap yok'}`);
197
+ this.usageAt = 0;
198
+ this.schedule();
199
+ });
200
+ hub.on('notify', (m) => this.flash(`✉ ${m.from}: ${flat(m.text)}`, 15000));
201
+ }
202
+ get paneSize() {
203
+ const L = this.layout(out.columns, out.rows);
204
+ return { cols: Math.max(20, L.pw), rows: Math.max(3, L.P) };
205
+ }
206
+ start() {
207
+ out.write('\x1b[?1049h\x1b[?2004h\x1b[2J');
208
+ process.stdin.setRawMode(true);
209
+ process.stdin.resume();
210
+ process.stdin.on('data', (d) => this.input(d.toString('utf8')));
211
+ out.on('resize', () => {
212
+ this.prev = [];
213
+ out.write('\x1b[2J');
214
+ this.render(); // recomputes layout and resizes every agent's pty
215
+ });
216
+ this.schedule();
217
+ }
218
+ stop() {
219
+ out.write('\x1b[0m\x1b[?25h\x1b[?1l\x1b[?1000l\x1b[?1002l\x1b[?1003l\x1b[?1006l\x1b[?2004l\x1b[?1049l');
220
+ process.stdin.setRawMode(false);
221
+ }
222
+ flash(text, ms = 4000) {
223
+ this.notice = text;
224
+ clearTimeout(this.noticeTimer);
225
+ this.noticeTimer = setTimeout(() => { this.notice = ''; this.schedule(); }, ms);
226
+ out.write('\x07');
227
+ this.schedule();
228
+ }
229
+ // ── hub observation: feed events + animations ────────────────
230
+ event(id, glyph, color, text, detail) {
231
+ this.events.push({ ts: Date.now(), id, glyph, color, text, detail });
232
+ if (this.events.length > 300)
233
+ this.events.shift();
234
+ }
235
+ observe() {
236
+ const hub = this.hub, now = Date.now();
237
+ for (const [id, a] of hub.agents) {
238
+ const prev = this.lastStatus.get(id);
239
+ if (prev === undefined) {
240
+ this.bornAt.set(id, now);
241
+ const role = a.role === 'orchestrator' ? 'orkestratör' : a.role === 'manual' ? 'manuel agent' : 'işçi';
242
+ this.event(id, '✦', hex(a.profile.color), `${role} açıldı · ${a.profile.title}`, a.task ? flat(a.task.replace(/^\[coban\][\s\S]*?GÖREV:\s*/, '')) : undefined);
243
+ }
244
+ else if (prev !== a.status) {
245
+ if (a.status === 'working') {
246
+ this.workingSince.set(id, now);
247
+ this.doneCandidates.delete(id);
248
+ }
249
+ if (a.status === 'blocked')
250
+ this.event(id, '!', STATUS_STYLE.blocked[1], 'izin bekliyor');
251
+ if (a.status === 'limited')
252
+ this.event(id, '⧗', STATUS_STYLE.limited[1], 'kullanım limiti doldu');
253
+ if (a.status === 'exited')
254
+ this.event(id, '×', DIM, 'süreç kapandı');
255
+ if (a.status === 'idle' && prev === 'working')
256
+ this.doneCandidates.set(id, now);
257
+ if (a.status === 'idle' && prev === 'starting')
258
+ this.event(id, '○', DIM, 'hazır');
259
+ }
260
+ this.lastStatus.set(id, a.status);
261
+ }
262
+ for (const id of [...this.lastStatus.keys()]) {
263
+ if (!hub.agents.has(id)) {
264
+ this.lastStatus.delete(id);
265
+ this.doneCandidates.delete(id);
266
+ this.event(id, '×', DIM, 'kapatıldı');
267
+ }
268
+ }
269
+ // new messages → packets travelling along the graph
270
+ for (const m of hub.log) {
271
+ if (m.id <= this.lastLogId)
272
+ continue;
273
+ this.lastLogId = m.id;
274
+ const from = hub.agents.has(m.from) ? m.from : m.from === 'user' ? '' : hub.orchestratorId;
275
+ const color = rgbOf(hub.agents.get(m.from)?.profile.color) ?? GOLD_RGB;
276
+ const targets = m.to === 'all' ? hub.order.filter((id) => id !== m.from) : hub.agents.has(m.to) ? [m.to] : [];
277
+ targets.forEach((to, i) => { if (from && to !== from)
278
+ this.packets.push({ from, to, color, t0: now + i * 70, dur: 0 }); });
279
+ if (!targets.length && from)
280
+ this.arrivedAt.set(from, now);
281
+ if (!from)
282
+ for (const to of targets)
283
+ this.arrivedAt.set(to, now);
284
+ }
285
+ }
286
+ // "bitirdi" only once an agent has stayed idle a moment, so brief pauses between tool calls don't spam the feed
287
+ settleDone() {
288
+ for (const [id, at] of this.doneCandidates) {
289
+ const a = this.hub.agents.get(id);
290
+ if (!a || a.status !== 'idle') {
291
+ this.doneCandidates.delete(id);
292
+ continue;
293
+ }
294
+ if (this.now - at < 2500)
295
+ continue;
296
+ this.doneCandidates.delete(id);
297
+ const took = this.workingSince.has(id) ? Math.round((at - this.workingSince.get(id)) / 1000) : 0;
298
+ this.event(id, '✓', MARK, took >= 5 ? `durdu · ${took} sn çalıştı` : 'durdu');
299
+ }
300
+ }
301
+ animating() {
302
+ const now = this.now;
303
+ if (this.packets.length || this.doneCandidates.size)
304
+ return true;
305
+ for (const a of this.hub.agents.values())
306
+ if (a.status === 'working' || a.status === 'starting' || a.status === 'blocked')
307
+ return true;
308
+ for (const t of this.bornAt.values())
309
+ if (now - t < 800)
310
+ return true;
311
+ for (const t of this.arrivedAt.values())
312
+ if (now - t < 700)
313
+ return true;
314
+ const last = this.hub.log.at(-1);
315
+ return !!last && now - last.ts < 2000;
316
+ }
317
+ // ── input ────────────────────────────────────────────────────
318
+ input(d) {
319
+ if (d.includes('\x1b[<')) {
320
+ d = d.replace(MOUSE_RE, (_, b, x, y, m) => { this.mouse(Number(b), Number(x) - 1, Number(y) - 1, m === 'M'); return ''; });
321
+ if (!d)
322
+ return;
323
+ }
324
+ const hub = this.hub;
325
+ if (this.mode === 'pick')
326
+ return this.pickKey(d);
327
+ if (this.prefix) {
328
+ this.prefix = false;
329
+ return this.command(d);
330
+ }
331
+ if (d === PREFIX) {
332
+ this.prefix = true;
333
+ return this.schedule();
334
+ }
335
+ if (d[0] === PREFIX) {
336
+ this.prefix = true;
337
+ return this.input(d.slice(1));
338
+ } // ^b and the key in one read
339
+ if (this.composing)
340
+ return this.composeKey(d);
341
+ const a = hub.agents.get(hub.focus);
342
+ if (!a) {
343
+ if (!hub.orchestratorId && (d === '\r' || d === ' ' || d === '+'))
344
+ this.openPicker('orchestrator');
345
+ return;
346
+ }
347
+ if (a.exitCode !== null)
348
+ return;
349
+ if (!a.term.modes.bracketedPasteMode)
350
+ d = d.replace(/\x1b\[20[01]~/g, '');
351
+ a.proc.write(d);
352
+ }
353
+ command(c) {
354
+ const hub = this.hub;
355
+ const idx = hub.order.indexOf(hub.focus);
356
+ const move = (n) => hub.order.length && hub.setFocus(hub.order[(idx + n + hub.order.length) % hub.order.length]);
357
+ switch (c) {
358
+ case 'n':
359
+ this.openPicker('manual');
360
+ break;
361
+ case 'j':
362
+ case '\x1b[B':
363
+ case '\x1bOB':
364
+ move(1);
365
+ break;
366
+ case 'k':
367
+ case '\x1b[A':
368
+ case '\x1bOA':
369
+ move(-1);
370
+ break;
371
+ case 'o':
372
+ if (hub.orchestratorId)
373
+ hub.setFocus(hub.orchestratorId);
374
+ else
375
+ this.openPicker('orchestrator');
376
+ break;
377
+ case 'm':
378
+ this.showBoard = !this.showBoard;
379
+ this.prev = [];
380
+ out.write('\x1b[2J');
381
+ break;
382
+ case 'l':
383
+ this.feedHidden = !this.feedHidden;
384
+ this.prev = [];
385
+ out.write('\x1b[2J');
386
+ break;
387
+ case 's':
388
+ this.selectMode = !this.selectMode;
389
+ break;
390
+ case 'c':
391
+ this.composing = !this.composing;
392
+ break;
393
+ case 'y':
394
+ this.answerSelected('yes');
395
+ break;
396
+ case 'a':
397
+ this.answerSelected('always');
398
+ break;
399
+ case 'r':
400
+ this.answerSelected('no');
401
+ break;
402
+ case 'i':
403
+ if (this.geo?.mode === 'stack' && !this.showApprovals)
404
+ this.showApprovals = true;
405
+ else if (this.pending.length > 1)
406
+ this.approvalSel = (this.approvalSel + 1) % this.pending.length;
407
+ else if (this.geo?.mode === 'stack')
408
+ this.showApprovals = false;
409
+ break;
410
+ case 'u': {
411
+ const u = hub.order.find((id) => hub.agents.get(id).unread);
412
+ if (u)
413
+ hub.setFocus(u);
414
+ break;
415
+ }
416
+ case 'x': {
417
+ const a = hub.agents.get(hub.focus);
418
+ if (!a)
419
+ break;
420
+ if (a.role === 'orchestrator' && a.exitCode === null)
421
+ this.flash('orkestratör kapatılamaz (çıkış: ^b q)');
422
+ else
423
+ hub.kill(a.id);
424
+ break;
425
+ }
426
+ case 'q':
427
+ process.emit('SIGTERM');
428
+ return;
429
+ case PREFIX:
430
+ hub.agents.get(hub.focus)?.proc.write(PREFIX);
431
+ break;
432
+ default:
433
+ if (/^[1-9]$/.test(c) && hub.order[Number(c) - 1])
434
+ hub.setFocus(hub.order[Number(c) - 1]);
435
+ }
436
+ this.schedule();
437
+ }
438
+ // ── coban's own input line: "@id text" goes to that agent, "@all text" to the board, plain text to the focus ──
439
+ composeKey(d) {
440
+ const paste = d.match(/^\x1b\[200~([\s\S]*)\x1b\[201~$/);
441
+ if (paste) {
442
+ this.draft += paste[1].replace(/\r\n?/g, '\n');
443
+ return this.schedule();
444
+ }
445
+ if (d === '\x1b')
446
+ this.composing = false;
447
+ else if (d === '\r')
448
+ this.send();
449
+ else if (d === '\x7f' || d === '\b')
450
+ this.draft = [...this.draft].slice(0, -1).join('');
451
+ else if (d === '\x15')
452
+ this.draft = '';
453
+ else if (d === '\t') {
454
+ const m = this.draft.match(/@(\S*)$/);
455
+ if (m) {
456
+ const hit = ['all', ...this.hub.order].find((id) => id.startsWith(m[1]) && id !== m[1]);
457
+ if (hit)
458
+ this.draft = this.draft.slice(0, this.draft.length - m[1].length) + hit + ' ';
459
+ }
460
+ }
461
+ else if (!d.startsWith('\x1b'))
462
+ this.draft += d.replace(/[\x00-\x08\x0b-\x1f\x7f]/g, '');
463
+ this.schedule();
464
+ }
465
+ target() {
466
+ const m = this.draft.match(/^\s*@(\S+)\s*([\s\S]*)$/);
467
+ if (!m)
468
+ return { to: this.hub.focus, text: this.draft.trim() };
469
+ const id = m[1].toLowerCase();
470
+ return { to: id === 'herkes' ? 'all' : m[1], text: m[2].trim() };
471
+ }
472
+ send() {
473
+ const { to, text } = this.target();
474
+ if (!text)
475
+ return;
476
+ if (to !== 'all' && !this.hub.agents.has(to))
477
+ return this.flash(`agent yok: @${to}`);
478
+ try {
479
+ this.hub.post('user', to, text);
480
+ this.draft = '';
481
+ }
482
+ catch (e) {
483
+ this.flash(e.message);
484
+ }
485
+ }
486
+ async answerSelected(choice, id = this.pending[this.approvalSel]?.id) {
487
+ const ap = this.pending.find((x) => x.id === id);
488
+ if (!ap)
489
+ return this.flash('izin bekleyen yok');
490
+ if (!ap.prompt) {
491
+ this.hub.setFocus(ap.id);
492
+ return this.flash(`${ap.id}: soru okunamadı, terminalde yanıtla`);
493
+ }
494
+ try {
495
+ const picked = await this.hub.answer(ap.id, choice, ap.prompt.key);
496
+ this.event(ap.id, choice === 'no' ? '✗' : '✓', choice === 'no' ? STATUS_STYLE.blocked[1] : MARK, `izin: ${picked}`, ap.prompt.body[0] ?? ap.prompt.title);
497
+ }
498
+ catch (e) {
499
+ this.flash(e.message);
500
+ }
501
+ this.schedule();
502
+ }
503
+ openPicker(role) {
504
+ if (role === 'orchestrator' && this.hub.orchestratorId)
505
+ return this.hub.setFocus(this.hub.orchestratorId);
506
+ this.mode = 'pick';
507
+ this.pickRole = role;
508
+ this.pickIdx = 0;
509
+ this.schedule();
510
+ }
511
+ pickKey(d) {
512
+ const n = this.hub.cfg.profiles.length;
513
+ if (d === '\x1b[A' || d === '\x1bOA' || d === 'k')
514
+ this.pickIdx = (this.pickIdx - 1 + n) % n;
515
+ else if (d === '\x1b[B' || d === '\x1bOB' || d === 'j' || d === '\t')
516
+ this.pickIdx = (this.pickIdx + 1) % n;
517
+ else if (d === '\r' || d === '\n' || d === ' ')
518
+ return this.choose(this.pickIdx);
519
+ else if (/^[1-9]$/.test(d) && Number(d) <= n)
520
+ return this.choose(Number(d) - 1);
521
+ else if (d === '\x1b' || d === 'q' || d === PREFIX || d === '\x03')
522
+ this.mode = 'normal';
523
+ this.schedule();
524
+ }
525
+ choose(i) {
526
+ const hub = this.hub;
527
+ const p = hub.cfg.profiles[i];
528
+ const role = this.pickRole === 'orchestrator' && hub.orchestratorId ? 'manual' : this.pickRole;
529
+ this.mode = 'normal';
530
+ if (p) {
531
+ try {
532
+ hub.setFocus(hub.spawn({ profile: p.id, role }).id);
533
+ }
534
+ catch (e) {
535
+ this.flash(e.message);
536
+ }
537
+ }
538
+ this.schedule();
539
+ }
540
+ // b: SGR button byte, x/y: 0-based screen cell, press: 'M' vs 'm'
541
+ mouse(b, x, y, press) {
542
+ const wheel = (b & 64) !== 0, motion = (b & 32) !== 0, btn = b & 3;
543
+ const click = press && !wheel && !motion && btn === 0;
544
+ const inside = (r, px = x, py = y) => px >= r.x && px < r.x + r.w && py >= r.y && py < r.y + r.h;
545
+ if (this.mode === 'pick') {
546
+ if (!click)
547
+ return;
548
+ const h = this.pickHits.find((r) => r.y === y && x >= r.x0 && x < r.x1);
549
+ if (h)
550
+ return this.choose(h.idx);
551
+ if (!this.pickBox || !inside(this.pickBox)) {
552
+ this.mode = 'normal';
553
+ this.schedule();
554
+ }
555
+ return;
556
+ }
557
+ if (this.boardBox && inside(this.boardBox))
558
+ return; // panel swallows clicks
559
+ if (this.approvalsBox && inside(this.approvalsBox)) {
560
+ if (!click)
561
+ return;
562
+ const h = this.approvalHits.find((r) => r.y === y && x >= r.x0 && x < r.x1);
563
+ if (h?.choice)
564
+ void this.answerSelected(h.choice, h.id);
565
+ else if (h) {
566
+ this.approvalSel = Math.max(0, this.pending.findIndex((p) => p.id === h.id));
567
+ this.hub.setFocus(h.id);
568
+ }
569
+ return;
570
+ }
571
+ const L = this.geo;
572
+ if (!L)
573
+ return;
574
+ // feed column: wheel scrolls history
575
+ if (L.fw && x >= L.fx && x < L.fx + L.fw) {
576
+ if (wheel) {
577
+ this.feedScroll = Math.max(0, this.feedScroll + (btn === 0 ? 3 : -3));
578
+ this.schedule();
579
+ }
580
+ return;
581
+ }
582
+ // graph: bubbles are clickable
583
+ const inGraph = L.mode === 'cols' ? x >= L.gx && x < L.gx + L.gw : y < L.paneTop;
584
+ if (inGraph) {
585
+ if (!click)
586
+ return;
587
+ const h = this.hits.find((r) => inside(r, x - L.gx, y))?.hit;
588
+ if (!h)
589
+ return;
590
+ if (h.kind === 'agent')
591
+ this.hub.setFocus(h.id);
592
+ else
593
+ this.openPicker(h.kind === 'add-orch' ? 'orchestrator' : 'manual');
594
+ return;
595
+ }
596
+ // composer: a click focuses it; a click in the chat gives the keyboard back to the agent
597
+ if ((y === L.cy || y === L.cy + 1) && x >= L.px && x < L.px + L.pw) {
598
+ if (click) {
599
+ this.composing = true;
600
+ this.schedule();
601
+ }
602
+ return;
603
+ }
604
+ if (click && this.composing) {
605
+ this.composing = false;
606
+ this.schedule();
607
+ }
608
+ // chat pane: forward when the agent asked for mouse reports, otherwise swallow
609
+ const a = this.hub.agents.get(this.hub.focus);
610
+ const row = y - L.paneTop, col = x - L.px;
611
+ if (!a || a.exitCode !== null || row < 0 || row >= L.P || col < 0 || col >= L.pw || a.term.modes.mouseTrackingMode === 'none')
612
+ return;
613
+ const ms = a.term._core?.coreMouseService;
614
+ if (!ms)
615
+ return;
616
+ let button, action;
617
+ if (wheel) {
618
+ if (btn > 1)
619
+ return;
620
+ button = 4;
621
+ action = btn === 0 ? 0 : 1;
622
+ } // 4=wheel, 0=up 1=down
623
+ else if (motion) {
624
+ button = btn;
625
+ action = 32;
626
+ }
627
+ else {
628
+ button = btn;
629
+ action = press ? 1 : 0;
630
+ }
631
+ ms.triggerMouseEvent({ col, row, button, action, ctrl: (b & 16) !== 0, alt: (b & 8) !== 0, shift: (b & 4) !== 0 });
632
+ }
633
+ // ── layout ───────────────────────────────────────────────────
634
+ layout(W, H) {
635
+ const Hb = Math.max(4, H - 1); // body rows above the status bar
636
+ if (W < 100) {
637
+ const p = this.plan(W, H, false);
638
+ const P = Math.max(1, H - p.G - 4);
639
+ return { mode: 'stack', fx: 0, fw: 0, gx: 0, gw: W, px: 0, pw: W, paneTop: p.G + 1, P, cy: p.G + 1 + P, p };
640
+ }
641
+ let fw = this.feedHidden || W < 140 ? 0 : clamp(Math.round(W * 0.24), 30, 56);
642
+ let pw = clamp(Math.round(W * 0.42), 64, 130);
643
+ let gw = W - fw - pw - (fw ? 2 : 1);
644
+ if (gw < 40) {
645
+ const t = Math.min(40 - gw, pw - 60);
646
+ pw -= t;
647
+ gw += t;
648
+ }
649
+ if (gw < 40 && fw) {
650
+ const t = Math.min(40 - gw, fw - 26);
651
+ fw -= t;
652
+ gw += t;
653
+ }
654
+ const gx = fw ? fw + 1 : 0;
655
+ return { mode: 'cols', fx: 0, fw, gx, gw, px: gx + gw + 1, pw, paneTop: 1, P: Hb - 3, cy: Hb - 2, p: this.plan(gw, Hb, true) };
656
+ }
657
+ glyph(a) {
658
+ if (a.status === 'working')
659
+ return SPINNER[Math.floor(this.now / FRAME_MS + hash(a.id)) % SPINNER.length];
660
+ if (a.status === 'blocked')
661
+ return Math.floor(this.now / 450) % 2 ? '!' : ' ';
662
+ return STATUS_STYLE[a.status][0];
663
+ }
664
+ label(a) {
665
+ const l = STATUS_STYLE[a.status][2];
666
+ return a.status === 'working' ? l + [' ', '. ', '.. ', '...'][Math.floor(this.now / 400) % 4] : l;
667
+ }
668
+ marks(a) {
669
+ const mail = a.inbox.filter((m) => !m.delivered).length;
670
+ const note = this.hub.boardUnread(a.id);
671
+ const got = this.now - (this.arrivedAt.get(a.id) ?? 0) < 700;
672
+ return (a.unread ? ' ✓' : '') + (mail || got ? ` ✉${mail || ''}` : '') + (note ? ` ▤${note}` : '');
673
+ }
674
+ // Border colour with life: fade in when born, breathe while working, blink when blocked, glow on mail.
675
+ tint(a, base, focus) {
676
+ const now = this.now;
677
+ let c = base, f = 1;
678
+ if (a) {
679
+ const born = this.bornAt.get(a.id);
680
+ if (born !== undefined && now - born < 800)
681
+ c = mix(BG_RGB, c, (now - born) / 800);
682
+ if (a.status === 'working' || a.status === 'starting')
683
+ f = 0.62 + 0.38 * (0.5 + 0.5 * Math.sin(now / 320 + (hash(a.id) % 7)));
684
+ if (a.status === 'blocked' && Math.floor(now / 450) % 2)
685
+ c = RED;
686
+ if (a.status === 'exited')
687
+ f = 0.45;
688
+ const got = this.arrivedAt.get(a.id);
689
+ if (got !== undefined && now - got < 700)
690
+ c = mix(WHITE, c, (now - got) / 700);
691
+ }
692
+ return (focus ? '1;' : '') + fg(c, f);
693
+ }
694
+ boardPanel(W, H, G) {
695
+ const notes = this.hub.board.slice(-10);
696
+ const L = this.geo;
697
+ const w = L.mode === 'cols' ? clamp(L.gw - 2, 20, 110) : clamp(W - 4, 20, 110);
698
+ const h = clamp(notes.length + 4, 5, Math.max(5, H - G - 3));
699
+ const x = L.mode === 'cols' ? L.gx + 1 : (W - w - 1 >= 0 ? W - w - 1 : 0), y = G + 1;
700
+ const c = new Canvas(w, h);
701
+ const bs = `0;1;${GOLD}`, bg = ';48;5;234';
702
+ c.put(0, 0, '╭' + '─'.repeat(w - 2) + '╮', bs);
703
+ c.put(2, 0, ` Ortak kutu (${this.hub.board.length}) `, bs);
704
+ for (let i = 1; i < h - 1; i++) {
705
+ c.put(0, i, '│', bs);
706
+ c.put(1, i, ' '.repeat(w - 2), `0${bg}`);
707
+ c.put(w - 1, i, '│', bs);
708
+ }
709
+ const shown = notes.slice(-(h - 4));
710
+ if (!shown.length)
711
+ c.segs(2, 1, w - 4, [['henüz not yok — coban post all "..."', DIM]], bg, false);
712
+ shown.forEach((m, i) => {
713
+ const col = hex(this.hub.agents.get(m.from)?.profile.color);
714
+ c.segs(2, 1 + i, w - 4, [[hhmm(m.ts) + ' ', DIM], [m.from, `1;${col}`], [' ' + flat(m.text), '39']], bg, false);
715
+ });
716
+ c.segs(2, h - 2, w - 4, [['^b m kapat', DIM]], bg, false);
717
+ c.put(0, h - 1, '╰' + '─'.repeat(w - 2) + '╯', bs);
718
+ this.boardBox = { x, y, w, h };
719
+ return { c, x, y };
720
+ }
721
+ workerLines(a, level) {
722
+ const [, color] = STATUS_STYLE[a.status];
723
+ const pc = hex(a.profile.color);
724
+ const id = [' ' + a.id, (a.id === this.hub.focus ? '1;' : '') + (level === 'mini' ? pc : '39')];
725
+ const head = [[this.glyph(a), color], id, [this.marks(a), MARK]];
726
+ if (level === 'mini')
727
+ return [head];
728
+ if (level === 'compact')
729
+ return [[...head, [' ' + a.profile.title, pc]]];
730
+ return [head, [[a.profile.title, pc], [' · ' + this.label(a), DIM]]];
731
+ }
732
+ // Width a worker bubble needs, sized for its longest status label so bubbles don't jitter.
733
+ workerWidth(a, level) {
734
+ const head = 2 + a.id.length + 6;
735
+ if (level === 'mini')
736
+ return head;
737
+ if (level === 'compact')
738
+ return head + 1 + a.profile.title.length;
739
+ return Math.max(head, a.profile.title.length + 3 + Math.max(...Object.values(STATUS_STYLE).map(([, , l]) => l.length)) + 3);
740
+ }
741
+ orchLines(a, compact) {
742
+ if (!a)
743
+ return compact ? [[['+ ', '1;38;5;214'], ['ORKESTRATÖR', `1;${GOLD}`]]] : [[['ORKESTRATÖR', `1;${GOLD}`]], [['+', '1;38;5;214']]];
744
+ const [, color] = STATUS_STYLE[a.status];
745
+ const main = [[this.glyph(a), color], [' ' + a.profile.title.replace(' ', ' · '), `1;${hex(a.profile.color)}`], [this.marks(a), MARK]];
746
+ if (compact)
747
+ return [[['★ ', GOLD], ...main]];
748
+ return [[['ORKESTRATÖR', GOLD]], [...main, [' · ' + this.label(a), DIM]]];
749
+ }
750
+ addLines(compact) {
751
+ return compact ? [[['+ işçi', MARK]]] : [[['+', `1;${MARK}`]], [['işçi', DIM]]];
752
+ }
753
+ // full: the graph owns a whole column (centred vertically), otherwise it is a band above the chat
754
+ plan(W, H, full) {
755
+ const hub = this.hub;
756
+ const workers = hub.order.map((id) => hub.agents.get(id)).filter((a) => a.role !== 'orchestrator');
757
+ const n = workers.length + (hub.order.length ? 1 : 0); // + the "add worker" bubble
758
+ const capG = full ? H - 1 : Math.max(3, Math.min(Math.max(7, Math.round(H * 0.3)), H - 8));
759
+ const cx = Math.floor(W / 2);
760
+ let p;
761
+ for (const level of ['full', 'compact', 'mini']) {
762
+ const bh = level === 'full' ? 4 : 3;
763
+ const widest = Math.max(0, ...workers.map((a) => this.workerWidth(a, level)));
764
+ const bw = Math.min(Math.max(1, W - 2), clamp(widest + 4, level === 'full' ? 16 : 10, level === 'full' ? 30 : 26));
765
+ const pitch = bw + 1;
766
+ const nL = Math.floor(cx / pitch), nR = Math.floor((W - 1 - cx) / pitch);
767
+ const perRow = Math.max(1, nL + nR);
768
+ const want = Math.ceil(n / perRow);
769
+ const fitRows = Math.max(0, Math.floor((capG - bh) / (bh + 1)));
770
+ p = { G: 0, level, bh, bw, perRow, nL, nR, rows: Math.min(want, fitRows), items: [] };
771
+ // in a narrow column prefer smaller bubbles side by side over a single-file stack
772
+ if (want <= fitRows && (perRow >= Math.min(2, n) || level === 'mini'))
773
+ break;
774
+ }
775
+ p = p;
776
+ const cap = p.rows * p.perRow;
777
+ const items = workers.map((a) => ({ kind: 'agent', a }));
778
+ if (n > cap && cap >= 2) {
779
+ const shown = items.slice(0, cap - 2);
780
+ const fi = workers.findIndex((a) => a.id === hub.focus);
781
+ if (fi >= cap - 2 && shown.length)
782
+ shown[shown.length - 1] = items[fi]; // keep the focused bubble visible
783
+ const visible = new Set(shown.map((it) => it.a.id));
784
+ const hidden = workers.filter((a) => !visible.has(a.id));
785
+ p.items = [...shown, { kind: 'more', n: hidden.length, first: hidden[0].id }, { kind: 'add' }];
786
+ }
787
+ else
788
+ p.items = n ? [...items, { kind: 'add' }].slice(0, cap) : [];
789
+ const used = p.bh + p.rows * (p.bh + 1);
790
+ p.G = full ? capG : Math.min(capG, Math.max(7, used));
791
+ return p;
792
+ }
793
+ graph(W, p, full) {
794
+ const hub = this.hub;
795
+ const c = new Canvas(W, p.G + 1);
796
+ this.hits = [];
797
+ this.nodes.clear();
798
+ const compact = p.bh === 3;
799
+ const cx = Math.floor(W / 2);
800
+ const used = p.bh + p.rows * (p.bh + 1);
801
+ const top = Math.max(0, Math.floor(((full ? p.G + 1 : p.G) - used) / 2));
802
+ const num = (id) => String(hub.order.indexOf(id) + 1);
803
+ // root: the orchestrator (double-line gold box)
804
+ const orch = hub.agents.get(hub.orchestratorId);
805
+ const ol = this.orchLines(orch, compact);
806
+ const ow = clamp(Math.max(...ol.map(segLen)) + 6, compact ? 18 : 21, W);
807
+ const ox = clamp(cx - Math.floor(ow / 2), 0, W - ow);
808
+ const ofocus = !!orch && orch.id === hub.focus;
809
+ this.box(c, ox, top, ow, p.bh, ol, {
810
+ style: 'double', color: this.tint(orch, GOLD_RGB, ofocus), bg: ofocus ? ';48;5;236' : '',
811
+ label: orch ? num(orch.id) : '', hit: orch ? { kind: 'agent', id: orch.id } : { kind: 'add-orch' },
812
+ });
813
+ this.orchJoint = [cx, top + p.bh - 1];
814
+ if (p.rows && p.items.length) {
815
+ c.put(cx, top + p.bh - 1, '╤', `0;${ofocus ? '1;' : ''}${GOLD}`);
816
+ const pitch = p.bw + 1;
817
+ let prevY = top + p.bh - 1;
818
+ for (let r = 0; r < p.rows; r++) {
819
+ const row = p.items.slice(r * p.perRow, (r + 1) * p.perRow);
820
+ if (!row.length)
821
+ break;
822
+ const last = (r + 1) * p.perRow >= p.items.length;
823
+ const busY = top + p.bh + r * (p.bh + 1);
824
+ let xs;
825
+ if (last) { // final row: centred under the trunk
826
+ const blockW = row.length * pitch - 1;
827
+ const start = clamp(cx - Math.floor(blockW / 2), 0, Math.max(0, W - blockW));
828
+ xs = row.map((_, i) => start + i * pitch);
829
+ }
830
+ else { // the trunk passes between the left and right halves
831
+ const kL = Math.min(p.nL, Math.ceil(row.length / 2));
832
+ xs = row.map((_, i) => (i < kL ? cx - 1 - p.bw - (kL - 1 - i) * pitch : cx + 2 + (i - kL) * pitch));
833
+ }
834
+ const anchors = xs.map((x) => x + Math.floor(p.bw / 2));
835
+ c.vline(cx, prevY, busY);
836
+ c.hline(busY, Math.min(anchors[0], cx), Math.max(anchors[anchors.length - 1], cx));
837
+ row.forEach((it, i) => {
838
+ const x = xs[i], y = busY + 1, ax = anchors[i];
839
+ c.vline(ax, busY, y);
840
+ if (it.kind === 'agent') {
841
+ const a = it.a, focus = a.id === hub.focus;
842
+ this.nodes.set(a.id, { ax, ay: y, bus: busY });
843
+ this.box(c, x, y, p.bw, p.bh, this.workerLines(a, p.level), {
844
+ style: focus ? 'heavy' : 'round', color: this.tint(a, rgbOf(a.profile.color) ?? [200, 200, 200], focus),
845
+ bg: focus ? ';48;5;236' : '', label: num(a.id), hit: { kind: 'agent', id: a.id }, joint: ax,
846
+ });
847
+ }
848
+ else if (it.kind === 'more') {
849
+ this.box(c, x, y, p.bw, p.bh, [[[p.level === 'mini' ? `… +${it.n}` : `… +${it.n} işçi`, DIM]]], {
850
+ style: 'dash', color: DIM, bg: '', label: '', hit: { kind: 'agent', id: it.first }, joint: ax,
851
+ });
852
+ }
853
+ else {
854
+ const aw = compact ? 10 : 8;
855
+ this.box(c, x + Math.floor((p.bw - aw) / 2), y, aw, p.bh, this.addLines(compact), {
856
+ style: 'dash', color: DIM, bg: '', label: '', hit: { kind: 'add-worker' }, joint: ax,
857
+ });
858
+ }
859
+ });
860
+ prevY = busY;
861
+ }
862
+ }
863
+ this.drawPackets(c);
864
+ if (full) {
865
+ if (!hub.orchestratorId)
866
+ c.segs(0, Math.min(p.G, top + p.bh + 1), W, [['+ baloncuğuna tıkla ya da Enter\'a bas', DIM]], '');
867
+ return c;
868
+ }
869
+ // stacked layout: separator + title of the chat below
870
+ c.put(0, p.G, '─'.repeat(W), SEP);
871
+ const f = hub.agents.get(hub.focus);
872
+ if (f) {
873
+ c.put(1, p.G, ' ', '0');
874
+ let x = 2;
875
+ for (const [t, s] of this.paneTitle(f)) {
876
+ c.put(x, p.G, t, `0;${s}`);
877
+ x += [...t].length;
878
+ }
879
+ }
880
+ else if (!hub.orchestratorId) {
881
+ c.put(2, p.G, ' ORKESTRATÖR baloncuğundaki + işaretine tıkla (veya Enter) ', `0;${DIM}`);
882
+ }
883
+ return c;
884
+ }
885
+ // Path along the drawn edges from the orchestrator's bottom joint down to an agent's top joint.
886
+ pathTo(id) {
887
+ if (id === this.hub.orchestratorId)
888
+ return [this.orchJoint];
889
+ const n = this.nodes.get(id);
890
+ if (!n)
891
+ return null;
892
+ const [cx, oy] = this.orchJoint;
893
+ const pts = [];
894
+ for (let y = oy; y <= n.bus; y++)
895
+ pts.push([cx, y]);
896
+ const dx = Math.sign(n.ax - cx);
897
+ for (let x = cx + dx; dx && x !== n.ax + dx; x += dx)
898
+ pts.push([x, n.bus]);
899
+ for (let y = n.bus + 1; y <= n.ay; y++)
900
+ pts.push([n.ax, y]);
901
+ return pts;
902
+ }
903
+ drawPackets(c) {
904
+ const now = this.now;
905
+ const live = [];
906
+ for (const pk of this.packets) {
907
+ const a = this.pathTo(pk.from), b = this.pathTo(pk.to);
908
+ if (!a || !b)
909
+ continue; // endpoint not on screen (closed / folded into "+N")
910
+ let k = 0; // shared prefix (the common trunk) is not walked twice
911
+ while (k < a.length && k < b.length && a[k][0] === b[k][0] && a[k][1] === b[k][1])
912
+ k++;
913
+ const path = [...a.slice(Math.max(0, k - 1)).reverse(), ...b.slice(k)];
914
+ if (!pk.dur)
915
+ pk.dur = clamp(path.length * 28, 380, 1400);
916
+ const t = (now - pk.t0) / pk.dur;
917
+ if (t < 0) {
918
+ live.push(pk);
919
+ continue;
920
+ }
921
+ if (t >= 1) {
922
+ this.arrivedAt.set(pk.to, now);
923
+ continue;
924
+ }
925
+ const i = Math.floor(t * (path.length - 1));
926
+ const tail = [['●', 1], ['•', 0.7], ['·', 0.45]];
927
+ tail.forEach(([g, f], j) => { const q = path[i - j]; if (q)
928
+ c.put(q[0], q[1], g, `0;1;${fg(pk.color, f)}`); });
929
+ live.push(pk);
930
+ }
931
+ this.packets = live;
932
+ }
933
+ box(c, x, y, w, h, lines, o) {
934
+ const k = {
935
+ round: ['╭', '╮', '╰', '╯', '─', '│', '┸'],
936
+ heavy: ['┏', '┓', '┗', '┛', '━', '┃', '┻'],
937
+ double: ['╔', '╗', '╚', '╝', '═', '║', '╨'],
938
+ dash: ['╭', '╮', '╰', '╯', '┄', '┆', '┸'],
939
+ }[o.style];
940
+ const bs = `0;${o.color}`;
941
+ c.put(x, y, k[0] + k[4].repeat(Math.max(0, w - 2)) + k[1], bs);
942
+ if (o.label)
943
+ c.put(x + 2, y, o.label, `0;${DIM}`);
944
+ if (o.joint !== undefined)
945
+ c.put(o.joint, y, k[6], bs);
946
+ const inner = h - 2;
947
+ const start = Math.floor((inner - lines.length) / 2);
948
+ for (let i = 0; i < inner; i++) {
949
+ c.put(x, y + 1 + i, k[5], bs);
950
+ c.put(x + 1, y + 1 + i, ' ', `0${o.bg}`);
951
+ c.segs(x + 2, y + 1 + i, w - 4, lines[i - start] ?? [], o.bg);
952
+ c.put(x + w - 2, y + 1 + i, ' ', `0${o.bg}`);
953
+ c.put(x + w - 1, y + 1 + i, k[5], bs);
954
+ }
955
+ c.put(x, y + h - 1, k[2] + k[4].repeat(Math.max(0, w - 2)) + k[3], bs);
956
+ this.hits.push({ x, y, w, h, hit: o.hit });
957
+ }
958
+ picker(W, H) {
959
+ const ps = this.hub.cfg.profiles;
960
+ const title = this.pickRole === 'orchestrator' ? ' Orkestratör seç ' : ' Yeni işçi ';
961
+ const rows = ps.map((p, i) => [[`${i + 1} `, DIM], ['● ', hex(p.color)], [p.title, `1;${hex(p.color)}`], [` ${p.id}`, DIM]]);
962
+ const footer = [['↑↓ · 1-9 · enter · esc', DIM]];
963
+ const w = clamp(Math.max(segLen(footer), ...rows.map(segLen), title.length) + 6, 30, W);
964
+ const h = ps.length + 4;
965
+ const x = Math.floor((W - w) / 2), y = clamp(Math.floor((H - 1 - h) / 2), 0, Math.max(0, H - 1 - h));
966
+ const c = new Canvas(w, h);
967
+ const bs = `0;1;${GOLD}`, bg = ';48;5;234';
968
+ c.put(0, 0, '╭' + '─'.repeat(w - 2) + '╮', bs);
969
+ c.put(2, 0, title, `0;1;${GOLD}`);
970
+ for (let i = 1; i < h - 1; i++) {
971
+ c.put(0, i, '│', bs);
972
+ c.put(1, i, ' '.repeat(w - 2), `0${bg}`);
973
+ c.put(w - 1, i, '│', bs);
974
+ }
975
+ this.pickHits = [];
976
+ rows.forEach((segs, i) => {
977
+ const sel = i === this.pickIdx;
978
+ const rbg = sel ? ';48;5;238' : bg;
979
+ c.put(1, 1 + i, ' '.repeat(w - 2), `0${rbg}`);
980
+ c.put(2, 1 + i, sel ? '▸' : ' ', `0;1;${GOLD}${rbg}`);
981
+ c.segs(4, 1 + i, w - 6, segs, rbg, false);
982
+ this.pickHits.push({ y: y + 1 + i, x0: x + 1, x1: x + w - 1, idx: i });
983
+ });
984
+ c.segs(2, h - 2, w - 4, footer, bg, false);
985
+ c.put(0, h - 1, '╰' + '─'.repeat(w - 2) + '╯', bs);
986
+ this.pickBox = { x, y, w, h };
987
+ return { c, x, y };
988
+ }
989
+ // ── feed: who said what to whom, plus lifecycle events ───────
990
+ feed(w, h) {
991
+ const c = new Canvas(w, h);
992
+ const hub = this.hub, now = this.now;
993
+ const name = (id) => id === 'all' ? ['ortak kutu', `1;${GOLD}`] : id === 'user' ? ['sen', '1;38;5;117']
994
+ : [id, `1;${hex(hub.agents.get(id)?.profile.color)}`];
995
+ const wrap = (text, width, max) => {
996
+ const lines = [];
997
+ for (const para of text.split('\n')) {
998
+ let cur = '';
999
+ for (const word of para.split(/\s+/).filter(Boolean)) {
1000
+ for (let wd = word; wd.length;) {
1001
+ const room = width - (cur ? cur.length + 1 : 0);
1002
+ if (wd.length <= room) {
1003
+ cur = cur ? cur + ' ' + wd : wd;
1004
+ wd = '';
1005
+ }
1006
+ else if (!cur) {
1007
+ lines.push(wd.slice(0, width));
1008
+ wd = wd.slice(width);
1009
+ }
1010
+ else {
1011
+ lines.push(cur);
1012
+ cur = '';
1013
+ }
1014
+ }
1015
+ }
1016
+ if (cur)
1017
+ lines.push(cur);
1018
+ if (lines.length > max)
1019
+ break;
1020
+ }
1021
+ return lines.length > max ? [...lines.slice(0, max - 1), fit(lines[max - 1], width - 1) + '…'] : lines;
1022
+ };
1023
+ const rows = [];
1024
+ const tw = Math.max(8, w - 4);
1025
+ const items = [
1026
+ ...hub.log.slice(-150).map((m) => ({ ts: m.ts, m })),
1027
+ ...this.events.slice(-150).map((e) => ({ ts: e.ts, e })),
1028
+ ].sort((a, b) => a.ts - b.ts);
1029
+ for (const it of items) {
1030
+ const fresh = now - it.ts < 1600;
1031
+ const bg = fresh ? `;48;5;${236 + Math.round((1 - (now - it.ts) / 1600) * 2)}` : '';
1032
+ if ('m' in it) {
1033
+ const m = it.m;
1034
+ let text = m.text;
1035
+ if (text.startsWith('[coban] Sen coban'))
1036
+ text = '(işçi talimatı) ' + text.replace(/^[\s\S]*?GÖREV:\s*/, '');
1037
+ text = text.replace(/\n\nİLGİLİ KOD[\s\S]*$/, ' [+ ilgili kod]');
1038
+ rows.push({ segs: [[hhmm(m.ts) + ' ', DIM], name(m.from), [' → ', DIM], name(m.to)], bg });
1039
+ for (const l of wrap(text, tw, 6))
1040
+ rows.push({ segs: [[' ' + l, m.to === 'all' ? '38;5;223' : '38;5;252']], bg });
1041
+ }
1042
+ else {
1043
+ const e = it.e;
1044
+ rows.push({ segs: [[hhmm(e.ts) + ' ', DIM], [e.glyph + ' ', e.color], [e.id, `1;${hex(hub.agents.get(e.id)?.profile.color)}`], [' ' + e.text, '3;' + DIM]], bg });
1045
+ if (e.detail)
1046
+ for (const l of wrap(e.detail, tw, 2))
1047
+ rows.push({ segs: [[' ' + l, DIM]], bg });
1048
+ }
1049
+ rows.push({ segs: [], bg: '' });
1050
+ }
1051
+ // header
1052
+ c.segs(0, 0, w, [[' ◆ ', GOLD], ['Konuşmalar', `1;${GOLD}`], [` ${hub.log.length} mesaj`, DIM]], '', false);
1053
+ c.put(0, 1, '─'.repeat(w), SEP);
1054
+ const room = h - 2;
1055
+ const maxScroll = Math.max(0, rows.length - room);
1056
+ this.feedScroll = Math.min(this.feedScroll, maxScroll);
1057
+ const end = rows.length - this.feedScroll;
1058
+ const view = rows.slice(Math.max(0, end - room), end);
1059
+ if (!items.length)
1060
+ c.segs(1, 3, w - 2, [['agent\'lar konuşunca burada görünür', DIM]], '', false);
1061
+ view.forEach((r, i) => c.segs(1, 2 + i, w - 1, r.segs, r.bg, false));
1062
+ if (this.feedScroll)
1063
+ c.segs(0, h - 1, w, [[` ↓ ${this.feedScroll} satır yukarıdasın · tekerlekle in `, '30;48;5;214']], '', false);
1064
+ return c;
1065
+ }
1066
+ // ── approval inbox ───────────────────────────────────────
1067
+ approvalsPanel(L, H) {
1068
+ this.approvalHits = [];
1069
+ if (!this.pending.length || (L.mode === 'stack' && !this.showApprovals))
1070
+ return undefined;
1071
+ this.approvalSel = Math.min(this.approvalSel, this.pending.length - 1);
1072
+ const x = L.gx + 1, w = Math.max(24, L.gw - 2);
1073
+ const maxItems = Math.max(1, Math.floor((H - 5) / 3));
1074
+ const shown = this.pending.slice(0, maxItems);
1075
+ const h = 3 + shown.length * 3 + (this.pending.length > shown.length ? 1 : 0);
1076
+ const y = L.mode === 'cols' ? Math.max(0, H - 1 - h) : L.paneTop;
1077
+ const c = new Canvas(w, h);
1078
+ const blink = Math.floor(this.now / 600) % 2 === 0;
1079
+ const bs = `0;1;${blink ? '38;5;203' : '38;5;167'}`, bg = ';48;5;234';
1080
+ c.put(0, 0, '╭' + '─'.repeat(w - 2) + '╮', bs);
1081
+ c.put(2, 0, ` ! İZİN BEKLEYENLER (${this.pending.length}) `, bs);
1082
+ for (let i = 1; i < h - 1; i++) {
1083
+ c.put(0, i, '│', bs);
1084
+ c.put(1, i, ' '.repeat(w - 2), `0${bg}`);
1085
+ c.put(w - 1, i, '│', bs);
1086
+ }
1087
+ shown.forEach((ap, i) => {
1088
+ const r = 1 + i * 3, sel = i === this.approvalSel, rbg = sel ? ';48;5;237' : bg;
1089
+ const a = this.hub.agents.get(ap.id);
1090
+ const p = ap.prompt;
1091
+ for (let k = 0; k < 3; k++)
1092
+ c.put(1, r + k, ' '.repeat(w - 2), `0${rbg}`);
1093
+ c.segs(2, r, w - 4, [[sel ? '▸ ' : ' ', `1;${GOLD}`], [ap.id, `1;${hex(a?.profile.color)}`], [` · ${ap.title} · `, DIM], [p?.title ?? 'izin', '1;39']], rbg, false);
1094
+ c.segs(4, r + 1, w - 6, [[p ? (p.body.length ? p.body.join(' · ') : p.question) : 'soru okunamadı — tıkla, terminalde yanıtla', p ? '38;5;223' : DIM]], rbg, false);
1095
+ this.approvalHits.push({ x0: x + 1, x1: x + w - 1, y: y + r, id: ap.id }, { x0: x + 1, x1: x + w - 1, y: y + r + 1, id: ap.id });
1096
+ if (!p)
1097
+ return;
1098
+ // buttons: the exact option text, so "always" never hides what it grants
1099
+ let bx = 4;
1100
+ const pick = (choice, key, color) => {
1101
+ const idx = p.kind === 'yn' ? (choice === 'always' ? -1 : 0) : pickOption(p, choice); // same rule as the keys sent
1102
+ if (idx < 0)
1103
+ return;
1104
+ const label = p.kind === 'yn' ? (choice === 'no' ? 'hayır' : 'evet') : p.options[idx].label;
1105
+ const room = w - 2 - bx;
1106
+ if (room < 8)
1107
+ return;
1108
+ const text = fit(`[${key}] ${label}`, Math.min(room, choice === 'always' ? 60 : 16)).trimEnd();
1109
+ c.segs(bx, r + 2, text.length, [[text, color]], rbg, false);
1110
+ this.approvalHits.push({ x0: x + bx, x1: x + bx + text.length, y: y + r + 2, id: ap.id, choice });
1111
+ bx += text.length + 2;
1112
+ };
1113
+ pick('yes', 'y', `1;${MARK}`);
1114
+ pick('no', 'r', '1;38;5;203');
1115
+ pick('always', 'a', '38;5;179'); // the long label goes last
1116
+ });
1117
+ if (this.pending.length > shown.length)
1118
+ c.segs(2, h - 2, w - 4, [[`+${this.pending.length - shown.length} daha · ^b i ile sırayla`, DIM]], bg, false);
1119
+ c.put(0, h - 1, '╰' + '─'.repeat(w - 2) + '╯', bs);
1120
+ c.put(2, h - 1, ' ^b y onayla · a hep · r reddet · i sıradaki ', `0;${DIM}`);
1121
+ this.approvalsBox = { x, y, w, h };
1122
+ return { c, x, y };
1123
+ }
1124
+ composer(w) {
1125
+ const c = new Canvas(w, 2);
1126
+ const { to } = this.target();
1127
+ const known = to === 'all' || this.hub.agents.has(to);
1128
+ const toSeg = to === 'all' ? ['ortak kutu', `1;${GOLD}`] : [to || '—', known ? `1;${hex(this.hub.agents.get(to)?.profile.color)}` : '1;38;5;203'];
1129
+ c.put(0, 0, '─'.repeat(w), SEP);
1130
+ c.segs(1, 0, w - 2, [[' sen → ', DIM], toSeg, [this.composing ? ' · enter gönder · esc çık · tab @tamamla ' : ' ', DIM]], '', false);
1131
+ const bg = this.composing ? ';48;5;236' : '';
1132
+ const flatDraft = this.draft.replace(/\n/g, ' ⏎ ');
1133
+ if (!this.draft && !this.composing)
1134
+ c.segs(0, 1, w, [[' › ', DIM], ['@agent ile birine, @all ile herkese yaz… (^b c ya da tıkla)', `3;${DIM}`]], bg, false);
1135
+ else {
1136
+ const room = w - 4;
1137
+ const vis = flatDraft.length > room ? '…' + flatDraft.slice(-(room - 1)) : flatDraft;
1138
+ const m = vis.match(/^(@\S+)([\s\S]*)$/);
1139
+ c.segs(0, 1, w, [[' › ', `1;${GOLD}`], ...(m ? [[m[1], toSeg[1]], [m[2], '39']] : [[vis, '39']])], bg, false);
1140
+ }
1141
+ return c;
1142
+ }
1143
+ paneTitle(f) {
1144
+ return [[this.glyph(f), STATUS_STYLE[f.status][1]], [' ' + f.id, '1'], [' · ', DIM], [f.profile.title, hex(f.profile.color)],
1145
+ [f.model ? ' · ' + f.model.replace(/^claude-/, '') : '', DIM], [' · ' + STATUS_STYLE[f.status][2], DIM]];
1146
+ }
1147
+ // ── render ───────────────────────────────────────────────────
1148
+ schedule() {
1149
+ if (this.scheduled)
1150
+ return;
1151
+ this.scheduled = true;
1152
+ setTimeout(() => { this.scheduled = false; this.render(); }, 16);
1153
+ }
1154
+ render() {
1155
+ const hub = this.hub;
1156
+ const W = out.columns, H = out.rows;
1157
+ this.now = Date.now();
1158
+ this.settleDone();
1159
+ const L = this.layout(W, H);
1160
+ this.geo = L;
1161
+ if (hub.size.cols !== L.pw || hub.size.rows !== L.P)
1162
+ hub.resizeAll(L.pw, L.P);
1163
+ const full = L.mode === 'cols';
1164
+ const graph = this.graph(L.gw, L.p, full);
1165
+ const feed = L.fw ? this.feed(L.fw, H - 1) : undefined;
1166
+ const focus = hub.agents.get(hub.focus);
1167
+ let head;
1168
+ if (full) {
1169
+ head = new Canvas(L.pw, 1);
1170
+ if (focus)
1171
+ head.segs(0, 0, L.pw, [[' ▍', this.tint(focus, rgbOf(focus.profile.color) ?? GOLD_RGB, false)], ['SOHBET ', `1;${DIM}`], ...this.paneTitle(focus)], ';48;5;235', false);
1172
+ else
1173
+ head.segs(0, 0, L.pw, [[' ▍SOHBET', `1;${DIM}`]], ';48;5;235', false);
1174
+ }
1175
+ const ov = this.mode === 'pick' ? this.picker(W, H) : undefined;
1176
+ if (!ov)
1177
+ this.pickBox = undefined;
1178
+ const bp = this.showBoard && !ov ? this.boardPanel(W, H, full ? 0 : L.p.G) : undefined;
1179
+ if (!bp)
1180
+ this.boardBox = undefined;
1181
+ this.pending = hub.approvals();
1182
+ if (!this.pending.length)
1183
+ this.showApprovals = false;
1184
+ const ap = !ov ? this.approvalsPanel(L, H) : undefined;
1185
+ if (!ap)
1186
+ this.approvalsBox = undefined;
1187
+ const comp = this.composer(L.pw);
1188
+ const chatRow = (y, a, b) => y === L.cy || y === L.cy + 1 ? comp.row(y - L.cy, a, b) : this.paneRow(focus, y - L.paneTop, L.P, L.pw, a, b);
1189
+ // one screen row, clipped to [x0, x1): each column renders only its own slice
1190
+ const base = (y, x0, x1) => {
1191
+ if (!full)
1192
+ return y <= L.p.G ? graph.row(y, x0, x1) : chatRow(y, x0, x1);
1193
+ const cols = [];
1194
+ if (feed)
1195
+ cols.push([L.fx, L.fw, (a, b) => feed.row(y, a, b)], [L.fx + L.fw, 1, () => `\x1b[${SEP}m│\x1b[0m`]);
1196
+ cols.push([L.gx, L.gw, (a, b) => graph.row(y, a, b)], [L.gx + L.gw, 1, () => `\x1b[${SEP}m│\x1b[0m`]);
1197
+ cols.push([L.px, L.pw, (a, b) => (y === 0 ? head.row(0, a, b) : chatRow(y, a, b))]);
1198
+ let s = '';
1199
+ for (const [cx, cw, fn] of cols) {
1200
+ const a = Math.max(x0, cx), b = Math.min(x1, cx + cw);
1201
+ if (a < b)
1202
+ s += fn(a - cx, b - cx);
1203
+ }
1204
+ return s;
1205
+ };
1206
+ const lines = [];
1207
+ const overlays = [ov, bp, ap].filter((o) => !!o);
1208
+ for (let y = 0; y < H - 1; y++) {
1209
+ // overlays covering this row, left to right; an overlay starting inside an earlier one is skipped
1210
+ const here = overlays.filter((o) => y >= o.y && y < o.y + o.c.h).sort((p, q) => p.x - q.x);
1211
+ let s = '', x = 0;
1212
+ for (const o of here) {
1213
+ if (o.x < x)
1214
+ continue;
1215
+ s += base(y, x, o.x) + o.c.row(y - o.y);
1216
+ x = o.x + o.c.w;
1217
+ }
1218
+ lines.push(s + base(y, x, W));
1219
+ }
1220
+ lines.push(this.statusBar(W));
1221
+ let buf = '\x1b[?2026h\x1b[?25l'; // synchronized update
1222
+ for (let y = 0; y < lines.length; y++) {
1223
+ if (this.prev[y] !== lines[y])
1224
+ buf += `${ESC}${y + 1};1H${lines[y]}\x1b[0m`;
1225
+ }
1226
+ this.prev = lines;
1227
+ // outer mouse mode: always clicks (1000) in SGR (1006); motion only if the focused agent wants it
1228
+ const want = focus?.term.modes.mouseTrackingMode;
1229
+ const mouse = this.selectMode ? '\x1b[?1000l\x1b[?1006l'
1230
+ : '\x1b[?1000h\x1b[?1006h' + (want === 'drag' ? '\x1b[?1002h' : want === 'any' ? '\x1b[?1003h' : '');
1231
+ if (mouse !== this.outerMouse) {
1232
+ buf += '\x1b[?1002l\x1b[?1003l' + mouse;
1233
+ this.outerMouse = mouse;
1234
+ }
1235
+ if (focus) {
1236
+ const appCursor = focus.term.modes.applicationCursorKeysMode;
1237
+ if (appCursor !== this.outerAppCursor) {
1238
+ buf += appCursor ? '\x1b[?1h' : '\x1b[?1l';
1239
+ this.outerAppCursor = appCursor;
1240
+ }
1241
+ const b = focus.term.buffer.active;
1242
+ if (this.composing && this.mode === 'normal' && !this.prefix) {
1243
+ const n = Math.min([...this.draft.replace(/\n/g, ' ⏎ ')].length, L.pw - 4);
1244
+ buf += `${ESC}${L.cy + 2};${L.px + 4 + n}H\x1b[?25h`;
1245
+ }
1246
+ else if (focus.cursorVisible && this.mode === 'normal' && !this.prefix && b.cursorY < L.P && b.cursorX < L.pw) {
1247
+ buf += `${ESC}${L.paneTop + b.cursorY + 1};${L.px + b.cursorX + 1}H\x1b[?25h`;
1248
+ }
1249
+ }
1250
+ out.write(buf + '\x1b[?2026l');
1251
+ // keep frames coming only while something is moving
1252
+ if (this.animating() && !this.animPending) {
1253
+ this.animPending = true;
1254
+ setTimeout(() => { this.animPending = false; this.schedule(); }, FRAME_MS);
1255
+ }
1256
+ }
1257
+ // One pane row, clipped to columns [x0, x1) of a pane W cells wide.
1258
+ paneRow(a, y, P, W, x0, x1) {
1259
+ const w = x1 - x0;
1260
+ if (w <= 0)
1261
+ return '';
1262
+ const plain = (s, sgr = '0') => `\x1b[${sgr}m${fit(s, W).slice(x0, x1)}\x1b[0m`;
1263
+ if (!a) {
1264
+ const hint = this.hub.orchestratorId ? '' : 'orkestratör seçmek için + baloncuğuna tıkla ya da Enter\'a bas';
1265
+ const pad = ' '.repeat(Math.max(0, Math.floor((W - hint.length) / 2)));
1266
+ return plain(y === Math.floor(P / 2) ? pad + hint : '', `0;${DIM}`);
1267
+ }
1268
+ if (a.status === 'exited' && y === P - 1)
1269
+ return plain(` süreç kapandı (kod ${a.exitCode}) — ^b x ile kaldır`, '0;7');
1270
+ const b = a.term.buffer.active;
1271
+ const line = b.getLine(b.viewportY + y);
1272
+ if (!line)
1273
+ return ' '.repeat(w);
1274
+ const cell = b.getNullCell();
1275
+ let s = '', last = '', col = 0;
1276
+ for (let x = 0; x < a.term.cols && col < x1; x++) {
1277
+ line.getCell(x, cell);
1278
+ const cw = cell.getWidth();
1279
+ if (cw === 0)
1280
+ continue;
1281
+ if (col + cw <= x0) {
1282
+ col += cw;
1283
+ continue;
1284
+ }
1285
+ if (col < x0 || col + cw > x1) { // wide glyph cut by the clip edge
1286
+ const n = Math.min(col + cw, x1) - Math.max(col, x0);
1287
+ s += '\x1b[0m' + ' '.repeat(n);
1288
+ last = '0';
1289
+ col += cw;
1290
+ continue;
1291
+ }
1292
+ const sgr = sgrOf(cell);
1293
+ if (sgr !== last) {
1294
+ s += `\x1b[${sgr}m`;
1295
+ last = sgr;
1296
+ }
1297
+ s += cell.getChars() || ' ';
1298
+ col += cw;
1299
+ }
1300
+ return s + '\x1b[0m' + ' '.repeat(Math.max(0, x1 - Math.max(col, x0)));
1301
+ }
1302
+ usage = [];
1303
+ usageAt = 0;
1304
+ refreshUsage() {
1305
+ if (this.now - this.usageAt < USAGE_MS)
1306
+ return;
1307
+ this.usageAt = this.now;
1308
+ setImmediate(() => {
1309
+ try {
1310
+ this.usage = this.hub.usage();
1311
+ }
1312
+ catch {
1313
+ this.usage = [];
1314
+ }
1315
+ this.schedule();
1316
+ });
1317
+ }
1318
+ statusBar(w) {
1319
+ const hub = this.hub;
1320
+ let left;
1321
+ if (this.showBoard)
1322
+ hub.markBoardRead('user'); // the open panel counts as read
1323
+ const unreadBoard = hub.boardUnread('user');
1324
+ if (this.mode === 'pick')
1325
+ left = ' ↑↓ / 1-9 seç · enter onayla · esc iptal ';
1326
+ else if (this.selectMode && !this.prefix)
1327
+ left = ' SEÇİM MODU · fareyle seç ve kopyala (⌘C) · çıkmak için ^b s ';
1328
+ else if (this.prefix)
1329
+ left = ' ^b → c:mesaj y/a/r:izin i:sıradaki n:yeni 1-9:seç j/k:gez o:orkestra m:kutu l:konuşmalar s:kopyala x:kapat q:çık ';
1330
+ else
1331
+ left = ` ^b komut · ${hub.focus || '-'}${this.pending.length ? ` ! ${this.pending.length} izin bekliyor (^b y/a/r${this.geo?.mode === 'stack' ? ', ^b i' : ''})` : ''}${unreadBoard ? ` ▤${unreadBoard} ortak kutu (^b m)` : ''} `;
1332
+ const bg = this.prefix || this.mode !== 'normal' || this.selectMode ? '\x1b[0;30;48;5;214m' : '\x1b[0;38;5;250;48;5;235m';
1333
+ const right = this.notice ? ` ${this.notice} ` : '';
1334
+ const room = Math.max(0, w - right.length);
1335
+ if (room < 10)
1336
+ return `${bg}${fit(left, w)}\x1b[0m`;
1337
+ const l = left.length <= room ? left : fit(left, room);
1338
+ const last = hub.log.at(-1);
1339
+ const feedVisible = this.geo?.fw > 0;
1340
+ this.refreshUsage();
1341
+ const ub = this.usage.length && !this.prefix && this.mode === 'normal' && !this.selectMode ? usageBarSegs(this.usage, room - l.length - 1) : null;
1342
+ if (ub?.len) {
1343
+ const pad = room - l.length - ub.len - 1;
1344
+ const body = ub.segs.map(([t, g]) => `\x1b[${g};48;5;235m${t}`).join('');
1345
+ return `${bg}${l}\x1b[0;48;5;235m ${body}\x1b[0;38;5;244;48;5;235m${' '.repeat(Math.max(0, pad))}\x1b[0;30;48;5;223m${right}\x1b[0m`;
1346
+ }
1347
+ const mid = !feedVisible && !right && !this.prefix && this.mode === 'normal' && last ? ` ✉ ${last.from}→${last.to}: ${flat(last.text)}` : '';
1348
+ return `${bg}${l}\x1b[0;38;5;244;48;5;235m${fit(mid, room - l.length)}\x1b[0;30;48;5;223m${right}\x1b[0m`;
1349
+ }
1350
+ }
1351
+ function sgrOf(c) {
1352
+ const p = ['0'];
1353
+ if (c.isBold())
1354
+ p.push('1');
1355
+ if (c.isDim())
1356
+ p.push('2');
1357
+ if (c.isItalic())
1358
+ p.push('3');
1359
+ if (c.isUnderline())
1360
+ p.push('4');
1361
+ if (c.isInverse())
1362
+ p.push('7');
1363
+ if (c.isStrikethrough?.())
1364
+ p.push('9');
1365
+ if (c.isFgRGB()) {
1366
+ const v = c.getFgColor();
1367
+ p.push(`38;2;${(v >> 16) & 255};${(v >> 8) & 255};${v & 255}`);
1368
+ }
1369
+ else if (c.isFgPalette()) {
1370
+ const v = c.getFgColor();
1371
+ p.push(v < 8 ? `${30 + v}` : v < 16 ? `${82 + v}` : `38;5;${v}`);
1372
+ }
1373
+ if (c.isBgRGB()) {
1374
+ const v = c.getBgColor();
1375
+ p.push(`48;2;${(v >> 16) & 255};${(v >> 8) & 255};${v & 255}`);
1376
+ }
1377
+ else if (c.isBgPalette()) {
1378
+ const v = c.getBgColor();
1379
+ p.push(v < 8 ? `${40 + v}` : v < 16 ? `${92 + v}` : `48;5;${v}`);
1380
+ }
1381
+ return p.join(';');
1382
+ }