@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.
Files changed (43) hide show
  1. package/README.md +312 -436
  2. package/assets/pro/backend_output_model.txt +5 -0
  3. package/assets/pro/claude-module-prompt.txt +65 -0
  4. package/assets/pro/common.txt +9 -0
  5. package/assets/pro/confirm_model.txt +5 -0
  6. package/assets/pro/front_output_model.txt +9 -0
  7. package/assets/pro/genimage_model.txt +1 -0
  8. package/assets/pro/main_model.txt +5 -0
  9. package/assets/pro/model-template.json +376 -0
  10. package/assets/pro/modeling_model.txt +5 -0
  11. package/assets/pro/plan_model.txt +8 -0
  12. package/assets/pro/thinking_model.txt +3 -0
  13. package/dist/app.js +43 -7
  14. package/dist/bin.js +5 -2
  15. package/dist/browser.js +57 -0
  16. package/dist/config.js +179 -26
  17. package/dist/index.js +1 -1
  18. package/dist/llm/approval.js +6 -2
  19. package/dist/llm/client.js +197 -28
  20. package/dist/llm/subagent.js +37 -1
  21. package/dist/pro/agents.js +104 -0
  22. package/dist/pro/config.js +88 -0
  23. package/dist/pro/preflight.js +172 -0
  24. package/dist/screens/chat.js +54 -38
  25. package/dist/screens/modelSelect.js +15 -4
  26. package/dist/screens/settings.js +9 -7
  27. package/dist/screens/start.js +48 -15
  28. package/dist/ui.js +58 -0
  29. package/dist/web/hub.js +31 -4
  30. package/dist/web/server.js +611 -50
  31. package/dist/web/session.js +320 -20
  32. package/dist/web/settings.js +117 -24
  33. package/package.json +37 -37
  34. package/web/README.md +158 -16
  35. package/web/app.js +1603 -128
  36. package/web/i18n.js +382 -0
  37. package/web/index.html +289 -37
  38. package/web/markdown.js +710 -0
  39. package/web/neutrino-pro.gif +0 -0
  40. package/web/neutrino.png +0 -0
  41. package/web/pro.js +184 -0
  42. package/web/settings.js +322 -193
  43. package/web/style.css +1378 -42
@@ -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
- /** Public settings use a whitelist: API keys and cached credentials stay on disk. */
4
- export function getWebSettings() {
5
- const config = { ...DEFAULT_CONFIG, ...loadConfig() };
6
- const providers = loadModels().providers.map((provider) => ({
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) => ({ name: model.name, configured: Boolean(model.api_key?.trim()), context_window: contextWindow(model.context_window) })),
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 intensity = String(config.selected_intensity ?? config.default_intensity);
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: INTENSITY_LEVELS.includes(intensity) ? intensity : "High",
52
+ intensity,
22
53
  },
23
54
  settings: {
24
- default_intensity: String(config.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 === "default_intensity") {
46
- if (typeof val !== "string" || !INTENSITY_LEVELS.includes(val))
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
- config.default_intensity = val;
49
- config.selected_intensity = val;
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 (body.selection !== undefined) {
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 selection = body.selection ?? getWebSettings().selection;
94
- const chosen = resolveModelSelection(selection, false);
95
- const models = loadModels();
96
- const model = models.providers.find((p) => p.name === chosen.provider).models.find((m) => m.name === chosen.model.name);
97
- model.context_window = window;
98
- saveModels(models);
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.2.1",
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 defaults, context compression and approval settings in a Settings dialog;
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 defaults for new conversations,
116
- the context compression threshold, the approval reviewer model, and the
117
- extreme-risk automatic approval option. Save settings explicitly; cancelling
118
- leaves the saved values intact. Conversation model changes are separate from
119
- these defaults. Models come from the same catalog as the terminal, and API keys
120
- stay on the server. Configure missing keys in the terminal model screen first.
121
- Intensity uses the same Low–Ultra sampling settings as the terminal.
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; a browser client can render the same timeline from the snapshot. The page
173
- itself currently shows the plan as the message it already is.
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/settings` | Model catalog and public settings; no credentials |
197
- | POST | `/api/settings` | Save validated `{ selection?, settings? }` defaults |
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
- Long proposals remain complete and scroll inside the card. Automatic reviews
223
- show the same proposal details while they run.
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