@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,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
|
+
}
|