netheriteai-code 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/tui.js ADDED
@@ -0,0 +1,1490 @@
1
+ const ESC = "\u001b[";
2
+ const RESET = "\u001b[0m";
3
+ const BEL = "\u0007";
4
+
5
+ const COLORS = {
6
+ fg: "\u001b[38;2;232;232;232m",
7
+ muted: "\u001b[38;2;138;138;138m",
8
+ dim: "\u001b[38;2;103;103;103m",
9
+ blue: "\u001b[38;2;83;149;255m",
10
+ blueSoft: "\u001b[38;2;66;118;204m",
11
+ blueFaint: "\u001b[38;2;49;82;138m",
12
+ amber: "\u001b[38;2;217;170;78m",
13
+ bg: "\u001b[48;2;0;0;0m",
14
+ panel: "\u001b[48;2;24;24;24m",
15
+ panelAlt: "\u001b[48;2;31;31;31m",
16
+ peach: "\u001b[48;2;246;180;130m",
17
+ blackFg: "\u001b[38;2;0;0;0m",
18
+ };
19
+
20
+ const LOGO_TOP = [
21
+ "NN NN EEEEEEE TTTTTTT H H EEEEEEE RRRRR IIIII TTTTTTT EEEEEEE",
22
+ "NNN NN EE T H H EE RR RR III T EE ",
23
+ "NN N NN EEEEE T HHHHH EEEEE RRRRR III T EEEEE ",
24
+ "NN NNN EE T H H EE RR RR III T EE ",
25
+ "NN NN EEEEEEE T H H EEEEEEE RR RR IIIII T EEEEEEE",
26
+ ];
27
+
28
+ const LOGO_BOTTOM = [
29
+ " CCC OOO DDDD EEEEEEE",
30
+ "CC OO OO DD DD EE ",
31
+ "CC OO OO DD DD EEEEE ",
32
+ "CC OO OO DD DD EE ",
33
+ " CCC OOO DDDD EEEEEEE",
34
+ ];
35
+
36
+ const SLASH_COMMANDS = [
37
+ { command: "/agents", description: "Switch agent" },
38
+ { command: "/clear", description: "Clear transcript" },
39
+ { command: "/compact", description: "Compact session" },
40
+ { command: "/copy", description: "Copy session transcript" },
41
+ { command: "/help", description: "Help" },
42
+ { command: "/model", description: "Switch model" },
43
+ { command: "/new", description: "New session" },
44
+ { command: "/pwd", description: "Show workspace" },
45
+ ];
46
+
47
+ function setTerminalTitle(title) {
48
+ process.stdout.write(`\u001b]2;${title}${BEL}`);
49
+ }
50
+
51
+ function move(row, col) {
52
+ return `${ESC}${row};${col}H`;
53
+ }
54
+
55
+ function style(text, ...codes) {
56
+ const hasBackground = codes.some((code) => code.includes("[48;"));
57
+ const applied = hasBackground ? codes : [...codes, COLORS.bg];
58
+ return `${applied.join("")}${text}${RESET}`;
59
+ }
60
+
61
+ function truncate(text, width) {
62
+ const value = String(text ?? "");
63
+ if (width <= 0) return "";
64
+ if (value.length <= width) return value;
65
+ return width === 1 ? value.slice(0, 1) : `${value.slice(0, width - 1)}…`;
66
+ }
67
+
68
+ function pad(text, width) {
69
+ const value = truncate(text, width);
70
+ return value + " ".repeat(Math.max(0, width - value.length));
71
+ }
72
+
73
+ function wrapText(text, width) {
74
+ const words = String(text ?? "").split(/\s+/);
75
+ const lines = [];
76
+ let current = "";
77
+ for (const word of words) {
78
+ const next = current ? `${current} ${word}` : word;
79
+ if (next.length <= width) current = next;
80
+ else {
81
+ if (current) lines.push(current);
82
+ current = word;
83
+ }
84
+ }
85
+ if (current) lines.push(current);
86
+ return lines.length ? lines : [""];
87
+ }
88
+
89
+ function paintStatusBadge(frame, row, col, panelColor, agentName) {
90
+ frame.paintText(row, col, "▣", `${panelColor}${COLORS.blue}`);
91
+ frame.paintText(row, col + 2, agentName, `${panelColor}${COLORS.fg}`);
92
+ frame.paintText(row, col + 2 + agentName.length + 1, "·", `${panelColor}${COLORS.dim}`);
93
+ }
94
+
95
+ function displayModelName(model) {
96
+ const name = String(model || "");
97
+ if (name.toLowerCase().includes("glm5") || name.toLowerCase().includes("glm-5")) return "NetheriteAI:Code";
98
+ return name;
99
+ }
100
+
101
+ function safeJsonParse(value, fallback = {}) {
102
+ try {
103
+ return JSON.parse(value);
104
+ } catch {
105
+ return fallback;
106
+ }
107
+ }
108
+
109
+ function compactCodeLines(text, maxLines = 8, width = 120) {
110
+ const lines = String(text || "").replace(/\r/g, "").split("\n");
111
+ const clipped = lines.slice(0, maxLines).map((line) => truncate(line, width));
112
+ if (lines.length > maxLines) {
113
+ clipped.push(`... ${lines.length - maxLines} more line${lines.length - maxLines === 1 ? "" : "s"}`);
114
+ }
115
+ return clipped;
116
+ }
117
+
118
+ function visibleCodeLines(text, width) {
119
+ return String(text || "").replace(/\r/g, "").split("\n").map((line) => truncate(line, width));
120
+ }
121
+
122
+ function compactWrappedText(text, width, maxLines = 6) {
123
+ const wrapped = wrapText(text, width);
124
+ return wrapped.slice(0, maxLines);
125
+ }
126
+
127
+ function getInputLines(text, width) {
128
+ return wrapText(text || "", Math.max(1, width));
129
+ }
130
+
131
+ function paintInputCursor(frame, row, col) {
132
+ frame.paintText(row, col, " ", `\u001b[48;2;232;232;232m\u001b[38;2;24;24;24m`);
133
+ }
134
+
135
+ function keywordColor(word) {
136
+ const keywords = new Set([
137
+ "const", "let", "var", "function", "return", "if", "else", "for", "while", "switch", "case",
138
+ "break", "continue", "class", "new", "import", "from", "export", "default", "async", "await",
139
+ "try", "catch", "finally", "throw", "true", "false", "null", "undefined",
140
+ ]);
141
+ return keywords.has(word) ? COLORS.blue : COLORS.fg;
142
+ }
143
+
144
+ function codeSegments(line) {
145
+ const text = String(line ?? "");
146
+ const commentIndex = text.indexOf("//");
147
+ const source = commentIndex === -1 ? text : text.slice(0, commentIndex);
148
+ const segments = [];
149
+ const tokenPattern = /(".*?"|'.*?'|`.*?`|\b[A-Za-z_][A-Za-z0-9_]*\b)/g;
150
+ let lastIndex = 0;
151
+ let match = tokenPattern.exec(source);
152
+ while (match) {
153
+ if (match.index > lastIndex) {
154
+ segments.push({ text: source.slice(lastIndex, match.index), color: COLORS.fg });
155
+ }
156
+ const token = match[0];
157
+ const isString = token.startsWith("\"") || token.startsWith("'") || token.startsWith("`");
158
+ segments.push({ text: token, color: isString ? COLORS.amber : keywordColor(token) });
159
+ lastIndex = match.index + token.length;
160
+ match = tokenPattern.exec(source);
161
+ }
162
+ if (lastIndex < source.length) {
163
+ segments.push({ text: source.slice(lastIndex), color: COLORS.fg });
164
+ }
165
+ if (commentIndex !== -1) {
166
+ segments.push({ text: text.slice(commentIndex), color: COLORS.dim });
167
+ }
168
+ return segments.length ? segments : [{ text, color: COLORS.fg }];
169
+ }
170
+
171
+ function paintCodeLine(frame, row, col, text, panelColor, width) {
172
+ const clipped = truncate(text, width);
173
+ let cursor = col;
174
+ for (const segment of codeSegments(clipped)) {
175
+ frame.paintText(row, cursor, segment.text, `${panelColor}${segment.color}`);
176
+ cursor += segment.text.length;
177
+ }
178
+ }
179
+
180
+ function extractLiveCode(text) {
181
+ const source = String(text || "").replace(/\r/g, "");
182
+ const fenced = source.match(/```[a-zA-Z0-9_-]*\n([\s\S]*?)```/);
183
+ if (fenced?.[1]?.trim()) {
184
+ return fenced[1].trimEnd();
185
+ }
186
+
187
+ const lines = source.split("\n");
188
+ const codeSignals = [
189
+ /<!DOCTYPE html>/i,
190
+ /<html|<head|<body|<div|<script|<style/i,
191
+ /\b(function|const|let|var|return|class|import|export)\b/,
192
+ /[{}();]/,
193
+ /^\s{2,}\S/m,
194
+ ];
195
+ const score = codeSignals.reduce((sum, pattern) => sum + (pattern.test(source) ? 1 : 0), 0);
196
+ if (score >= 2 && lines.length >= 2) {
197
+ return source.trimEnd();
198
+ }
199
+
200
+ return "";
201
+ }
202
+
203
+ function getToolPreview(name, rawArgs, contentWidth) {
204
+ const args = safeJsonParse(rawArgs, {});
205
+ const previewWidth = Math.max(20, contentWidth - 12);
206
+
207
+ if (name === "write_file" || name === "create_file" || name === "append_file") {
208
+ return {
209
+ command: `${name.replace("_file", "")} ${args.path || ""}`.trim(),
210
+ output: compactCodeLines(args.content || "", 8, previewWidth),
211
+ outputKind: "code",
212
+ pendingLabel: "Writing",
213
+ };
214
+ }
215
+
216
+ if (name === "edit_file") {
217
+ return {
218
+ command: `edit ${args.path || ""}`.trim(),
219
+ output: compactCodeLines(args.newText || "", 8, previewWidth),
220
+ outputKind: "code",
221
+ pendingLabel: "Editing",
222
+ };
223
+ }
224
+
225
+ if (name === "run_command" || name === "batch_command") {
226
+ return {
227
+ command: args.commandLine || [args.command, ...(args.args || [])].filter(Boolean).join(" ") || name,
228
+ output: ["Running command..."],
229
+ outputKind: "code",
230
+ pendingLabel: "Executing",
231
+ };
232
+ }
233
+
234
+ return {
235
+ command: `${name} ${rawArgs || ""}`.trim(),
236
+ output: ["Working..."],
237
+ outputKind: "text",
238
+ pendingLabel: "Working",
239
+ };
240
+ }
241
+
242
+ function parseAssistantBlocks(text) {
243
+ const source = String(text ?? "").replace(/\r/g, "");
244
+ const parts = source.split("```");
245
+ const blocks = [];
246
+
247
+ for (let index = 0; index < parts.length; index += 1) {
248
+ const part = parts[index];
249
+ if (!part.trim()) continue;
250
+
251
+ if (index % 2 === 1) {
252
+ const code = part.split("\n").slice(1).join("\n") || part;
253
+ blocks.push({ kind: "code", text: code.trimEnd() });
254
+ continue;
255
+ }
256
+
257
+ const normalized = part
258
+ .replace(/\*\*(.*?)\*\*/g, "$1")
259
+ .replace(/`([^`]+)`/g, "$1")
260
+ .split("\n")
261
+ .map((line) => line.trimEnd());
262
+
263
+ let paragraph = [];
264
+ for (const line of normalized) {
265
+ if (!line.trim()) {
266
+ if (paragraph.length) {
267
+ blocks.push({ kind: "text", text: paragraph.join(" ").trim() });
268
+ paragraph = [];
269
+ }
270
+ continue;
271
+ }
272
+
273
+ if (/^[-*]\s+/.test(line) || /^\d+\.\s+/.test(line)) {
274
+ if (paragraph.length) {
275
+ blocks.push({ kind: "text", text: paragraph.join(" ").trim() });
276
+ paragraph = [];
277
+ }
278
+ blocks.push({ kind: "bullet", text: line.replace(/^[-*]\s+/, "").trim() });
279
+ continue;
280
+ }
281
+
282
+ paragraph.push(line.trim());
283
+ }
284
+
285
+ if (paragraph.length) {
286
+ blocks.push({ kind: "text", text: paragraph.join(" ").trim() });
287
+ }
288
+ }
289
+
290
+ return blocks.length ? blocks : [{ kind: "text", text: source.trim() }];
291
+ }
292
+
293
+ function flattenRenderRows(items, contentWidth) {
294
+ const rows = [];
295
+ let groupId = 0;
296
+ for (const item of items) {
297
+ if (item.kind === "header") {
298
+ rows.push({ kind: "header_top", groupId: -1 });
299
+ rows.push({ kind: "header_line", text: item.text, meta: item.meta, groupId: -1 });
300
+ rows.push({ kind: "header_bottom", groupId: -1 });
301
+ rows.push({ kind: "spacer" });
302
+ continue;
303
+ }
304
+ if (item.kind === "user_box") {
305
+ groupId += 1;
306
+ rows.push({ kind: "user_top", groupId });
307
+ for (const line of item.lines) rows.push({ kind: "user_line", text: line, groupId });
308
+ rows.push({ kind: "user_bottom", groupId });
309
+ rows.push({ kind: "spacer" });
310
+ continue;
311
+ }
312
+ if (item.kind === "thinking") {
313
+ groupId += 1;
314
+ item.lines.forEach((line, index) => rows.push({ kind: "thinking_line", text: line, first: index === 0, groupId }));
315
+ rows.push({ kind: "spacer" });
316
+ continue;
317
+ }
318
+ if (item.kind === "code_block") {
319
+ groupId += 1;
320
+ rows.push({ kind: "code_top", groupId });
321
+ for (const line of item.lines) rows.push({ kind: "code_line", text: line, groupId });
322
+ rows.push({ kind: "code_bottom", groupId });
323
+ rows.push({ kind: "spacer" });
324
+ continue;
325
+ }
326
+ if (item.kind === "tool_block") {
327
+ groupId += 1;
328
+ rows.push({ kind: "tool_top", groupId });
329
+ rows.push({ kind: "tool_label", text: item.label || "# Execute", groupId });
330
+ for (const line of item.command) rows.push({ kind: "tool_command", text: line, groupId });
331
+ if (item.output.length) rows.push({ kind: "tool_gap", groupId });
332
+ for (const line of item.output) rows.push({ kind: "tool_output", text: line, outputKind: item.outputKind || "text", groupId });
333
+ rows.push({ kind: "tool_bottom", groupId });
334
+ rows.push({ kind: "spacer" });
335
+ continue;
336
+ }
337
+ if (item.kind === "assistant" || item.kind === "assistant_live") {
338
+ groupId += 1;
339
+ rows.push({ kind: item.kind, text: item.text, groupId });
340
+ continue;
341
+ }
342
+ if (item.kind === "meta") {
343
+ rows.push({ kind: "meta", text: item.text, groupId });
344
+ rows.push({ kind: "spacer" });
345
+ continue;
346
+ }
347
+ rows.push({ kind: "spacer" });
348
+ }
349
+ return rows.map((row) => ({
350
+ ...row,
351
+ text: row.text ? truncate(row.text, contentWidth - 8) : row.text,
352
+ meta: row.meta ? truncate(row.meta, 24) : row.meta,
353
+ }));
354
+ }
355
+
356
+ function center(total, size) {
357
+ return Math.max(1, Math.floor((total - size) / 2));
358
+ }
359
+
360
+ function clamp(value, min, max) {
361
+ return Math.max(min, Math.min(max, value));
362
+ }
363
+
364
+ function parseMouseSequence(text) {
365
+ const match = text.match(/^\u001b\[<(\d+);(\d+);(\d+)([Mm])$/);
366
+ if (!match) return null;
367
+ return {
368
+ button: Number(match[1]),
369
+ col: Number(match[2]),
370
+ row: Number(match[3]),
371
+ state: match[4],
372
+ };
373
+ }
374
+
375
+ function parseMousePrefix(text) {
376
+ const match = text.match(/^\u001b\[<(\d+);(\d+);(\d+)([Mm])/);
377
+ if (!match) return null;
378
+ return {
379
+ raw: match[0],
380
+ button: Number(match[1]),
381
+ col: Number(match[2]),
382
+ row: Number(match[3]),
383
+ state: match[4],
384
+ };
385
+ }
386
+
387
+ function paintBusyIndicator(frame, row, col, now = Date.now()) {
388
+ const width = 8;
389
+ const cycleMs = 680;
390
+ const travel = width + 7;
391
+ const t = now % cycleMs;
392
+ const head = -3 + (t / cycleMs) * travel;
393
+
394
+ for (let index = 0; index < width; index += 1) {
395
+ const distance = Math.abs(index - head);
396
+ let color = COLORS.dim;
397
+ let char = "·";
398
+ if (distance < 0.7) {
399
+ color = COLORS.blue;
400
+ char = "■";
401
+ } else if (distance < 1.5) {
402
+ color = COLORS.blueSoft;
403
+ char = "■";
404
+ } else if (distance < 2.3) {
405
+ color = COLORS.blueFaint;
406
+ char = "▪";
407
+ }
408
+ frame.paintText(row, col + index, char, `${COLORS.bg}${color}`);
409
+ }
410
+ }
411
+
412
+ function makeFrame(width, height) {
413
+ const rows = Array.from({ length: height }, () => Array(width).fill(" "));
414
+ const styles = Array.from({ length: height }, () => Array(width).fill(`${COLORS.bg}${COLORS.fg}`));
415
+
416
+ function paintText(row, col, text, code = `${COLORS.bg}${COLORS.fg}`) {
417
+ if (row < 1 || row > height) return;
418
+ const chars = [...text];
419
+ for (let i = 0; i < chars.length; i += 1) {
420
+ const x = col + i;
421
+ if (x < 1 || x > width) continue;
422
+ rows[row - 1][x - 1] = chars[i];
423
+ styles[row - 1][x - 1] = code;
424
+ }
425
+ }
426
+
427
+ function fillRect(row, col, w, h, code = `${COLORS.panel}${COLORS.fg}`, char = " ") {
428
+ for (let y = 0; y < h; y += 1) {
429
+ for (let x = 0; x < w; x += 1) {
430
+ const rr = row + y;
431
+ const cc = col + x;
432
+ if (rr < 1 || rr > height || cc < 1 || cc > width) continue;
433
+ rows[rr - 1][cc - 1] = char;
434
+ styles[rr - 1][cc - 1] = code;
435
+ }
436
+ }
437
+ }
438
+
439
+ function flush(cursorRow, cursorCol) {
440
+ const output = [`${ESC}2J${ESC}H${COLORS.bg}`];
441
+ for (let r = 0; r < height; r += 1) {
442
+ output.push(move(r + 1, 1));
443
+ let currentStyle = "";
444
+ let line = "";
445
+ for (let c = 0; c < width; c += 1) {
446
+ const nextStyle = styles[r][c];
447
+ if (nextStyle !== currentStyle) {
448
+ if (line) output.push(line);
449
+ output.push(nextStyle);
450
+ line = rows[r][c];
451
+ currentStyle = nextStyle;
452
+ } else {
453
+ line += rows[r][c];
454
+ }
455
+ }
456
+ output.push(line, RESET);
457
+ }
458
+ output.push(move(cursorRow, cursorCol));
459
+ process.stdout.write(output.join(""));
460
+ }
461
+
462
+ return { paintText, fillRect, flush };
463
+ }
464
+
465
+ export function runTui({
466
+ model,
467
+ getModel,
468
+ agentName,
469
+ providerName,
470
+ version,
471
+ workspaceLabel,
472
+ sessionId,
473
+ initialSessionTitle = "",
474
+ initialTranscript = [],
475
+ onTranscriptChange,
476
+ onTitleChange,
477
+ onSubmit,
478
+ }) {
479
+ return new Promise((resolve) => {
480
+ if (process.stdin.isTTY) process.stdin.setRawMode(true);
481
+ process.stdout.write("\u001b[?1049h\u001b[?25l\u001b[?1000h\u001b[?1006h");
482
+ setTerminalTitle("NetheriteAI:Code by hurdacu");
483
+
484
+ let mode = "home";
485
+ let input = "";
486
+ let busy = false;
487
+ let status = 'Ask anything... "Fix broken tests"';
488
+ let activity = "";
489
+ let streamingText = "";
490
+ let sessionTitle = initialSessionTitle || "New Session";
491
+ let scrollOffset = 0;
492
+ let busyAnimationTime = Date.now();
493
+ let historyIndex = -1;
494
+ let historyDraft = "";
495
+ let picker = null;
496
+ let slashIndex = 0;
497
+ let selectedGroup = 1;
498
+ let cursorVisible = true;
499
+ let lastCursorToggle = Date.now();
500
+ let currentAbortController = null;
501
+ let autoFollow = true;
502
+ let busyStartedAt = 0;
503
+ let lastProgressAt = 0;
504
+ let currentPhase = "";
505
+ const promptHistory = [];
506
+ const transcript = Array.isArray(initialTranscript) ? [...initialTranscript] : [];
507
+ let liveReasoning = "";
508
+ let inputBuffer = "";
509
+
510
+ const timer = setInterval(() => {
511
+ const now = Date.now();
512
+ const hasAnimatingTool = transcript.some((item) =>
513
+ item.role === "tool"
514
+ && item.pending
515
+ && item.streamSource
516
+ && ((item.streamOffset || 0) < item.streamSource.length || item.finalPreview),
517
+ );
518
+ if (now - lastCursorToggle >= 650) {
519
+ cursorVisible = !cursorVisible;
520
+ lastCursorToggle = now;
521
+ }
522
+ if (busy) {
523
+ busyAnimationTime = now;
524
+ const elapsed = ((now - busyStartedAt) / 1000).toFixed(1);
525
+ const idleFor = lastProgressAt ? ((now - lastProgressAt) / 1000).toFixed(1) : "0.0";
526
+ const phase = currentPhase || activity || status || "Working";
527
+ upsertLiveMessage("status_live", `${phase} · ${elapsed}s elapsed · ${idleFor}s since update`);
528
+ } else {
529
+ removeLiveMessage("status_live");
530
+ }
531
+ if (busy || hasAnimatingTool) {
532
+ for (const item of transcript) {
533
+ if (item.role !== "tool" || !item.pending || !item.streamSource) continue;
534
+ const nextOffset = Math.min(item.streamSource.length, (item.streamOffset || 0) + 140);
535
+ if (nextOffset !== item.streamOffset) {
536
+ item.streamOffset = nextOffset;
537
+ item.outputLines = visibleCodeLines(item.streamSource.slice(0, nextOffset), Math.max(20, (process.stdout.columns || 120) - 18));
538
+ onTranscriptChange?.([...transcript]);
539
+ }
540
+ if (item.streamOffset >= item.streamSource.length && item.finalPreview) {
541
+ finalizeToolMessage(item, item.finalPreview);
542
+ onTranscriptChange?.([...transcript]);
543
+ }
544
+ }
545
+ }
546
+ render();
547
+ }, 45);
548
+
549
+ function addPromptHistory(prompt) {
550
+ if (!prompt.trim()) return;
551
+ if (promptHistory[0] !== prompt) promptHistory.unshift(prompt);
552
+ historyIndex = -1;
553
+ historyDraft = "";
554
+ }
555
+
556
+ function makeTitleFromPrompt(prompt, maxLength = 36) {
557
+ const text = String(prompt || "").trim().replace(/\s+/g, " ");
558
+ if (!text) return "";
559
+ if (text.length <= maxLength) return text;
560
+ return `${text.slice(0, Math.max(0, maxLength - 3))}...`;
561
+ }
562
+
563
+ function ensureSessionTitle(prompt) {
564
+ if (sessionTitle && sessionTitle !== "New Session") return;
565
+ const nextTitle = makeTitleFromPrompt(prompt);
566
+ if (!nextTitle) return;
567
+ sessionTitle = nextTitle;
568
+ onTitleChange?.(nextTitle);
569
+ }
570
+
571
+ function resetTranscriptView() {
572
+ transcript.length = 0;
573
+ streamingText = "";
574
+ liveReasoning = "";
575
+ activity = "";
576
+ status = 'Ask anything... "Fix broken tests"';
577
+ scrollToBottom();
578
+ onTranscriptChange?.([]);
579
+ }
580
+
581
+ function resetSessionView() {
582
+ resetTranscriptView();
583
+ sessionTitle = "New Session";
584
+ onTitleChange?.("");
585
+ }
586
+
587
+ function browseHistory(direction) {
588
+ if (!promptHistory.length) return;
589
+ if (historyIndex === -1) {
590
+ historyDraft = input;
591
+ }
592
+ const next = Math.max(-1, Math.min(promptHistory.length - 1, historyIndex + direction));
593
+ historyIndex = next;
594
+ input = next === -1 ? historyDraft : promptHistory[next];
595
+ }
596
+
597
+ function pushMessage(role, text, meta = "", extra = {}) {
598
+ transcript.push({ role, text, meta, ...extra });
599
+ if (transcript.length > 200) transcript.shift();
600
+ onTranscriptChange?.([...transcript]);
601
+ autoFollow = true;
602
+ }
603
+
604
+ function upsertLiveMessage(role, text, meta = "", extra = {}) {
605
+ const existingIndex = transcript.findIndex((item) => item.role === role);
606
+ if (existingIndex !== -1) {
607
+ transcript[existingIndex] = { ...transcript[existingIndex], text, meta, ...extra };
608
+ } else {
609
+ transcript.push({ role, text, meta, ...extra });
610
+ }
611
+ if (transcript.length > 200) transcript.shift();
612
+ onTranscriptChange?.([...transcript]);
613
+ autoFollow = true;
614
+ }
615
+
616
+ function removeLiveMessage(role) {
617
+ const index = transcript.findIndex((item) => item.role === role);
618
+ if (index !== -1) {
619
+ transcript.splice(index, 1);
620
+ onTranscriptChange?.([...transcript]);
621
+ }
622
+ }
623
+
624
+ function finalizeToolMessage(item, preview) {
625
+ if (!preview.preserveCommand && preview.command) {
626
+ item.text = preview.command;
627
+ }
628
+ item.meta = preview.output;
629
+ item.outputLines = String(preview.output || "").split("\n");
630
+ item.outputKind = preview.outputKind || "text";
631
+ item.label = preview.label || "# Done";
632
+ item.pending = false;
633
+ delete item.streamSource;
634
+ delete item.streamOffset;
635
+ delete item.finalPreview;
636
+ }
637
+
638
+ function scrollToBottom() {
639
+ autoFollow = true;
640
+ scrollOffset = Number.MAX_SAFE_INTEGER;
641
+ }
642
+
643
+ function scrollBy(delta) {
644
+ scrollOffset = Math.max(0, scrollOffset + delta);
645
+ if (delta < 0) {
646
+ autoFollow = false;
647
+ return;
648
+ }
649
+ if (scrollOffset === 0) {
650
+ autoFollow = true;
651
+ }
652
+ }
653
+
654
+ function previewToolResult(name, result) {
655
+ if (result && result.ok === false) {
656
+ return {
657
+ command: "",
658
+ output: `Error: ${result.error || "Tool failed."}`,
659
+ label: "# Error",
660
+ outputKind: "text",
661
+ preserveCommand: true,
662
+ };
663
+ }
664
+
665
+ if (name === "run_command") {
666
+ const command = result.commandLine || [result.command, ...(result.args || [])].filter(Boolean).join(" ");
667
+ const output = [result.stdout, result.stderr].filter(Boolean).join("\n").trim();
668
+ return {
669
+ command: command || "command",
670
+ output: truncate(output || "Completed with no output.", 500),
671
+ label: "# Done",
672
+ outputKind: "text",
673
+ };
674
+ }
675
+
676
+ if (name === "batch_command") {
677
+ const first = (result.results || [])[0] || {};
678
+ const command = first.commandLine || [first.command, ...(first.args || [])].filter(Boolean).join(" ") || "batch command";
679
+ const output = [first.stdout, first.stderr].filter(Boolean).join("\n").trim();
680
+ return {
681
+ command,
682
+ output: truncate(output || `Ran ${result.results?.length || 0} commands.`, 500),
683
+ label: "# Done",
684
+ outputKind: "text",
685
+ };
686
+ }
687
+
688
+ if (name === "read_file") {
689
+ return { command: `read ${result.path}`, output: truncate(result.content || "", 500), label: "# Done", outputKind: "text" };
690
+ }
691
+
692
+ if (name === "create_file") {
693
+ return { command: `create ${result.path}`, output: `${result.bytesWritten || 0} bytes written`, label: "# Done", outputKind: "text" };
694
+ }
695
+
696
+ if (name === "write_file") {
697
+ return { command: `write ${result.path}`, output: `${result.bytesWritten || 0} bytes written`, label: "# Done", outputKind: "text" };
698
+ }
699
+
700
+ if (name === "append_file") {
701
+ return { command: `append ${result.path}`, output: `${result.bytesWritten || 0} bytes appended`, label: "# Done", outputKind: "text" };
702
+ }
703
+
704
+ if (name === "edit_file") {
705
+ return { command: `edit ${result.path}`, output: `${result.replacements || 0} replacement${result.replacements === 1 ? "" : "s"}`, label: "# Done", outputKind: "text" };
706
+ }
707
+
708
+ if (name === "make_dir") {
709
+ return { command: `mkdir ${result.path}`, output: "Directory ready", label: "# Done", outputKind: "text" };
710
+ }
711
+
712
+ if (name === "list_files") {
713
+ const entries = (result.entries || []).slice(0, 10).map((entry) => entry.path).join("\n");
714
+ return { command: "ls", output: entries || "No entries", label: "# Done", outputKind: "text" };
715
+ }
716
+
717
+ return { command: name, output: truncate(JSON.stringify(result), 500), label: "# Done", outputKind: "text" };
718
+ }
719
+
720
+ function renderHome(frame, width, height) {
721
+ const currentModel = displayModelName(getModel ? getModel() : model);
722
+ const logoWidth = Math.max(LOGO_TOP[0].length, LOGO_BOTTOM[0].length);
723
+ const logoLeft = center(width, logoWidth);
724
+ const logoTop = Math.max(4, Math.floor(height / 2) - 10);
725
+ frame.fillRect(logoTop, logoLeft, logoWidth, LOGO_TOP.length + LOGO_BOTTOM.length + 1, `${COLORS.bg}${COLORS.fg}`);
726
+ LOGO_TOP.forEach((line, index) => {
727
+ frame.paintText(logoTop + index, logoLeft, line, `${COLORS.bg}${COLORS.dim}`);
728
+ });
729
+ const bottomLeft = center(width, LOGO_BOTTOM[0].length);
730
+ LOGO_BOTTOM.forEach((line, index) => {
731
+ frame.paintText(logoTop + LOGO_TOP.length + 1 + index, bottomLeft, line, `${COLORS.bg}${COLORS.fg}`);
732
+ });
733
+
734
+ const panelWidth = Math.min(74, width - 14);
735
+ const panelLeft = center(width, panelWidth);
736
+ const promptText = input || status;
737
+ const promptLines = getInputLines(promptText, panelWidth - 4);
738
+ const panelHeight = promptLines.length + 3;
739
+ const panelTop = logoTop + LOGO_TOP.length + LOGO_BOTTOM.length + 3;
740
+ frame.fillRect(panelTop, panelLeft, panelWidth, panelHeight, `${COLORS.panel}${COLORS.fg}`);
741
+ for (let y = 0; y < panelHeight; y += 1) frame.paintText(panelTop + y, panelLeft, "│", `${COLORS.panel}${COLORS.blue}`);
742
+ promptLines.forEach((line, index) => {
743
+ frame.paintText(panelTop + 1 + index, panelLeft + 2, pad(line, panelWidth - 4), `${COLORS.panel}${input ? COLORS.fg : COLORS.dim}`);
744
+ });
745
+ if (cursorVisible && input) {
746
+ const cursorLine = promptLines.length - 1;
747
+ const cursorCol = panelLeft + 2 + promptLines[cursorLine].length;
748
+ paintInputCursor(frame, panelTop + 1 + cursorLine, cursorCol);
749
+ }
750
+ paintStatusBadge(frame, panelTop + promptLines.length + 1, panelLeft + 2, COLORS.panel, agentName);
751
+ renderSlashMenu(frame, panelLeft + 1, panelTop + promptLines.length + 1, panelWidth - 2);
752
+
753
+ const hints = "ctrl+t variants tab agents ctrl+p commands";
754
+ frame.paintText(panelTop + panelHeight + 1, center(width, hints.length), hints, `${COLORS.bg}${COLORS.fg}`);
755
+ frame.paintText(height, 2, truncate(workspaceLabel, width - 14), `${COLORS.bg}${COLORS.dim}`);
756
+ frame.paintText(height, width - version.length - 1, version, `${COLORS.bg}${COLORS.dim}`);
757
+ }
758
+
759
+ function renderPicker(frame, width, height) {
760
+ if (!picker) return;
761
+ const boxWidth = Math.min(72, width - 20);
762
+ const visibleCount = Math.min(8, picker.items.length);
763
+ const boxHeight = visibleCount + 4;
764
+ const top = center(height, boxHeight);
765
+ const left = center(width, boxWidth);
766
+ frame.fillRect(top, left, boxWidth, boxHeight, `${COLORS.panel}${COLORS.fg}`);
767
+ frame.paintText(top + 1, left + 2, truncate(picker.title, boxWidth - 4), `${COLORS.panel}${COLORS.fg}`);
768
+ for (let i = 0; i < visibleCount; i += 1) {
769
+ const item = picker.items[i];
770
+ const active = i === picker.selected;
771
+ frame.paintText(
772
+ top + 2 + i,
773
+ left + 2,
774
+ `${active ? "›" : " "} ${truncate(item.label, boxWidth - 6)}`,
775
+ `${COLORS.panel}${active ? COLORS.blue : COLORS.fg}`,
776
+ );
777
+ }
778
+ }
779
+
780
+ function getSlashMatches() {
781
+ if (!input.startsWith("/")) return [];
782
+ const term = input.trim().toLowerCase();
783
+ return SLASH_COMMANDS.filter((item) => item.command.startsWith(term) || item.command.includes(term.slice(1)));
784
+ }
785
+
786
+ function renderSlashMenu(frame, left, bottomRow, widthLimit) {
787
+ const matches = getSlashMatches();
788
+ if (!matches.length || picker) return;
789
+ slashIndex = clamp(slashIndex, 0, matches.length - 1);
790
+ const visibleCount = Math.min(8, matches.length);
791
+ const boxWidth = Math.min(widthLimit, 64);
792
+ const boxHeight = visibleCount + 1;
793
+ const top = Math.max(2, bottomRow - boxHeight);
794
+
795
+ frame.fillRect(top, left, boxWidth, boxHeight, `${COLORS.panel}${COLORS.fg}`);
796
+ for (let i = 0; i < visibleCount; i += 1) {
797
+ const item = matches[i];
798
+ const active = i === slashIndex;
799
+ const bg = active ? COLORS.peach : COLORS.panel;
800
+ const fg = active ? COLORS.blackFg : COLORS.fg;
801
+ frame.fillRect(top + i, left, boxWidth, 1, `${bg}${fg}`);
802
+ frame.paintText(top + i, left + 1, truncate(item.command, 12), `${bg}${fg}`);
803
+ frame.paintText(top + i, left + 14, truncate(item.description, boxWidth - 16), `${bg}${active ? COLORS.blackFg : COLORS.muted}`);
804
+ }
805
+ }
806
+
807
+ function buildTranscriptLines(contentWidth) {
808
+ const currentModel = displayModelName(getModel ? getModel() : model);
809
+ const lines = [];
810
+ const hasPendingTool = transcript.some((item) => item.role === "tool" && item.pending);
811
+ lines.push({ kind: "header", text: `# ${sessionTitle}`, meta: sessionId || "local session" });
812
+ lines.push({ kind: "spacer", text: "" });
813
+ for (const item of transcript) {
814
+ if (item.role === "user") {
815
+ const wrapped = wrapText(item.text, contentWidth - 6);
816
+ lines.push({ kind: "user_box", lines: wrapped });
817
+ lines.push({ kind: "spacer", text: "" });
818
+ continue;
819
+ }
820
+ if (item.role === "thinking") {
821
+ const wrapped = wrapText(item.text, contentWidth - 8);
822
+ lines.push({ kind: "thinking", lines: wrapped });
823
+ lines.push({ kind: "spacer", text: "" });
824
+ continue;
825
+ }
826
+ if (item.role === "thinking_live") {
827
+ const wrapped = compactWrappedText(`Thinking: ${String(item.text || "").replace(/\s+/g, " ")}`, contentWidth - 8, 8);
828
+ lines.push({ kind: "thinking", lines: wrapped });
829
+ lines.push({ kind: "spacer", text: "" });
830
+ continue;
831
+ }
832
+ if (item.role === "tool") {
833
+ const commandLines = wrapText(item.text, contentWidth - 10);
834
+ const sourceOutput = Array.isArray(item.outputLines)
835
+ ? item.outputLines
836
+ : String(item.meta || "").split("\n").flatMap((line) => wrapText(line, contentWidth - 10));
837
+ lines.push({
838
+ kind: "tool_block",
839
+ command: commandLines,
840
+ output: sourceOutput.slice(0, 8),
841
+ outputKind: item.outputKind || "text",
842
+ label: item.label || "# Execute",
843
+ });
844
+ lines.push({ kind: "spacer", text: "" });
845
+ continue;
846
+ }
847
+ if (item.role === "status_live") {
848
+ lines.push({ kind: "meta", text: item.text });
849
+ lines.push({ kind: "spacer", text: "" });
850
+ continue;
851
+ }
852
+ if (item.role === "assistant_live") {
853
+ const liveCode = extractLiveCode(item.text);
854
+ if (liveCode) {
855
+ const codeLines = visibleCodeLines(liveCode, contentWidth - 8);
856
+ lines.push({ kind: "code_block", lines: codeLines });
857
+ lines.push({ kind: "spacer", text: "" });
858
+ continue;
859
+ }
860
+ const wrapped = compactWrappedText(item.text, contentWidth - 4, 10);
861
+ lines.push(...wrapped.map((line) => ({ kind: "assistant_live", text: line })));
862
+ lines.push({ kind: "spacer", text: "" });
863
+ continue;
864
+ }
865
+ const blocks = parseAssistantBlocks(item.text);
866
+ for (const block of blocks) {
867
+ if (block.kind === "code") {
868
+ const codeLines = compactCodeLines(block.text, 10, contentWidth - 8);
869
+ lines.push({ kind: "code_block", lines: codeLines });
870
+ lines.push({ kind: "spacer", text: "" });
871
+ continue;
872
+ }
873
+ const prefix = block.kind === "bullet" ? "• " : "";
874
+ const wrapped = wrapText(prefix + block.text, contentWidth - 6);
875
+ lines.push(...wrapped.map((line) => ({ kind: "assistant", text: line })));
876
+ lines.push({ kind: "spacer", text: "" });
877
+ }
878
+ lines.push({ kind: "meta", text: `${agentName} · ${currentModel}${item.meta ? ` · ${item.meta}` : ""}` });
879
+ lines.push({ kind: "spacer", text: "" });
880
+ }
881
+ if (busy && !transcript.some((item) => item.role === "thinking_live") && (liveReasoning || !streamingText)) {
882
+ const reasoningText = liveReasoning.trim()
883
+ ? `Thinking: ${liveReasoning.trim().replace(/\s+/g, " ")}`
884
+ : activity || (status && status !== "idle" ? `Thinking: ${status}` : "Thinking: Working on your request...");
885
+ const wrapped = compactWrappedText(reasoningText, contentWidth - 8, hasPendingTool ? 4 : 8);
886
+ lines.push({ kind: "thinking", lines: wrapped });
887
+ lines.push({ kind: "spacer", text: "" });
888
+ }
889
+ if (busy && !transcript.some((item) => item.role === "assistant_live") && streamingText && !hasPendingTool) {
890
+ const liveCode = extractLiveCode(streamingText);
891
+ if (liveCode) {
892
+ const codeLines = visibleCodeLines(liveCode, contentWidth - 8);
893
+ lines.push({ kind: "code_block", lines: codeLines });
894
+ } else {
895
+ const wrapped = compactWrappedText(streamingText, contentWidth - 4, 8);
896
+ lines.push(...wrapped.map((line) => ({ kind: "assistant_live", text: line })));
897
+ }
898
+ }
899
+ return lines;
900
+ }
901
+
902
+ function renderSession(frame, width, height) {
903
+ const currentModel = displayModelName(getModel ? getModel() : model);
904
+ const hasSidebar = width >= 120;
905
+ const sidebarWidth = hasSidebar ? 30 : 0;
906
+ const contentWidth = width - sidebarWidth - 6;
907
+ const inputLines = getInputLines(input, contentWidth - 5);
908
+ const inputHeight = inputLines.length + 3;
909
+ const inputTop = height - inputHeight - 1;
910
+ const contentHeight = Math.max(1, inputTop - 2);
911
+ const scrollCol = hasSidebar ? contentWidth + 1 : width - 2;
912
+
913
+ frame.paintText(1, center(width, `NetheriteAI:Code | ${sessionTitle}`.length), `NetheriteAI:Code | ${sessionTitle}`, `${COLORS.bg}${COLORS.fg}`);
914
+ if (hasSidebar) {
915
+ for (let r = 2; r <= height; r += 1) {
916
+ frame.paintText(r, contentWidth + 3, "│", `${COLORS.bg}${COLORS.dim}`);
917
+ }
918
+ const sideCol = contentWidth + 6;
919
+ const sideWidth = sidebarWidth - 8;
920
+ frame.paintText(3, sideCol, truncate(sessionTitle, sideWidth), `${COLORS.bg}${COLORS.fg}`);
921
+ frame.paintText(4, sideCol, "made by hurdacu", `${COLORS.bg}${COLORS.dim}`);
922
+ frame.paintText(6, sideCol, "Context", `${COLORS.bg}${COLORS.fg}`);
923
+ frame.paintText(7, sideCol, truncate("local session", sideWidth), `${COLORS.bg}${COLORS.muted}`);
924
+ frame.paintText(8, sideCol, truncate(busy ? "streaming" : "idle", sideWidth), `${COLORS.bg}${COLORS.muted}`);
925
+ frame.paintText(10, sideCol, "LSP", `${COLORS.bg}${COLORS.fg}`);
926
+ frame.paintText(11, sideCol, truncate("LSPs activate as files are read", sideWidth), `${COLORS.bg}${COLORS.muted}`);
927
+ frame.paintText(height - 3, sideCol, truncate(workspaceLabel, sideWidth), `${COLORS.bg}${COLORS.muted}`);
928
+ frame.paintText(height - 1, sideCol, truncate(`• NetheriteAI:Code ${version}`, sideWidth), `${COLORS.bg}${COLORS.fg}`);
929
+ }
930
+
931
+ const rows = flattenRenderRows(buildTranscriptLines(contentWidth), contentWidth);
932
+ const maxGroup = Math.max(1, ...rows.map((item) => item.groupId || 0));
933
+ selectedGroup = clamp(selectedGroup, 1, maxGroup);
934
+ const maxOffset = Math.max(0, rows.length - contentHeight);
935
+ if (autoFollow) {
936
+ scrollOffset = maxOffset;
937
+ } else {
938
+ scrollOffset = Math.max(0, Math.min(scrollOffset, maxOffset));
939
+ if (scrollOffset >= maxOffset) {
940
+ autoFollow = true;
941
+ }
942
+ }
943
+ const visible = rows.slice(scrollOffset, scrollOffset + contentHeight);
944
+
945
+ let row = 2;
946
+ for (const item of visible) {
947
+ if (row > contentHeight + 1) break;
948
+ const selected = item.groupId > 0 && item.groupId === selectedGroup;
949
+ const linePanel = `${selected ? COLORS.panelAlt : COLORS.panel}${COLORS.fg}`;
950
+ const lineBg = `${COLORS.bg}${COLORS.fg}`;
951
+ const lineMuted = `${selected ? COLORS.panelAlt : COLORS.panel}${COLORS.muted}`;
952
+ const borderStyle = `${selected ? COLORS.panelAlt : COLORS.panel}${COLORS.blue}`;
953
+ if (item.kind === "header_top" || item.kind === "header_bottom") {
954
+ frame.fillRect(row, 2, contentWidth, 1, `${COLORS.panel}${COLORS.fg}`);
955
+ row += 1;
956
+ continue;
957
+ }
958
+ if (item.kind === "header_line") {
959
+ frame.fillRect(row, 2, contentWidth, 1, `${COLORS.panel}${COLORS.fg}`);
960
+ frame.paintText(row, 4, item.text, `${COLORS.panel}${COLORS.fg}`);
961
+ frame.paintText(row, Math.max(4, contentWidth - String(item.meta || "").length), item.meta || "", `${COLORS.panel}${COLORS.muted}`);
962
+ row += 1;
963
+ continue;
964
+ }
965
+ if (item.kind === "user_top" || item.kind === "user_bottom") {
966
+ frame.fillRect(row, 2, contentWidth - 1, 1, linePanel);
967
+ frame.paintText(row, 2, "│", borderStyle);
968
+ row += 1;
969
+ continue;
970
+ }
971
+ if (item.kind === "user_line") {
972
+ frame.fillRect(row, 2, contentWidth - 1, 1, linePanel);
973
+ frame.paintText(row, 2, "│", borderStyle);
974
+ frame.paintText(row, 4, pad(item.text, contentWidth - 5), linePanel);
975
+ row += 1;
976
+ continue;
977
+ }
978
+ if (item.kind === "thinking_line") {
979
+ if (selected) frame.fillRect(row, 3, contentWidth - 3, 1, `${COLORS.bg}${COLORS.fg}`);
980
+ frame.paintText(row, 3, "│", `${COLORS.bg}${COLORS.dim}`);
981
+ if (item.first && item.text.startsWith("Thinking:")) {
982
+ frame.paintText(row, 5, "Thinking:", `${COLORS.bg}${COLORS.amber}`);
983
+ frame.paintText(row, 15, ` ${item.text.slice(9).trim()}`, `${COLORS.bg}${COLORS.muted}`);
984
+ } else {
985
+ frame.paintText(row, 5, item.text, `${COLORS.bg}${COLORS.muted}`);
986
+ }
987
+ row += 1;
988
+ continue;
989
+ }
990
+ if (item.kind === "assistant" || item.kind === "assistant_live") {
991
+ if (selected) frame.fillRect(row, 3, contentWidth - 3, 1, `${COLORS.bg}${COLORS.fg}`);
992
+ frame.paintText(row, 4, truncate(item.text, contentWidth - 8), lineBg);
993
+ row += 1;
994
+ continue;
995
+ }
996
+ if (item.kind === "code_top" || item.kind === "code_bottom") {
997
+ frame.fillRect(row, 3, contentWidth - 7, 1, linePanel);
998
+ row += 1;
999
+ continue;
1000
+ }
1001
+ if (item.kind === "code_line") {
1002
+ frame.fillRect(row, 3, contentWidth - 7, 1, linePanel);
1003
+ paintCodeLine(frame, row, 5, item.text, selected ? COLORS.panelAlt : COLORS.panel, contentWidth - 11);
1004
+ row += 1;
1005
+ continue;
1006
+ }
1007
+ if (item.kind === "tool_top" || item.kind === "tool_bottom") {
1008
+ frame.fillRect(row, 3, contentWidth - 7, 1, linePanel);
1009
+ row += 1;
1010
+ continue;
1011
+ }
1012
+ if (item.kind === "tool_label") {
1013
+ frame.fillRect(row, 3, contentWidth - 7, 1, linePanel);
1014
+ frame.paintText(row, 5, item.text, `${selected ? COLORS.panelAlt : COLORS.panel}${COLORS.blueSoft}`);
1015
+ row += 1;
1016
+ continue;
1017
+ }
1018
+ if (item.kind === "tool_command") {
1019
+ frame.fillRect(row, 3, contentWidth - 7, 1, linePanel);
1020
+ frame.paintText(row, 5, `$ ${truncate(item.text, contentWidth - 13)}`, linePanel);
1021
+ row += 1;
1022
+ continue;
1023
+ }
1024
+ if (item.kind === "tool_gap") {
1025
+ frame.fillRect(row, 3, contentWidth - 7, 1, linePanel);
1026
+ row += 1;
1027
+ continue;
1028
+ }
1029
+ if (item.kind === "tool_output") {
1030
+ frame.fillRect(row, 3, contentWidth - 7, 1, linePanel);
1031
+ if (item.outputKind === "code") {
1032
+ paintCodeLine(frame, row, 5, item.text, selected ? COLORS.panelAlt : COLORS.panel, contentWidth - 11);
1033
+ } else {
1034
+ frame.paintText(row, 5, truncate(item.text, contentWidth - 11), lineMuted);
1035
+ }
1036
+ row += 1;
1037
+ continue;
1038
+ }
1039
+ if (item.kind === "meta") {
1040
+ if (selected) frame.fillRect(row, 3, contentWidth - 3, 1, `${COLORS.bg}${COLORS.fg}`);
1041
+ frame.paintText(row, 4, `◻ ${truncate(item.text, contentWidth - 8)}`, `${COLORS.bg}${selected ? COLORS.fg : COLORS.blue}`);
1042
+ row += 1;
1043
+ continue;
1044
+ }
1045
+ row += 1;
1046
+ }
1047
+ for (let r = 2; r < inputTop; r += 1) {
1048
+ frame.paintText(r, scrollCol, "│", `${COLORS.bg}${COLORS.dim}`);
1049
+ }
1050
+ if (maxOffset > 0) {
1051
+ const trackHeight = Math.max(1, inputTop - 2);
1052
+ const thumbHeight = Math.max(3, Math.floor((contentHeight / rows.length) * trackHeight));
1053
+ const thumbTravel = Math.max(0, trackHeight - thumbHeight);
1054
+ const thumbTop = 2 + Math.floor((scrollOffset / maxOffset) * thumbTravel);
1055
+ for (let r = 0; r < thumbHeight; r += 1) {
1056
+ frame.paintText(thumbTop + r, scrollCol, "█", `${COLORS.bg}${COLORS.muted}`);
1057
+ }
1058
+ }
1059
+
1060
+ frame.fillRect(inputTop, 2, contentWidth - 1, inputHeight, `${COLORS.panel}${COLORS.fg}`);
1061
+ for (let y = 0; y < inputHeight; y += 1) frame.paintText(inputTop + y, 2, "│", `${COLORS.panel}${COLORS.blue}`);
1062
+ paintStatusBadge(frame, inputTop + inputLines.length + 1, 4, COLORS.panel, agentName);
1063
+ inputLines.forEach((line, index) => {
1064
+ frame.paintText(inputTop + 1 + index, 4, pad(line, contentWidth - 5), `${COLORS.panel}${COLORS.fg}`);
1065
+ });
1066
+ if (cursorVisible) {
1067
+ const cursorLine = inputLines.length - 1;
1068
+ const cursorCol = 4 + inputLines[cursorLine].length;
1069
+ paintInputCursor(frame, inputTop + 1 + cursorLine, cursorCol);
1070
+ }
1071
+ renderSlashMenu(frame, 3, inputTop + inputLines.length + 1, contentWidth - 2);
1072
+
1073
+ const hints = "tab agents ctrl+p commands";
1074
+ if (busy) {
1075
+ paintBusyIndicator(frame, height, 2, busyAnimationTime);
1076
+ frame.paintText(height, 12, "esc interrupt", `${COLORS.bg}${COLORS.fg}`);
1077
+ const busyLabel = truncate(activity || status || "Working...", Math.max(10, contentWidth - hints.length - 20));
1078
+ frame.paintText(height - 1, 2, busyLabel, `${COLORS.bg}${COLORS.muted}`);
1079
+ }
1080
+ frame.paintText(height, contentWidth - hints.length, hints, `${COLORS.bg}${COLORS.fg}`);
1081
+ }
1082
+
1083
+ async function submit() {
1084
+ const prompt = input.trim();
1085
+ if (!prompt || busy) return;
1086
+ if (mode === "home") mode = "session";
1087
+
1088
+ addPromptHistory(prompt);
1089
+ ensureSessionTitle(prompt);
1090
+ input = "";
1091
+ busy = true;
1092
+ busyStartedAt = Date.now();
1093
+ lastProgressAt = busyStartedAt;
1094
+ currentPhase = "Thinking";
1095
+ status = "thinking";
1096
+ activity = "";
1097
+ streamingText = "";
1098
+ liveReasoning = "";
1099
+ pushMessage("user", prompt);
1100
+ render();
1101
+
1102
+ try {
1103
+ const started = Date.now();
1104
+ currentAbortController = new AbortController();
1105
+ await onSubmit(prompt, {
1106
+ signal: currentAbortController.signal,
1107
+ onStatus(message) {
1108
+ status = message;
1109
+ currentPhase = message;
1110
+ lastProgressAt = Date.now();
1111
+ render();
1112
+ },
1113
+ onEvent(message) {
1114
+ activity = message;
1115
+ currentPhase = message;
1116
+ lastProgressAt = Date.now();
1117
+ render();
1118
+ },
1119
+ onToolStart(event) {
1120
+ currentPhase = `${event.name}`;
1121
+ lastProgressAt = Date.now();
1122
+ if (liveReasoning.trim()) {
1123
+ pushMessage("thinking", liveReasoning.trim());
1124
+ liveReasoning = "";
1125
+ }
1126
+ if (streamingText.trim()) {
1127
+ pushMessage("assistant", streamingText.trim());
1128
+ streamingText = "";
1129
+ }
1130
+ removeLiveMessage("thinking_live");
1131
+ removeLiveMessage("assistant_live");
1132
+ const preview = getToolPreview(event.name, event.args, process.stdout.columns || 120);
1133
+ pushMessage("tool", preview.command, "", {
1134
+ outputLines: preview.outputKind === "code" ? [] : preview.output,
1135
+ outputKind: preview.outputKind,
1136
+ label: `# ${preview.pendingLabel}`,
1137
+ pending: true,
1138
+ streamSource: preview.outputKind === "code" ? preview.output.join("\n") : "",
1139
+ streamOffset: 0,
1140
+ });
1141
+ scrollToBottom();
1142
+ render();
1143
+ },
1144
+ onToolResult(event) {
1145
+ currentPhase = `${event.name} done`;
1146
+ lastProgressAt = Date.now();
1147
+ const preview = previewToolResult(event.name, event.result || {});
1148
+ for (let index = transcript.length - 1; index >= 0; index -= 1) {
1149
+ if (transcript[index].role === "tool" && transcript[index].pending) {
1150
+ if (transcript[index].streamSource && (transcript[index].streamOffset || 0) < transcript[index].streamSource.length) {
1151
+ transcript[index].finalPreview = preview;
1152
+ } else {
1153
+ finalizeToolMessage(transcript[index], preview);
1154
+ }
1155
+ onTranscriptChange?.([...transcript]);
1156
+ break;
1157
+ }
1158
+ }
1159
+ scrollToBottom();
1160
+ render();
1161
+ },
1162
+ onToolProgress(event) {
1163
+ currentPhase = `${event.name}`;
1164
+ lastProgressAt = Date.now();
1165
+ for (let index = transcript.length - 1; index >= 0; index -= 1) {
1166
+ if (transcript[index].role === "tool" && transcript[index].pending) {
1167
+ const nextLines = String(event.progress?.text || "").replace(/\r/g, "").split("\n");
1168
+ const existing = Array.isArray(transcript[index].outputLines) ? transcript[index].outputLines : [];
1169
+ transcript[index].outputLines = visibleCodeLines([...existing, ...nextLines].join("\n"), Math.max(20, (process.stdout.columns || 120) - 18));
1170
+ transcript[index].outputKind = "code";
1171
+ transcript[index].label = "# Executing";
1172
+ onTranscriptChange?.([...transcript]);
1173
+ break;
1174
+ }
1175
+ }
1176
+ scrollToBottom();
1177
+ render();
1178
+ },
1179
+ onPicker(data) {
1180
+ picker = { ...data, selected: 0 };
1181
+ render();
1182
+ },
1183
+ onClearTranscript() {
1184
+ resetTranscriptView();
1185
+ render();
1186
+ },
1187
+ onResetSession() {
1188
+ resetSessionView();
1189
+ render();
1190
+ },
1191
+ onReasoning(chunk) {
1192
+ liveReasoning += chunk;
1193
+ currentPhase = "Thinking";
1194
+ lastProgressAt = Date.now();
1195
+ upsertLiveMessage("thinking_live", liveReasoning.trim());
1196
+ scrollToBottom();
1197
+ render();
1198
+ },
1199
+ onReasoningDone(text) {
1200
+ if (text?.trim()) {
1201
+ liveReasoning = text;
1202
+ }
1203
+ removeLiveMessage("thinking_live");
1204
+ render();
1205
+ },
1206
+ onStream(chunk) {
1207
+ streamingText += chunk;
1208
+ currentPhase = "Writing response";
1209
+ lastProgressAt = Date.now();
1210
+ upsertLiveMessage("assistant_live", streamingText);
1211
+ scrollToBottom();
1212
+ render();
1213
+ },
1214
+ onAssistant(message) {
1215
+ const seconds = ((Date.now() - started) / 1000).toFixed(1);
1216
+ currentPhase = "Done";
1217
+ lastProgressAt = Date.now();
1218
+ if (liveReasoning.trim()) {
1219
+ pushMessage("thinking", liveReasoning.trim());
1220
+ }
1221
+ removeLiveMessage("thinking_live");
1222
+ removeLiveMessage("assistant_live");
1223
+ pushMessage("assistant", message, `${seconds}s`);
1224
+ streamingText = "";
1225
+ liveReasoning = "";
1226
+ scrollToBottom();
1227
+ render();
1228
+ },
1229
+ });
1230
+ } catch (error) {
1231
+ const aborted = currentAbortController?.signal.aborted
1232
+ || error?.name === "AbortError"
1233
+ || String(error).includes("AbortError");
1234
+ if (aborted) {
1235
+ if (liveReasoning.trim()) {
1236
+ pushMessage("thinking", liveReasoning.trim());
1237
+ }
1238
+ if (streamingText.trim()) {
1239
+ pushMessage("assistant", streamingText.trim(), "interrupted");
1240
+ } else {
1241
+ pushMessage("assistant", "Interrupted.", "");
1242
+ }
1243
+ removeLiveMessage("thinking_live");
1244
+ removeLiveMessage("assistant_live");
1245
+ } else {
1246
+ removeLiveMessage("thinking_live");
1247
+ removeLiveMessage("assistant_live");
1248
+ pushMessage("assistant", `Error: ${error instanceof Error ? error.message : String(error)}`);
1249
+ }
1250
+ } finally {
1251
+ busy = false;
1252
+ status = "idle";
1253
+ activity = "";
1254
+ liveReasoning = "";
1255
+ currentPhase = "";
1256
+ busyStartedAt = 0;
1257
+ lastProgressAt = 0;
1258
+ currentAbortController = null;
1259
+ render();
1260
+ }
1261
+ }
1262
+
1263
+ function render() {
1264
+ const width = process.stdout.columns || 120;
1265
+ const height = process.stdout.rows || 36;
1266
+ const frame = makeFrame(width, height);
1267
+ if (mode === "home") renderHome(frame, width, height);
1268
+ else renderSession(frame, width, height);
1269
+ renderPicker(frame, width, height);
1270
+ frame.flush(height, width);
1271
+ }
1272
+
1273
+ function cleanup(reason = "close") {
1274
+ clearInterval(timer);
1275
+ process.stdin.removeListener("data", onData);
1276
+ process.stdout.removeListener("resize", render);
1277
+ if (process.stdin.isTTY) process.stdin.setRawMode(false);
1278
+ process.stdout.write("\u001b[?1000l\u001b[?1006l\u001b[?25h\u001b[?1049l");
1279
+ resolve({ reason, transcript: [...transcript] });
1280
+ }
1281
+
1282
+ function handleMouse(mouse) {
1283
+ const isWheel = mouse.button === 64 || mouse.button === 65;
1284
+ if (!isWheel || mode !== "session") return;
1285
+ if (mouse.button === 64) {
1286
+ scrollBy(-3);
1287
+ } else {
1288
+ scrollBy(3);
1289
+ }
1290
+ render();
1291
+ }
1292
+
1293
+ function handleKey(str, key = {}) {
1294
+ const slashMatches = getSlashMatches();
1295
+ if (picker) {
1296
+ if (key.name === "escape") {
1297
+ picker = null;
1298
+ return render();
1299
+ }
1300
+ if (key.name === "up") {
1301
+ picker.selected = Math.max(0, picker.selected - 1);
1302
+ return render();
1303
+ }
1304
+ if (key.name === "down") {
1305
+ picker.selected = Math.min(picker.items.length - 1, picker.selected + 1);
1306
+ return render();
1307
+ }
1308
+ if (key.name === "return") {
1309
+ const selected = picker.items[picker.selected];
1310
+ const onSelect = picker.onSelect;
1311
+ picker = null;
1312
+ Promise.resolve(onSelect?.(selected.value)).then((message) => {
1313
+ if (message) {
1314
+ pushMessage("assistant", message);
1315
+ }
1316
+ render();
1317
+ });
1318
+ return;
1319
+ }
1320
+ return;
1321
+ }
1322
+ if (key.ctrl && key.name === "c") return cleanup();
1323
+ if (key.ctrl && key.name === "l") return render();
1324
+ if (key.ctrl && key.name === "p") {
1325
+ activity = "Command palette not wired yet";
1326
+ return render();
1327
+ }
1328
+ if (key.name === "escape" && busy) {
1329
+ activity = "Interrupt requested";
1330
+ currentAbortController?.abort();
1331
+ return render();
1332
+ }
1333
+ if (key.name === "pageup") {
1334
+ scrollBy(-8);
1335
+ return render();
1336
+ }
1337
+ if (key.name === "pagedown") {
1338
+ scrollBy(8);
1339
+ return render();
1340
+ }
1341
+ if (key.name === "up") {
1342
+ if (slashMatches.length) {
1343
+ slashIndex = Math.max(0, slashIndex - 1);
1344
+ return render();
1345
+ }
1346
+ if (input || historyIndex !== -1 || promptHistory.length) browseHistory(1);
1347
+ else scrollBy(-3);
1348
+ return render();
1349
+ }
1350
+ if (key.name === "down") {
1351
+ if (slashMatches.length) {
1352
+ slashIndex = Math.min(slashMatches.length - 1, slashIndex + 1);
1353
+ return render();
1354
+ }
1355
+ if (historyIndex !== -1 || input) browseHistory(-1);
1356
+ else scrollBy(3);
1357
+ return render();
1358
+ }
1359
+ if (key.name === "return") {
1360
+ if (slashMatches.length) {
1361
+ const selected = slashMatches[slashIndex];
1362
+ if (input.trim() !== selected.command) {
1363
+ input = selected.command;
1364
+ if (selected.command === "/model") input += " ";
1365
+ return render();
1366
+ }
1367
+ }
1368
+ return submit();
1369
+ }
1370
+ if (key.name === "backspace") {
1371
+ input = input.slice(0, -1);
1372
+ historyIndex = -1;
1373
+ if (!input.startsWith("/")) slashIndex = 0;
1374
+ return render();
1375
+ }
1376
+ if (key.name === "tab") {
1377
+ activity = "Agent switching UI not wired yet";
1378
+ return render();
1379
+ }
1380
+ if (mode === "session" && key.name === "left") {
1381
+ selectedGroup = Math.max(1, selectedGroup - 1);
1382
+ return render();
1383
+ }
1384
+ if (mode === "session" && key.name === "right") {
1385
+ selectedGroup += 1;
1386
+ return render();
1387
+ }
1388
+ if (typeof str === "string" && !key.ctrl && !key.meta) {
1389
+ input += str;
1390
+ historyIndex = -1;
1391
+ if (input.startsWith("/")) slashIndex = 0;
1392
+ return render();
1393
+ }
1394
+ }
1395
+
1396
+ function onData(chunk) {
1397
+ inputBuffer += chunk.toString("utf8");
1398
+
1399
+ while (inputBuffer.length) {
1400
+ const mouse = parseMousePrefix(inputBuffer);
1401
+ if (mouse) {
1402
+ inputBuffer = inputBuffer.slice(mouse.raw.length);
1403
+ handleMouse(mouse);
1404
+ continue;
1405
+ }
1406
+
1407
+ if (inputBuffer.startsWith("\u0003")) {
1408
+ inputBuffer = inputBuffer.slice(1);
1409
+ cleanup("interrupt");
1410
+ return;
1411
+ }
1412
+ if (inputBuffer.startsWith("\u000c")) {
1413
+ inputBuffer = inputBuffer.slice(1);
1414
+ render();
1415
+ continue;
1416
+ }
1417
+ if (inputBuffer.startsWith("\u0010")) {
1418
+ inputBuffer = inputBuffer.slice(1);
1419
+ activity = "Command palette not wired yet";
1420
+ render();
1421
+ continue;
1422
+ }
1423
+ if (inputBuffer.startsWith("\t")) {
1424
+ inputBuffer = inputBuffer.slice(1);
1425
+ handleKey("", { name: "tab" });
1426
+ continue;
1427
+ }
1428
+ if (inputBuffer.startsWith("\r")) {
1429
+ inputBuffer = inputBuffer.slice(1);
1430
+ handleKey("", { name: "return" });
1431
+ continue;
1432
+ }
1433
+ if (inputBuffer.startsWith("\u007f")) {
1434
+ inputBuffer = inputBuffer.slice(1);
1435
+ handleKey("", { name: "backspace" });
1436
+ continue;
1437
+ }
1438
+ if (inputBuffer.startsWith("\u001b[A")) {
1439
+ inputBuffer = inputBuffer.slice(3);
1440
+ handleKey("", { name: "up" });
1441
+ continue;
1442
+ }
1443
+ if (inputBuffer.startsWith("\u001b[B")) {
1444
+ inputBuffer = inputBuffer.slice(3);
1445
+ handleKey("", { name: "down" });
1446
+ continue;
1447
+ }
1448
+ if (inputBuffer.startsWith("\u001b[C")) {
1449
+ inputBuffer = inputBuffer.slice(3);
1450
+ handleKey("", { name: "right" });
1451
+ continue;
1452
+ }
1453
+ if (inputBuffer.startsWith("\u001b[D")) {
1454
+ inputBuffer = inputBuffer.slice(3);
1455
+ handleKey("", { name: "left" });
1456
+ continue;
1457
+ }
1458
+ if (inputBuffer.startsWith("\u001b[5~")) {
1459
+ inputBuffer = inputBuffer.slice(4);
1460
+ handleKey("", { name: "pageup" });
1461
+ continue;
1462
+ }
1463
+ if (inputBuffer.startsWith("\u001b[6~")) {
1464
+ inputBuffer = inputBuffer.slice(4);
1465
+ handleKey("", { name: "pagedown" });
1466
+ continue;
1467
+ }
1468
+ if (inputBuffer.startsWith("\u001b")) {
1469
+ if (/^\u001b\[<[\d;Mm]*$/.test(inputBuffer)) {
1470
+ return;
1471
+ }
1472
+ inputBuffer = inputBuffer.slice(1);
1473
+ handleKey("", { name: "escape" });
1474
+ continue;
1475
+ }
1476
+
1477
+ const char = [...inputBuffer][0];
1478
+ const size = char.length;
1479
+ inputBuffer = inputBuffer.slice(size);
1480
+ if (char >= " " || char === "\n") {
1481
+ handleKey(char, { name: char });
1482
+ }
1483
+ }
1484
+ }
1485
+
1486
+ process.stdin.on("data", onData);
1487
+ process.stdout.on("resize", render);
1488
+ render();
1489
+ });
1490
+ }