@xingjisoft/neutrino 0.1.0 → 0.1.1
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/README.md +16 -2
- package/dist/app.js +20 -0
- package/dist/colors.js +1 -0
- package/dist/header.js +11 -6
- package/dist/markdown.js +630 -0
- package/dist/screens/agentConversation.js +12 -14
- package/dist/screens/agentHistory.js +76 -24
- package/dist/screens/agentSelect.js +54 -7
- package/dist/screens/chat.js +42 -21
- package/dist/screens/history.js +73 -20
- package/dist/screens/modelSelect.js +66 -7
- package/dist/screens/settings.js +64 -2
- package/dist/screens/start.js +96 -10
- package/dist/state.js +1 -0
- package/dist/terminal/input.js +14 -0
- package/dist/terminal/renderer.js +6 -2
- package/dist/ui.js +70 -25
- package/package.json +1 -1
|
@@ -2,14 +2,20 @@ import { Screen } from "../terminal/screen.js";
|
|
|
2
2
|
import { C } from "../colors.js";
|
|
3
3
|
import { drawHeader } from "../header.js";
|
|
4
4
|
import { truncateByWidth } from "../width.js";
|
|
5
|
+
import { drawHint } from "../ui.js";
|
|
5
6
|
import { listAgentSessions, loadAgentMessages, } from "../agents.js";
|
|
6
7
|
import { AgentConversationScreen } from "./agentConversation.js";
|
|
8
|
+
const LIST_TOP = 5;
|
|
7
9
|
export class AgentHistoryScreen extends Screen {
|
|
8
10
|
agent;
|
|
9
11
|
label;
|
|
10
12
|
sessions = [];
|
|
11
13
|
selectedIdx = 0;
|
|
12
14
|
scrollTop = 0;
|
|
15
|
+
lastHoverX = -1;
|
|
16
|
+
lastHoverY = -1;
|
|
17
|
+
rows = [];
|
|
18
|
+
itemBounds = [];
|
|
13
19
|
constructor(agent, label) {
|
|
14
20
|
super();
|
|
15
21
|
this.agent = agent;
|
|
@@ -19,6 +25,8 @@ export class AgentHistoryScreen extends Screen {
|
|
|
19
25
|
this.sessions = listAgentSessions(this.agent);
|
|
20
26
|
this.selectedIdx = 0;
|
|
21
27
|
this.scrollTop = 0;
|
|
28
|
+
this.rows = [];
|
|
29
|
+
this.itemBounds = [];
|
|
22
30
|
}
|
|
23
31
|
move(delta) {
|
|
24
32
|
if (this.sessions.length === 0)
|
|
@@ -37,10 +45,50 @@ export class AgentHistoryScreen extends Screen {
|
|
|
37
45
|
this.app.popScreen();
|
|
38
46
|
}
|
|
39
47
|
onMouse(ev) {
|
|
48
|
+
if (ev.type === "move") {
|
|
49
|
+
if (ev.x === this.lastHoverX && ev.y === this.lastHoverY)
|
|
50
|
+
return;
|
|
51
|
+
this.lastHoverX = ev.x;
|
|
52
|
+
this.lastHoverY = ev.y;
|
|
53
|
+
const idx = this.itemIndexAtRow(ev.y);
|
|
54
|
+
if (idx >= 0 && idx !== this.selectedIdx) {
|
|
55
|
+
this.selectedIdx = idx;
|
|
56
|
+
this.app.render();
|
|
57
|
+
}
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
40
60
|
if (ev.type === "wheel") {
|
|
41
61
|
this.scrollTop = Math.max(0, this.scrollTop + (ev.dir === -1 ? -3 : 3));
|
|
42
62
|
this.app.render();
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
if (ev.type === "click") {
|
|
66
|
+
if (ev.button === 2) {
|
|
67
|
+
this.app.popScreen();
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
const idx = this.itemIndexAtRow(ev.y);
|
|
71
|
+
if (idx < 0)
|
|
72
|
+
return;
|
|
73
|
+
this.selectedIdx = idx;
|
|
74
|
+
this.loadSelected();
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
itemIndexAtRow(y) {
|
|
78
|
+
if (y < LIST_TOP)
|
|
79
|
+
return -1;
|
|
80
|
+
const h = this.app.height;
|
|
81
|
+
const listBottom = h - 2;
|
|
82
|
+
if (y >= listBottom)
|
|
83
|
+
return -1;
|
|
84
|
+
const ri = this.scrollTop + (y - LIST_TOP);
|
|
85
|
+
if (ri < 0 || ri >= this.rows.length)
|
|
86
|
+
return -1;
|
|
87
|
+
for (let i = 0; i < this.itemBounds.length; i++) {
|
|
88
|
+
if (ri >= this.itemBounds[i].start && ri < this.itemBounds[i].end)
|
|
89
|
+
return i;
|
|
43
90
|
}
|
|
91
|
+
return -1;
|
|
44
92
|
}
|
|
45
93
|
loadSelected() {
|
|
46
94
|
if (this.selectedIdx < 0 || this.selectedIdx >= this.sessions.length)
|
|
@@ -49,20 +97,7 @@ export class AgentHistoryScreen extends Screen {
|
|
|
49
97
|
const messages = loadAgentMessages(this.agent, s.id);
|
|
50
98
|
this.app.pushScreen(new AgentConversationScreen(`${this.label} · ${s.title || s.id}`, messages));
|
|
51
99
|
}
|
|
52
|
-
|
|
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
|
-
}
|
|
100
|
+
buildRows() {
|
|
66
101
|
const rows = [];
|
|
67
102
|
const itemBounds = [];
|
|
68
103
|
for (const s of this.sessions) {
|
|
@@ -73,27 +108,44 @@ export class AgentHistoryScreen extends Screen {
|
|
|
73
108
|
rows.push(` ${s.model}`);
|
|
74
109
|
itemBounds.push({ start, end: rows.length });
|
|
75
110
|
}
|
|
111
|
+
this.rows = rows;
|
|
112
|
+
this.itemBounds = itemBounds;
|
|
113
|
+
}
|
|
114
|
+
render(buf) {
|
|
115
|
+
drawHeader(buf, this.app.state);
|
|
116
|
+
const w = buf.width;
|
|
117
|
+
const h = buf.height;
|
|
118
|
+
buf.centerText(2, this.label, { fg: C.accent, bold: true });
|
|
119
|
+
buf.centerText(3, "Select a session to view", { fg: C.dim });
|
|
120
|
+
const listBottom = h - 2;
|
|
121
|
+
const viewH = Math.max(1, listBottom - LIST_TOP);
|
|
122
|
+
if (this.sessions.length === 0) {
|
|
123
|
+
buf.centerText(LIST_TOP + 1, "No sessions yet", { fg: C.dim });
|
|
124
|
+
drawHint(buf, listBottom, "Esc back");
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
this.buildRows();
|
|
76
128
|
if (this.selectedIdx < 0)
|
|
77
129
|
this.selectedIdx = 0;
|
|
78
|
-
if (this.selectedIdx >= itemBounds.length)
|
|
79
|
-
this.selectedIdx = itemBounds.length - 1;
|
|
80
|
-
const selected = itemBounds[this.selectedIdx];
|
|
130
|
+
if (this.selectedIdx >= this.itemBounds.length)
|
|
131
|
+
this.selectedIdx = Math.max(0, this.itemBounds.length - 1);
|
|
132
|
+
const selected = this.itemBounds[this.selectedIdx];
|
|
81
133
|
const firstRowOfSelected = selected.start;
|
|
82
134
|
if (firstRowOfSelected < this.scrollTop)
|
|
83
135
|
this.scrollTop = firstRowOfSelected;
|
|
84
136
|
if (firstRowOfSelected >= this.scrollTop + viewH)
|
|
85
137
|
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 =
|
|
89
|
-
const ri = this.scrollTop + (y -
|
|
90
|
-
if (ri < 0 || ri >= rows.length)
|
|
138
|
+
if (this.scrollTop > this.rows.length - viewH)
|
|
139
|
+
this.scrollTop = Math.max(0, this.rows.length - viewH);
|
|
140
|
+
for (let y = LIST_TOP; y < listBottom; y++) {
|
|
141
|
+
const ri = this.scrollTop + (y - LIST_TOP);
|
|
142
|
+
if (ri < 0 || ri >= this.rows.length)
|
|
91
143
|
break;
|
|
92
144
|
const isSelected = ri >= selected.start && ri < selected.end;
|
|
93
145
|
const isSelectedFirst = ri === selected.start;
|
|
94
|
-
const text = (isSelectedFirst ? "▸ " : " ") + rows[ri];
|
|
146
|
+
const text = (isSelectedFirst ? "▸ " : " ") + this.rows[ri];
|
|
95
147
|
buf.writeText(0, y, truncateByWidth(text, w), isSelected ? { fg: C.accent, bg: C.highlightBg } : { fg: C.assistant });
|
|
96
148
|
}
|
|
97
|
-
buf
|
|
149
|
+
drawHint(buf, listBottom, "↑/↓ select · Enter view · click to view · right-click back · Esc back");
|
|
98
150
|
}
|
|
99
151
|
}
|
|
@@ -1,16 +1,21 @@
|
|
|
1
1
|
import { Screen } from "../terminal/screen.js";
|
|
2
2
|
import { C } from "../colors.js";
|
|
3
3
|
import { drawHeader } from "../header.js";
|
|
4
|
-
import { drawTabButton } from "../ui.js";
|
|
4
|
+
import { drawHint, drawTabButton, inRect } from "../ui.js";
|
|
5
5
|
import { AgentHistoryScreen } from "./agentHistory.js";
|
|
6
6
|
const AGENTS = [
|
|
7
7
|
{ id: "claude", label: "Claude Code" },
|
|
8
8
|
{ id: "codex", label: "Codex" },
|
|
9
9
|
{ id: "opencode", label: "OpenCode" },
|
|
10
10
|
];
|
|
11
|
+
const LIST_TOP = 5;
|
|
12
|
+
const BTN_H = 3;
|
|
13
|
+
const BTN_X = 1;
|
|
11
14
|
export class AgentSelectScreen extends Screen {
|
|
12
15
|
selectedIdx = 0;
|
|
13
16
|
scrollBtn = 0;
|
|
17
|
+
lastHoverX = -1;
|
|
18
|
+
lastHoverY = -1;
|
|
14
19
|
onActivate() {
|
|
15
20
|
this.selectedIdx = 0;
|
|
16
21
|
this.scrollBtn = 0;
|
|
@@ -36,6 +41,49 @@ export class AgentSelectScreen extends Screen {
|
|
|
36
41
|
else if (key.name === "escape")
|
|
37
42
|
this.app.popScreen();
|
|
38
43
|
}
|
|
44
|
+
onMouse(ev) {
|
|
45
|
+
if (ev.type === "move") {
|
|
46
|
+
if (ev.x === this.lastHoverX && ev.y === this.lastHoverY)
|
|
47
|
+
return;
|
|
48
|
+
this.lastHoverX = ev.x;
|
|
49
|
+
this.lastHoverY = ev.y;
|
|
50
|
+
const i = this.indexAtRow(ev.y);
|
|
51
|
+
if (i >= 0 && i !== this.selectedIdx) {
|
|
52
|
+
this.selectedIdx = i;
|
|
53
|
+
this.app.render();
|
|
54
|
+
}
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
if (ev.type === "wheel") {
|
|
58
|
+
this.move(ev.dir === -1 ? -1 : 1);
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
if (ev.type === "click") {
|
|
62
|
+
if (ev.button === 2) {
|
|
63
|
+
this.app.popScreen();
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
const i = this.indexAtRow(ev.y);
|
|
67
|
+
if (i < 0)
|
|
68
|
+
return;
|
|
69
|
+
if (!inRect(ev.x, ev.y, { x: BTN_X, y: LIST_TOP + (i - this.scrollBtn) * BTN_H, w: this.app.width - 2, h: BTN_H })) {
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
this.selectedIdx = i;
|
|
73
|
+
this.confirm();
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
indexAtRow(y) {
|
|
77
|
+
const h = this.app.height;
|
|
78
|
+
const hintRow = h - 1;
|
|
79
|
+
const maxVisible = Math.max(1, Math.floor((hintRow - LIST_TOP) / BTN_H));
|
|
80
|
+
const i = Math.floor((y - LIST_TOP) / BTN_H) + this.scrollBtn;
|
|
81
|
+
if (i < 0 || i >= AGENTS.length)
|
|
82
|
+
return -1;
|
|
83
|
+
if (LIST_TOP + (i - this.scrollBtn) * BTN_H + BTN_H > hintRow)
|
|
84
|
+
return -1;
|
|
85
|
+
return i;
|
|
86
|
+
}
|
|
39
87
|
confirm() {
|
|
40
88
|
const agent = AGENTS[this.selectedIdx];
|
|
41
89
|
if (!agent)
|
|
@@ -48,24 +96,23 @@ export class AgentSelectScreen extends Screen {
|
|
|
48
96
|
const h = buf.height;
|
|
49
97
|
buf.centerText(2, "Other Agents", { fg: C.accent, bold: true });
|
|
50
98
|
buf.centerText(3, "Select an agent to open its history", { fg: C.white });
|
|
51
|
-
const
|
|
52
|
-
const btnH = 3;
|
|
99
|
+
const btnH = BTN_H;
|
|
53
100
|
const stride = btnH;
|
|
54
101
|
const hintRow = h - 1;
|
|
55
|
-
const maxVisible = Math.max(1, Math.floor((hintRow -
|
|
102
|
+
const maxVisible = Math.max(1, Math.floor((hintRow - LIST_TOP) / stride));
|
|
56
103
|
if (this.selectedIdx < this.scrollBtn)
|
|
57
104
|
this.scrollBtn = this.selectedIdx;
|
|
58
105
|
if (this.selectedIdx >= this.scrollBtn + maxVisible)
|
|
59
106
|
this.scrollBtn = this.selectedIdx - maxVisible + 1;
|
|
60
107
|
if (this.scrollBtn < 0)
|
|
61
108
|
this.scrollBtn = 0;
|
|
62
|
-
let y =
|
|
109
|
+
let y = LIST_TOP;
|
|
63
110
|
for (let i = this.scrollBtn; i < AGENTS.length; i++) {
|
|
64
111
|
if (y + btnH > hintRow)
|
|
65
112
|
break;
|
|
66
|
-
drawTabButton(buf,
|
|
113
|
+
drawTabButton(buf, BTN_X, y, w - 2, AGENTS[i].label, i === this.selectedIdx, "left");
|
|
67
114
|
y += stride;
|
|
68
115
|
}
|
|
69
|
-
buf
|
|
116
|
+
drawHint(buf, hintRow, "↑/↓ select · Enter open · click to open · right-click back · Esc back");
|
|
70
117
|
}
|
|
71
118
|
}
|
package/dist/screens/chat.js
CHANGED
|
@@ -2,7 +2,8 @@ import { Screen } from "../terminal/screen.js";
|
|
|
2
2
|
import { C } from "../colors.js";
|
|
3
3
|
import { loadPrompt, saveSession, } from "../config.js";
|
|
4
4
|
import { drawHeader } from "../header.js";
|
|
5
|
-
import { drawInputBox, wrapText } from "../ui.js";
|
|
5
|
+
import { computeInputWindow, cursorFromColumn, drawInputBox, wrapText } from "../ui.js";
|
|
6
|
+
import { drawSpans, renderMarkdownText } from "../markdown.js";
|
|
6
7
|
import { ellipsize, displayWidth } from "../width.js";
|
|
7
8
|
import { LLMClient } from "../llm/client.js";
|
|
8
9
|
import { executeTool, getToolDefinitions } from "../llm/tools.js";
|
|
@@ -190,14 +191,34 @@ export class ChatScreen extends Screen {
|
|
|
190
191
|
}
|
|
191
192
|
onMouse(ev) {
|
|
192
193
|
if (ev.type === "click") {
|
|
193
|
-
if (ev.
|
|
194
|
-
|
|
194
|
+
if (ev.button === 2)
|
|
195
|
+
return; // right click: no-op in chat
|
|
196
|
+
// header row: right corner toggles token stats, the rest opens model config
|
|
197
|
+
if (ev.y === 0) {
|
|
198
|
+
if (ev.x >= this.app.width - 3) {
|
|
199
|
+
this.showInfo = !this.showInfo;
|
|
200
|
+
}
|
|
201
|
+
else {
|
|
202
|
+
this.app.openModelConfig();
|
|
203
|
+
}
|
|
195
204
|
this.app.render();
|
|
196
205
|
return;
|
|
197
206
|
}
|
|
198
207
|
if (this.showInfo) {
|
|
199
208
|
this.showInfo = false;
|
|
200
209
|
this.app.render();
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
// click on the input box -> focus and position the cursor
|
|
213
|
+
const inputY = this.app.height - 4;
|
|
214
|
+
const inW = Math.max(12, this.app.width - 2);
|
|
215
|
+
if (ev.y === inputY + 1 && ev.x >= 3 && ev.x < 3 + (inW - 4)) {
|
|
216
|
+
const innerW = inW - 4;
|
|
217
|
+
const col = Math.min(innerW - 1, ev.x - 3);
|
|
218
|
+
const win = computeInputWindow(this.inputValue, this.cursor, innerW);
|
|
219
|
+
this.cursor = cursorFromColumn(this.inputValue, col, win);
|
|
220
|
+
this.app.render();
|
|
221
|
+
return;
|
|
201
222
|
}
|
|
202
223
|
return;
|
|
203
224
|
}
|
|
@@ -370,36 +391,37 @@ export class ChatScreen extends Screen {
|
|
|
370
391
|
}
|
|
371
392
|
buildLines(width) {
|
|
372
393
|
const lines = [];
|
|
373
|
-
const
|
|
374
|
-
for (const ln of wrapText(text, width))
|
|
375
|
-
lines.push({ text: ln, style });
|
|
394
|
+
const pushText = (text, style) => {
|
|
395
|
+
for (const ln of wrapText(text, width)) {
|
|
396
|
+
lines.push({ spans: [{ text: ln, style }] });
|
|
397
|
+
}
|
|
376
398
|
};
|
|
377
399
|
for (const m of this.messages) {
|
|
378
400
|
if (m.role === "system") {
|
|
379
|
-
|
|
401
|
+
pushText(m.content, { fg: C.dim });
|
|
380
402
|
}
|
|
381
403
|
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({
|
|
404
|
+
lines.push({ spans: [{ text: "You:", style: { fg: C.accent, bold: true } }] });
|
|
405
|
+
lines.push(...renderMarkdownText(m.content, width, { fg: C.user }));
|
|
406
|
+
lines.push({ spans: [] });
|
|
385
407
|
}
|
|
386
408
|
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({
|
|
409
|
+
lines.push({ spans: [{ text: "Neutrino:", style: { fg: C.accent, bold: true } }] });
|
|
410
|
+
lines.push(...renderMarkdownText(m.content, width, { fg: C.assistant }));
|
|
411
|
+
lines.push({ spans: [] });
|
|
390
412
|
}
|
|
391
413
|
else if (m.role === "tool") {
|
|
392
|
-
lines.push({ text: `⚙ ${m.name}`, style: { fg: C.toolHeader, bold: true } });
|
|
414
|
+
lines.push({ spans: [{ text: `⚙ ${m.name}`, style: { fg: C.toolHeader, bold: true } }] });
|
|
393
415
|
const argText = m.arguments.command ?? m.arguments.path ?? "";
|
|
394
416
|
if (argText)
|
|
395
|
-
lines.push({ text: " " + argText, style: { fg: C.dim } });
|
|
396
|
-
|
|
397
|
-
lines.push({
|
|
417
|
+
lines.push({ spans: [{ text: " " + argText, style: { fg: C.dim } }] });
|
|
418
|
+
pushText(m.result || "(no output)", { fg: C.dim });
|
|
419
|
+
lines.push({ spans: [] });
|
|
398
420
|
}
|
|
399
421
|
}
|
|
400
422
|
if (this.streamingContent !== null) {
|
|
401
|
-
lines.push({ text: "Neutrino:", style: { fg: C.accent, bold: true } });
|
|
402
|
-
push(this.streamingContent, { fg: C.assistant });
|
|
423
|
+
lines.push({ spans: [{ text: "Neutrino:", style: { fg: C.accent, bold: true } }] });
|
|
424
|
+
lines.push(...renderMarkdownText(this.streamingContent, width, { fg: C.assistant }));
|
|
403
425
|
}
|
|
404
426
|
return lines;
|
|
405
427
|
}
|
|
@@ -441,8 +463,7 @@ export class ChatScreen extends Screen {
|
|
|
441
463
|
const li = this.scrollTop + (y - logTop);
|
|
442
464
|
if (li >= lines.length)
|
|
443
465
|
break;
|
|
444
|
-
|
|
445
|
-
buf.writeText(2, y, line.text.slice(0, w - 2), line.style);
|
|
466
|
+
drawSpans(buf, 2, y, lines[li].spans, Math.max(4, w - 4));
|
|
446
467
|
}
|
|
447
468
|
const inW = Math.max(12, w - 2);
|
|
448
469
|
drawInputBox(buf, 1, inputY, inW, this.inputValue, this.cursor, {
|
package/dist/screens/history.js
CHANGED
|
@@ -2,14 +2,22 @@ import { Screen } from "../terminal/screen.js";
|
|
|
2
2
|
import { C } from "../colors.js";
|
|
3
3
|
import { deleteSession, listSessions, loadSession } from "../config.js";
|
|
4
4
|
import { drawHeader } from "../header.js";
|
|
5
|
+
import { drawHint } from "../ui.js";
|
|
6
|
+
const LIST_TOP = 5;
|
|
5
7
|
export class HistoryScreen extends Screen {
|
|
6
8
|
sessions = [];
|
|
7
9
|
selectedIdx = 0;
|
|
8
10
|
scrollTop = 0;
|
|
11
|
+
lastHoverX = -1;
|
|
12
|
+
lastHoverY = -1;
|
|
13
|
+
rows = [];
|
|
14
|
+
itemBounds = [];
|
|
9
15
|
onActivate() {
|
|
10
16
|
this.sessions = listSessions();
|
|
11
17
|
this.selectedIdx = this.sessions.length > 0 ? 1 : 0;
|
|
12
18
|
this.scrollTop = 0;
|
|
19
|
+
this.rows = [];
|
|
20
|
+
this.itemBounds = [];
|
|
13
21
|
}
|
|
14
22
|
move(delta) {
|
|
15
23
|
const count = this.sessions.length + 1;
|
|
@@ -29,10 +37,51 @@ export class HistoryScreen extends Screen {
|
|
|
29
37
|
this.app.popScreen();
|
|
30
38
|
}
|
|
31
39
|
onMouse(ev) {
|
|
40
|
+
if (ev.type === "move") {
|
|
41
|
+
if (ev.x === this.lastHoverX && ev.y === this.lastHoverY)
|
|
42
|
+
return;
|
|
43
|
+
this.lastHoverX = ev.x;
|
|
44
|
+
this.lastHoverY = ev.y;
|
|
45
|
+
const idx = this.itemIndexAtRow(ev.y);
|
|
46
|
+
if (idx >= 0 && idx !== this.selectedIdx) {
|
|
47
|
+
this.selectedIdx = idx;
|
|
48
|
+
this.app.render();
|
|
49
|
+
}
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
32
52
|
if (ev.type === "wheel") {
|
|
33
53
|
this.scrollTop = Math.max(0, this.scrollTop + (ev.dir === -1 ? -3 : 3));
|
|
34
54
|
this.app.render();
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
if (ev.type === "click") {
|
|
58
|
+
if (ev.button === 2) {
|
|
59
|
+
this.app.popScreen();
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
const idx = this.itemIndexAtRow(ev.y);
|
|
63
|
+
if (idx < 0)
|
|
64
|
+
return;
|
|
65
|
+
this.selectedIdx = idx;
|
|
66
|
+
this.loadSelected();
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
/** Map a terminal row to the history item index (0 = "Open other Agent's history"). */
|
|
70
|
+
itemIndexAtRow(y) {
|
|
71
|
+
if (y < LIST_TOP)
|
|
72
|
+
return -1;
|
|
73
|
+
const h = this.app.height;
|
|
74
|
+
const listBottom = h - 2;
|
|
75
|
+
if (y >= listBottom)
|
|
76
|
+
return -1;
|
|
77
|
+
const ri = this.scrollTop + (y - LIST_TOP);
|
|
78
|
+
if (ri < 0 || ri >= this.rows.length)
|
|
79
|
+
return -1;
|
|
80
|
+
for (let i = 0; i < this.itemBounds.length; i++) {
|
|
81
|
+
if (ri >= this.itemBounds[i].start && ri < this.itemBounds[i].end)
|
|
82
|
+
return i;
|
|
35
83
|
}
|
|
84
|
+
return -1;
|
|
36
85
|
}
|
|
37
86
|
loadSelected() {
|
|
38
87
|
if (this.selectedIdx === 0) {
|
|
@@ -59,15 +108,7 @@ export class HistoryScreen extends Screen {
|
|
|
59
108
|
this.selectedIdx = Math.min(this.selectedIdx, this.sessions.length);
|
|
60
109
|
this.app.render();
|
|
61
110
|
}
|
|
62
|
-
|
|
63
|
-
drawHeader(buf, this.app.state);
|
|
64
|
-
const w = buf.width;
|
|
65
|
-
const h = buf.height;
|
|
66
|
-
buf.centerText(2, "History", { fg: C.accent, bold: true });
|
|
67
|
-
buf.centerText(3, "Select a session to load", { fg: C.dim });
|
|
68
|
-
const listTop = 5;
|
|
69
|
-
const listBottom = h - 2;
|
|
70
|
-
const viewH = Math.max(1, listBottom - listTop);
|
|
111
|
+
buildRows() {
|
|
71
112
|
const rows = [];
|
|
72
113
|
const itemBounds = [];
|
|
73
114
|
rows.push("Open other Agent's history");
|
|
@@ -83,32 +124,44 @@ export class HistoryScreen extends Screen {
|
|
|
83
124
|
rows.push(` ${s.cwd}`);
|
|
84
125
|
itemBounds.push({ start, end: rows.length });
|
|
85
126
|
}
|
|
127
|
+
this.rows = rows;
|
|
128
|
+
this.itemBounds = itemBounds;
|
|
129
|
+
}
|
|
130
|
+
render(buf) {
|
|
131
|
+
drawHeader(buf, this.app.state);
|
|
132
|
+
const w = buf.width;
|
|
133
|
+
const h = buf.height;
|
|
134
|
+
buf.centerText(2, "History", { fg: C.accent, bold: true });
|
|
135
|
+
buf.centerText(3, "Select a session to load", { fg: C.dim });
|
|
136
|
+
const listBottom = h - 2;
|
|
137
|
+
const viewH = Math.max(1, listBottom - LIST_TOP);
|
|
138
|
+
this.buildRows();
|
|
86
139
|
if (this.selectedIdx < 0)
|
|
87
140
|
this.selectedIdx = 0;
|
|
88
|
-
if (this.selectedIdx >= itemBounds.length)
|
|
89
|
-
this.selectedIdx = itemBounds.length - 1;
|
|
90
|
-
const selected = itemBounds[this.selectedIdx];
|
|
141
|
+
if (this.selectedIdx >= this.itemBounds.length)
|
|
142
|
+
this.selectedIdx = Math.max(0, this.itemBounds.length - 1);
|
|
143
|
+
const selected = this.itemBounds[this.selectedIdx];
|
|
91
144
|
const firstRowOfSelected = selected.start;
|
|
92
145
|
if (firstRowOfSelected < this.scrollTop)
|
|
93
146
|
this.scrollTop = firstRowOfSelected;
|
|
94
147
|
if (firstRowOfSelected >= this.scrollTop + viewH)
|
|
95
148
|
this.scrollTop = firstRowOfSelected - viewH + 1;
|
|
96
|
-
if (this.scrollTop > rows.length - viewH)
|
|
97
|
-
this.scrollTop = Math.max(0, rows.length - viewH);
|
|
98
|
-
for (let y =
|
|
99
|
-
const ri = this.scrollTop + (y -
|
|
100
|
-
if (ri < 0 || ri >= rows.length)
|
|
149
|
+
if (this.scrollTop > this.rows.length - viewH)
|
|
150
|
+
this.scrollTop = Math.max(0, this.rows.length - viewH);
|
|
151
|
+
for (let y = LIST_TOP; y < listBottom; y++) {
|
|
152
|
+
const ri = this.scrollTop + (y - LIST_TOP);
|
|
153
|
+
if (ri < 0 || ri >= this.rows.length)
|
|
101
154
|
break;
|
|
102
155
|
const isSelected = ri >= selected.start && ri < selected.end;
|
|
103
156
|
const isSelectedFirst = ri === selected.start;
|
|
104
|
-
const row = rows[ri];
|
|
157
|
+
const row = this.rows[ri];
|
|
105
158
|
const prefix = isSelectedFirst ? "▸ " : " ";
|
|
106
159
|
const style = isSelected ? { fg: C.accent, bg: C.highlightBg } : { fg: C.assistant };
|
|
107
160
|
buf.writeText(0, y, (prefix + row).slice(0, w), style);
|
|
108
161
|
}
|
|
109
162
|
if (this.sessions.length === 0) {
|
|
110
|
-
buf.centerText(
|
|
163
|
+
buf.centerText(LIST_TOP + 1, "No sessions yet", { fg: C.dim });
|
|
111
164
|
}
|
|
112
|
-
buf
|
|
165
|
+
drawHint(buf, listBottom, "↑/↓ select · Enter load · click to load · Delete delete · right-click back · Esc back");
|
|
113
166
|
}
|
|
114
167
|
}
|
|
@@ -2,7 +2,10 @@ import { Screen } from "../terminal/screen.js";
|
|
|
2
2
|
import { C } from "../colors.js";
|
|
3
3
|
import { INTENSITY_LEVELS, getModelsByProvider } from "../config.js";
|
|
4
4
|
import { drawHeader } from "../header.js";
|
|
5
|
-
import { drawTabButton } from "../ui.js";
|
|
5
|
+
import { drawHint, drawTabButton, inRect } from "../ui.js";
|
|
6
|
+
const LIST_TOP = 5;
|
|
7
|
+
const BTN_H = 3;
|
|
8
|
+
const BTN_X = 1;
|
|
6
9
|
export class ModelSelectScreen extends Screen {
|
|
7
10
|
level = 0;
|
|
8
11
|
providers = [];
|
|
@@ -11,6 +14,8 @@ export class ModelSelectScreen extends Screen {
|
|
|
11
14
|
selectedModelIdx = 0;
|
|
12
15
|
selectedIntensityIdx = 2;
|
|
13
16
|
scrollBtn = 0;
|
|
17
|
+
lastHoverX = -1;
|
|
18
|
+
lastHoverY = -1;
|
|
14
19
|
onActivate() {
|
|
15
20
|
const map = getModelsByProvider();
|
|
16
21
|
this.providers = Object.keys(map);
|
|
@@ -62,6 +67,7 @@ export class ModelSelectScreen extends Screen {
|
|
|
62
67
|
return;
|
|
63
68
|
this.level = 1;
|
|
64
69
|
this.selectedModelIdx = 0;
|
|
70
|
+
this.scrollBtn = 0;
|
|
65
71
|
this.app.render();
|
|
66
72
|
return;
|
|
67
73
|
}
|
|
@@ -70,6 +76,7 @@ export class ModelSelectScreen extends Screen {
|
|
|
70
76
|
if (models.length === 0)
|
|
71
77
|
return;
|
|
72
78
|
this.level = 2;
|
|
79
|
+
this.scrollBtn = 0;
|
|
73
80
|
this.app.render();
|
|
74
81
|
return;
|
|
75
82
|
}
|
|
@@ -85,12 +92,65 @@ export class ModelSelectScreen extends Screen {
|
|
|
85
92
|
goBack() {
|
|
86
93
|
if (this.level > 0) {
|
|
87
94
|
this.level--;
|
|
95
|
+
this.scrollBtn = 0;
|
|
88
96
|
this.app.render();
|
|
89
97
|
}
|
|
90
98
|
else {
|
|
91
99
|
this.app.popScreen();
|
|
92
100
|
}
|
|
93
101
|
}
|
|
102
|
+
/** Map a terminal row to the list index rendered there, if any. */
|
|
103
|
+
indexAtRow(y) {
|
|
104
|
+
const list = this.currentList();
|
|
105
|
+
const h = this.app.height;
|
|
106
|
+
const hintRow = h - 1;
|
|
107
|
+
const maxVisible = Math.max(1, Math.floor((hintRow - LIST_TOP) / BTN_H));
|
|
108
|
+
const i = Math.floor((y - LIST_TOP) / BTN_H) + this.scrollBtn;
|
|
109
|
+
if (i < 0 || i >= list.length)
|
|
110
|
+
return -1;
|
|
111
|
+
const visibleRow = LIST_TOP + (i - this.scrollBtn) * BTN_H;
|
|
112
|
+
if (visibleRow + BTN_H > hintRow)
|
|
113
|
+
return -1;
|
|
114
|
+
return i;
|
|
115
|
+
}
|
|
116
|
+
onMouse(ev) {
|
|
117
|
+
if (ev.type === "move") {
|
|
118
|
+
if (ev.x === this.lastHoverX && ev.y === this.lastHoverY)
|
|
119
|
+
return;
|
|
120
|
+
this.lastHoverX = ev.x;
|
|
121
|
+
this.lastHoverY = ev.y;
|
|
122
|
+
const list = this.currentList();
|
|
123
|
+
if (list.length === 0)
|
|
124
|
+
return;
|
|
125
|
+
const i = this.indexAtRow(ev.y);
|
|
126
|
+
if (i >= 0 && i !== this.currentIdx()) {
|
|
127
|
+
this.setIdx(i);
|
|
128
|
+
this.app.render();
|
|
129
|
+
}
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
if (ev.type === "wheel") {
|
|
133
|
+
this.move(ev.dir === -1 ? -1 : 1);
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
if (ev.type === "click") {
|
|
137
|
+
if (ev.button === 2) {
|
|
138
|
+
this.goBack();
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
if (ev.y < LIST_TOP || ev.y >= this.app.height - 1)
|
|
142
|
+
return;
|
|
143
|
+
const i = this.indexAtRow(ev.y);
|
|
144
|
+
if (i < 0)
|
|
145
|
+
return;
|
|
146
|
+
if (!inRect(ev.x, ev.y, { x: BTN_X, y: LIST_TOP + (i - this.scrollBtn) * BTN_H, w: this.app.width - 2, h: BTN_H })) {
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
this.setIdx(i);
|
|
150
|
+
this.confirm();
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
94
154
|
onKey(key) {
|
|
95
155
|
if (key.name === "up")
|
|
96
156
|
this.move(-1);
|
|
@@ -118,26 +178,25 @@ export class ModelSelectScreen extends Screen {
|
|
|
118
178
|
else
|
|
119
179
|
subtitle = `Model: ${this.currentModelName()} - Select Intensity`;
|
|
120
180
|
buf.centerText(3, subtitle, { fg: C.white });
|
|
121
|
-
const
|
|
122
|
-
const btnH = 3;
|
|
181
|
+
const btnH = BTN_H;
|
|
123
182
|
const btnGap = 0;
|
|
124
183
|
const stride = btnH + btnGap;
|
|
125
184
|
const hintRow = h - 1;
|
|
126
|
-
const maxVisible = Math.max(1, Math.floor((hintRow -
|
|
185
|
+
const maxVisible = Math.max(1, Math.floor((hintRow - LIST_TOP) / stride));
|
|
127
186
|
if (idx < this.scrollBtn)
|
|
128
187
|
this.scrollBtn = idx;
|
|
129
188
|
if (idx >= this.scrollBtn + maxVisible)
|
|
130
189
|
this.scrollBtn = idx - maxVisible + 1;
|
|
131
190
|
if (this.scrollBtn < 0)
|
|
132
191
|
this.scrollBtn = 0;
|
|
133
|
-
let y =
|
|
192
|
+
let y = LIST_TOP;
|
|
134
193
|
for (let i = this.scrollBtn; i < list.length; i++) {
|
|
135
194
|
if (y + btnH > hintRow)
|
|
136
195
|
break;
|
|
137
|
-
drawTabButton(buf,
|
|
196
|
+
drawTabButton(buf, BTN_X, y, w - 2, list[i], i === idx, "left");
|
|
138
197
|
y += stride;
|
|
139
198
|
}
|
|
140
|
-
buf
|
|
199
|
+
drawHint(buf, hintRow, "↑/↓ select · Enter confirm · click to choose · right-click back · Esc back");
|
|
141
200
|
}
|
|
142
201
|
currentModelName() {
|
|
143
202
|
const models = this.modelMap[this.providers[this.selectedProviderIdx]] ?? [];
|