@xingjisoft/neutrino 0.6.0 → 0.7.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/README.md +6 -1
- package/assets/fast-planner.txt +57 -0
- package/assets/fast-replanner.txt +28 -0
- package/dist/agents.d.ts +25 -0
- package/dist/app.d.ts +206 -0
- package/dist/app.js +15 -0
- package/dist/background.d.ts +96 -0
- package/dist/bin.d.ts +2 -0
- package/dist/bin.js +40 -3
- package/dist/browser.d.ts +18 -0
- package/dist/clipboard.d.ts +16 -0
- package/dist/colors.d.ts +29 -0
- package/dist/compact.d.ts +52 -0
- package/dist/config.d.ts +221 -0
- package/dist/config.js +22 -7
- package/dist/cwd.d.ts +19 -0
- package/dist/fast/artifacts.d.ts +26 -0
- package/dist/fast/artifacts.js +109 -0
- package/dist/fast/engine.d.ts +78 -0
- package/dist/fast/engine.js +657 -0
- package/dist/fast/evidence.d.ts +21 -0
- package/dist/fast/evidence.js +68 -0
- package/dist/fast/planner.d.ts +10 -0
- package/dist/fast/planner.js +51 -0
- package/dist/fast/pool.d.ts +17 -0
- package/dist/fast/pool.js +78 -0
- package/dist/fast/scheduler.d.ts +54 -0
- package/dist/fast/scheduler.js +383 -0
- package/dist/fast/types.d.ts +100 -0
- package/dist/fast/types.js +21 -0
- package/dist/fast/worker.d.ts +23 -0
- package/dist/fast/worker.js +185 -0
- package/dist/header.d.ts +3 -0
- package/dist/index.d.ts +5 -0
- package/dist/llm/approval.d.ts +204 -0
- package/dist/llm/approval.js +14 -3
- package/dist/llm/client.d.ts +118 -0
- package/dist/llm/client.js +57 -8
- package/dist/llm/codeExecutor.d.ts +14 -0
- package/dist/llm/codemode.d.ts +15 -0
- package/dist/llm/context.d.ts +29 -0
- package/dist/llm/context.js +17 -4
- package/dist/llm/diagnostics.d.ts +91 -0
- package/dist/llm/diagnostics.js +242 -0
- package/dist/llm/plan.d.ts +148 -0
- package/dist/llm/retry.d.ts +80 -0
- package/dist/llm/risk.d.ts +21 -0
- package/dist/llm/risk.js +7 -0
- package/dist/llm/runtime.d.ts +189 -0
- package/dist/llm/runtime.js +178 -24
- package/dist/llm/search.d.ts +2 -0
- package/dist/llm/searchNetwork.d.ts +8 -0
- package/dist/llm/subagent.d.ts +112 -0
- package/dist/llm/subagent.js +25 -12
- package/dist/llm/tools.d.ts +20 -0
- package/dist/llm/tools.js +4 -1
- package/dist/llm/workspace.d.ts +6 -0
- package/dist/llm/workspace.js +271 -0
- package/dist/markdown.d.ts +58 -0
- package/dist/migration.d.ts +8 -0
- package/dist/persistence.d.ts +14 -0
- package/dist/plugins/catalog.d.ts +20 -0
- package/dist/plugins/catalog.js +45 -0
- package/dist/plugins/cli.d.ts +1 -0
- package/dist/plugins/cli.js +106 -0
- package/dist/plugins/host.d.ts +121 -0
- package/dist/plugins/host.js +450 -0
- package/dist/plugins/manager.d.ts +24 -0
- package/dist/plugins/manager.js +288 -0
- package/dist/plugins/package.d.ts +5 -0
- package/dist/plugins/package.js +53 -0
- package/dist/plugins/runner.d.ts +1 -0
- package/dist/plugins/runner.js +227 -0
- package/dist/plugins/session.d.ts +12 -0
- package/dist/plugins/session.js +97 -0
- package/dist/plugins/terminal.d.ts +22 -0
- package/dist/plugins/terminal.js +33 -0
- package/dist/plugins/types.d.ts +178 -0
- package/dist/plugins/types.js +6 -0
- package/dist/plugins/validation.d.ts +14 -0
- package/dist/plugins/validation.js +174 -0
- package/dist/plugins/web-sdk.d.ts +58 -0
- package/dist/plugins/web-sdk.js +1 -0
- package/dist/pro/agents.d.ts +34 -0
- package/dist/pro/agents.js +1 -1
- package/dist/pro/config.d.ts +75 -0
- package/dist/pro/config.js +3 -2
- package/dist/pro/preflight.d.ts +21 -0
- package/dist/sanitize.d.ts +34 -0
- package/dist/screens/agentConversation.d.ts +28 -0
- package/dist/screens/agentHistory.d.ts +25 -0
- package/dist/screens/agentSelect.d.ts +16 -0
- package/dist/screens/chat.d.ts +292 -0
- package/dist/screens/chat.js +179 -65
- package/dist/screens/history.d.ts +23 -0
- package/dist/screens/modelSelect.d.ts +29 -0
- package/dist/screens/plugins.d.ts +29 -0
- package/dist/screens/plugins.js +196 -0
- package/dist/screens/settings.d.ts +42 -0
- package/dist/screens/start.d.ts +43 -0
- package/dist/screens/start.js +25 -17
- package/dist/screens/updatePrompt.d.ts +41 -0
- package/dist/selection.d.ts +75 -0
- package/dist/state.d.ts +26 -0
- package/dist/terminal/buffer.d.ts +47 -0
- package/dist/terminal/input.d.ts +70 -0
- package/dist/terminal/renderer.d.ts +43 -0
- package/dist/terminal/restore.d.ts +41 -0
- package/dist/terminal/screen.d.ts +57 -0
- package/dist/tray.d.ts +14 -0
- package/dist/ui.d.ts +123 -0
- package/dist/update.d.ts +71 -0
- package/dist/updateWatch.d.ts +43 -0
- package/dist/web/approvalQueue.d.ts +8 -0
- package/dist/web/approvalQueue.js +59 -0
- package/dist/web/auth.d.ts +14 -0
- package/dist/web/clientState.d.ts +16 -0
- package/dist/web/folderPicker.d.ts +34 -0
- package/dist/web/folderPicker.js +544 -0
- package/dist/web/hub.d.ts +213 -0
- package/dist/web/hub.js +14 -2
- package/dist/web/localApi.d.ts +11 -0
- package/dist/web/localApi.js +260 -0
- package/dist/web/plugins.d.ts +8 -0
- package/dist/web/plugins.js +64 -0
- package/dist/web/server.d.ts +81 -0
- package/dist/web/server.js +127 -19
- package/dist/web/service.d.ts +7 -0
- package/dist/web/service.js +1 -0
- package/dist/web/session.d.ts +176 -0
- package/dist/web/session.js +170 -21
- package/dist/web/settings.d.ts +65 -0
- package/dist/web/terminalBridge.d.ts +15 -0
- package/dist/web/terminalBridge.js +8 -2
- package/dist/width.d.ts +6 -0
- package/docs/plugins/README.md +126 -0
- package/docs/plugins/agent-sdk.md +92 -0
- package/docs/plugins/architecture.md +64 -0
- package/docs/plugins/compatibility.md +34 -0
- package/docs/plugins/development.md +55 -0
- package/docs/plugins/manifest.md +59 -0
- package/docs/plugins/manifest.schema.json +286 -0
- package/docs/plugins/security.md +53 -0
- package/docs/plugins/skills-prompts.md +69 -0
- package/docs/plugins/ui-sdk.md +109 -0
- package/examples/local-api.py +105 -0
- package/examples/plugins/cards/card.html +6 -0
- package/examples/plugins/cards/neutrino.plugin.json +42 -0
- package/examples/plugins/composer/editor.html +10 -0
- package/examples/plugins/composer/neutrino.plugin.json +10 -0
- package/examples/plugins/function-tools/agent.js +32 -0
- package/examples/plugins/function-tools/neutrino.plugin.json +15 -0
- package/examples/plugins/menus-tabs/neutrino.plugin.json +15 -0
- package/examples/plugins/menus-tabs/panel.html +6 -0
- package/examples/plugins/skills-prompts/neutrino.plugin.json +13 -0
- package/examples/plugins/skills-prompts/prompts/evidence.md +1 -0
- package/examples/plugins/skills-prompts/prompts/reviewer.md +1 -0
- package/examples/plugins/skills-prompts/skills/review/SKILL.md +8 -0
- package/examples/plugins/theme/neutrino.plugin.json +12 -0
- package/package.json +66 -42
- package/web/LOCAL-API.md +65 -0
- package/web/README.md +30 -8
- package/web/app.js +256 -67
- package/web/i18n.js +18 -3
- package/web/index.html +3 -1
- package/web/plugins.css +10 -0
- package/web/plugins.js +230 -0
- package/web/style.css +12 -3
- package/web/turns.js +7 -8
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import { Screen } from "../terminal/screen.js";
|
|
2
|
+
import { C } from "../colors.js";
|
|
3
|
+
import { drawHeader } from "../header.js";
|
|
4
|
+
import { drawHint } from "../ui.js";
|
|
5
|
+
import { configurePlugin, installPlugin, listPlugins, removePlugin, rollbackPlugin, selectProvider } from "../plugins/manager.js";
|
|
6
|
+
import { invalidateTuiPlugins, terminalUI } from "../plugins/terminal.js";
|
|
7
|
+
import { sanitizeText } from "../sanitize.js";
|
|
8
|
+
export class PluginViewScreen extends Screen {
|
|
9
|
+
item;
|
|
10
|
+
offset = 0;
|
|
11
|
+
constructor(item) {
|
|
12
|
+
super();
|
|
13
|
+
this.item = item;
|
|
14
|
+
}
|
|
15
|
+
onKey(key) { if (key.name === "escape" || key.name === "backspace") {
|
|
16
|
+
this.app.popScreen();
|
|
17
|
+
return;
|
|
18
|
+
} if (key.name === "down" || key.name === "pagedown")
|
|
19
|
+
this.offset++; if (key.name === "up" || key.name === "pageup")
|
|
20
|
+
this.offset = Math.max(0, this.offset - 1); this.app.render(); }
|
|
21
|
+
onMouse(ev) { if (ev.type === "click" && ev.button === 2)
|
|
22
|
+
this.app.popScreen(); }
|
|
23
|
+
render(buf) { drawHeader(buf, this.app.state); buf.writeText(2, 3, this.item.title, { fg: C.accent, bold: true }); const text = sanitizeText(this.item.text ?? "This view is available in the browser. / 此视图可在浏览器中打开。"); text.split("\n").slice(this.offset, this.offset + Math.max(1, buf.height - 8)).forEach((line, i) => buf.writeText(2, 5 + i, line.slice(0, Math.max(1, buf.width - 4)), { fg: C.white })); drawHint(buf, buf.height - 2, "Esc back · ↑/↓ scroll"); }
|
|
24
|
+
}
|
|
25
|
+
export class PluginsScreen extends Screen {
|
|
26
|
+
profile = "standard";
|
|
27
|
+
plugins = [];
|
|
28
|
+
index = 0;
|
|
29
|
+
detail = false;
|
|
30
|
+
editing = null;
|
|
31
|
+
draft = "";
|
|
32
|
+
message = "";
|
|
33
|
+
trustPending = false;
|
|
34
|
+
onActivate() { this.refresh(); }
|
|
35
|
+
refresh() { try {
|
|
36
|
+
this.plugins = listPlugins(this.profile).plugins;
|
|
37
|
+
this.index = Math.min(this.index, Math.max(0, this.plugins.length - 1));
|
|
38
|
+
invalidateTuiPlugins();
|
|
39
|
+
}
|
|
40
|
+
catch (e) {
|
|
41
|
+
this.message = e.message;
|
|
42
|
+
} }
|
|
43
|
+
action(run) { try {
|
|
44
|
+
run();
|
|
45
|
+
this.message = "Saved / 已保存";
|
|
46
|
+
this.refresh();
|
|
47
|
+
}
|
|
48
|
+
catch (e) {
|
|
49
|
+
this.message = e.message;
|
|
50
|
+
} this.app.render(); }
|
|
51
|
+
onKey(key) {
|
|
52
|
+
if (this.editing) {
|
|
53
|
+
if (key.name === "escape") {
|
|
54
|
+
this.editing = null;
|
|
55
|
+
this.app.render();
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
if (key.name === "return") {
|
|
59
|
+
const type = this.editing;
|
|
60
|
+
this.editing = null;
|
|
61
|
+
this.action(() => { if (type === "install")
|
|
62
|
+
installPlugin(this.draft);
|
|
63
|
+
else
|
|
64
|
+
configurePlugin(this.plugins[this.index].manifest.id, { config: JSON.parse(this.draft) }, this.profile); });
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
if (key.name === "backspace")
|
|
68
|
+
this.draft = this.draft.slice(0, -1);
|
|
69
|
+
else if (!key.ctrl && !key.meta && key.name.length === 1)
|
|
70
|
+
this.draft += key.name;
|
|
71
|
+
this.app.render();
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
if (key.name === "escape" || key.name === "backspace") {
|
|
75
|
+
if (this.detail) {
|
|
76
|
+
this.detail = false;
|
|
77
|
+
this.trustPending = false;
|
|
78
|
+
this.app.render();
|
|
79
|
+
}
|
|
80
|
+
else
|
|
81
|
+
this.app.popScreen();
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
if (key.name === "tab") {
|
|
85
|
+
this.profile = this.profile === "standard" ? "pro" : "standard";
|
|
86
|
+
this.refresh();
|
|
87
|
+
this.app.render();
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
if (key.name === "up") {
|
|
91
|
+
this.index = Math.max(0, this.index - 1);
|
|
92
|
+
this.app.render();
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
if (key.name === "down") {
|
|
96
|
+
this.index = Math.min(this.plugins.length - 1, this.index + 1);
|
|
97
|
+
this.app.render();
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
if (key.name === "i") {
|
|
101
|
+
this.editing = "install";
|
|
102
|
+
this.draft = "";
|
|
103
|
+
this.app.render();
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
if (key.name === "m") {
|
|
107
|
+
const items = terminalUI().filter(u => ["app.menu", "workspace.tabs", "settings.sections"].includes(u.slot));
|
|
108
|
+
if (items.length)
|
|
109
|
+
this.app.pushScreen(new PluginViewScreen({ ...items[0], text: items.map(i => `${i.title}\n${i.text ?? ""}`).join("\n\n") }));
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
const p = this.plugins[this.index];
|
|
113
|
+
if (!p)
|
|
114
|
+
return;
|
|
115
|
+
if (key.name === "return") {
|
|
116
|
+
this.detail = !this.detail;
|
|
117
|
+
this.app.render();
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
if (key.name === "e") {
|
|
121
|
+
if (!p.enabled && p.manifest.runtime === "native" && p.manifest.entries?.agent) {
|
|
122
|
+
this.trustPending = true;
|
|
123
|
+
this.message = "Native plugin can execute unrestricted local code. Press Y to trust this exact revision, Esc to cancel.";
|
|
124
|
+
this.app.render();
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
this.action(() => configurePlugin(p.manifest.id, { enabled: !p.enabled }, this.profile));
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
if (key.name === "y" && this.trustPending) {
|
|
131
|
+
this.trustPending = false;
|
|
132
|
+
this.action(() => configurePlugin(p.manifest.id, { enabled: true, trustNative: true }, this.profile));
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
if (key.name === "c") {
|
|
136
|
+
this.editing = "config";
|
|
137
|
+
this.draft = JSON.stringify(p.config);
|
|
138
|
+
this.app.render();
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
if (key.name === "u") {
|
|
142
|
+
this.action(() => { installPlugin(p.source); });
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
if (key.name === "r") {
|
|
146
|
+
this.action(() => rollbackPlugin(p.manifest.id));
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
if (key.name === "d") {
|
|
150
|
+
this.action(() => removePlugin(p.manifest.id));
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
if (key.name === "v") {
|
|
154
|
+
const replacements = (p.manifest.contributes?.ui ?? []).filter(u => ["chat.composer", "chat.message.renderer", "chat.card.plan", "chat.card.tool"].includes(u.slot));
|
|
155
|
+
this.action(() => { for (const u of replacements)
|
|
156
|
+
selectProvider(u.slot, `${p.manifest.id}/${u.id}`, this.profile); for (const prompt of p.manifest.contributes?.prompts ?? [])
|
|
157
|
+
if (prompt.replace)
|
|
158
|
+
selectProvider("persona", `${p.manifest.id}/${prompt.id}`, this.profile); });
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
onPaste(text) { if (this.editing) {
|
|
162
|
+
this.draft += text;
|
|
163
|
+
this.app.render();
|
|
164
|
+
} }
|
|
165
|
+
onMouse(ev) { if (ev.type === "click" && ev.button === 2)
|
|
166
|
+
this.app.popScreen();
|
|
167
|
+
else if (ev.type === "click" && ev.button === 0 && ev.y >= 5) {
|
|
168
|
+
this.index = Math.min(this.plugins.length - 1, Math.max(0, ev.y - 5));
|
|
169
|
+
this.app.render();
|
|
170
|
+
} }
|
|
171
|
+
render(buf) {
|
|
172
|
+
drawHeader(buf, this.app.state);
|
|
173
|
+
buf.writeText(2, 2, `Plugins / 插件 · ${this.profile}`, { fg: C.accent, bold: true });
|
|
174
|
+
let y = 4;
|
|
175
|
+
if (this.editing) {
|
|
176
|
+
buf.writeText(2, y++, this.editing === "install" ? "Local plugin directory / 本机插件目录" : "Configuration JSON / 配置 JSON", { fg: C.white });
|
|
177
|
+
buf.writeText(2, y++, this.draft.slice(-Math.max(1, buf.width - 5)), { fg: C.accent });
|
|
178
|
+
}
|
|
179
|
+
else if (this.detail && this.plugins[this.index]) {
|
|
180
|
+
const p = this.plugins[this.index];
|
|
181
|
+
const lines = [p.manifest.name, `${p.manifest.id} @ ${p.manifest.version}`, p.manifest.description ?? "", `Runtime: ${p.manifest.runtime ?? "sandbox"}`, `Permissions: ${(p.manifest.permissions ?? []).join(", ")}`, `Source: ${p.source}`, `Integrity: ${p.integrity}`, p.error ?? "", JSON.stringify(p.config)];
|
|
182
|
+
for (const line of lines)
|
|
183
|
+
buf.writeText(2, y++, sanitizeText(line, { singleLine: true }).slice(0, buf.width - 4), { fg: C.white });
|
|
184
|
+
}
|
|
185
|
+
else {
|
|
186
|
+
if (!this.plugins.length)
|
|
187
|
+
buf.writeText(2, 5, "No plugins. Press I to install a local directory. / 按 I 安装本机插件。", { fg: C.dim });
|
|
188
|
+
const start = Math.max(0, this.index - Math.max(1, buf.height - 12) + 1);
|
|
189
|
+
this.plugins.slice(start, start + Math.max(1, buf.height - 12)).forEach((p, i) => buf.writeText(2, 5 + i, `${i + start === this.index ? "›" : " "} ${p.enabled ? "●" : "○"} ${p.manifest.name} ${p.manifest.version}${p.error ? " !" : ""}`.slice(0, buf.width - 4), { fg: i + start === this.index ? C.accent : C.white, bold: i + start === this.index }));
|
|
190
|
+
}
|
|
191
|
+
if (this.message)
|
|
192
|
+
buf.writeText(2, buf.height - 4, sanitizeText(this.message, { singleLine: true }).slice(0, buf.width - 4), { fg: C.amber });
|
|
193
|
+
drawHint(buf, buf.height - 3, "↑/↓ select · Enter details · E enable/disable · I install · C config · U update");
|
|
194
|
+
drawHint(buf, buf.height - 2, "R rollback · D remove · V select providers · M extensions · Tab profile · Esc back");
|
|
195
|
+
}
|
|
196
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { Buffer } from "../terminal/buffer.js";
|
|
2
|
+
import { Screen } from "../terminal/screen.js";
|
|
3
|
+
import type { Key, MouseEvent } from "../terminal/input.js";
|
|
4
|
+
export declare class SettingsScreen extends Screen {
|
|
5
|
+
private intensityIdx;
|
|
6
|
+
/** Levels the current chat model accepts, in canonical order. */
|
|
7
|
+
private intensityLevels;
|
|
8
|
+
private contextTokens;
|
|
9
|
+
private contextChanged;
|
|
10
|
+
/** 0 = same model as the chat, otherwise index + 1 into `models`. */
|
|
11
|
+
private approvalModelIdx;
|
|
12
|
+
/** 0 = off (human only), 1 = on (two independent assessments). */
|
|
13
|
+
private extremeIdx;
|
|
14
|
+
/** 0 = silent, 1 = audible cue on turn end and on approval requests. */
|
|
15
|
+
private soundIdx;
|
|
16
|
+
/** 0 = web UI off, 1 = mounted alongside the terminal. */
|
|
17
|
+
private webIdx;
|
|
18
|
+
/** 0 = terminal only, 1 = the Web console is run by a detached service. */
|
|
19
|
+
private backgroundIdx;
|
|
20
|
+
/** Only a row the user actually touched rewrites the saved default. */
|
|
21
|
+
private webChanged;
|
|
22
|
+
private backgroundPending;
|
|
23
|
+
private models;
|
|
24
|
+
private rowIdx;
|
|
25
|
+
private lastHoverX;
|
|
26
|
+
private lastHoverY;
|
|
27
|
+
private editingPassword;
|
|
28
|
+
private passwordDraft;
|
|
29
|
+
private passwordMessage;
|
|
30
|
+
onActivate(): void;
|
|
31
|
+
/** Change the value of a row by `delta` steps and persist it immediately. */
|
|
32
|
+
private cycle;
|
|
33
|
+
private applyBackground;
|
|
34
|
+
onKey(key: Key): void;
|
|
35
|
+
onMouse(ev: MouseEvent): void;
|
|
36
|
+
onPaste(text: string): void;
|
|
37
|
+
onDeactivate(): void;
|
|
38
|
+
private rowTexts;
|
|
39
|
+
private layout;
|
|
40
|
+
private save;
|
|
41
|
+
render(buf: Buffer): void;
|
|
42
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { Buffer } from "../terminal/buffer.js";
|
|
2
|
+
import { Screen } from "../terminal/screen.js";
|
|
3
|
+
import type { Key, MouseEvent } from "../terminal/input.js";
|
|
4
|
+
export declare class StartScreen extends Screen {
|
|
5
|
+
private extensions;
|
|
6
|
+
private get tabs();
|
|
7
|
+
private get focusCount();
|
|
8
|
+
onActivate(): void;
|
|
9
|
+
private get approvalMode();
|
|
10
|
+
private toggleApproval;
|
|
11
|
+
private logoLines;
|
|
12
|
+
private inputValue;
|
|
13
|
+
private cursor;
|
|
14
|
+
private focusIdx;
|
|
15
|
+
private hoverIdx;
|
|
16
|
+
private proHover;
|
|
17
|
+
private webHover;
|
|
18
|
+
private lastHoverX;
|
|
19
|
+
private lastHoverY;
|
|
20
|
+
showsCursor(): boolean;
|
|
21
|
+
onPaste(text: string): void;
|
|
22
|
+
onKey(key: Key): void;
|
|
23
|
+
onMouse(ev: MouseEvent): void;
|
|
24
|
+
/**
|
|
25
|
+
* The message box, the tabs below it and the hint row are persistent chrome;
|
|
26
|
+
* repainting them every frame keeps them self-healing against terminal-side
|
|
27
|
+
* drift (see Screen.alwaysRepaintRows).
|
|
28
|
+
*/
|
|
29
|
+
alwaysRepaintRows(): number[];
|
|
30
|
+
private layout;
|
|
31
|
+
/**
|
|
32
|
+
* First content row. The centered block normally floats in the middle, but
|
|
33
|
+
* it must start below the top-left buttons when they are visible: the boxes
|
|
34
|
+
* are opaque chrome and would otherwise clip the top of the logo.
|
|
35
|
+
*/
|
|
36
|
+
private contentTop;
|
|
37
|
+
private move;
|
|
38
|
+
/** Move the text cursor by one character, staying within bounds and on
|
|
39
|
+
* surrogate-pair boundaries. */
|
|
40
|
+
private moveCursor;
|
|
41
|
+
private enterTab;
|
|
42
|
+
render(buf: Buffer): void;
|
|
43
|
+
}
|
package/dist/screens/start.js
CHANGED
|
@@ -1,17 +1,21 @@
|
|
|
1
|
+
import { terminalUI } from "../plugins/terminal.js";
|
|
1
2
|
import { Screen } from "../terminal/screen.js";
|
|
2
3
|
import { C } from "../colors.js";
|
|
3
4
|
import { loadLogo, loadVersion, loadConfig, saveConfig, normalizeApprovalMode } from "../config.js";
|
|
4
5
|
import { drawHeader } from "../header.js";
|
|
5
6
|
import { centerVertically, computeInputWindow, cursorFromColumn, drawHint, drawInputBox, drawTabButton, inRect, approvalToggle, drawApprovalToggle, drawHeaderButton, proModeButton, webModeButton, } from "../ui.js";
|
|
6
|
-
const TABS = ["Model", "History", "Settings"];
|
|
7
|
+
const TABS = ["Model", "History", "Settings", "Plugins"];
|
|
7
8
|
const LOGO = loadLogo();
|
|
8
|
-
const FOCUS_COUNT = TABS.length + 2; // input box + 3 tabs + approval mode
|
|
9
9
|
const INPUT_H = 3;
|
|
10
10
|
const TAB_H = 3;
|
|
11
11
|
function isLowSurrogate(code) {
|
|
12
12
|
return code >= 0xdc00 && code <= 0xdfff;
|
|
13
13
|
}
|
|
14
14
|
export class StartScreen extends Screen {
|
|
15
|
+
extensions = [];
|
|
16
|
+
get tabs() { return [...TABS, ...this.extensions.slice(0, Math.max(0, Math.floor(((this.app?.width ?? 80) - 2) / 10) - TABS.length)).map(item => item.title)]; }
|
|
17
|
+
get focusCount() { return this.tabs.length + 2; }
|
|
18
|
+
onActivate() { this.extensions = terminalUI().filter(item => item.slot === "app.menu" || item.slot === "workspace.tabs"); }
|
|
15
19
|
get approvalMode() {
|
|
16
20
|
return normalizeApprovalMode(loadConfig().approval_mode);
|
|
17
21
|
}
|
|
@@ -81,7 +85,7 @@ export class StartScreen extends Screen {
|
|
|
81
85
|
if (text)
|
|
82
86
|
this.app.startNewChat(text);
|
|
83
87
|
}
|
|
84
|
-
else if (this.focusIdx ===
|
|
88
|
+
else if (this.focusIdx === this.focusCount - 1) {
|
|
85
89
|
this.toggleApproval();
|
|
86
90
|
}
|
|
87
91
|
else {
|
|
@@ -149,7 +153,7 @@ export class StartScreen extends Screen {
|
|
|
149
153
|
// must not light up.
|
|
150
154
|
if (!proHover && !webHover) {
|
|
151
155
|
if (inRect(ev.x, ev.y, approvalToggle(this.app.width, this.app.height - 4, this.approvalMode))) {
|
|
152
|
-
hover =
|
|
156
|
+
hover = this.focusCount - 1;
|
|
153
157
|
}
|
|
154
158
|
else if (layout && inRect(ev.x, ev.y, { x: layout.inputX, y: layout.inputY, w: layout.inputW, h: INPUT_H })) {
|
|
155
159
|
hover = 0;
|
|
@@ -191,7 +195,7 @@ export class StartScreen extends Screen {
|
|
|
191
195
|
return;
|
|
192
196
|
}
|
|
193
197
|
if (ev.y >= 1 && inRect(ev.x, ev.y, approvalToggle(this.app.width, this.app.height - 4, this.approvalMode))) {
|
|
194
|
-
this.focusIdx =
|
|
198
|
+
this.focusIdx = this.focusCount - 1;
|
|
195
199
|
this.toggleApproval();
|
|
196
200
|
return;
|
|
197
201
|
}
|
|
@@ -256,12 +260,12 @@ export class StartScreen extends Screen {
|
|
|
256
260
|
const inputY = this.contentTop(h, contentH) + logoLines.length + logoGap;
|
|
257
261
|
const tabY = inputY + INPUT_H + gap;
|
|
258
262
|
const tabGap = 2;
|
|
259
|
-
const tabInner = inputW - tabGap * (
|
|
260
|
-
const baseW = Math.floor(tabInner /
|
|
261
|
-
const extra = tabInner - baseW *
|
|
263
|
+
const tabInner = inputW - tabGap * (this.tabs.length - 1);
|
|
264
|
+
const baseW = Math.floor(tabInner / this.tabs.length);
|
|
265
|
+
const extra = tabInner - baseW * this.tabs.length;
|
|
262
266
|
const tabs = [];
|
|
263
267
|
let tx = inputX;
|
|
264
|
-
for (let i = 0; i <
|
|
268
|
+
for (let i = 0; i < this.tabs.length; i++) {
|
|
265
269
|
const tw = baseW + (i < extra ? 1 : 0);
|
|
266
270
|
tabs.push({ x: tx, w: tw });
|
|
267
271
|
tx += tw + tabGap;
|
|
@@ -279,7 +283,7 @@ export class StartScreen extends Screen {
|
|
|
279
283
|
return pro ? Math.max(top, pro.y + pro.h) : top;
|
|
280
284
|
}
|
|
281
285
|
move(delta) {
|
|
282
|
-
this.focusIdx = (this.focusIdx + delta +
|
|
286
|
+
this.focusIdx = (this.focusIdx + delta + this.focusCount) % this.focusCount;
|
|
283
287
|
this.hoverIdx = -1;
|
|
284
288
|
this.app.render();
|
|
285
289
|
}
|
|
@@ -306,8 +310,12 @@ export class StartScreen extends Screen {
|
|
|
306
310
|
this.app.openModelConfig();
|
|
307
311
|
else if (this.focusIdx === 2)
|
|
308
312
|
this.app.openHistory();
|
|
309
|
-
else
|
|
313
|
+
else if (this.focusIdx === 3)
|
|
310
314
|
this.app.openSettings();
|
|
315
|
+
else if (this.focusIdx === 4)
|
|
316
|
+
this.app.openPlugins();
|
|
317
|
+
else if (this.extensions[this.focusIdx - 5])
|
|
318
|
+
this.app.openPluginContribution(this.extensions[this.focusIdx - 5]);
|
|
311
319
|
}
|
|
312
320
|
render(buf) {
|
|
313
321
|
drawHeader(buf, this.app.state);
|
|
@@ -335,14 +343,14 @@ export class StartScreen extends Screen {
|
|
|
335
343
|
});
|
|
336
344
|
y += INPUT_H + gap;
|
|
337
345
|
const tabGap = 2;
|
|
338
|
-
const tabInner = inputW - tabGap * (
|
|
339
|
-
const baseW = Math.floor(tabInner /
|
|
340
|
-
const extra = tabInner - baseW *
|
|
346
|
+
const tabInner = inputW - tabGap * (this.tabs.length - 1);
|
|
347
|
+
const baseW = Math.floor(tabInner / this.tabs.length);
|
|
348
|
+
const extra = tabInner - baseW * this.tabs.length;
|
|
341
349
|
let tx = inputX;
|
|
342
|
-
for (let i = 0; i <
|
|
350
|
+
for (let i = 0; i < this.tabs.length; i++) {
|
|
343
351
|
const tw = baseW + (i < extra ? 1 : 0);
|
|
344
352
|
const selected = i === this.focusIdx - 1 || i === this.hoverIdx - 1;
|
|
345
|
-
drawTabButton(buf, tx, y, tw,
|
|
353
|
+
drawTabButton(buf, tx, y, tw, this.tabs[i], selected);
|
|
346
354
|
tx += tw + tabGap;
|
|
347
355
|
}
|
|
348
356
|
// The version label is pinned one row above the very bottom of the screen,
|
|
@@ -363,7 +371,7 @@ export class StartScreen extends Screen {
|
|
|
363
371
|
if (showVersion) {
|
|
364
372
|
buf.centerText(versionY, `v${version}`, { fg: C.dim });
|
|
365
373
|
}
|
|
366
|
-
drawApprovalToggle(buf, approvalY, this.approvalMode, this.focusIdx ===
|
|
374
|
+
drawApprovalToggle(buf, approvalY, this.approvalMode, this.focusIdx === this.focusCount - 1 || this.hoverIdx === this.focusCount - 1);
|
|
367
375
|
// Painted last so their opaque footprints are never clipped by the
|
|
368
376
|
// decorative content drawn before them. Hit testing shares the geometry.
|
|
369
377
|
drawHeaderButton(buf, proModeButton(w, h), this.proHover);
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { Buffer } from "../terminal/buffer.js";
|
|
2
|
+
import { Screen } from "../terminal/screen.js";
|
|
3
|
+
import type { Key, MouseEvent } from "../terminal/input.js";
|
|
4
|
+
import { type UpdateStatus } from "../update.js";
|
|
5
|
+
/**
|
|
6
|
+
* The "a newer Neutrino is published" notice.
|
|
7
|
+
*
|
|
8
|
+
* It is pushed over the start screen only, never over a conversation: the
|
|
9
|
+
* answer arrives a moment after launch, and stealing a chat the user has
|
|
10
|
+
* already opened would be worse than a missed notice.
|
|
11
|
+
*
|
|
12
|
+
* Confirming runs npm here, in this process; the running code is not replaced,
|
|
13
|
+
* so a successful update ends by telling the user to restart. A failed one
|
|
14
|
+
* shows the command to run by hand, because npm usually fails for a reason the
|
|
15
|
+
* user has to fix (permissions, a proxy, an offline machine).
|
|
16
|
+
*/
|
|
17
|
+
export declare class UpdatePromptScreen extends Screen {
|
|
18
|
+
private phase;
|
|
19
|
+
/** Why the install failed, or which version landed. */
|
|
20
|
+
private detail;
|
|
21
|
+
private selectedIdx;
|
|
22
|
+
private lastHoverX;
|
|
23
|
+
private lastHoverY;
|
|
24
|
+
private readonly current;
|
|
25
|
+
private readonly latest;
|
|
26
|
+
constructor(status: UpdateStatus);
|
|
27
|
+
/** The buttons drawn right now; a click always lands on one of these. */
|
|
28
|
+
private buttons;
|
|
29
|
+
private buttonRow;
|
|
30
|
+
/** Columns the body paragraphs may use before wrapping. */
|
|
31
|
+
private bodyWidth;
|
|
32
|
+
private start;
|
|
33
|
+
private close;
|
|
34
|
+
private activate;
|
|
35
|
+
private move;
|
|
36
|
+
onKey(key: Key): void;
|
|
37
|
+
onMouse(ev: MouseEvent): void;
|
|
38
|
+
/** What the dialog says, in the order it is drawn. */
|
|
39
|
+
private body;
|
|
40
|
+
render(buf: Buffer): void;
|
|
41
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import type { Buffer } from "./terminal/buffer.js";
|
|
2
|
+
export interface Point {
|
|
3
|
+
x: number;
|
|
4
|
+
y: number;
|
|
5
|
+
}
|
|
6
|
+
/** A point in *content* space: a line of the scrollable log, plus a column. */
|
|
7
|
+
export interface LinePoint {
|
|
8
|
+
line: number;
|
|
9
|
+
x: number;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* A mouse text selection. `anchor` is the cell where the drag started and
|
|
13
|
+
* `cursor` is the cell currently under the pointer; either may be above/left of
|
|
14
|
+
* the other, so consumers normalise before use.
|
|
15
|
+
*/
|
|
16
|
+
export interface Selection {
|
|
17
|
+
anchor: Point;
|
|
18
|
+
cursor: Point;
|
|
19
|
+
}
|
|
20
|
+
/** Order the two ends of a selection into reading order (top-left first). */
|
|
21
|
+
export declare function normalizeSelection(sel: Selection): {
|
|
22
|
+
start: Point;
|
|
23
|
+
end: Point;
|
|
24
|
+
};
|
|
25
|
+
/** Order the two ends of a line-space selection into reading order. */
|
|
26
|
+
export declare function orderLinePoints(a: LinePoint, b: LinePoint): {
|
|
27
|
+
start: LinePoint;
|
|
28
|
+
end: LinePoint;
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* Paint the selection highlight directly onto the rendered buffer. Working on
|
|
32
|
+
* the buffer (rather than on each screen) means the highlight automatically
|
|
33
|
+
* follows whatever is on screen — chat output, the input box, menus, …
|
|
34
|
+
*
|
|
35
|
+
* Intermediate lines stop at the last non-blank cell so a multi-line selection
|
|
36
|
+
* doesn't paint a block out to the right edge of the terminal.
|
|
37
|
+
*/
|
|
38
|
+
export declare function applySelection(buf: Buffer, sel: Selection): void;
|
|
39
|
+
/**
|
|
40
|
+
* Extract the plain text covered by a selection.
|
|
41
|
+
*
|
|
42
|
+
* Three kinds of cell are skipped so the result is clean, pasteable text:
|
|
43
|
+
* - UI chrome (the scrollbar), which is never content;
|
|
44
|
+
* - wide-character continuation cells (`""`), which would duplicate a CJK
|
|
45
|
+
* glyph;
|
|
46
|
+
* - trailing blanks, trimmed per line (shell-style).
|
|
47
|
+
*
|
|
48
|
+
* Intermediate lines are copied out to the buffer's full width and only then
|
|
49
|
+
* trimmed, which keeps indentation intact while dropping the padding on the
|
|
50
|
+
* right. Skipping the scrollbar *before* trimming is what lets that trim
|
|
51
|
+
* actually reach the end of the text instead of stopping at the bar.
|
|
52
|
+
*/
|
|
53
|
+
export declare function selectionText(buf: Buffer, sel: Selection): string;
|
|
54
|
+
/**
|
|
55
|
+
* One buffer row as column-indexed text: `cells[x]` is what column `x` holds,
|
|
56
|
+
* or `""` for a cell that never copies (UI chrome, and the tail of a
|
|
57
|
+
* double-width glyph).
|
|
58
|
+
*
|
|
59
|
+
* Keeping the column index is the point: it lets a row be sliced by column
|
|
60
|
+
* later, after the frame it came from is long gone. A drag that scrolls the log
|
|
61
|
+
* needs exactly that, because the rows it started on leave the screen while the
|
|
62
|
+
* selection keeps growing.
|
|
63
|
+
*/
|
|
64
|
+
export declare function rowCells(buf: Buffer, y: number): string[];
|
|
65
|
+
/**
|
|
66
|
+
* Extract the text of a selection expressed in content lines (`rowSelectionText`
|
|
67
|
+
* is to `selectionText` what `LinePoint` is to `Point`).
|
|
68
|
+
*
|
|
69
|
+
* `getRow` supplies the cells of a line — from the current frame while the line
|
|
70
|
+
* is on screen, from the snapshot taken during the drag once it has scrolled
|
|
71
|
+
* away, or `null` for a line that was never captured. Trailing blanks and edge
|
|
72
|
+
* blank lines are trimmed exactly like `selectionText`, so both paths produce
|
|
73
|
+
* the same text for a selection that fits on one screen.
|
|
74
|
+
*/
|
|
75
|
+
export declare function rowSelectionText(getRow: (line: number) => readonly string[] | null, start: LinePoint, end: LinePoint): string;
|
package/dist/state.d.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { Selection } from "./selection.js";
|
|
2
|
+
export interface AppState {
|
|
3
|
+
selectedProvider: string;
|
|
4
|
+
selectedModelName: string;
|
|
5
|
+
selectedModelUrl: string;
|
|
6
|
+
selectedModelApiKey: string;
|
|
7
|
+
selectedModelStreaming: boolean;
|
|
8
|
+
selectedIntensity: string;
|
|
9
|
+
isThinking: boolean;
|
|
10
|
+
/** Blink phase for the "thinking" indicator in the top status bar. */
|
|
11
|
+
blinkOn: boolean;
|
|
12
|
+
/** Blink phase for the text cursors (true = lit, false = hidden). */
|
|
13
|
+
cursorOn: boolean;
|
|
14
|
+
/** Pending human approval; while set the top bar turns orange. */
|
|
15
|
+
approval: {
|
|
16
|
+
tool: string;
|
|
17
|
+
level: "high" | "extreme";
|
|
18
|
+
} | null;
|
|
19
|
+
/** URL of the running browser UI, or null when it is not mounted. */
|
|
20
|
+
webUrl: string | null;
|
|
21
|
+
/** True when that console is owned by the detached background service. */
|
|
22
|
+
webBackground: boolean;
|
|
23
|
+
/** Active mouse text selection, or null when nothing is selected. */
|
|
24
|
+
selection: Selection | null;
|
|
25
|
+
}
|
|
26
|
+
export declare function defaultState(): AppState;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { type Style } from "../colors.js";
|
|
2
|
+
export interface Cell {
|
|
3
|
+
ch: string;
|
|
4
|
+
style: Style;
|
|
5
|
+
/**
|
|
6
|
+
* True for UI chrome (scrollbars, borders, …). Chrome is drawn like any
|
|
7
|
+
* other cell, but is deliberately *not* copied: extracting a selection
|
|
8
|
+
* skips it, so a copied line never ends up with a scrollbar glued to its
|
|
9
|
+
* right edge.
|
|
10
|
+
*/
|
|
11
|
+
chrome?: boolean;
|
|
12
|
+
}
|
|
13
|
+
export declare class Buffer {
|
|
14
|
+
width: number;
|
|
15
|
+
height: number;
|
|
16
|
+
cells: Cell[][];
|
|
17
|
+
constructor(width: number, height: number);
|
|
18
|
+
/**
|
|
19
|
+
* Write a single cell, keeping the wide-glyph invariant intact. The renderer
|
|
20
|
+
* (and the selection logic) rely on the rule that a `""` cell is *always* the
|
|
21
|
+
* right half of a double-width glyph at `x - 1`. If the invariant is broken an
|
|
22
|
+
* orphan tail is left behind: the renderer skips it, so that column is never
|
|
23
|
+
* repainted and a stale glyph from the previous frame stays on screen (the
|
|
24
|
+
* "residual line" bug). To preserve it we blank the *other* half whenever we
|
|
25
|
+
* overwrite one half of a wide glyph.
|
|
26
|
+
*/
|
|
27
|
+
private overwriteCell;
|
|
28
|
+
/**
|
|
29
|
+
* Mark `x` as the tail (`""`) of the wide glyph at `x - 1`. Unlike
|
|
30
|
+
* `overwriteCell` this never blanks the cell to the left, because that cell is
|
|
31
|
+
* the glyph we are currently writing.
|
|
32
|
+
*/
|
|
33
|
+
private writeTail;
|
|
34
|
+
private writeChar;
|
|
35
|
+
set(x: number, y: number, ch: string, style?: Style): void;
|
|
36
|
+
writeText(x: number, y: number, text: string, style?: Style): void;
|
|
37
|
+
centerText(y: number, text: string, style?: Style): void;
|
|
38
|
+
fillRect(x: number, y: number, w: number, h: number, style?: Style): void;
|
|
39
|
+
drawBox(x: number, y: number, w: number, h: number, style?: Style): void;
|
|
40
|
+
/**
|
|
41
|
+
* Flag a cell as non-copyable chrome (e.g. a scrollbar). The flag lives on
|
|
42
|
+
* the cell and is cleared automatically whenever the cell is written again,
|
|
43
|
+
* since every frame starts from a fresh buffer.
|
|
44
|
+
*/
|
|
45
|
+
markChrome(x: number, y: number): void;
|
|
46
|
+
clear(): void;
|
|
47
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
export interface Key {
|
|
2
|
+
name: string;
|
|
3
|
+
ctrl: boolean;
|
|
4
|
+
meta: boolean;
|
|
5
|
+
shift: boolean;
|
|
6
|
+
char: string;
|
|
7
|
+
sequence: string;
|
|
8
|
+
}
|
|
9
|
+
export type MouseEvent = {
|
|
10
|
+
type: "wheel";
|
|
11
|
+
dir: -1 | 1;
|
|
12
|
+
x: number;
|
|
13
|
+
y: number;
|
|
14
|
+
} | {
|
|
15
|
+
type: "click";
|
|
16
|
+
x: number;
|
|
17
|
+
y: number;
|
|
18
|
+
button: number;
|
|
19
|
+
} | {
|
|
20
|
+
type: "move";
|
|
21
|
+
x: number;
|
|
22
|
+
y: number;
|
|
23
|
+
drag: boolean;
|
|
24
|
+
} | {
|
|
25
|
+
type: "release";
|
|
26
|
+
x: number;
|
|
27
|
+
y: number;
|
|
28
|
+
button: number;
|
|
29
|
+
};
|
|
30
|
+
export type InputEvent = {
|
|
31
|
+
type: "key";
|
|
32
|
+
key: Key;
|
|
33
|
+
} | {
|
|
34
|
+
type: "paste";
|
|
35
|
+
text: string;
|
|
36
|
+
} | {
|
|
37
|
+
type: "mouse";
|
|
38
|
+
event: MouseEvent;
|
|
39
|
+
};
|
|
40
|
+
export declare class InputReader {
|
|
41
|
+
private decoder;
|
|
42
|
+
private pending;
|
|
43
|
+
private state;
|
|
44
|
+
private csiBuf;
|
|
45
|
+
private escTimer;
|
|
46
|
+
private inPaste;
|
|
47
|
+
private pasteBuf;
|
|
48
|
+
private handler;
|
|
49
|
+
private dataHandler;
|
|
50
|
+
start(handler: (ev: InputEvent) => void): void;
|
|
51
|
+
stop(): void;
|
|
52
|
+
private onData;
|
|
53
|
+
private process;
|
|
54
|
+
private scheduleEscTimeout;
|
|
55
|
+
private cancelEscTimeout;
|
|
56
|
+
private finishCsi;
|
|
57
|
+
/**
|
|
58
|
+
* Parse SGR (extended) mouse sequences: ESC [ < b ; x ; y M/m
|
|
59
|
+
*
|
|
60
|
+
* `b` is a bit field: the low two bits are the button (0 left, 1 middle,
|
|
61
|
+
* 2 right, 3 none/release), bit 5 (32) marks a motion event and bit 6 (64)
|
|
62
|
+
* marks a wheel event. A trailing `M` is a press/motion while `m` is a
|
|
63
|
+
* release. So motion with the left button held (a drag) is `b = 32`, whereas
|
|
64
|
+
* plain hover is `b = 35` (32 | 3).
|
|
65
|
+
*/
|
|
66
|
+
private handleMouse;
|
|
67
|
+
private emitKey;
|
|
68
|
+
private emitPaste;
|
|
69
|
+
private emitMouse;
|
|
70
|
+
}
|