@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.
Files changed (169) hide show
  1. package/README.md +6 -1
  2. package/assets/fast-planner.txt +57 -0
  3. package/assets/fast-replanner.txt +28 -0
  4. package/dist/agents.d.ts +25 -0
  5. package/dist/app.d.ts +206 -0
  6. package/dist/app.js +15 -0
  7. package/dist/background.d.ts +96 -0
  8. package/dist/bin.d.ts +2 -0
  9. package/dist/bin.js +40 -3
  10. package/dist/browser.d.ts +18 -0
  11. package/dist/clipboard.d.ts +16 -0
  12. package/dist/colors.d.ts +29 -0
  13. package/dist/compact.d.ts +52 -0
  14. package/dist/config.d.ts +221 -0
  15. package/dist/config.js +22 -7
  16. package/dist/cwd.d.ts +19 -0
  17. package/dist/fast/artifacts.d.ts +26 -0
  18. package/dist/fast/artifacts.js +109 -0
  19. package/dist/fast/engine.d.ts +78 -0
  20. package/dist/fast/engine.js +657 -0
  21. package/dist/fast/evidence.d.ts +21 -0
  22. package/dist/fast/evidence.js +68 -0
  23. package/dist/fast/planner.d.ts +10 -0
  24. package/dist/fast/planner.js +51 -0
  25. package/dist/fast/pool.d.ts +17 -0
  26. package/dist/fast/pool.js +78 -0
  27. package/dist/fast/scheduler.d.ts +54 -0
  28. package/dist/fast/scheduler.js +383 -0
  29. package/dist/fast/types.d.ts +100 -0
  30. package/dist/fast/types.js +21 -0
  31. package/dist/fast/worker.d.ts +23 -0
  32. package/dist/fast/worker.js +185 -0
  33. package/dist/header.d.ts +3 -0
  34. package/dist/index.d.ts +5 -0
  35. package/dist/llm/approval.d.ts +204 -0
  36. package/dist/llm/approval.js +14 -3
  37. package/dist/llm/client.d.ts +118 -0
  38. package/dist/llm/client.js +57 -8
  39. package/dist/llm/codeExecutor.d.ts +14 -0
  40. package/dist/llm/codemode.d.ts +15 -0
  41. package/dist/llm/context.d.ts +29 -0
  42. package/dist/llm/context.js +17 -4
  43. package/dist/llm/diagnostics.d.ts +91 -0
  44. package/dist/llm/diagnostics.js +242 -0
  45. package/dist/llm/plan.d.ts +148 -0
  46. package/dist/llm/retry.d.ts +80 -0
  47. package/dist/llm/risk.d.ts +21 -0
  48. package/dist/llm/risk.js +7 -0
  49. package/dist/llm/runtime.d.ts +189 -0
  50. package/dist/llm/runtime.js +178 -24
  51. package/dist/llm/search.d.ts +2 -0
  52. package/dist/llm/searchNetwork.d.ts +8 -0
  53. package/dist/llm/subagent.d.ts +112 -0
  54. package/dist/llm/subagent.js +25 -12
  55. package/dist/llm/tools.d.ts +20 -0
  56. package/dist/llm/tools.js +4 -1
  57. package/dist/llm/workspace.d.ts +6 -0
  58. package/dist/llm/workspace.js +271 -0
  59. package/dist/markdown.d.ts +58 -0
  60. package/dist/migration.d.ts +8 -0
  61. package/dist/persistence.d.ts +14 -0
  62. package/dist/plugins/catalog.d.ts +20 -0
  63. package/dist/plugins/catalog.js +45 -0
  64. package/dist/plugins/cli.d.ts +1 -0
  65. package/dist/plugins/cli.js +106 -0
  66. package/dist/plugins/host.d.ts +121 -0
  67. package/dist/plugins/host.js +450 -0
  68. package/dist/plugins/manager.d.ts +24 -0
  69. package/dist/plugins/manager.js +288 -0
  70. package/dist/plugins/package.d.ts +5 -0
  71. package/dist/plugins/package.js +53 -0
  72. package/dist/plugins/runner.d.ts +1 -0
  73. package/dist/plugins/runner.js +227 -0
  74. package/dist/plugins/session.d.ts +12 -0
  75. package/dist/plugins/session.js +97 -0
  76. package/dist/plugins/terminal.d.ts +22 -0
  77. package/dist/plugins/terminal.js +33 -0
  78. package/dist/plugins/types.d.ts +178 -0
  79. package/dist/plugins/types.js +6 -0
  80. package/dist/plugins/validation.d.ts +14 -0
  81. package/dist/plugins/validation.js +174 -0
  82. package/dist/plugins/web-sdk.d.ts +58 -0
  83. package/dist/plugins/web-sdk.js +1 -0
  84. package/dist/pro/agents.d.ts +34 -0
  85. package/dist/pro/agents.js +1 -1
  86. package/dist/pro/config.d.ts +75 -0
  87. package/dist/pro/config.js +3 -2
  88. package/dist/pro/preflight.d.ts +21 -0
  89. package/dist/sanitize.d.ts +34 -0
  90. package/dist/screens/agentConversation.d.ts +28 -0
  91. package/dist/screens/agentHistory.d.ts +25 -0
  92. package/dist/screens/agentSelect.d.ts +16 -0
  93. package/dist/screens/chat.d.ts +292 -0
  94. package/dist/screens/chat.js +179 -65
  95. package/dist/screens/history.d.ts +23 -0
  96. package/dist/screens/modelSelect.d.ts +29 -0
  97. package/dist/screens/plugins.d.ts +29 -0
  98. package/dist/screens/plugins.js +196 -0
  99. package/dist/screens/settings.d.ts +42 -0
  100. package/dist/screens/start.d.ts +43 -0
  101. package/dist/screens/start.js +25 -17
  102. package/dist/screens/updatePrompt.d.ts +41 -0
  103. package/dist/selection.d.ts +75 -0
  104. package/dist/state.d.ts +26 -0
  105. package/dist/terminal/buffer.d.ts +47 -0
  106. package/dist/terminal/input.d.ts +70 -0
  107. package/dist/terminal/renderer.d.ts +43 -0
  108. package/dist/terminal/restore.d.ts +41 -0
  109. package/dist/terminal/screen.d.ts +57 -0
  110. package/dist/tray.d.ts +14 -0
  111. package/dist/ui.d.ts +123 -0
  112. package/dist/update.d.ts +71 -0
  113. package/dist/updateWatch.d.ts +43 -0
  114. package/dist/web/approvalQueue.d.ts +8 -0
  115. package/dist/web/approvalQueue.js +59 -0
  116. package/dist/web/auth.d.ts +14 -0
  117. package/dist/web/clientState.d.ts +16 -0
  118. package/dist/web/folderPicker.d.ts +34 -0
  119. package/dist/web/folderPicker.js +544 -0
  120. package/dist/web/hub.d.ts +213 -0
  121. package/dist/web/hub.js +14 -2
  122. package/dist/web/localApi.d.ts +11 -0
  123. package/dist/web/localApi.js +260 -0
  124. package/dist/web/plugins.d.ts +8 -0
  125. package/dist/web/plugins.js +64 -0
  126. package/dist/web/server.d.ts +81 -0
  127. package/dist/web/server.js +127 -19
  128. package/dist/web/service.d.ts +7 -0
  129. package/dist/web/service.js +1 -0
  130. package/dist/web/session.d.ts +176 -0
  131. package/dist/web/session.js +170 -21
  132. package/dist/web/settings.d.ts +65 -0
  133. package/dist/web/terminalBridge.d.ts +15 -0
  134. package/dist/web/terminalBridge.js +8 -2
  135. package/dist/width.d.ts +6 -0
  136. package/docs/plugins/README.md +126 -0
  137. package/docs/plugins/agent-sdk.md +92 -0
  138. package/docs/plugins/architecture.md +64 -0
  139. package/docs/plugins/compatibility.md +34 -0
  140. package/docs/plugins/development.md +55 -0
  141. package/docs/plugins/manifest.md +59 -0
  142. package/docs/plugins/manifest.schema.json +286 -0
  143. package/docs/plugins/security.md +53 -0
  144. package/docs/plugins/skills-prompts.md +69 -0
  145. package/docs/plugins/ui-sdk.md +109 -0
  146. package/examples/local-api.py +105 -0
  147. package/examples/plugins/cards/card.html +6 -0
  148. package/examples/plugins/cards/neutrino.plugin.json +42 -0
  149. package/examples/plugins/composer/editor.html +10 -0
  150. package/examples/plugins/composer/neutrino.plugin.json +10 -0
  151. package/examples/plugins/function-tools/agent.js +32 -0
  152. package/examples/plugins/function-tools/neutrino.plugin.json +15 -0
  153. package/examples/plugins/menus-tabs/neutrino.plugin.json +15 -0
  154. package/examples/plugins/menus-tabs/panel.html +6 -0
  155. package/examples/plugins/skills-prompts/neutrino.plugin.json +13 -0
  156. package/examples/plugins/skills-prompts/prompts/evidence.md +1 -0
  157. package/examples/plugins/skills-prompts/prompts/reviewer.md +1 -0
  158. package/examples/plugins/skills-prompts/skills/review/SKILL.md +8 -0
  159. package/examples/plugins/theme/neutrino.plugin.json +12 -0
  160. package/package.json +66 -42
  161. package/web/LOCAL-API.md +65 -0
  162. package/web/README.md +30 -8
  163. package/web/app.js +256 -67
  164. package/web/i18n.js +18 -3
  165. package/web/index.html +3 -1
  166. package/web/plugins.css +10 -0
  167. package/web/plugins.js +230 -0
  168. package/web/style.css +12 -3
  169. 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
+ }
@@ -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 === FOCUS_COUNT - 1) {
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 = FOCUS_COUNT - 1;
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 = FOCUS_COUNT - 1;
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 * (TABS.length - 1);
260
- const baseW = Math.floor(tabInner / TABS.length);
261
- const extra = tabInner - baseW * TABS.length;
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 < TABS.length; 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 + FOCUS_COUNT) % FOCUS_COUNT;
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 * (TABS.length - 1);
339
- const baseW = Math.floor(tabInner / TABS.length);
340
- const extra = tabInner - baseW * TABS.length;
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 < TABS.length; 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, TABS[i], selected);
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 === FOCUS_COUNT - 1 || this.hoverIdx === FOCUS_COUNT - 1);
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;
@@ -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
+ }