@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,114 @@
1
+ import { Screen } from "../terminal/screen.js";
2
+ import { C } from "../colors.js";
3
+ import { deleteSession, listSessions, loadSession } from "../config.js";
4
+ import { drawHeader } from "../header.js";
5
+ export class HistoryScreen extends Screen {
6
+ sessions = [];
7
+ selectedIdx = 0;
8
+ scrollTop = 0;
9
+ onActivate() {
10
+ this.sessions = listSessions();
11
+ this.selectedIdx = this.sessions.length > 0 ? 1 : 0;
12
+ this.scrollTop = 0;
13
+ }
14
+ move(delta) {
15
+ const count = this.sessions.length + 1;
16
+ this.selectedIdx = (this.selectedIdx + delta + count) % count;
17
+ this.app.render();
18
+ }
19
+ onKey(key) {
20
+ if (key.name === "up")
21
+ this.move(-1);
22
+ else if (key.name === "down")
23
+ this.move(1);
24
+ else if (key.name === "enter" || key.name === "return")
25
+ this.loadSelected();
26
+ else if (key.name === "delete")
27
+ this.deleteSelected();
28
+ else if (key.name === "escape")
29
+ this.app.popScreen();
30
+ }
31
+ onMouse(ev) {
32
+ if (ev.type === "wheel") {
33
+ this.scrollTop = Math.max(0, this.scrollTop + (ev.dir === -1 ? -3 : 3));
34
+ this.app.render();
35
+ }
36
+ }
37
+ loadSelected() {
38
+ if (this.selectedIdx === 0) {
39
+ this.app.openAgentSelect();
40
+ return;
41
+ }
42
+ const session = this.sessions[this.selectedIdx - 1];
43
+ if (!session)
44
+ return;
45
+ const data = loadSession(session.id);
46
+ if (data) {
47
+ this.app.popScreen();
48
+ this.app.loadSession(data);
49
+ }
50
+ }
51
+ deleteSelected() {
52
+ if (this.selectedIdx === 0)
53
+ return;
54
+ const session = this.sessions[this.selectedIdx - 1];
55
+ if (!session)
56
+ return;
57
+ deleteSession(session.id);
58
+ this.sessions = listSessions();
59
+ this.selectedIdx = Math.min(this.selectedIdx, this.sessions.length);
60
+ this.app.render();
61
+ }
62
+ render(buf) {
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);
71
+ const rows = [];
72
+ const itemBounds = [];
73
+ rows.push("Open other Agent's history");
74
+ itemBounds.push({ start: 0, end: 1 });
75
+ rows.push("");
76
+ for (const s of this.sessions) {
77
+ const start = rows.length;
78
+ const created = (s.created_at || "").slice(0, 19);
79
+ rows.push(`${created} | ${s.provider}/${s.model} (${s.intensity}) | ${s.message_count} msgs`);
80
+ if (s.title)
81
+ rows.push(` ${s.title}`);
82
+ if (s.cwd)
83
+ rows.push(` ${s.cwd}`);
84
+ itemBounds.push({ start, end: rows.length });
85
+ }
86
+ if (this.selectedIdx < 0)
87
+ this.selectedIdx = 0;
88
+ if (this.selectedIdx >= itemBounds.length)
89
+ this.selectedIdx = itemBounds.length - 1;
90
+ const selected = itemBounds[this.selectedIdx];
91
+ const firstRowOfSelected = selected.start;
92
+ if (firstRowOfSelected < this.scrollTop)
93
+ this.scrollTop = firstRowOfSelected;
94
+ if (firstRowOfSelected >= this.scrollTop + viewH)
95
+ 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 = listTop; y < listBottom; y++) {
99
+ const ri = this.scrollTop + (y - listTop);
100
+ if (ri < 0 || ri >= rows.length)
101
+ break;
102
+ const isSelected = ri >= selected.start && ri < selected.end;
103
+ const isSelectedFirst = ri === selected.start;
104
+ const row = rows[ri];
105
+ const prefix = isSelectedFirst ? "▸ " : " ";
106
+ const style = isSelected ? { fg: C.accent, bg: C.highlightBg } : { fg: C.assistant };
107
+ buf.writeText(0, y, (prefix + row).slice(0, w), style);
108
+ }
109
+ if (this.sessions.length === 0) {
110
+ buf.centerText(listTop + 1, "No sessions yet", { fg: C.dim });
111
+ }
112
+ buf.centerText(listBottom, "↑/↓ select · Enter load · Delete delete · Esc back", { fg: C.dim });
113
+ }
114
+ }
@@ -0,0 +1,146 @@
1
+ import { Screen } from "../terminal/screen.js";
2
+ import { C } from "../colors.js";
3
+ import { INTENSITY_LEVELS, getModelsByProvider } from "../config.js";
4
+ import { drawHeader } from "../header.js";
5
+ import { drawTabButton } from "../ui.js";
6
+ export class ModelSelectScreen extends Screen {
7
+ level = 0;
8
+ providers = [];
9
+ modelMap = {};
10
+ selectedProviderIdx = 0;
11
+ selectedModelIdx = 0;
12
+ selectedIntensityIdx = 2;
13
+ scrollBtn = 0;
14
+ onActivate() {
15
+ const map = getModelsByProvider();
16
+ this.providers = Object.keys(map);
17
+ this.modelMap = map;
18
+ this.selectedProviderIdx = 0;
19
+ this.selectedModelIdx = 0;
20
+ this.selectedIntensityIdx = 2;
21
+ this.scrollBtn = 0;
22
+ }
23
+ currentList() {
24
+ if (this.level === 0)
25
+ return this.providers;
26
+ if (this.level === 1)
27
+ return (this.modelMap[this.providers[this.selectedProviderIdx]] ?? []).map((m) => m.name);
28
+ return INTENSITY_LEVELS;
29
+ }
30
+ currentIdx() {
31
+ if (this.level === 0)
32
+ return this.selectedProviderIdx;
33
+ if (this.level === 1)
34
+ return this.selectedModelIdx;
35
+ return this.selectedIntensityIdx;
36
+ }
37
+ setIdx(idx) {
38
+ if (this.level === 0)
39
+ this.selectedProviderIdx = idx;
40
+ else if (this.level === 1)
41
+ this.selectedModelIdx = idx;
42
+ else
43
+ this.selectedIntensityIdx = idx;
44
+ }
45
+ move(delta) {
46
+ const list = this.currentList();
47
+ if (list.length === 0)
48
+ return;
49
+ let idx = this.currentIdx() + delta;
50
+ if (idx < 0)
51
+ idx = list.length - 1;
52
+ if (idx >= list.length)
53
+ idx = 0;
54
+ this.setIdx(idx);
55
+ if (this.level === 0)
56
+ this.selectedModelIdx = 0;
57
+ this.app.render();
58
+ }
59
+ confirm() {
60
+ if (this.level === 0) {
61
+ if (this.providers.length === 0)
62
+ return;
63
+ this.level = 1;
64
+ this.selectedModelIdx = 0;
65
+ this.app.render();
66
+ return;
67
+ }
68
+ if (this.level === 1) {
69
+ const models = this.modelMap[this.providers[this.selectedProviderIdx]] ?? [];
70
+ if (models.length === 0)
71
+ return;
72
+ this.level = 2;
73
+ this.app.render();
74
+ return;
75
+ }
76
+ const providerName = this.providers[this.selectedProviderIdx];
77
+ const models = this.modelMap[providerName] ?? [];
78
+ const model = models[this.selectedModelIdx];
79
+ if (!model)
80
+ return;
81
+ const intensity = INTENSITY_LEVELS[this.selectedIntensityIdx];
82
+ this.app.setModelConfig(providerName, model, intensity);
83
+ this.app.popScreen();
84
+ }
85
+ goBack() {
86
+ if (this.level > 0) {
87
+ this.level--;
88
+ this.app.render();
89
+ }
90
+ else {
91
+ this.app.popScreen();
92
+ }
93
+ }
94
+ onKey(key) {
95
+ if (key.name === "up")
96
+ this.move(-1);
97
+ else if (key.name === "down")
98
+ this.move(1);
99
+ else if (key.name === "enter" || key.name === "return")
100
+ this.confirm();
101
+ else if (key.name === "escape")
102
+ this.goBack();
103
+ else if (key.name === "left")
104
+ this.goBack();
105
+ }
106
+ render(buf) {
107
+ drawHeader(buf, this.app.state);
108
+ const w = buf.width;
109
+ const h = buf.height;
110
+ const list = this.currentList();
111
+ const idx = this.currentIdx();
112
+ buf.centerText(2, "Model Selection", { fg: C.accent, bold: true });
113
+ let subtitle = "";
114
+ if (this.level === 0)
115
+ subtitle = "Select a provider";
116
+ else if (this.level === 1)
117
+ subtitle = this.providers[this.selectedProviderIdx] ?? "";
118
+ else
119
+ subtitle = `Model: ${this.currentModelName()} - Select Intensity`;
120
+ buf.centerText(3, subtitle, { fg: C.white });
121
+ const listTop = 5;
122
+ const btnH = 3;
123
+ const btnGap = 0;
124
+ const stride = btnH + btnGap;
125
+ const hintRow = h - 1;
126
+ const maxVisible = Math.max(1, Math.floor((hintRow - listTop) / stride));
127
+ if (idx < this.scrollBtn)
128
+ this.scrollBtn = idx;
129
+ if (idx >= this.scrollBtn + maxVisible)
130
+ this.scrollBtn = idx - maxVisible + 1;
131
+ if (this.scrollBtn < 0)
132
+ this.scrollBtn = 0;
133
+ let y = listTop;
134
+ for (let i = this.scrollBtn; i < list.length; i++) {
135
+ if (y + btnH > hintRow)
136
+ break;
137
+ drawTabButton(buf, 1, y, w - 2, list[i], i === idx, "left");
138
+ y += stride;
139
+ }
140
+ buf.centerText(hintRow, "↑/↓ select · Enter confirm · Esc back", { fg: C.dim });
141
+ }
142
+ currentModelName() {
143
+ const models = this.modelMap[this.providers[this.selectedProviderIdx]] ?? [];
144
+ return models[this.selectedModelIdx]?.name ?? "";
145
+ }
146
+ }
@@ -0,0 +1,70 @@
1
+ import { Screen } from "../terminal/screen.js";
2
+ import { C } from "../colors.js";
3
+ import { INTENSITY_LEVELS, MODEL_FILE, loadConfig, saveConfig } from "../config.js";
4
+ import { drawHeader } from "../header.js";
5
+ import { centerVertically } from "../ui.js";
6
+ export class SettingsScreen extends Screen {
7
+ intensityIdx = 2;
8
+ onActivate() {
9
+ const config = loadConfig();
10
+ const current = String(config.default_intensity ?? "High");
11
+ const i = INTENSITY_LEVELS.indexOf(current);
12
+ this.intensityIdx = i >= 0 ? i : 2;
13
+ }
14
+ onKey(key) {
15
+ if (key.name === "up") {
16
+ this.intensityIdx = (this.intensityIdx + INTENSITY_LEVELS.length - 1) % INTENSITY_LEVELS.length;
17
+ this.app.render();
18
+ }
19
+ else if (key.name === "down") {
20
+ this.intensityIdx = (this.intensityIdx + 1) % INTENSITY_LEVELS.length;
21
+ this.app.render();
22
+ }
23
+ else if (key.name === "enter" || key.name === "return") {
24
+ this.save();
25
+ }
26
+ else if (key.name === "escape") {
27
+ this.save();
28
+ this.app.popScreen();
29
+ }
30
+ }
31
+ save() {
32
+ const config = loadConfig();
33
+ config.default_intensity = INTENSITY_LEVELS[this.intensityIdx];
34
+ saveConfig(config);
35
+ }
36
+ render(buf) {
37
+ drawHeader(buf, this.app.state);
38
+ const h = buf.height;
39
+ const lines = [
40
+ "Settings",
41
+ "",
42
+ `Model: ${this.app.state.selectedProvider}/${this.app.state.selectedModelName}`,
43
+ `Default intensity:`,
44
+ ];
45
+ const content = INTENSITY_LEVELS.map((l, i) => ({
46
+ text: l,
47
+ selected: i === this.intensityIdx,
48
+ }));
49
+ const total = lines.length + content.length + 2;
50
+ const top = centerVertically(h, total);
51
+ let y = top;
52
+ buf.centerText(y, lines[0], { fg: C.accent, bold: true });
53
+ y += 2;
54
+ buf.centerText(y, lines[2], { fg: C.assistant });
55
+ y += 1;
56
+ buf.centerText(y, lines[3], { fg: C.dim });
57
+ y += 1;
58
+ for (const c of content) {
59
+ const text = (c.selected ? "▸ " : " ") + c.text;
60
+ buf.centerText(y, text, c.selected ? { fg: C.accent, bg: C.highlightBg } : { fg: C.assistant });
61
+ y += 1;
62
+ }
63
+ y += 1;
64
+ buf.centerText(y, `model.json: ${MODEL_FILE}`, { fg: C.dim });
65
+ y += 1;
66
+ if (y < h) {
67
+ buf.centerText(y, "↑/↓ change · Enter apply · Esc back", { fg: C.dim });
68
+ }
69
+ }
70
+ }
@@ -0,0 +1,129 @@
1
+ import { Screen } from "../terminal/screen.js";
2
+ import { C } from "../colors.js";
3
+ import { loadLogo } from "../config.js";
4
+ import { drawHeader } from "../header.js";
5
+ import { centerVertically, drawInputBox, drawTabButton } from "../ui.js";
6
+ const TABS = ["Model", "History", "Settings"];
7
+ const LOGO = loadLogo();
8
+ const FOCUS_COUNT = TABS.length + 1; // input box + 3 tabs
9
+ export class StartScreen extends Screen {
10
+ inputValue = "";
11
+ cursor = 0;
12
+ focusIdx = 0; // 0 = input box, 1..3 = tabs
13
+ onKey(key) {
14
+ if (key.name === "left" || key.name === "up") {
15
+ this.move(-1);
16
+ return;
17
+ }
18
+ if (key.name === "right" || key.name === "down") {
19
+ this.move(1);
20
+ return;
21
+ }
22
+ if (key.name === "return") {
23
+ if (this.focusIdx === 0) {
24
+ const text = this.inputValue.trim();
25
+ if (text)
26
+ this.app.startNewChat(text);
27
+ }
28
+ else {
29
+ this.enterTab();
30
+ }
31
+ return;
32
+ }
33
+ if (this.focusIdx !== 0)
34
+ return;
35
+ if (key.name === "backspace") {
36
+ if (this.cursor > 0) {
37
+ this.inputValue =
38
+ this.inputValue.slice(0, this.cursor - 1) + this.inputValue.slice(this.cursor);
39
+ this.cursor--;
40
+ this.app.render();
41
+ }
42
+ return;
43
+ }
44
+ if (key.name === "delete") {
45
+ if (this.cursor < this.inputValue.length) {
46
+ this.inputValue =
47
+ this.inputValue.slice(0, this.cursor) + this.inputValue.slice(this.cursor + 1);
48
+ this.app.render();
49
+ }
50
+ return;
51
+ }
52
+ if (key.name === "home") {
53
+ this.cursor = 0;
54
+ this.app.render();
55
+ return;
56
+ }
57
+ if (key.name === "end") {
58
+ this.cursor = this.inputValue.length;
59
+ this.app.render();
60
+ return;
61
+ }
62
+ if (key.ctrl && key.name === "u") {
63
+ this.inputValue = "";
64
+ this.cursor = 0;
65
+ this.app.render();
66
+ return;
67
+ }
68
+ if (!key.ctrl && !key.meta && key.name.length === 1) {
69
+ this.inputValue =
70
+ this.inputValue.slice(0, this.cursor) + key.name + this.inputValue.slice(this.cursor);
71
+ this.cursor++;
72
+ this.app.render();
73
+ }
74
+ }
75
+ move(delta) {
76
+ this.focusIdx = (this.focusIdx + delta + FOCUS_COUNT) % FOCUS_COUNT;
77
+ this.app.render();
78
+ }
79
+ enterTab() {
80
+ if (this.focusIdx === 1)
81
+ this.app.openModelConfig();
82
+ else if (this.focusIdx === 2)
83
+ this.app.openHistory();
84
+ else
85
+ this.app.openSettings();
86
+ }
87
+ render(buf) {
88
+ drawHeader(buf, this.app.state);
89
+ const w = buf.width;
90
+ const h = buf.height;
91
+ const inputW = Math.min(78, w - 4);
92
+ const inputX = Math.floor((w - inputW) / 2);
93
+ const logoLines = LOGO.replace(/\n+$/, "").split("\n");
94
+ const logoMax = logoLines.reduce((m, l) => Math.max(m, l.length), 0);
95
+ const logoX = Math.max(0, Math.floor((w - logoMax) / 2));
96
+ const gap = 1;
97
+ const inputBoxH = 3;
98
+ const tabH = 3;
99
+ const contentH = logoLines.length + 1 + inputBoxH + gap + tabH;
100
+ const top = centerVertically(h, contentH);
101
+ let y = top;
102
+ for (const line of logoLines) {
103
+ buf.writeText(logoX, y, line, { fg: C.accent, bold: true });
104
+ y++;
105
+ }
106
+ y += 1;
107
+ drawInputBox(buf, inputX, y, inputW, this.inputValue, this.cursor, {
108
+ active: this.focusIdx === 0,
109
+ placeholder: "Type a message to start a new chat...",
110
+ });
111
+ y += inputBoxH + gap;
112
+ const tabGap = 2;
113
+ const tabInner = inputW - tabGap * (TABS.length - 1);
114
+ const baseW = Math.floor(tabInner / TABS.length);
115
+ const extra = tabInner - baseW * TABS.length;
116
+ let tx = inputX;
117
+ for (let i = 0; i < TABS.length; i++) {
118
+ const tw = baseW + (i < extra ? 1 : 0);
119
+ drawTabButton(buf, tx, y, tw, TABS[i], i === this.focusIdx - 1);
120
+ tx += tw + tabGap;
121
+ }
122
+ const hintY = y + tabH + 1;
123
+ if (hintY < h) {
124
+ buf.centerText(hintY, "←/→ / ↑/↓ switch · Enter confirm · Ctrl+E model · Ctrl+Q quit", {
125
+ fg: C.dim,
126
+ });
127
+ }
128
+ }
129
+ }
package/dist/state.js ADDED
@@ -0,0 +1,11 @@
1
+ export function defaultState() {
2
+ return {
3
+ selectedProvider: "DeepSeek",
4
+ selectedModelName: "deepseek-chat",
5
+ selectedModelUrl: "https://api.deepseek.com/v1",
6
+ selectedModelApiKey: "",
7
+ selectedModelStreaming: true,
8
+ selectedIntensity: "High",
9
+ isThinking: false,
10
+ };
11
+ }
@@ -0,0 +1,80 @@
1
+ import { C, EMPTY_STYLE } from "../colors.js";
2
+ import { displayWidth, wcwidth } from "../width.js";
3
+ const emptyCell = () => ({ ch: " ", style: { bg: C.black } });
4
+ function normalize(style) {
5
+ return style.bg ? style : { ...style, bg: C.black };
6
+ }
7
+ export class Buffer {
8
+ width;
9
+ height;
10
+ cells;
11
+ constructor(width, height) {
12
+ this.width = width;
13
+ this.height = height;
14
+ this.cells = Array.from({ length: height }, () => Array.from({ length: width }, emptyCell));
15
+ }
16
+ set(x, y, ch, style = EMPTY_STYLE) {
17
+ if (ch === "\r" || ch === "\n")
18
+ return;
19
+ if (y < 0 || y >= this.height || x < 0 || x >= this.width)
20
+ return;
21
+ this.cells[y][x] = { ch, style: normalize(style) };
22
+ }
23
+ writeText(x, y, text, style = EMPTY_STYLE) {
24
+ if (y < 0 || y >= this.height)
25
+ return;
26
+ let col = x;
27
+ for (const ch of text) {
28
+ const w = wcwidth(ch.codePointAt(0) ?? 0);
29
+ if (w === 0) {
30
+ if (col - 1 >= x && col - 1 >= 0 && col - 1 < this.width) {
31
+ this.cells[y][col - 1].ch += ch;
32
+ }
33
+ continue;
34
+ }
35
+ if (col >= this.width)
36
+ break;
37
+ if (col < 0) {
38
+ col += w;
39
+ continue;
40
+ }
41
+ this.cells[y][col] = { ch, style: normalize(style) };
42
+ if (w === 2 && col + 1 < this.width) {
43
+ this.cells[y][col + 1] = { ch: "", style: normalize(style) };
44
+ }
45
+ col += w;
46
+ }
47
+ }
48
+ centerText(y, text, style = EMPTY_STYLE) {
49
+ const w = displayWidth(text);
50
+ const x = Math.floor((this.width - w) / 2);
51
+ this.writeText(x, y, text, style);
52
+ }
53
+ fillRect(x, y, w, h, style = EMPTY_STYLE) {
54
+ for (let j = y; j < y + h; j++) {
55
+ for (let i = x; i < x + w; i++) {
56
+ this.set(i, j, " ", style);
57
+ }
58
+ }
59
+ }
60
+ drawBox(x, y, w, h, style = EMPTY_STYLE) {
61
+ if (w < 2 || h < 2)
62
+ return;
63
+ const top = "┌" + "─".repeat(w - 2) + "┐";
64
+ const bottom = "└" + "─".repeat(w - 2) + "┘";
65
+ this.writeText(x, y, top, style);
66
+ for (let j = y + 1; j < y + h - 1; j++) {
67
+ this.set(x, j, "│", style);
68
+ this.set(x + w - 1, j, "│", style);
69
+ }
70
+ this.writeText(x, y + h - 1, bottom, style);
71
+ }
72
+ clear() {
73
+ for (let j = 0; j < this.height; j++) {
74
+ const row = this.cells[j];
75
+ for (let i = 0; i < this.width; i++) {
76
+ row[i] = { ch: " ", style: { bg: C.black } };
77
+ }
78
+ }
79
+ }
80
+ }