@xingjisoft/neutrino 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.
@@ -0,0 +1,99 @@
1
+ import { Screen } from "../terminal/screen.js";
2
+ import { C } from "../colors.js";
3
+ import { drawHeader } from "../header.js";
4
+ import { truncateByWidth } from "../width.js";
5
+ import { listAgentSessions, loadAgentMessages, } from "../agents.js";
6
+ import { AgentConversationScreen } from "./agentConversation.js";
7
+ export class AgentHistoryScreen extends Screen {
8
+ agent;
9
+ label;
10
+ sessions = [];
11
+ selectedIdx = 0;
12
+ scrollTop = 0;
13
+ constructor(agent, label) {
14
+ super();
15
+ this.agent = agent;
16
+ this.label = label;
17
+ }
18
+ onActivate() {
19
+ this.sessions = listAgentSessions(this.agent);
20
+ this.selectedIdx = 0;
21
+ this.scrollTop = 0;
22
+ }
23
+ move(delta) {
24
+ if (this.sessions.length === 0)
25
+ return;
26
+ this.selectedIdx = (this.selectedIdx + delta + this.sessions.length) % this.sessions.length;
27
+ this.app.render();
28
+ }
29
+ onKey(key) {
30
+ if (key.name === "up")
31
+ this.move(-1);
32
+ else if (key.name === "down")
33
+ this.move(1);
34
+ else if (key.name === "enter" || key.name === "return")
35
+ this.loadSelected();
36
+ else if (key.name === "escape")
37
+ this.app.popScreen();
38
+ }
39
+ onMouse(ev) {
40
+ if (ev.type === "wheel") {
41
+ this.scrollTop = Math.max(0, this.scrollTop + (ev.dir === -1 ? -3 : 3));
42
+ this.app.render();
43
+ }
44
+ }
45
+ loadSelected() {
46
+ if (this.selectedIdx < 0 || this.selectedIdx >= this.sessions.length)
47
+ return;
48
+ const s = this.sessions[this.selectedIdx];
49
+ const messages = loadAgentMessages(this.agent, s.id);
50
+ this.app.pushScreen(new AgentConversationScreen(`${this.label} · ${s.title || s.id}`, messages));
51
+ }
52
+ render(buf) {
53
+ drawHeader(buf, this.app.state);
54
+ const w = buf.width;
55
+ const h = buf.height;
56
+ buf.centerText(2, this.label, { fg: C.accent, bold: true });
57
+ buf.centerText(3, "Select a session to view", { fg: C.dim });
58
+ const listTop = 5;
59
+ const listBottom = h - 2;
60
+ const viewH = Math.max(1, listBottom - listTop);
61
+ if (this.sessions.length === 0) {
62
+ buf.centerText(listTop + 1, "No sessions yet", { fg: C.dim });
63
+ buf.centerText(listBottom, "Esc back", { fg: C.dim });
64
+ return;
65
+ }
66
+ const rows = [];
67
+ const itemBounds = [];
68
+ for (const s of this.sessions) {
69
+ const start = rows.length;
70
+ const created = (s.updated_at || s.created_at || "").slice(0, 19);
71
+ rows.push(`${created} | ${s.title || s.id}`);
72
+ if (s.model)
73
+ rows.push(` ${s.model}`);
74
+ itemBounds.push({ start, end: rows.length });
75
+ }
76
+ if (this.selectedIdx < 0)
77
+ this.selectedIdx = 0;
78
+ if (this.selectedIdx >= itemBounds.length)
79
+ this.selectedIdx = itemBounds.length - 1;
80
+ const selected = itemBounds[this.selectedIdx];
81
+ const firstRowOfSelected = selected.start;
82
+ if (firstRowOfSelected < this.scrollTop)
83
+ this.scrollTop = firstRowOfSelected;
84
+ if (firstRowOfSelected >= this.scrollTop + viewH)
85
+ this.scrollTop = firstRowOfSelected - viewH + 1;
86
+ if (this.scrollTop > rows.length - viewH)
87
+ this.scrollTop = Math.max(0, rows.length - viewH);
88
+ for (let y = listTop; y < listBottom; y++) {
89
+ const ri = this.scrollTop + (y - listTop);
90
+ if (ri < 0 || ri >= rows.length)
91
+ break;
92
+ const isSelected = ri >= selected.start && ri < selected.end;
93
+ const isSelectedFirst = ri === selected.start;
94
+ const text = (isSelectedFirst ? "▸ " : " ") + rows[ri];
95
+ buf.writeText(0, y, truncateByWidth(text, w), isSelected ? { fg: C.accent, bg: C.highlightBg } : { fg: C.assistant });
96
+ }
97
+ buf.centerText(listBottom, "↑/↓ select · Enter view · Esc back", { fg: C.dim });
98
+ }
99
+ }
@@ -0,0 +1,71 @@
1
+ import { Screen } from "../terminal/screen.js";
2
+ import { C } from "../colors.js";
3
+ import { drawHeader } from "../header.js";
4
+ import { drawTabButton } from "../ui.js";
5
+ import { AgentHistoryScreen } from "./agentHistory.js";
6
+ const AGENTS = [
7
+ { id: "claude", label: "Claude Code" },
8
+ { id: "codex", label: "Codex" },
9
+ { id: "opencode", label: "OpenCode" },
10
+ ];
11
+ export class AgentSelectScreen extends Screen {
12
+ selectedIdx = 0;
13
+ scrollBtn = 0;
14
+ onActivate() {
15
+ this.selectedIdx = 0;
16
+ this.scrollBtn = 0;
17
+ }
18
+ move(delta) {
19
+ if (AGENTS.length === 0)
20
+ return;
21
+ let idx = this.selectedIdx + delta;
22
+ if (idx < 0)
23
+ idx = AGENTS.length - 1;
24
+ if (idx >= AGENTS.length)
25
+ idx = 0;
26
+ this.selectedIdx = idx;
27
+ this.app.render();
28
+ }
29
+ onKey(key) {
30
+ if (key.name === "up")
31
+ this.move(-1);
32
+ else if (key.name === "down")
33
+ this.move(1);
34
+ else if (key.name === "enter" || key.name === "return")
35
+ this.confirm();
36
+ else if (key.name === "escape")
37
+ this.app.popScreen();
38
+ }
39
+ confirm() {
40
+ const agent = AGENTS[this.selectedIdx];
41
+ if (!agent)
42
+ return;
43
+ this.app.pushScreen(new AgentHistoryScreen(agent.id, agent.label));
44
+ }
45
+ render(buf) {
46
+ drawHeader(buf, this.app.state);
47
+ const w = buf.width;
48
+ const h = buf.height;
49
+ buf.centerText(2, "Other Agents", { fg: C.accent, bold: true });
50
+ buf.centerText(3, "Select an agent to open its history", { fg: C.white });
51
+ const listTop = 5;
52
+ const btnH = 3;
53
+ const stride = btnH;
54
+ const hintRow = h - 1;
55
+ const maxVisible = Math.max(1, Math.floor((hintRow - listTop) / stride));
56
+ if (this.selectedIdx < this.scrollBtn)
57
+ this.scrollBtn = this.selectedIdx;
58
+ if (this.selectedIdx >= this.scrollBtn + maxVisible)
59
+ this.scrollBtn = this.selectedIdx - maxVisible + 1;
60
+ if (this.scrollBtn < 0)
61
+ this.scrollBtn = 0;
62
+ let y = listTop;
63
+ for (let i = this.scrollBtn; i < AGENTS.length; i++) {
64
+ if (y + btnH > hintRow)
65
+ break;
66
+ drawTabButton(buf, 1, y, w - 2, AGENTS[i].label, i === this.selectedIdx, "left");
67
+ y += stride;
68
+ }
69
+ buf.centerText(hintRow, "↑/↓ select · Enter open · Esc back", { fg: C.dim });
70
+ }
71
+ }
@@ -0,0 +1,490 @@
1
+ import { Screen } from "../terminal/screen.js";
2
+ import { C } from "../colors.js";
3
+ import { loadPrompt, saveSession, } from "../config.js";
4
+ import { drawHeader } from "../header.js";
5
+ import { drawInputBox, wrapText } from "../ui.js";
6
+ import { ellipsize, displayWidth } from "../width.js";
7
+ import { LLMClient } from "../llm/client.js";
8
+ import { executeTool, getToolDefinitions } from "../llm/tools.js";
9
+ const TOOLS = getToolDefinitions();
10
+ function parseArgs(s) {
11
+ if (!s || !s.trim())
12
+ return {};
13
+ try {
14
+ const v = JSON.parse(s);
15
+ return typeof v === "object" && v !== null ? v : {};
16
+ }
17
+ catch {
18
+ return {};
19
+ }
20
+ }
21
+ export class ChatScreen extends Screen {
22
+ initialMessage;
23
+ loadedMessages = null;
24
+ sessionId = null;
25
+ loadedProvider = null;
26
+ loadedModel = null;
27
+ loadedIntensity = null;
28
+ loadedTokens = null;
29
+ modelProvider = "DeepSeek";
30
+ model = { name: "deepseek-chat", url: "https://api.deepseek.com/v1", api_key: "", supports_streaming: true };
31
+ intensity = "High";
32
+ tokens = { prompt: 0, completion: 0 };
33
+ showInfo = false;
34
+ messages = [];
35
+ streamingContent = null;
36
+ busy = false;
37
+ paused = false;
38
+ queued = null;
39
+ turnToken = 0;
40
+ inputValue = "";
41
+ cursor = 0;
42
+ scrollTop = 0;
43
+ followBottom = true;
44
+ lastEscTime = 0;
45
+ initialized = false;
46
+ constructor(initialMessage = "") {
47
+ super();
48
+ this.initialMessage = initialMessage;
49
+ }
50
+ onActivate() {
51
+ if (this.initialized)
52
+ return;
53
+ this.initialized = true;
54
+ const st = this.app.state;
55
+ this.modelProvider = this.loadedProvider ?? st.selectedProvider;
56
+ this.model = {
57
+ name: this.loadedModel ?? st.selectedModelName,
58
+ url: st.selectedModelUrl,
59
+ api_key: st.selectedModelApiKey,
60
+ supports_streaming: st.selectedModelStreaming,
61
+ };
62
+ this.intensity = this.loadedIntensity ?? st.selectedIntensity;
63
+ if (this.loadedTokens) {
64
+ this.tokens = { prompt: this.loadedTokens.prompt, completion: this.loadedTokens.completion };
65
+ }
66
+ this.messages.push({
67
+ role: "system",
68
+ content: `Model: ${this.modelProvider}/${this.model.name} | Intensity: ${this.intensity}`,
69
+ });
70
+ for (const m of this.loadedMessages ?? []) {
71
+ this.messages.push(m);
72
+ }
73
+ if (this.initialMessage) {
74
+ this.send(this.initialMessage);
75
+ }
76
+ else {
77
+ this.app.render();
78
+ }
79
+ }
80
+ onKey(key) {
81
+ if (this.showInfo) {
82
+ this.showInfo = false;
83
+ this.app.render();
84
+ if (key.name === "escape")
85
+ return;
86
+ }
87
+ if (key.name === "escape") {
88
+ const now = Date.now();
89
+ if (now - this.lastEscTime < 400) {
90
+ this.lastEscTime = 0;
91
+ this.togglePause();
92
+ }
93
+ else {
94
+ this.lastEscTime = now;
95
+ }
96
+ return;
97
+ }
98
+ if (key.name === "return") {
99
+ const text = this.inputValue.trim();
100
+ if (text)
101
+ this.send(text);
102
+ return;
103
+ }
104
+ if (key.name === "up") {
105
+ this.followBottom = false;
106
+ this.scrollTop = Math.max(0, this.scrollTop - 1);
107
+ this.app.render();
108
+ return;
109
+ }
110
+ if (key.name === "down") {
111
+ this.scrollTop += 1;
112
+ this.app.render();
113
+ return;
114
+ }
115
+ if (key.name === "pageup") {
116
+ this.followBottom = false;
117
+ this.scrollTop = Math.max(0, this.scrollTop - 10);
118
+ this.app.render();
119
+ return;
120
+ }
121
+ if (key.name === "pagedown") {
122
+ this.scrollTop += 10;
123
+ this.app.render();
124
+ return;
125
+ }
126
+ if (key.name === "left") {
127
+ if (this.cursor > 0) {
128
+ this.cursor--;
129
+ this.app.render();
130
+ }
131
+ return;
132
+ }
133
+ if (key.name === "right") {
134
+ if (this.cursor < this.inputValue.length) {
135
+ this.cursor++;
136
+ this.app.render();
137
+ }
138
+ return;
139
+ }
140
+ if (key.name === "backspace") {
141
+ if (this.cursor > 0) {
142
+ this.inputValue = this.inputValue.slice(0, this.cursor - 1) + this.inputValue.slice(this.cursor);
143
+ this.cursor--;
144
+ this.app.render();
145
+ }
146
+ return;
147
+ }
148
+ if (key.name === "delete") {
149
+ if (this.cursor < this.inputValue.length) {
150
+ this.inputValue = this.inputValue.slice(0, this.cursor) + this.inputValue.slice(this.cursor + 1);
151
+ this.app.render();
152
+ }
153
+ return;
154
+ }
155
+ if (key.name === "home") {
156
+ this.cursor = 0;
157
+ this.app.render();
158
+ return;
159
+ }
160
+ if (key.name === "end") {
161
+ this.cursor = this.inputValue.length;
162
+ this.app.render();
163
+ return;
164
+ }
165
+ if (key.ctrl && key.name === "b") {
166
+ this.save();
167
+ this.app.popScreen();
168
+ return;
169
+ }
170
+ if (key.ctrl && key.name === "l") {
171
+ const sysText = this.messages[0] && this.messages[0].role === "system" ? this.messages[0].content : "";
172
+ this.messages = [{ role: "system", content: sysText }];
173
+ this.streamingContent = null;
174
+ this.scrollTop = 0;
175
+ this.followBottom = true;
176
+ this.app.render();
177
+ return;
178
+ }
179
+ if (key.ctrl && key.name === "u") {
180
+ this.inputValue = "";
181
+ this.cursor = 0;
182
+ this.app.render();
183
+ return;
184
+ }
185
+ if (!key.ctrl && !key.meta && key.name.length === 1) {
186
+ this.inputValue = this.inputValue.slice(0, this.cursor) + key.name + this.inputValue.slice(this.cursor);
187
+ this.cursor++;
188
+ this.app.render();
189
+ }
190
+ }
191
+ onMouse(ev) {
192
+ if (ev.type === "click") {
193
+ if (ev.y === 0 && ev.x >= this.app.width - 3) {
194
+ this.showInfo = !this.showInfo;
195
+ this.app.render();
196
+ return;
197
+ }
198
+ if (this.showInfo) {
199
+ this.showInfo = false;
200
+ this.app.render();
201
+ }
202
+ return;
203
+ }
204
+ if (ev.type === "wheel") {
205
+ if (ev.dir === -1) {
206
+ this.followBottom = false;
207
+ this.scrollTop = Math.max(0, this.scrollTop - 3);
208
+ }
209
+ else {
210
+ this.scrollTop += 3;
211
+ }
212
+ this.app.render();
213
+ }
214
+ }
215
+ togglePause() {
216
+ if (!this.busy && !this.queued)
217
+ return;
218
+ this.paused = !this.paused;
219
+ if (this.paused) {
220
+ this.turnToken++;
221
+ this.busy = false;
222
+ this.app.state.isThinking = false;
223
+ this.save();
224
+ }
225
+ this.app.render();
226
+ this.maybeFlushQueue();
227
+ }
228
+ maybeFlushQueue() {
229
+ if (this.queued) {
230
+ this.queued = null;
231
+ this.startTurn();
232
+ }
233
+ }
234
+ send(text) {
235
+ const trimmed = text.trim();
236
+ if (!trimmed)
237
+ return;
238
+ this.messages.push({ role: "user", content: trimmed });
239
+ this.inputValue = "";
240
+ this.cursor = 0;
241
+ this.followBottom = true;
242
+ if (this.busy && !this.paused) {
243
+ this.queued = trimmed;
244
+ this.app.render();
245
+ return;
246
+ }
247
+ this.startTurn();
248
+ }
249
+ startTurn() {
250
+ this.busy = true;
251
+ this.paused = false;
252
+ this.app.state.isThinking = true;
253
+ const token = ++this.turnToken;
254
+ const apiMessages = this.buildApiMessages();
255
+ this.streamingContent = "";
256
+ this.app.render();
257
+ void this.agentLoop(apiMessages, token);
258
+ }
259
+ buildApiMessages() {
260
+ const api = [{ role: "system", content: loadPrompt() }];
261
+ for (const m of this.messages) {
262
+ if (m.role === "user" || m.role === "assistant") {
263
+ api.push({ role: m.role, content: m.content });
264
+ }
265
+ }
266
+ return api;
267
+ }
268
+ async agentLoop(apiMessages, token) {
269
+ try {
270
+ while (true) {
271
+ if (token !== this.turnToken)
272
+ return;
273
+ const client = new LLMClient(this.model, this.modelProvider, this.intensity);
274
+ let content = "";
275
+ let hasContent = false;
276
+ let aborted = false;
277
+ for await (const chunk of client.streamTurn(apiMessages, TOOLS)) {
278
+ if (token !== this.turnToken || this.paused) {
279
+ aborted = true;
280
+ break;
281
+ }
282
+ content += chunk;
283
+ hasContent = true;
284
+ this.streamingContent = content;
285
+ this.app.render();
286
+ }
287
+ if (client.usage) {
288
+ this.tokens.prompt += client.usage.prompt;
289
+ this.tokens.completion += client.usage.completion;
290
+ }
291
+ else {
292
+ this.tokens.completion += Math.max(1, Math.ceil(content.length / 4));
293
+ }
294
+ if (token !== this.turnToken) {
295
+ if (content)
296
+ this.messages.push({ role: "assistant", content });
297
+ this.streamingContent = null;
298
+ this.app.render();
299
+ return;
300
+ }
301
+ if (aborted || this.paused) {
302
+ if (content)
303
+ this.messages.push({ role: "assistant", content });
304
+ this.streamingContent = null;
305
+ this.app.render();
306
+ this.onTurnFinished();
307
+ return;
308
+ }
309
+ const toolCalls = client.toolCalls;
310
+ if (toolCalls.length === 0) {
311
+ if (hasContent)
312
+ this.messages.push({ role: "assistant", content });
313
+ this.streamingContent = null;
314
+ break;
315
+ }
316
+ if (hasContent)
317
+ this.messages.push({ role: "assistant", content });
318
+ this.streamingContent = null;
319
+ apiMessages.push({
320
+ role: "assistant",
321
+ content,
322
+ tool_calls: toolCalls.map((tc) => ({
323
+ id: tc.id,
324
+ type: "function",
325
+ function: { name: tc.name, arguments: tc.arguments },
326
+ })),
327
+ });
328
+ for (const tc of toolCalls) {
329
+ if (token !== this.turnToken || this.paused)
330
+ return;
331
+ const args = parseArgs(tc.arguments);
332
+ const toolMsg = { role: "tool", name: tc.name, arguments: args, result: "" };
333
+ this.messages.push(toolMsg);
334
+ this.app.render();
335
+ const result = await executeTool(tc.name, args);
336
+ if (token !== this.turnToken || this.paused)
337
+ return;
338
+ toolMsg.result = result;
339
+ apiMessages.push({ role: "tool", tool_call_id: tc.id, content: result });
340
+ this.app.render();
341
+ }
342
+ }
343
+ this.onTurnFinished();
344
+ }
345
+ catch (e) {
346
+ const err = e instanceof Error ? e.message : String(e);
347
+ this.messages.push({ role: "assistant", content: (this.streamingContent ?? "") + `\n[Error: ${err}]` });
348
+ this.streamingContent = null;
349
+ this.onTurnFinished();
350
+ }
351
+ }
352
+ onTurnFinished() {
353
+ this.busy = false;
354
+ this.paused = false;
355
+ this.app.state.isThinking = false;
356
+ this.save();
357
+ this.app.render();
358
+ this.maybeFlushQueue();
359
+ }
360
+ save() {
361
+ const chatMessages = this.messages.filter((m) => m.role === "user" || m.role === "assistant" || m.role === "tool");
362
+ if (chatMessages.length === 0)
363
+ return;
364
+ try {
365
+ this.sessionId = saveSession(chatMessages, this.model.name, this.modelProvider, this.intensity, process.cwd(), this.sessionId ?? undefined, this.tokens);
366
+ }
367
+ catch {
368
+ // ignore save errors
369
+ }
370
+ }
371
+ buildLines(width) {
372
+ const lines = [];
373
+ const push = (text, style) => {
374
+ for (const ln of wrapText(text, width))
375
+ lines.push({ text: ln, style });
376
+ };
377
+ for (const m of this.messages) {
378
+ if (m.role === "system") {
379
+ push(m.content, { fg: C.dim });
380
+ }
381
+ else if (m.role === "user") {
382
+ lines.push({ text: "You:", style: { fg: C.accent, bold: true } });
383
+ push(m.content, { fg: C.user });
384
+ lines.push({ text: "", style: {} });
385
+ }
386
+ else if (m.role === "assistant") {
387
+ lines.push({ text: "Neutrino:", style: { fg: C.accent, bold: true } });
388
+ push(m.content, { fg: C.assistant });
389
+ lines.push({ text: "", style: {} });
390
+ }
391
+ else if (m.role === "tool") {
392
+ lines.push({ text: `⚙ ${m.name}`, style: { fg: C.toolHeader, bold: true } });
393
+ const argText = m.arguments.command ?? m.arguments.path ?? "";
394
+ if (argText)
395
+ lines.push({ text: " " + argText, style: { fg: C.dim } });
396
+ push(m.result || "(no output)", { fg: C.dim });
397
+ lines.push({ text: "", style: {} });
398
+ }
399
+ }
400
+ if (this.streamingContent !== null) {
401
+ lines.push({ text: "Neutrino:", style: { fg: C.accent, bold: true } });
402
+ push(this.streamingContent, { fg: C.assistant });
403
+ }
404
+ return lines;
405
+ }
406
+ render(buf) {
407
+ drawHeader(buf, this.app.state);
408
+ const w = buf.width;
409
+ const h = buf.height;
410
+ let status = "";
411
+ let statusStyle = { fg: C.dim };
412
+ if (this.queued) {
413
+ status = "Will send when paused or idle...";
414
+ statusStyle = { fg: C.amber };
415
+ }
416
+ else if (this.paused) {
417
+ status = "PAUSED — press ESC ESC to resume";
418
+ statusStyle = { fg: C.amber, bold: true };
419
+ }
420
+ else if (this.busy) {
421
+ status = "Streaming... press ESC ESC to pause";
422
+ statusStyle = { fg: C.dim };
423
+ }
424
+ if (status)
425
+ buf.centerText(1, status.slice(0, w), statusStyle);
426
+ const inputY = h - 4;
427
+ const logTop = 2;
428
+ const logBottom = inputY - 2;
429
+ const viewH = Math.max(1, logBottom - logTop + 1);
430
+ const lines = this.buildLines(Math.max(10, w - 4));
431
+ const maxScroll = Math.max(0, lines.length - viewH);
432
+ if (this.followBottom)
433
+ this.scrollTop = maxScroll;
434
+ if (this.scrollTop > maxScroll) {
435
+ this.scrollTop = maxScroll;
436
+ this.followBottom = true;
437
+ }
438
+ if (this.scrollTop < 0)
439
+ this.scrollTop = 0;
440
+ for (let y = logTop; y <= logBottom; y++) {
441
+ const li = this.scrollTop + (y - logTop);
442
+ if (li >= lines.length)
443
+ break;
444
+ const line = lines[li];
445
+ buf.writeText(2, y, line.text.slice(0, w - 2), line.style);
446
+ }
447
+ const inW = Math.max(12, w - 2);
448
+ drawInputBox(buf, 1, inputY, inW, this.inputValue, this.cursor, {
449
+ active: true,
450
+ dimmed: this.busy,
451
+ placeholder: "Type a message...",
452
+ });
453
+ const hintY = h - 1;
454
+ const hints = "Enter send · ESC ESC pause · Ctrl+B menu · Ctrl+L clear";
455
+ const hintW = displayWidth(hints);
456
+ const hintX = Math.max(0, w - 1 - hintW);
457
+ const pathAvail = Math.max(0, hintX - 2);
458
+ buf.writeText(1, hintY, ellipsize(process.cwd(), pathAvail), { fg: C.dim });
459
+ buf.writeText(hintX, hintY, hints, { fg: C.dim });
460
+ this.renderInfoOverlay(buf);
461
+ }
462
+ renderInfoOverlay(buf) {
463
+ const w = buf.width;
464
+ buf.set(w - 2, 0, "ⓘ", { fg: C.accent, bg: C.highlightBg, bold: true });
465
+ if (!this.showInfo)
466
+ return;
467
+ const total = this.tokens.prompt + this.tokens.completion;
468
+ const lines = [
469
+ "Tokens",
470
+ `Prompt: ${this.tokens.prompt}`,
471
+ `Completion: ${this.tokens.completion}`,
472
+ `Total: ${total}`,
473
+ ];
474
+ let contentW = 8;
475
+ for (const l of lines)
476
+ contentW = Math.max(contentW, displayWidth(l));
477
+ let boxW = contentW + 4;
478
+ if (boxW > w - 2)
479
+ boxW = Math.max(6, w - 2);
480
+ const boxX = w - 1 - boxW;
481
+ const boxY = 1;
482
+ const boxH = lines.length + 2;
483
+ buf.fillRect(boxX, boxY, boxW, boxH, { bg: C.black });
484
+ buf.drawBox(boxX, boxY, boxW, boxH, { fg: C.accent });
485
+ for (let i = 0; i < lines.length; i++) {
486
+ const style = i === 0 ? { fg: C.accent, bg: C.black, bold: true } : { fg: C.assistant, bg: C.black };
487
+ buf.writeText(boxX + 2, boxY + 1 + i, lines[i].slice(0, boxW - 4), style);
488
+ }
489
+ }
490
+ }