@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.
- package/LICENSE +201 -0
- package/README.md +96 -0
- package/assets/logo.txt +6 -0
- package/assets/prompt.txt +20 -0
- package/dist/agents.js +396 -0
- package/dist/app.js +169 -0
- package/dist/bin.js +9 -0
- package/dist/colors.js +14 -0
- package/dist/config.js +196 -0
- package/dist/header.js +15 -0
- package/dist/index.js +5 -0
- package/dist/llm/client.js +205 -0
- package/dist/llm/tools.js +169 -0
- package/dist/screens/agentConversation.js +98 -0
- package/dist/screens/agentHistory.js +99 -0
- package/dist/screens/agentSelect.js +71 -0
- package/dist/screens/chat.js +490 -0
- package/dist/screens/history.js +114 -0
- package/dist/screens/modelSelect.js +146 -0
- package/dist/screens/settings.js +70 -0
- package/dist/screens/start.js +129 -0
- package/dist/state.js +11 -0
- package/dist/terminal/buffer.js +80 -0
- package/dist/terminal/input.js +197 -0
- package/dist/terminal/renderer.js +96 -0
- package/dist/terminal/screen.js +6 -0
- package/dist/ui.js +150 -0
- package/dist/width.js +426 -0
- package/package.json +34 -0
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import process from "node:process";
|
|
2
|
+
const ESC_TIMEOUT = 30;
|
|
3
|
+
export class InputReader {
|
|
4
|
+
decoder = new TextDecoder("utf-8");
|
|
5
|
+
pending = "";
|
|
6
|
+
state = "normal";
|
|
7
|
+
csiBuf = "";
|
|
8
|
+
escTimer = null;
|
|
9
|
+
handler = null;
|
|
10
|
+
dataHandler = (chunk) => this.onData(chunk);
|
|
11
|
+
start(handler) {
|
|
12
|
+
this.handler = handler;
|
|
13
|
+
const stdin = process.stdin;
|
|
14
|
+
if (stdin.isTTY)
|
|
15
|
+
stdin.setRawMode(true);
|
|
16
|
+
stdin.resume();
|
|
17
|
+
stdin.on("data", this.dataHandler);
|
|
18
|
+
}
|
|
19
|
+
stop() {
|
|
20
|
+
const stdin = process.stdin;
|
|
21
|
+
stdin.removeListener("data", this.dataHandler);
|
|
22
|
+
if (this.escTimer)
|
|
23
|
+
clearTimeout(this.escTimer);
|
|
24
|
+
if (stdin.isTTY)
|
|
25
|
+
stdin.setRawMode(false);
|
|
26
|
+
stdin.pause();
|
|
27
|
+
}
|
|
28
|
+
onData(chunk) {
|
|
29
|
+
this.pending += this.decoder.decode(chunk, { stream: true });
|
|
30
|
+
this.process();
|
|
31
|
+
}
|
|
32
|
+
process() {
|
|
33
|
+
const s = this.pending;
|
|
34
|
+
let i = 0;
|
|
35
|
+
while (i < s.length) {
|
|
36
|
+
const c = s[i];
|
|
37
|
+
if (this.state === "normal") {
|
|
38
|
+
if (c === "\x1b") {
|
|
39
|
+
this.state = "esc";
|
|
40
|
+
this.scheduleEscTimeout();
|
|
41
|
+
i++;
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
const code = c.charCodeAt(0);
|
|
45
|
+
if (c === "\r" || c === "\n") {
|
|
46
|
+
this.emitKey("return", false, false, false, c);
|
|
47
|
+
i++;
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
if (c === "\t") {
|
|
51
|
+
this.emitKey("tab", false, false, false, c);
|
|
52
|
+
i++;
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
if (c === "\x7f" || c === "\x08") {
|
|
56
|
+
this.emitKey("backspace", false, false, false, c);
|
|
57
|
+
i++;
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
if (code >= 1 && code <= 26) {
|
|
61
|
+
const name = String.fromCharCode(code + 96);
|
|
62
|
+
this.emitKey(name, true, false, false, c);
|
|
63
|
+
i++;
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
if (code === 0) {
|
|
67
|
+
this.emitKey(" ", true, false, false, c);
|
|
68
|
+
i++;
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
this.emitKey(c, false, false, false, c);
|
|
72
|
+
i++;
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
if (this.state === "esc") {
|
|
76
|
+
this.cancelEscTimeout();
|
|
77
|
+
if (c === "[" || c === "O") {
|
|
78
|
+
this.state = "csi";
|
|
79
|
+
this.csiBuf = "";
|
|
80
|
+
i++;
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
if (c === "\x1b") {
|
|
84
|
+
this.emitKey("escape", false, false, false, "\x1b");
|
|
85
|
+
this.state = "esc";
|
|
86
|
+
this.scheduleEscTimeout();
|
|
87
|
+
i++;
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
this.emitKey(c, false, true, false, "\x1b" + c);
|
|
91
|
+
this.state = "normal";
|
|
92
|
+
i++;
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
// csi
|
|
96
|
+
this.csiBuf += c;
|
|
97
|
+
const cc = c.charCodeAt(0);
|
|
98
|
+
if (cc >= 0x40 && cc <= 0x7e) {
|
|
99
|
+
this.finishCsi();
|
|
100
|
+
this.state = "normal";
|
|
101
|
+
}
|
|
102
|
+
i++;
|
|
103
|
+
}
|
|
104
|
+
this.pending = "";
|
|
105
|
+
}
|
|
106
|
+
scheduleEscTimeout() {
|
|
107
|
+
if (this.escTimer)
|
|
108
|
+
clearTimeout(this.escTimer);
|
|
109
|
+
this.escTimer = setTimeout(() => {
|
|
110
|
+
this.escTimer = null;
|
|
111
|
+
if (this.state === "esc") {
|
|
112
|
+
this.state = "normal";
|
|
113
|
+
this.emitKey("escape", false, false, false, "\x1b");
|
|
114
|
+
}
|
|
115
|
+
}, ESC_TIMEOUT);
|
|
116
|
+
}
|
|
117
|
+
cancelEscTimeout() {
|
|
118
|
+
if (this.escTimer) {
|
|
119
|
+
clearTimeout(this.escTimer);
|
|
120
|
+
this.escTimer = null;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
finishCsi() {
|
|
124
|
+
const s = this.csiBuf;
|
|
125
|
+
const final = s[s.length - 1];
|
|
126
|
+
if (final === "A")
|
|
127
|
+
this.emitKey("up", false, false, false, "\x1b[A");
|
|
128
|
+
else if (final === "B")
|
|
129
|
+
this.emitKey("down", false, false, false, "\x1b[B");
|
|
130
|
+
else if (final === "C")
|
|
131
|
+
this.emitKey("right", false, false, false, "\x1b[C");
|
|
132
|
+
else if (final === "D")
|
|
133
|
+
this.emitKey("left", false, false, false, "\x1b[D");
|
|
134
|
+
else if (final === "H")
|
|
135
|
+
this.emitKey("home", false, false, false, "\x1b[H");
|
|
136
|
+
else if (final === "F")
|
|
137
|
+
this.emitKey("end", false, false, false, "\x1b[F");
|
|
138
|
+
else if (final === "Z")
|
|
139
|
+
this.emitKey("tab", false, false, true, "\x1b[Z");
|
|
140
|
+
else if (final === "M" || final === "m") {
|
|
141
|
+
if (s[0] === "<")
|
|
142
|
+
this.handleMouse(s, final);
|
|
143
|
+
}
|
|
144
|
+
else if (final === "~") {
|
|
145
|
+
const param = s.slice(0, -1);
|
|
146
|
+
if (param === "3")
|
|
147
|
+
this.emitKey("delete", false, false, false, "\x1b[3~");
|
|
148
|
+
else if (param === "5")
|
|
149
|
+
this.emitKey("pageup", false, false, false, "\x1b[5~");
|
|
150
|
+
else if (param === "6")
|
|
151
|
+
this.emitKey("pagedown", false, false, false, "\x1b[6~");
|
|
152
|
+
else if (param === "1" || param === "7")
|
|
153
|
+
this.emitKey("home", false, false, false, "\x1b[1~");
|
|
154
|
+
else if (param === "4" || param === "8")
|
|
155
|
+
this.emitKey("end", false, false, false, "\x1b[4~");
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
handleMouse(s, final) {
|
|
159
|
+
const parts = s.slice(1).split(";");
|
|
160
|
+
if (parts.length < 3)
|
|
161
|
+
return;
|
|
162
|
+
const b = parseInt(parts[0], 10);
|
|
163
|
+
const x = parseInt(parts[1], 10) - 1;
|
|
164
|
+
const y = parseInt(parts[2], 10) - 1;
|
|
165
|
+
if (Number.isNaN(b) || Number.isNaN(x) || Number.isNaN(y))
|
|
166
|
+
return;
|
|
167
|
+
if (b === 64) {
|
|
168
|
+
this.emitMouse({ type: "wheel", dir: -1, x, y });
|
|
169
|
+
}
|
|
170
|
+
else if (b === 65) {
|
|
171
|
+
this.emitMouse({ type: "wheel", dir: 1, x, y });
|
|
172
|
+
}
|
|
173
|
+
else if (final === "M" && (b === 0 || b === 1 || b === 2)) {
|
|
174
|
+
this.emitMouse({ type: "click", x, y, button: b });
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
emitKey(name, ctrl, meta, shift, char) {
|
|
178
|
+
if (!this.handler)
|
|
179
|
+
return;
|
|
180
|
+
this.handler({
|
|
181
|
+
type: "key",
|
|
182
|
+
key: {
|
|
183
|
+
name,
|
|
184
|
+
ctrl,
|
|
185
|
+
meta,
|
|
186
|
+
shift,
|
|
187
|
+
char,
|
|
188
|
+
sequence: char,
|
|
189
|
+
},
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
emitMouse(event) {
|
|
193
|
+
if (!this.handler)
|
|
194
|
+
return;
|
|
195
|
+
this.handler({ type: "mouse", event });
|
|
196
|
+
}
|
|
197
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import process from "node:process";
|
|
2
|
+
function styleKey(st) {
|
|
3
|
+
let k = "";
|
|
4
|
+
if (st.fg)
|
|
5
|
+
k += "f" + st.fg.join(",") + ";";
|
|
6
|
+
if (st.bg)
|
|
7
|
+
k += "b" + st.bg.join(",") + ";";
|
|
8
|
+
if (st.bold)
|
|
9
|
+
k += "B";
|
|
10
|
+
if (st.dim)
|
|
11
|
+
k += "d";
|
|
12
|
+
if (st.inverse)
|
|
13
|
+
k += "i";
|
|
14
|
+
return k;
|
|
15
|
+
}
|
|
16
|
+
function sgr(st) {
|
|
17
|
+
const codes = [];
|
|
18
|
+
if (st.bold)
|
|
19
|
+
codes.push("1");
|
|
20
|
+
if (st.dim)
|
|
21
|
+
codes.push("2");
|
|
22
|
+
if (st.inverse)
|
|
23
|
+
codes.push("7");
|
|
24
|
+
if (st.fg)
|
|
25
|
+
codes.push(`38;2;${st.fg[0]};${st.fg[1]};${st.fg[2]}`);
|
|
26
|
+
if (st.bg)
|
|
27
|
+
codes.push(`48;2;${st.bg[0]};${st.bg[1]};${st.bg[2]}`);
|
|
28
|
+
if (codes.length === 0)
|
|
29
|
+
return "\x1b[0m";
|
|
30
|
+
return "\x1b[0;" + codes.join(";") + "m";
|
|
31
|
+
}
|
|
32
|
+
function rowSignature(row) {
|
|
33
|
+
let out = "";
|
|
34
|
+
for (let i = 0; i < row.length; i++) {
|
|
35
|
+
const c = row[i];
|
|
36
|
+
out += c.ch + "\u0000" + styleKey(c.style) + "\u0001";
|
|
37
|
+
}
|
|
38
|
+
return out;
|
|
39
|
+
}
|
|
40
|
+
export class Renderer {
|
|
41
|
+
prevSig = [];
|
|
42
|
+
prevW = 0;
|
|
43
|
+
prevH = 0;
|
|
44
|
+
out = process.stdout;
|
|
45
|
+
init() {
|
|
46
|
+
this.write("\x1b[?1049h"); // alternate screen
|
|
47
|
+
this.write("\x1b[?25l"); // hide cursor
|
|
48
|
+
this.write("\x1b[?1000h\x1b[?1002h\x1b[?1006h\x1b[?1015h"); // mouse tracking
|
|
49
|
+
}
|
|
50
|
+
dispose() {
|
|
51
|
+
this.write("\x1b[?1000l\x1b[?1002l\x1b[?1006l\x1b[?1015l");
|
|
52
|
+
this.write("\x1b[?25h");
|
|
53
|
+
this.write("\x1b[?1049l");
|
|
54
|
+
this.write("\x1b[0m");
|
|
55
|
+
}
|
|
56
|
+
render(buf) {
|
|
57
|
+
const w = buf.width;
|
|
58
|
+
const h = buf.height;
|
|
59
|
+
if (w !== this.prevW || h !== this.prevH) {
|
|
60
|
+
this.prevSig = new Array(h).fill("");
|
|
61
|
+
this.prevW = w;
|
|
62
|
+
this.prevH = h;
|
|
63
|
+
}
|
|
64
|
+
let out = "";
|
|
65
|
+
for (let y = 0; y < h; y++) {
|
|
66
|
+
const row = buf.cells[y];
|
|
67
|
+
const sig = rowSignature(row);
|
|
68
|
+
if (sig !== this.prevSig[y]) {
|
|
69
|
+
out += this.renderRow(y, row);
|
|
70
|
+
this.prevSig[y] = sig;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
if (out)
|
|
74
|
+
this.write(out);
|
|
75
|
+
}
|
|
76
|
+
renderRow(y, row) {
|
|
77
|
+
let s = `\x1b[${y + 1};1H`;
|
|
78
|
+
let lastKey = "";
|
|
79
|
+
for (let x = 0; x < row.length; x++) {
|
|
80
|
+
const cell = row[x];
|
|
81
|
+
if (cell.ch === "")
|
|
82
|
+
continue;
|
|
83
|
+
const sk = styleKey(cell.style);
|
|
84
|
+
if (sk !== lastKey) {
|
|
85
|
+
s += sgr(cell.style);
|
|
86
|
+
lastKey = sk;
|
|
87
|
+
}
|
|
88
|
+
s += cell.ch;
|
|
89
|
+
}
|
|
90
|
+
s += "\x1b[0m";
|
|
91
|
+
return s;
|
|
92
|
+
}
|
|
93
|
+
write(chunk) {
|
|
94
|
+
this.out.write(chunk);
|
|
95
|
+
}
|
|
96
|
+
}
|
package/dist/ui.js
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { C } from "./colors.js";
|
|
2
|
+
import { displayWidth, truncateByWidth, wcwidth } from "./width.js";
|
|
3
|
+
function chunkByWidth(text, width) {
|
|
4
|
+
const chunks = [];
|
|
5
|
+
let cur = "";
|
|
6
|
+
let curW = 0;
|
|
7
|
+
for (const ch of text) {
|
|
8
|
+
const w = wcwidth(ch.codePointAt(0) ?? 0);
|
|
9
|
+
if (curW + w > width && cur.length > 0) {
|
|
10
|
+
chunks.push(cur);
|
|
11
|
+
cur = ch;
|
|
12
|
+
curW = w;
|
|
13
|
+
}
|
|
14
|
+
else {
|
|
15
|
+
cur += ch;
|
|
16
|
+
curW += w;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
if (cur.length > 0)
|
|
20
|
+
chunks.push(cur);
|
|
21
|
+
return chunks;
|
|
22
|
+
}
|
|
23
|
+
export function wrapText(text, width) {
|
|
24
|
+
if (width <= 0)
|
|
25
|
+
width = 1;
|
|
26
|
+
const lines = [];
|
|
27
|
+
for (const para of text.split("\n")) {
|
|
28
|
+
if (para.length === 0) {
|
|
29
|
+
lines.push("");
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
let line = "";
|
|
33
|
+
let lineW = 0;
|
|
34
|
+
for (const word of para.split(" ")) {
|
|
35
|
+
const wordW = displayWidth(word);
|
|
36
|
+
if (line.length === 0) {
|
|
37
|
+
if (wordW <= width) {
|
|
38
|
+
line = word;
|
|
39
|
+
lineW = wordW;
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
const chunks = chunkByWidth(word, width);
|
|
43
|
+
for (let i = 0; i < chunks.length - 1; i++)
|
|
44
|
+
lines.push(chunks[i]);
|
|
45
|
+
line = chunks[chunks.length - 1];
|
|
46
|
+
lineW = displayWidth(line);
|
|
47
|
+
}
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
if (lineW + 1 + wordW <= width) {
|
|
51
|
+
line += " " + word;
|
|
52
|
+
lineW += 1 + wordW;
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
lines.push(line);
|
|
56
|
+
if (wordW <= width) {
|
|
57
|
+
line = word;
|
|
58
|
+
lineW = wordW;
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
const chunks = chunkByWidth(word, width);
|
|
62
|
+
for (let i = 0; i < chunks.length - 1; i++)
|
|
63
|
+
lines.push(chunks[i]);
|
|
64
|
+
line = chunks[chunks.length - 1];
|
|
65
|
+
lineW = displayWidth(line);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
lines.push(line);
|
|
70
|
+
}
|
|
71
|
+
return lines;
|
|
72
|
+
}
|
|
73
|
+
export function drawInputBox(buf, x, y, w, value, cursorPos, opts = {}) {
|
|
74
|
+
const active = opts.active ?? true;
|
|
75
|
+
const dimmed = opts.dimmed ?? false;
|
|
76
|
+
const placeholder = opts.placeholder ?? "";
|
|
77
|
+
const h = 3;
|
|
78
|
+
const innerX = x + 2;
|
|
79
|
+
const innerW = w - 4;
|
|
80
|
+
const innerY = y + 1;
|
|
81
|
+
if (active)
|
|
82
|
+
buf.fillRect(x, y, w, h, { bg: C.highlightBg });
|
|
83
|
+
const textStyle = dimmed
|
|
84
|
+
? { fg: C.dim, bg: C.highlightBg }
|
|
85
|
+
: active
|
|
86
|
+
? { fg: C.white, bg: C.highlightBg }
|
|
87
|
+
: { fg: C.dim };
|
|
88
|
+
if (value.length === 0) {
|
|
89
|
+
buf.writeText(innerX, innerY, truncateByWidth(placeholder, innerW), textStylePlaceholder(active));
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
const chars = Array.from(value);
|
|
93
|
+
const widths = chars.map((ch) => wcwidth(ch.codePointAt(0) ?? 0));
|
|
94
|
+
const cursorCp = Math.min(Array.from(value.slice(0, cursorPos)).length, chars.length);
|
|
95
|
+
let cursorCol = 0;
|
|
96
|
+
for (let i = 0; i < cursorCp; i++)
|
|
97
|
+
cursorCol += widths[i];
|
|
98
|
+
let startCp = 0;
|
|
99
|
+
if (cursorCol >= innerW) {
|
|
100
|
+
let acc = 0;
|
|
101
|
+
startCp = cursorCp;
|
|
102
|
+
while (startCp > 0) {
|
|
103
|
+
const pw = widths[startCp - 1];
|
|
104
|
+
if (acc + pw <= innerW - 1) {
|
|
105
|
+
acc += pw;
|
|
106
|
+
startCp--;
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
break;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
let prefixW = 0;
|
|
114
|
+
for (let i = 0; i < startCp; i++)
|
|
115
|
+
prefixW += widths[i];
|
|
116
|
+
let endCp = chars.length;
|
|
117
|
+
let acc = 0;
|
|
118
|
+
for (let i = startCp; i < chars.length; i++) {
|
|
119
|
+
if (acc + widths[i] > innerW) {
|
|
120
|
+
endCp = i;
|
|
121
|
+
break;
|
|
122
|
+
}
|
|
123
|
+
acc += widths[i];
|
|
124
|
+
}
|
|
125
|
+
const visible = chars.slice(startCp, endCp).join("");
|
|
126
|
+
buf.writeText(innerX, innerY, visible, textStyle);
|
|
127
|
+
const cursorScreenCol = cursorCol - prefixW;
|
|
128
|
+
if (active && cursorScreenCol >= 0 && cursorScreenCol < innerW) {
|
|
129
|
+
buf.set(innerX + cursorScreenCol, innerY, " ", { fg: C.black, bg: C.accent });
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
function textStylePlaceholder(active) {
|
|
133
|
+
return active ? { fg: C.dim, bg: C.highlightBg } : { fg: C.dim };
|
|
134
|
+
}
|
|
135
|
+
export function drawTabButton(buf, x, y, w, label, selected, align = "center") {
|
|
136
|
+
const h = 3;
|
|
137
|
+
const labelW = displayWidth(label);
|
|
138
|
+
const lx = align === "left" ? x + 3 : x + Math.floor((w - labelW) / 2);
|
|
139
|
+
if (selected) {
|
|
140
|
+
buf.fillRect(x, y, w, h, { bg: C.highlightBg });
|
|
141
|
+
buf.writeText(lx, y + 1, label, { fg: C.accent, bg: C.highlightBg, bold: true });
|
|
142
|
+
}
|
|
143
|
+
else {
|
|
144
|
+
buf.writeText(lx, y + 1, label, { fg: C.dim });
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
export function centerVertically(totalHeight, contentHeight) {
|
|
148
|
+
const top = Math.floor((totalHeight - contentHeight) / 2);
|
|
149
|
+
return Math.max(1, top);
|
|
150
|
+
}
|