@xingjisoft/neutrino 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +35 -2
- package/dist/app.js +180 -2
- package/dist/clipboard.js +113 -0
- package/dist/colors.js +2 -0
- package/dist/config.js +19 -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 +48 -22
- 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 +153 -14
- package/dist/selection.js +75 -0
- package/dist/state.js +3 -0
- package/dist/terminal/buffer.js +63 -8
- package/dist/terminal/input.js +24 -6
- package/dist/terminal/renderer.js +13 -4
- package/dist/terminal/screen.js +5 -0
- package/dist/ui.js +93 -27
- package/package.json +2 -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";
|
|
@@ -77,6 +78,10 @@ export class ChatScreen extends Screen {
|
|
|
77
78
|
this.app.render();
|
|
78
79
|
}
|
|
79
80
|
}
|
|
81
|
+
showsCursor() {
|
|
82
|
+
// The chat input is always focused; a cursor is drawn when it holds text.
|
|
83
|
+
return this.inputValue.length > 0;
|
|
84
|
+
}
|
|
80
85
|
onKey(key) {
|
|
81
86
|
if (this.showInfo) {
|
|
82
87
|
this.showInfo = false;
|
|
@@ -190,14 +195,34 @@ export class ChatScreen extends Screen {
|
|
|
190
195
|
}
|
|
191
196
|
onMouse(ev) {
|
|
192
197
|
if (ev.type === "click") {
|
|
193
|
-
if (ev.
|
|
194
|
-
|
|
198
|
+
if (ev.button === 2)
|
|
199
|
+
return; // right click: no-op in chat
|
|
200
|
+
// header row: right corner toggles token stats, the rest opens model config
|
|
201
|
+
if (ev.y === 0) {
|
|
202
|
+
if (ev.x >= this.app.width - 3) {
|
|
203
|
+
this.showInfo = !this.showInfo;
|
|
204
|
+
}
|
|
205
|
+
else {
|
|
206
|
+
this.app.openModelConfig();
|
|
207
|
+
}
|
|
195
208
|
this.app.render();
|
|
196
209
|
return;
|
|
197
210
|
}
|
|
198
211
|
if (this.showInfo) {
|
|
199
212
|
this.showInfo = false;
|
|
200
213
|
this.app.render();
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
// click on the input box -> focus and position the cursor
|
|
217
|
+
const inputY = this.app.height - 4;
|
|
218
|
+
const inW = Math.max(12, this.app.width - 2);
|
|
219
|
+
if (ev.y === inputY + 1 && ev.x >= 3 && ev.x < 3 + (inW - 4)) {
|
|
220
|
+
const innerW = inW - 4;
|
|
221
|
+
const col = Math.min(innerW - 1, ev.x - 3);
|
|
222
|
+
const win = computeInputWindow(this.inputValue, this.cursor, innerW);
|
|
223
|
+
this.cursor = cursorFromColumn(this.inputValue, col, win);
|
|
224
|
+
this.app.render();
|
|
225
|
+
return;
|
|
201
226
|
}
|
|
202
227
|
return;
|
|
203
228
|
}
|
|
@@ -370,36 +395,37 @@ export class ChatScreen extends Screen {
|
|
|
370
395
|
}
|
|
371
396
|
buildLines(width) {
|
|
372
397
|
const lines = [];
|
|
373
|
-
const
|
|
374
|
-
for (const ln of wrapText(text, width))
|
|
375
|
-
lines.push({ text: ln, style });
|
|
398
|
+
const pushText = (text, style) => {
|
|
399
|
+
for (const ln of wrapText(text, width)) {
|
|
400
|
+
lines.push({ spans: [{ text: ln, style }] });
|
|
401
|
+
}
|
|
376
402
|
};
|
|
377
403
|
for (const m of this.messages) {
|
|
378
404
|
if (m.role === "system") {
|
|
379
|
-
|
|
405
|
+
pushText(m.content, { fg: C.dim });
|
|
380
406
|
}
|
|
381
407
|
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({
|
|
408
|
+
lines.push({ spans: [{ text: "You:", style: { fg: C.accent, bold: true } }] });
|
|
409
|
+
lines.push(...renderMarkdownText(m.content, width, { fg: C.user }));
|
|
410
|
+
lines.push({ spans: [] });
|
|
385
411
|
}
|
|
386
412
|
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({
|
|
413
|
+
lines.push({ spans: [{ text: "Neutrino:", style: { fg: C.accent, bold: true } }] });
|
|
414
|
+
lines.push(...renderMarkdownText(m.content, width, { fg: C.assistant }));
|
|
415
|
+
lines.push({ spans: [] });
|
|
390
416
|
}
|
|
391
417
|
else if (m.role === "tool") {
|
|
392
|
-
lines.push({ text: `⚙ ${m.name}`, style: { fg: C.toolHeader, bold: true } });
|
|
418
|
+
lines.push({ spans: [{ text: `⚙ ${m.name}`, style: { fg: C.toolHeader, bold: true } }] });
|
|
393
419
|
const argText = m.arguments.command ?? m.arguments.path ?? "";
|
|
394
420
|
if (argText)
|
|
395
|
-
lines.push({ text: " " + argText, style: { fg: C.dim } });
|
|
396
|
-
|
|
397
|
-
lines.push({
|
|
421
|
+
lines.push({ spans: [{ text: " " + argText, style: { fg: C.dim } }] });
|
|
422
|
+
pushText(m.result || "(no output)", { fg: C.dim });
|
|
423
|
+
lines.push({ spans: [] });
|
|
398
424
|
}
|
|
399
425
|
}
|
|
400
426
|
if (this.streamingContent !== null) {
|
|
401
|
-
lines.push({ text: "Neutrino:", style: { fg: C.accent, bold: true } });
|
|
402
|
-
push(this.streamingContent, { fg: C.assistant });
|
|
427
|
+
lines.push({ spans: [{ text: "Neutrino:", style: { fg: C.accent, bold: true } }] });
|
|
428
|
+
lines.push(...renderMarkdownText(this.streamingContent, width, { fg: C.assistant }));
|
|
403
429
|
}
|
|
404
430
|
return lines;
|
|
405
431
|
}
|
|
@@ -441,17 +467,17 @@ export class ChatScreen extends Screen {
|
|
|
441
467
|
const li = this.scrollTop + (y - logTop);
|
|
442
468
|
if (li >= lines.length)
|
|
443
469
|
break;
|
|
444
|
-
|
|
445
|
-
buf.writeText(2, y, line.text.slice(0, w - 2), line.style);
|
|
470
|
+
drawSpans(buf, 2, y, lines[li].spans, Math.max(4, w - 4));
|
|
446
471
|
}
|
|
447
472
|
const inW = Math.max(12, w - 2);
|
|
448
473
|
drawInputBox(buf, 1, inputY, inW, this.inputValue, this.cursor, {
|
|
449
474
|
active: true,
|
|
450
475
|
dimmed: this.busy,
|
|
476
|
+
cursorOn: this.app.state.cursorOn,
|
|
451
477
|
placeholder: "Type a message...",
|
|
452
478
|
});
|
|
453
479
|
const hintY = h - 1;
|
|
454
|
-
const hints = "Enter send
|
|
480
|
+
const hints = "Enter send · ESC ESC pause · Ctrl+B menu · Ctrl+L clear · drag to copy";
|
|
455
481
|
const hintW = displayWidth(hints);
|
|
456
482
|
const hintX = Math.max(0, w - 1 - hintW);
|
|
457
483
|
const pathAvail = Math.max(0, hintX - 2);
|
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]] ?? [];
|