@xingjisoft/neutrino 0.2.1 → 0.4.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 +312 -436
- package/assets/pro/backend_output_model.txt +5 -0
- package/assets/pro/claude-module-prompt.txt +65 -0
- package/assets/pro/common.txt +9 -0
- package/assets/pro/confirm_model.txt +5 -0
- package/assets/pro/front_output_model.txt +9 -0
- package/assets/pro/genimage_model.txt +1 -0
- package/assets/pro/main_model.txt +5 -0
- package/assets/pro/model-template.json +376 -0
- package/assets/pro/modeling_model.txt +5 -0
- package/assets/pro/plan_model.txt +8 -0
- package/assets/pro/thinking_model.txt +3 -0
- package/dist/app.js +43 -7
- package/dist/bin.js +5 -2
- package/dist/browser.js +57 -0
- package/dist/config.js +179 -26
- package/dist/index.js +1 -1
- package/dist/llm/approval.js +6 -2
- package/dist/llm/client.js +197 -28
- package/dist/llm/subagent.js +37 -1
- package/dist/pro/agents.js +104 -0
- package/dist/pro/config.js +88 -0
- package/dist/pro/preflight.js +172 -0
- package/dist/screens/chat.js +54 -38
- package/dist/screens/modelSelect.js +15 -4
- package/dist/screens/settings.js +9 -7
- package/dist/screens/start.js +48 -15
- package/dist/ui.js +58 -0
- package/dist/web/hub.js +31 -4
- package/dist/web/server.js +611 -50
- package/dist/web/session.js +320 -20
- package/dist/web/settings.js +117 -24
- package/package.json +37 -37
- package/web/README.md +158 -16
- package/web/app.js +1603 -128
- package/web/i18n.js +382 -0
- package/web/index.html +289 -37
- package/web/markdown.js +710 -0
- package/web/neutrino-pro.gif +0 -0
- package/web/neutrino.png +0 -0
- package/web/pro.js +184 -0
- package/web/settings.js +322 -193
- package/web/style.css +1378 -42
package/dist/web/settings.js
CHANGED
|
@@ -1,52 +1,109 @@
|
|
|
1
|
-
import { DEFAULT_CONFIG, INTENSITY_LEVELS, contextWindow, isContextWindow, loadConfig, loadModels, resolveModelSelection, saveConfig, saveModels, } from "../config.js";
|
|
1
|
+
import { DEFAULT_CONFIG, INTENSITY_LEVELS, WEB_LANGUAGES, contextWindow, intensityForModel, isContextWindow, loadConfig, loadModels, modelContextWindow, modelContextWindowKey, modelIntensities, normalizeApprovalMode, normalizeIntensity, normalizeWebLanguage, resolveModelSelection, saveConfig, saveModels, } from "../config.js";
|
|
2
2
|
import { hashWebPassword, validateWebPassword } from "./auth.js";
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
3
|
+
import { defaultProPreset, loadProModels, proMaxRecursionDepth, requireProPreset } from "../pro/config.js";
|
|
4
|
+
/**
|
|
5
|
+
* Public settings use a whitelist: API keys and cached credentials stay on disk.
|
|
6
|
+
*
|
|
7
|
+
* `mode` selects the workspace. The Pro workspace has its own defaults,
|
|
8
|
+
* language, plan and reviewer settings; the login password and the approval
|
|
9
|
+
* policy are layered in from the shared config by loadConfig(), so the two
|
|
10
|
+
* pages cannot drift apart on those.
|
|
11
|
+
*/
|
|
12
|
+
export function getWebSettings(mode = "standard") {
|
|
13
|
+
const config = { ...DEFAULT_CONFIG, ...loadConfig(mode) };
|
|
14
|
+
const catalog = loadModels();
|
|
15
|
+
const providers = catalog.providers.map((provider) => ({
|
|
7
16
|
name: provider.name,
|
|
8
|
-
models: provider.models.map((model) => ({
|
|
17
|
+
models: provider.models.map((model) => ({
|
|
18
|
+
name: model.name,
|
|
19
|
+
configured: Boolean(model.api_key?.trim()),
|
|
20
|
+
context_window: modelContextWindow(provider.name, model, mode),
|
|
21
|
+
// The browser only offers these, so it cannot submit a level the model
|
|
22
|
+
// does not declare. Missing field = every intensity.
|
|
23
|
+
intensities: modelIntensities(model),
|
|
24
|
+
})),
|
|
9
25
|
}));
|
|
10
26
|
const preferred = providers.find((p) => p.name === config.selected_provider);
|
|
11
27
|
const provider = preferred ?? providers.find((p) => p.models.some((m) => m.configured)) ?? providers[0];
|
|
12
28
|
const model = provider?.models.find((m) => m.name === config.selected_model) ??
|
|
13
29
|
provider?.models.find((m) => m.configured) ?? provider?.models[0];
|
|
14
|
-
const
|
|
30
|
+
const configuredModel = provider && model
|
|
31
|
+
? catalog.providers.find((p) => p.name === provider.name)?.models.find((m) => m.name === model.name) ?? null
|
|
32
|
+
: null;
|
|
33
|
+
const intensity = intensityForModel(configuredModel, config.selected_intensity ?? config.default_intensity, config.default_intensity);
|
|
15
34
|
return {
|
|
35
|
+
...(mode === "pro" ? { pro: {
|
|
36
|
+
presets: loadProModels().tier.map((preset) => ({
|
|
37
|
+
name: preset.name, chinese_name: preset.chinese_name,
|
|
38
|
+
"model-setting": preset["model-setting"].map((setting) => ({
|
|
39
|
+
type: setting.type, provider: setting.provider, model: setting.model,
|
|
40
|
+
intensity: setting.intensity, context_window: setting.context_window,
|
|
41
|
+
})),
|
|
42
|
+
})),
|
|
43
|
+
preset: defaultProPreset(),
|
|
44
|
+
max_recursion_depth: proMaxRecursionDepth(),
|
|
45
|
+
} } : {}),
|
|
16
46
|
providers,
|
|
17
47
|
intensities: INTENSITY_LEVELS,
|
|
48
|
+
languages: WEB_LANGUAGES.map((value) => ({ value, label: value === "zh" ? "简体中文" : "English" })),
|
|
18
49
|
selection: {
|
|
19
50
|
provider: provider?.name ?? "",
|
|
20
51
|
model: model?.name ?? "",
|
|
21
|
-
intensity
|
|
52
|
+
intensity,
|
|
22
53
|
},
|
|
23
54
|
settings: {
|
|
24
|
-
default_intensity:
|
|
55
|
+
default_intensity: intensityForModel(configuredModel, config.default_intensity, "High"),
|
|
25
56
|
model_context_window: model?.context_window ?? contextWindow(undefined),
|
|
26
57
|
approval_model: String(config.approval_model ?? ""),
|
|
27
58
|
auto_approve_extreme_risk: config.auto_approve_extreme_risk === true,
|
|
28
59
|
sound_alerts: config.sound_alerts !== false,
|
|
60
|
+
language: normalizeWebLanguage(config.language),
|
|
61
|
+
plan_first: config.plan_first !== false,
|
|
62
|
+
approval_mode: normalizeApprovalMode(config.approval_mode),
|
|
63
|
+
web_console: config.web_console !== false,
|
|
64
|
+
web_lan: config.web_lan === true,
|
|
29
65
|
},
|
|
30
66
|
};
|
|
31
67
|
}
|
|
32
68
|
/** Validate the complete patch before writing anything, preserving unrelated settings. */
|
|
33
|
-
export function updateWebSettings(value) {
|
|
69
|
+
export function updateWebSettings(value, mode = "standard") {
|
|
34
70
|
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
35
71
|
throw new Error("invalid settings body");
|
|
36
72
|
const body = value;
|
|
37
73
|
if (Object.keys(body).some((key) => key !== "selection" && key !== "settings"))
|
|
38
74
|
throw new Error("unknown settings field");
|
|
39
|
-
const config = loadConfig();
|
|
75
|
+
const config = loadConfig(mode);
|
|
76
|
+
// Resolve the requested model first: its declared intensities decide which
|
|
77
|
+
// default level can be saved with this patch. Settings that do not touch a
|
|
78
|
+
// model stay editable even when the catalogue is empty.
|
|
79
|
+
const chosen = body.selection !== undefined ? resolveModelSelection(body.selection, false) : undefined;
|
|
80
|
+
let target = chosen ?? null;
|
|
81
|
+
const settingsTarget = () => {
|
|
82
|
+
target ??= resolveModelSelection(getWebSettings(mode).selection, false);
|
|
83
|
+
return target;
|
|
84
|
+
};
|
|
40
85
|
if (body.settings !== undefined) {
|
|
41
86
|
if (!body.settings || typeof body.settings !== "object" || Array.isArray(body.settings))
|
|
42
87
|
throw new Error("invalid settings");
|
|
43
88
|
const patch = body.settings;
|
|
44
89
|
for (const [key, val] of Object.entries(patch)) {
|
|
45
|
-
if (key === "
|
|
46
|
-
|
|
90
|
+
if (key === "pro_preset" && mode === "pro") {
|
|
91
|
+
config.pro_preset = requireProPreset(val);
|
|
92
|
+
}
|
|
93
|
+
else if (key === "pro_max_recursion_depth" && mode === "pro") {
|
|
94
|
+
if (typeof val !== "number" || !Number.isSafeInteger(val) || val < 1 || val > 100) {
|
|
95
|
+
throw new Error("Pro recursion depth must be a whole number from 1 to 100");
|
|
96
|
+
}
|
|
97
|
+
config.pro_max_recursion_depth = val;
|
|
98
|
+
}
|
|
99
|
+
else if (key === "default_intensity") {
|
|
100
|
+
const level = normalizeIntensity(val);
|
|
101
|
+
if (!level)
|
|
47
102
|
throw new Error("invalid default intensity");
|
|
48
|
-
|
|
49
|
-
|
|
103
|
+
if (!modelIntensities(settingsTarget().model).includes(level))
|
|
104
|
+
throw new Error("this model does not support that thinking intensity");
|
|
105
|
+
config.default_intensity = level;
|
|
106
|
+
config.selected_intensity = level;
|
|
50
107
|
}
|
|
51
108
|
else if (key === "model_context_window") {
|
|
52
109
|
if (!isContextWindow(val))
|
|
@@ -67,6 +124,32 @@ export function updateWebSettings(value) {
|
|
|
67
124
|
throw new Error("auto-approval must be a boolean");
|
|
68
125
|
config[key] = val;
|
|
69
126
|
}
|
|
127
|
+
else if (key === "language") {
|
|
128
|
+
if (typeof val !== "string" || !WEB_LANGUAGES.includes(val)) {
|
|
129
|
+
throw new Error("invalid language");
|
|
130
|
+
}
|
|
131
|
+
config.language = val;
|
|
132
|
+
}
|
|
133
|
+
else if (key === "plan_first") {
|
|
134
|
+
if (typeof val !== "boolean")
|
|
135
|
+
throw new Error("invalid plan setting");
|
|
136
|
+
config.plan_first = val;
|
|
137
|
+
}
|
|
138
|
+
else if (key === "approval_mode") {
|
|
139
|
+
if (val !== "request" && val !== "auto")
|
|
140
|
+
throw new Error("invalid approval mode");
|
|
141
|
+
config.approval_mode = val;
|
|
142
|
+
}
|
|
143
|
+
else if (key === "web_console") {
|
|
144
|
+
if (typeof val !== "boolean")
|
|
145
|
+
throw new Error("web console must be a boolean");
|
|
146
|
+
config.web_console = val;
|
|
147
|
+
}
|
|
148
|
+
else if (key === "web_lan") {
|
|
149
|
+
if (typeof val !== "boolean")
|
|
150
|
+
throw new Error("LAN access must be a boolean");
|
|
151
|
+
config.web_lan = val;
|
|
152
|
+
}
|
|
70
153
|
else if (key === "web_password") {
|
|
71
154
|
validateWebPassword(val);
|
|
72
155
|
}
|
|
@@ -75,8 +158,7 @@ export function updateWebSettings(value) {
|
|
|
75
158
|
}
|
|
76
159
|
}
|
|
77
160
|
}
|
|
78
|
-
if (
|
|
79
|
-
const chosen = resolveModelSelection(body.selection, false);
|
|
161
|
+
if (chosen) {
|
|
80
162
|
config.selected_provider = chosen.provider;
|
|
81
163
|
config.selected_model = chosen.model.name;
|
|
82
164
|
config.selected_model_url = chosen.model.url;
|
|
@@ -90,12 +172,23 @@ export function updateWebSettings(value) {
|
|
|
90
172
|
config.web_password_hash = hashWebPassword(password);
|
|
91
173
|
const window = body.settings?.model_context_window;
|
|
92
174
|
if (window !== undefined) {
|
|
93
|
-
const
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
175
|
+
const selected = settingsTarget();
|
|
176
|
+
if (mode === "pro") {
|
|
177
|
+
// Context capacity is part of a workspace's compression settings. Pro
|
|
178
|
+
// stores its own copy instead of editing the shared model catalog.
|
|
179
|
+
const current = config.model_context_windows;
|
|
180
|
+
const overrides = current && typeof current === "object" && !Array.isArray(current)
|
|
181
|
+
? { ...current }
|
|
182
|
+
: {};
|
|
183
|
+
overrides[modelContextWindowKey(selected.provider, selected.model.name)] = window;
|
|
184
|
+
config.model_context_windows = overrides;
|
|
185
|
+
}
|
|
186
|
+
else {
|
|
187
|
+
const models = loadModels();
|
|
188
|
+
const model = models.providers.find((p) => p.name === selected.provider).models.find((m) => m.name === selected.model.name);
|
|
189
|
+
model.context_window = window;
|
|
190
|
+
saveModels(models);
|
|
191
|
+
}
|
|
99
192
|
}
|
|
100
|
-
saveConfig(config);
|
|
193
|
+
saveConfig(config, mode);
|
|
101
194
|
}
|
package/package.json
CHANGED
|
@@ -1,37 +1,37 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@xingjisoft/neutrino",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "XINGJI Neutrino - a CLI coding agent in the terminal.",
|
|
5
|
-
"type": "module",
|
|
6
|
-
"license": "Apache-2.0",
|
|
7
|
-
"bin": {
|
|
8
|
-
"neutrino": "dist/bin.js"
|
|
9
|
-
},
|
|
10
|
-
"main": "dist/app.js",
|
|
11
|
-
"files": [
|
|
12
|
-
"dist",
|
|
13
|
-
"assets",
|
|
14
|
-
"web"
|
|
15
|
-
],
|
|
16
|
-
"engines": {
|
|
17
|
-
"node": ">=18.0.0"
|
|
18
|
-
},
|
|
19
|
-
"scripts": {
|
|
20
|
-
"build": "tsc -p tsconfig.json",
|
|
21
|
-
"test": "npm run build && node scripts/test-paste.mjs && node scripts/test-mouse.mjs && node scripts/test-cursor.mjs && node scripts/test-selection.mjs && node scripts/test-drag-scroll.mjs && node scripts/test-selection-performance.mjs && node scripts/test-copy.mjs && node scripts/test-scrollbar.mjs && node scripts/test-render.mjs && node scripts/test-drift.mjs && node scripts/test-sanitize.mjs && node scripts/test-markdown.mjs && node scripts/test-wrap.mjs && node scripts/test-fetch.mjs && node scripts/test-compact.mjs && node scripts/test-history-fold.mjs && node scripts/test-web-history.mjs && node scripts/test-web-default.mjs && node scripts/test-turn-fold.mjs && node scripts/test-plan.mjs && node scripts/test-subagent.mjs && node scripts/test-web-settings.mjs && node scripts/test-tools.mjs && node scripts/test-approval.mjs && node scripts/test-approval-review.mjs && node scripts/test-approval-ui.mjs && node scripts/test-web-approval-details.mjs && node scripts/test-approval-mode.mjs && node scripts/test-approval-context.mjs && node scripts/test-settings-render.mjs && node scripts/test-approval-flow.mjs && node scripts/test-web.mjs && node scripts/test-web-settings-http.mjs && node scripts/test-web-auth.mjs && node scripts/test-web-auth-ui.mjs && node scripts/test-interrupt.mjs && node scripts/test-terminal-lifecycle.mjs && node scripts/test-persistence.mjs && node scripts/test-resilience.mjs && node scripts/test-approval-timeout.mjs && node scripts/test-approval-hardening.mjs && node scripts/test-approval-extreme.mjs && node scripts/test-approval-reason.mjs",
|
|
22
|
-
"prepare": "npm run build",
|
|
23
|
-
"web": "node dist/bin.js web"
|
|
24
|
-
},
|
|
25
|
-
"keywords": [
|
|
26
|
-
"cli",
|
|
27
|
-
"agent",
|
|
28
|
-
"ai",
|
|
29
|
-
"terminal",
|
|
30
|
-
"llm",
|
|
31
|
-
"coding"
|
|
32
|
-
],
|
|
33
|
-
"devDependencies": {
|
|
34
|
-
"@types/node": "^22.0.0",
|
|
35
|
-
"typescript": "^5.5.0"
|
|
36
|
-
}
|
|
37
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "@xingjisoft/neutrino",
|
|
3
|
+
"version": "0.4.0",
|
|
4
|
+
"description": "XINGJI Neutrino - a CLI coding agent in the terminal.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "Apache-2.0",
|
|
7
|
+
"bin": {
|
|
8
|
+
"neutrino": "dist/bin.js"
|
|
9
|
+
},
|
|
10
|
+
"main": "dist/app.js",
|
|
11
|
+
"files": [
|
|
12
|
+
"dist",
|
|
13
|
+
"assets",
|
|
14
|
+
"web"
|
|
15
|
+
],
|
|
16
|
+
"engines": {
|
|
17
|
+
"node": ">=18.0.0"
|
|
18
|
+
},
|
|
19
|
+
"scripts": {
|
|
20
|
+
"build": "tsc -p tsconfig.json",
|
|
21
|
+
"test": "npm run build && node scripts/test-paste.mjs && node scripts/test-mouse.mjs && node scripts/test-cursor.mjs && node scripts/test-selection.mjs && node scripts/test-drag-scroll.mjs && node scripts/test-selection-performance.mjs && node scripts/test-copy.mjs && node scripts/test-scrollbar.mjs && node scripts/test-render.mjs && node scripts/test-drift.mjs && node scripts/test-sanitize.mjs && node scripts/test-markdown.mjs && node scripts/test-wrap.mjs && node scripts/test-fetch.mjs && node scripts/test-compact.mjs && node scripts/test-history-fold.mjs && node scripts/test-web-history.mjs && node scripts/test-web-default.mjs && node scripts/test-turn-fold.mjs && node scripts/test-plan.mjs && node scripts/test-subagent.mjs && node scripts/test-web-settings.mjs && node scripts/test-model-intensity.mjs && node scripts/test-web-markdown.mjs && node scripts/test-web-i18n.mjs && node scripts/test-web-workspaces.mjs && node scripts/test-tools.mjs && node scripts/test-approval.mjs && node scripts/test-approval-review.mjs && node scripts/test-approval-ui.mjs && node scripts/test-web-approval-details.mjs && node scripts/test-approval-mode.mjs && node scripts/test-approval-context.mjs && node scripts/test-settings-render.mjs && node scripts/test-approval-flow.mjs && node scripts/test-web.mjs && node scripts/test-web-settings-http.mjs && node scripts/test-web-lan.mjs && node scripts/test-web-auth.mjs && node scripts/test-web-auth-ui.mjs && node scripts/test-web-pro.mjs && node scripts/test-pro-preflight.mjs && node scripts/test-pro-runtime.mjs && node scripts/test-pro-team.mjs && node scripts/test-pro-ui.mjs && node scripts/test-interrupt.mjs && node scripts/test-terminal-lifecycle.mjs && node scripts/test-persistence.mjs && node scripts/test-resilience.mjs && node scripts/test-approval-timeout.mjs && node scripts/test-approval-hardening.mjs && node scripts/test-approval-extreme.mjs && node scripts/test-approval-reason.mjs",
|
|
22
|
+
"prepare": "npm run build",
|
|
23
|
+
"web": "node dist/bin.js web"
|
|
24
|
+
},
|
|
25
|
+
"keywords": [
|
|
26
|
+
"cli",
|
|
27
|
+
"agent",
|
|
28
|
+
"ai",
|
|
29
|
+
"terminal",
|
|
30
|
+
"llm",
|
|
31
|
+
"coding"
|
|
32
|
+
],
|
|
33
|
+
"devDependencies": {
|
|
34
|
+
"@types/node": "^22.0.0",
|
|
35
|
+
"typescript": "^5.5.0"
|
|
36
|
+
}
|
|
37
|
+
}
|
package/web/README.md
CHANGED
|
@@ -8,8 +8,18 @@ A small, dependency-free single page app that is served by the Neutrino CLI at
|
|
|
8
8
|
- watch a running terminal conversation stream in real time: the work behind every
|
|
9
9
|
finished turn folds into one line that opens on a click, while the reply itself
|
|
10
10
|
stays on screen (the turn being answered is open, and a pause is not a fold);
|
|
11
|
+
- read replies as rendered markdown: headings, nested/task lists, tables, block
|
|
12
|
+
quotes, code fences, emphasis, safe links and images; raw HTML is escaped;
|
|
13
|
+
- start a new conversation in a working directory you choose, including a folder
|
|
14
|
+
created from the built-in folder picker;
|
|
15
|
+
- switch the workspace between every known folder and **All directories**;
|
|
11
16
|
- switch the provider, model and intensity for the next turn;
|
|
12
|
-
- edit
|
|
17
|
+
- edit every terminal setting — defaults, interface language, plan-first turns,
|
|
18
|
+
default approval mode, context compression, reviewer, Web password, and LAN
|
|
19
|
+
access;
|
|
20
|
+
- switch the interface between English and 简体中文, instantly and persistently;
|
|
21
|
+
- open the same plan / token card as the terminal's `ⓘ`, clear a conversation,
|
|
22
|
+
delete a saved transcript, and review other agents' read-only history;
|
|
13
23
|
- drive the running conversation: pause / resume the current turn and send new
|
|
14
24
|
messages (they queue while the agent is busy);
|
|
15
25
|
- pick up a saved conversation in the browser and keep chatting, even when no
|
|
@@ -26,7 +36,9 @@ neutrino
|
|
|
26
36
|
```
|
|
27
37
|
|
|
28
38
|
Use `--port` to move it, `--no-web` to skip it for that run, or turn it off
|
|
29
|
-
permanently in the terminal's **Settings → Web console**.
|
|
39
|
+
permanently in the terminal's **Settings → Web console**. LAN access can be
|
|
40
|
+
switched on and off at any time in the browser's **Settings → Web access**
|
|
41
|
+
without restarting anything.
|
|
30
42
|
|
|
31
43
|
Or serve only the saved history, without a terminal:
|
|
32
44
|
|
|
@@ -49,6 +61,17 @@ neutrino --host 100.64.0.7 # TUI + web UI, one specific addres
|
|
|
49
61
|
neutrino web --port 8765 --lan # headless web UI, every interface
|
|
50
62
|
```
|
|
51
63
|
|
|
64
|
+
You can also switch this at runtime from the browser: **Settings → Web access →
|
|
65
|
+
Allow access from other devices (LAN, 0.0.0.0)**. The server re-binds the same
|
|
66
|
+
port to `0.0.0.0` immediately — no restart, and the tab you are using keeps its
|
|
67
|
+
open connections — and saves the choice in `config.json`, so the next launch
|
|
68
|
+
starts on the LAN too. Turning the switch off returns the console to
|
|
69
|
+
`127.0.0.1`. An explicit `--host`/`--lan` decides the interface at startup; the
|
|
70
|
+
in-page switch takes over from there for the running process.
|
|
71
|
+
|
|
72
|
+
The console can read every transcript and drive an agent with shell access, so
|
|
73
|
+
keep the Web password on: anyone who can reach the port can use it.
|
|
74
|
+
|
|
52
75
|
Startup prints the addresses to open:
|
|
53
76
|
|
|
54
77
|
```
|
|
@@ -99,6 +122,61 @@ headers, token URLs, `--token` and `NEUTRINO_WEB_TOKEN` are no longer used.
|
|
|
99
122
|
Static files and the login/logout routes stay public. `--no-auth` explicitly
|
|
100
123
|
disables password login.
|
|
101
124
|
|
|
125
|
+
## Pro workspace
|
|
126
|
+
|
|
127
|
+
The composer has a preset card at its bottom-right. Open it and drag the slider
|
|
128
|
+
to select a team. There are eight roles: main, reasoning, frontend, backend,
|
|
129
|
+
approval, planning, modeling and reserved image generation. Presets are copied
|
|
130
|
+
unchanged from the supplied example into `~/.neutrino/pro/model.json` on first
|
|
131
|
+
use. Edit role assignments there; **Settings** controls the default preset and
|
|
132
|
+
maximum delegation depth (10 by default).
|
|
133
|
+
|
|
134
|
+
Every task checks all active models with a small completion request. Unavailable
|
|
135
|
+
roles recurse down to lower presets independently, and a popup shows the
|
|
136
|
+
replacements. These changes are task-local. The planner then writes the plan
|
|
137
|
+
before main execution; nested specialist calls and commands use the same
|
|
138
|
+
approval gate, with the approval role providing automated reviews.
|
|
139
|
+
|
|
140
|
+
Pro API additions: settings return `pro.presets`, `pro.preset` and
|
|
141
|
+
`pro.max_recursion_depth`; settings patches accept `pro_preset` and
|
|
142
|
+
`pro_max_recursion_depth`. New and resumed conversations accept `pro_preset`.
|
|
143
|
+
`POST /pro/api/live/:key/pro-preset` with `{"preset":"medium"}` changes an idle
|
|
144
|
+
or paused conversation. Snapshots include `pro.checking`, effective `pro.models`,
|
|
145
|
+
`pro.fallbacks`, `pro.checkId` and any `pro.error`.
|
|
146
|
+
|
|
147
|
+
The console has two workspaces. The regular one lives at `/`; the isolated **Pro** workspace lives at `/pro`. Both use the same page, the
|
|
148
|
+
same login cookie and the same model catalog, but nothing else is shared:
|
|
149
|
+
|
|
150
|
+
- **Conversations** — Pro transcripts are saved under
|
|
151
|
+
`~/.neutrino/pro/session/`, so neither sidebar ever lists the other
|
|
152
|
+
workspace's history. Live sessions are separated too: a Pro tab cannot read,
|
|
153
|
+
stream, drive or close a regular session, and vice versa.
|
|
154
|
+
- **Settings** — Pro has its own `~/.neutrino/pro/config.json` with its own
|
|
155
|
+
language, sound alerts, default preset, delegation depth and recent folders.
|
|
156
|
+
`~/.neutrino/pro/model.json` records role models, effort and context capacities;
|
|
157
|
+
provider credentials stay in the shared ordinary model catalog.
|
|
158
|
+
- **Shared on purpose** — the Web password, the approval policy (default
|
|
159
|
+
approval mode, extreme-risk auto-approval) and the console's LAN access. They
|
|
160
|
+
protect or configure the whole machine, so both workspaces, the terminal and
|
|
161
|
+
every browser read and write the same values.
|
|
162
|
+
|
|
163
|
+
In the regular console, click the orbit badge to the left of the **Neutrino**
|
|
164
|
+
title (or the blue hint pointing at it). A `#151515` curtain fades in and plays
|
|
165
|
+
the `neutrino-pro.gif` wordmark in the centre; after three seconds it fades out
|
|
166
|
+
onto `/pro`. The X on the blue hint dismisses it permanently for this browser.
|
|
167
|
+
In the terminal, the **启用Pro模式** box below the status bar starts the Web
|
|
168
|
+
console if necessary and opens `/pro` in the default browser. Pro mode uses the
|
|
169
|
+
static `web/neutrino.png` wordmark as its top-left logo, renames the sidebar
|
|
170
|
+
brand to **Neutrino Pro**, and puts an **Exit Pro mode / 返回普通模式** button
|
|
171
|
+
at the top-right of the workspace, which returns to the regular console at
|
|
172
|
+
`/`. The rest of the workspace is identical to the regular one. The interface
|
|
173
|
+
is available in English and 简体中文 in both workspaces (each remembers its own
|
|
174
|
+
choice).
|
|
175
|
+
|
|
176
|
+
Every API route below has an isolated twin under `/pro/api/` — for example,
|
|
177
|
+
`/pro/api/overview`, `/pro/api/settings` and `/pro/api/live/:key/stream`. The
|
|
178
|
+
login/logout routes are shared and stay unprefixed.
|
|
179
|
+
|
|
102
180
|
## Models and settings
|
|
103
181
|
|
|
104
182
|
Open a conversation to see the **Provider**, **Model** and **Thinking**
|
|
@@ -112,13 +190,47 @@ selection. Changing the selectors and then using **Continue here** or sending a
|
|
|
112
190
|
message also uses the draft selection, so a transcript whose old model was
|
|
113
191
|
removed can still be continued.
|
|
114
192
|
|
|
115
|
-
The **Settings** (gear) button in the sidebar opens
|
|
116
|
-
the
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
193
|
+
The **Settings** (gear) button in the sidebar opens the interface language,
|
|
194
|
+
plan-first turns, the default approval mode, defaults for new conversations, the
|
|
195
|
+
context compression threshold, the approval reviewer model, sound alerts, the
|
|
196
|
+
extreme-risk automatic approval option, LAN access, and the Web password. Save
|
|
197
|
+
settings explicitly; cancelling leaves the saved values intact. Conversation
|
|
198
|
+
model changes are separate from these defaults. Models come from the same
|
|
199
|
+
catalog as the terminal, and API keys stay on the server. Configure missing keys
|
|
200
|
+
in the terminal model screen first. Intensity uses the same Low–Ultra sampling
|
|
201
|
+
settings as the terminal, and both only offer the levels the selected model
|
|
202
|
+
declares in `model.json`. The regular and [Pro](#pro-workspace) workspaces keep
|
|
203
|
+
their own copy of every setting here except the approval policy, the Web password
|
|
204
|
+
and LAN access, which belong to the whole process.
|
|
205
|
+
|
|
206
|
+
## New conversations and working directories
|
|
207
|
+
|
|
208
|
+
**New conversation** starts a chat that has no transcript yet. The dialog takes a
|
|
209
|
+
working directory plus provider, model, thinking level and approval mode. The
|
|
210
|
+
conversation is browser-owned from that moment: it appears in the sidebar under
|
|
211
|
+
its folder and is written to its own session file once the first message exists.
|
|
212
|
+
|
|
213
|
+
**Browse…** opens the server-side folder picker. It lists folders only, offers
|
|
214
|
+
the existing drive roots (on Windows) or `/`, moves up through the directory
|
|
215
|
+
tree, and can create a folder in place. **Use this folder** fills the working
|
|
216
|
+
directory of the new conversation.
|
|
217
|
+
|
|
218
|
+
**Working directories** (the folder button under New conversation) shows every
|
|
219
|
+
folder the console has opened or that already holds conversations. Choosing one
|
|
220
|
+
scopes the sidebar to that folder and shows a landing page where typing starts a
|
|
221
|
+
conversation there. **All directories** returns to the grouped view. Opened
|
|
222
|
+
folders are stored in `config.json` under `web_workspaces`, so an empty folder
|
|
223
|
+
that has no conversations yet is still listed after a restart.
|
|
224
|
+
|
|
225
|
+
## Interface language
|
|
226
|
+
|
|
227
|
+
The sidebar footer and **Settings → Interface** both offer English and 简体中文.
|
|
228
|
+
The change is applied to the already-open page (labels, messages, status line,
|
|
229
|
+
plan card and dialogs) without a reload, is stored in the workspace's
|
|
230
|
+
`config.json`, and is shared by every browser showing that workspace. The
|
|
231
|
+
regular and Pro workspaces remember their own choice. The terminal UI stays in
|
|
232
|
+
English. New conversation content, of course, remains in the language the model
|
|
233
|
+
writes.
|
|
122
234
|
|
|
123
235
|
## History vs. live sessions
|
|
124
236
|
|
|
@@ -144,6 +256,22 @@ Browser-owned sessions are dropped after 30 idle minutes, or immediately through
|
|
|
144
256
|
`POST /api/live/:key/close`. Sessions owned by the terminal cannot be closed from
|
|
145
257
|
the web.
|
|
146
258
|
|
|
259
|
+
**Clear** empties the running conversation in place (the previous transcript is
|
|
260
|
+
checkpointed first and stays on disk), mirroring `Ctrl+L` in the terminal. A
|
|
261
|
+
read-only transcript gets **Delete** instead, which removes the saved JSON file
|
|
262
|
+
after confirmation; a conversation that is currently live cannot be deleted.
|
|
263
|
+
Sessions from the terminal's `Ctrl+L` also appear as fresh live conversations in
|
|
264
|
+
the sidebar.
|
|
265
|
+
|
|
266
|
+
## Other agents' history
|
|
267
|
+
|
|
268
|
+
**Other agents** in the sidebar lists read-only sessions recorded by Claude Code,
|
|
269
|
+
Codex and OpenCode for the current working directory (the scoped workspace, the
|
|
270
|
+
open conversation's folder, or the server's launch directory). Clicking one shows
|
|
271
|
+
the complete transcript in the conversation pane with a `read-only` badge. The
|
|
272
|
+
sources are the same as the terminal's history browser; OpenCode requires Node.js
|
|
273
|
+
22.13+ because it reads its SQLite database through `node:sqlite`.
|
|
274
|
+
|
|
147
275
|
## Plan-first turns
|
|
148
276
|
|
|
149
277
|
A browser-owned turn is planned before it runs, exactly like a turn in the
|
|
@@ -169,8 +297,10 @@ needed by a client:
|
|
|
169
297
|
|
|
170
298
|
`status` is `pending`, `active`, `done` or `failed`; `stopped: true` marks a plan
|
|
171
299
|
whose turn was paused before the last step. The terminal draws this in its `ⓘ`
|
|
172
|
-
card
|
|
173
|
-
|
|
300
|
+
card, and the web console renders the same timeline in its `ⓘ` plan card: the
|
|
301
|
+
active step is highlighted, finished steps are ticked, failed steps are red, and
|
|
302
|
+
the footer counts the steps completed. The card also shows token usage when no
|
|
303
|
+
plan is running.
|
|
174
304
|
|
|
175
305
|
Planning is the default for both loops. It can be turned off with
|
|
176
306
|
`"plan_first": false` in `~/.neutrino/config.json` or `NEUTRINO_PLAN=off`; a
|
|
@@ -193,21 +323,31 @@ to know whether a conversation is driven by the terminal or by the server.
|
|
|
193
323
|
| --- | --- | --- |
|
|
194
324
|
| GET | `/api/overview` | Sessions grouped by cwd + live sessions |
|
|
195
325
|
| GET | `/api/sessions` | Flat list of saved sessions |
|
|
196
|
-
| GET | `/api/
|
|
197
|
-
| POST | `/api/
|
|
326
|
+
| GET | `/api/workspaces` | Recent working directories, launch cwd and home |
|
|
327
|
+
| POST | `/api/workspaces` | Validate and remember `{ "path": "…" }` |
|
|
328
|
+
| GET | `/api/fs?path=…` | Folder listing for the picker (folders only, roots, parent) |
|
|
329
|
+
| POST | `/api/fs/mkdir` | Create `{ "path": "…" }` and return the new path |
|
|
330
|
+
| POST | `/api/conversations` | Start `{ cwd, selection?, approval_mode? }`, returns `{ key, snapshot }` |
|
|
331
|
+
| GET | `/api/settings` | Model catalog (each model lists its supported intensities), languages, public settings, and the live bind address (`web_host`, `web_urls`); no credentials |
|
|
332
|
+
| POST | `/api/settings` | Save validated `{ selection?, settings? }`, including `language`, `plan_first`, `approval_mode`, `web_lan` and `web_password` |
|
|
198
333
|
| GET | `/api/session/:id` | One saved transcript |
|
|
199
334
|
| POST | `/api/session/:id/continue` | Resume a transcript with optional `{ selection }`, returns `{ key }` |
|
|
335
|
+
| POST | `/api/session/:id/delete` | Delete a saved, non-running transcript |
|
|
336
|
+
| GET | `/api/agents/:agent/sessions?cwd=…` | Read-only sessions from Claude Code / Codex / OpenCode |
|
|
337
|
+
| GET | `/api/agents/:agent/sessions/:id` | Read-only messages of one other-agent session |
|
|
200
338
|
| GET | `/api/live/:key` | Snapshot of a live session |
|
|
201
339
|
| GET | `/api/live/:key/stream` | SSE stream of a live session |
|
|
202
340
|
| POST | `/api/live/:key/pause` | Pause the running turn |
|
|
203
341
|
| POST | `/api/live/:key/resume` | Resume a paused conversation |
|
|
204
342
|
| POST | `/api/live/:key/message` | Send `{ "text": "…" }` |
|
|
205
343
|
| POST | `/api/live/:key/model` | Apply `{ provider, model, intensity }` while idle or paused |
|
|
344
|
+
| POST | `/api/live/:key/clear` | Empty the conversation in place; keeps the old transcript |
|
|
345
|
+
| POST | `/api/live/:key/approve` | Answer `{ "approved": true|false }` |
|
|
346
|
+
| POST | `/api/live/:key/approval-mode` | Set `{ "mode": "request"|"auto" }` |
|
|
206
347
|
| POST | `/api/live/:key/close` | Dispose a browser-owned session |
|
|
207
348
|
| GET | `/api/health` | Liveness probe |
|
|
208
349
|
| POST | `/api/auth/login` | Sign in with `{ "password": "…" }` |
|
|
209
350
|
| POST | `/api/auth/logout` | Clear the login cookie |
|
|
210
|
-
| GET / POST | `/api/settings` | Read settings or update them, including `settings.web_password` |
|
|
211
351
|
|
|
212
352
|
Conversation and settings routes require the login cookie; see
|
|
213
353
|
[Authentication](#authentication). `OPTIONS` is answered unauthenticated so
|
|
@@ -219,8 +359,10 @@ When a running agent wants to do something risky (a delete-class command, an
|
|
|
219
359
|
install, a path outside the working directory) the browser shows an orange
|
|
220
360
|
**Request Approval** banner above the composer, showing the full command (or
|
|
221
361
|
formatted tool arguments), working directory and risk reasons, plus two buttons.
|
|
222
|
-
|
|
223
|
-
|
|
362
|
+
The banner only appears when a human decision is actually waiting. Long proposals
|
|
363
|
+
remain complete and scroll inside the card.
|
|
364
|
+
Automatic reviews are reported in the conversation's status badge instead
|
|
365
|
+
(`reviewing round 2/3`), so auto approval never looks like a request for approval.
|
|
224
366
|
`Approve` runs the call, `Deny` returns a note to the agent.
|
|
225
367
|
The reviewer uses the current turn's captured user request and earlier user
|
|
226
368
|
messages; queued input belongs to the next turn. Expand a denied tool result
|