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/LICENSE +21 -0
- package/README.md +193 -2
- package/bin/coban +10 -0
- package/dist/agent.js +161 -0
- package/dist/cli.js +201 -0
- package/dist/config.js +82 -0
- package/dist/context.js +174 -0
- package/dist/hub.js +543 -0
- package/dist/prompt.js +119 -0
- package/dist/tui.js +1382 -0
- package/dist/usage.js +100 -0
- package/package.json +58 -4
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
|
+
}
|