@xingjisoft/neutrino 0.1.7 → 0.2.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 CHANGED
@@ -17,10 +17,12 @@ A CLI coding agent that lives in your terminal. Neutrino gives you an interactiv
17
17
  - **Session history** — messages are saved before a turn starts, streamed replies are checkpointed every second, and tool calls/results are saved immediately. The history screen lists conversations started in the current working directory. Failed saves are shown in the terminal and browser UI.
18
18
  - **Auto-folding turns** — once a turn finishes, the work behind the reply (the thinking and the tool calls) folds into one summary line in both the terminal and the browser, while the answer itself always stays on screen. Click the line to read the whole round again; a turn with little to hide, or a long answer on its own, stays open. A turn stopped with `ESC ESC` is paused, not finished, so it stays open. Nothing is dropped: the transcript, the saved session and the model's context keep every message.
19
19
  - **Complete conversation history** — context compression keeps every original message and tool result on disk. The earlier portion starts collapsed in the terminal and browser; click **Earlier conversation** to expand it, including in reopened history. Model requests use a separate summary plus recent messages.
20
- - **Token usage stats** — click the `ⓘ` in the chat header to see prompt/completion/total token counts for the current session.
20
+ - **Plan-first turns** — before a request is answered, a planner agent splits it into ordered, independent steps ("model the case from box.png", "animate the camera: push in, orbit, pull back"), and the worker then executes those steps one at a time, top to bottom. Every step is a concrete deliverable, so later work is explicit instead of one vague instruction. See [Plan-first turns](#plan-first-turns).
21
+ - **The step card** — click `ⓘ` in the chat header (or press `Ctrl+P`) to open the info card in the top-right corner. It lists the plan as a vertical timeline of nodes: the step being worked on is highlighted, steps still to come are lit, and finished steps are drawn dark with a tick. The same card shows prompt/completion/total token counts when no plan is running.
22
+ - **Sub-agents** — independent work can be handed to a sub-agent: a second agent loop with its own context, its own tool calls and the same approval gate. Only its report comes back, so a survey that reads thirty files does not fill the conversation with thirty files. See [Sub-agents](#sub-agents).
21
23
  - **Agent history browser** — view past sessions from other CLI agents (Claude Code, Codex, OpenCode) directly from the history screen.
22
24
  - **Approval gate** — risky commands and paths outside the working directory need your sign-off. The header turns orange and reads **Request Approval**; press `Y` or `N`. Delete-class commands always ask, unless you explicitly granted full trust, and there is an opt-in reviewer agent for automatic decisions.
23
- - **Browser UI on any port** — mount a local website that lists every saved conversation grouped by working directory, streams the running one in real time, and lets you pause/resume it, send messages, switch provider/model/intensity, and edit settings from the browser.
25
+ - **Browser UI on by default** — every `neutrino` launch also mounts a local website at <http://127.0.0.1:8765> that lists every saved conversation grouped by working directory, streams the running one in real time, and lets you pause/resume it, send messages, switch provider/model/intensity, and edit settings from the browser. Use `--port`/`--host` to move it, `--lan` to share it, or `--no-web` to keep the terminal only.
24
26
 
25
27
  ## Requirements
26
28
 
@@ -50,6 +52,8 @@ neutrino
50
52
  ```
51
53
 
52
54
  Type a message on the start screen and press `Enter` to begin a new chat.
55
+ The browser UI is mounted at <http://127.0.0.1:8765> at the same time; see
56
+ [Web UI](#web-ui) below for `--port`, `--lan` and `--no-web`.
53
57
 
54
58
  ### Keys
55
59
 
@@ -66,7 +70,8 @@ Type a message on the start screen and press `Enter` to begin a new chat.
66
70
  | `Ctrl+C` | Do nothing while text is selected; otherwise pause the running conversation or show the exit hint at the top |
67
71
  | `Ctrl+C` × 3 | Quit after three consecutive presses, no more than 1.5 seconds apart (another key, paste, click or scroll resets the count) |
68
72
  | `Ctrl+Shift+C` | Quit immediately (requires the terminal to forward the shortcut) |
69
- | `Ctrl+A` | Toggle Request Approval / Auto Approval in a conversation |
73
+ | `Ctrl+A` | Toggle Request Approval / Auto Approval in a conversation |
74
+ | `Ctrl+P` | Toggle the plan / token info card in the top-right corner |
70
75
  | `Y` | Approve the pending request (while the approval card is up) |
71
76
  | `N` / `Esc` | Deny the pending request |
72
77
  | `Ctrl+Q` | Quit |
@@ -83,7 +88,7 @@ Type a message on the start screen and press `Enter` to begin a new chat.
83
88
  | Click and drag | Select text under the rectangle; releasing copies it to the clipboard |
84
89
  | Drag onto the first/last row of the log | Scroll the log a line at a time (faster the longer you hold it) and keep growing the selection past the screen |
85
90
  | Click the header (left of `ⓘ`) | Open the model selector |
86
- | Click `ⓘ` (chat header) | Toggle token usage stats |
91
+ | Click `ⓘ` (chat header) | Toggle the plan / token info card |
87
92
  | Drag the scrollbar | Grab the thumb on the right edge of the chat/log and drag it to scroll |
88
93
  | Scroll wheel | Scroll the chat/log, or move through lists |
89
94
  | Right-click | Go back (like `Esc`) |
@@ -96,13 +101,78 @@ highlight stops at the log's edge instead of spilling into the header or the
96
101
  input box, and the copy includes the lines that scrolled out of view. Scroll back
97
102
  and the selection shrinks again; press `Esc` to drop it.
98
103
 
104
+ ## Plan-first turns
105
+
106
+ A request is answered in two passes. A **planner agent** first turns the message
107
+ into an ordered list of independent steps; a **worker agent** then executes
108
+ exactly one step per pass, from the top down. Each step names one deliverable, so
109
+ a long request stops being a single vague instruction and becomes a sequence of
110
+ concrete, verifiable ones. The plan is written into the conversation as a
111
+ message and saved with the session, so the steps of a reopened conversation are
112
+ still there.
113
+
114
+ The steps are also listed in the `ⓘ` card in the top-right corner of the chat —
115
+ click it, or press `Ctrl+P`:
116
+
117
+ - nodes run top to bottom in execution order, joined by a connector,
118
+ - the step being worked on is the highlighted row,
119
+ - steps still to come are lit, and
120
+ - finished steps are drawn dark with a tick; a failed one is marked in red.
121
+
122
+ The card opens by itself as soon as a plan is written, and closing it loses
123
+ nothing: `Ctrl+P` brings it back. On a short terminal the card shows a window
124
+ around the running step and counts the steps above and below it.
125
+
126
+ A step that fails stops the plan: the remaining steps stay pending and the next
127
+ message can re-plan them. A turn paused with `Esc Esc` stops the plan and marks
128
+ it **stopped**, so no step keeps looking live. When no usable plan comes back —
129
+ a single-action request, a provider error, an answer that is not a plan — the
130
+ turn runs in one pass exactly as before, so planning is never a new way to fail.
131
+
132
+ The prompts are files, not code: [`assets/planner.txt`](./assets/planner.txt)
133
+ says how steps are written, [`assets/step.txt`](./assets/step.txt) is the
134
+ instruction for each step, and [`assets/prompt.txt`](./assets/prompt.txt) is the
135
+ worker's base system prompt. Turn planning off for one run with
136
+ `NEUTRINO_PLAN=off`, or permanently with `"plan_first": false` in `config.json`.
137
+
138
+ ## Sub-agents
139
+
140
+ The agent can hand one self-contained sub-task to a **sub-agent**: a second agent
141
+ loop with its own transcript, its own tool calls and the same working directory.
142
+ Only its final report comes back — the files it read and the commands it ran
143
+ never enter the main conversation. That is the point: surveying a large codebase
144
+ or probing how a command behaves costs many messages, and the parent needs the
145
+ conclusion, not the transcript. The child's tokens still count in the
146
+ conversation's usage.
147
+
148
+ - The parent calls the `delegate` tool with a complete task and, optionally, a
149
+ little context. The sub-agent cannot see the conversation and cannot ask the
150
+ user, so a task that refers to "the file above" fails; the system prompt tells
151
+ the model to write a brief that stands alone.
152
+ - Its tool calls go through the **same approval gate**: a risky call parks the
153
+ turn and shows the usual card (the header turns orange, the browser shows its
154
+ banner). Denying it tells the sub-agent not to retry unchanged, and the parent
155
+ receives the refusal in the report.
156
+ - Delegation is one level deep — `delegate` is not in a sub-agent's toolset — so
157
+ the work always has one owner and one pause button.
158
+ - While it runs, the terminal status line and the browser badge name the current
159
+ phase (`Sub-agent — read_file`), and the `delegate` tool block shows the task
160
+ it was given. The report replaces that marker when it arrives, as the result of
161
+ the tool call.
162
+ - The round limit (12) bounds a child that never finishes; the parent then gets
163
+ the partial report, clearly marked as partial. A paused turn aborts the
164
+ sub-agent and says so in the tool result.
165
+
166
+ The sub-agent's prompt is a file too: [`assets/subagent.txt`](./assets/subagent.txt).
167
+
99
168
  ## Configuration
100
169
 
101
170
  Settings are stored under `~/.neutrino/`:
102
171
 
103
172
  - `model.json` — provider and model definitions (edit to add your own endpoints).
104
173
  - `config.json` — app configuration (selected provider/model/intensity, theme,
105
- language, approval mode, `auto_approve_extreme_risk`, `sound_alerts`).
174
+ language, approval mode, `auto_approve_extreme_risk`, `sound_alerts`,
175
+ `web_console`).
106
176
  - `session/` — saved conversation sessions, one JSON file per session.
107
177
 
108
178
  Set your API key in the model select screen or directly in `model.json`.
@@ -111,8 +181,11 @@ A few environment variables override flags:
111
181
 
112
182
  | Variable | Effect |
113
183
  | --- | --- |
114
- | `NEUTRINO_WEB_PORT` | Web UI port: like `--web-port` in TUI mode, and the port in `neutrino web`. |
115
- | `NEUTRINO_WEB_HOST` | Interface to bind, like `--host`. |
184
+ | `NEUTRINO_WEB_PORT` | Web UI port: like `--port` in either mode, and it mounts the UI even when `web_console` is off. |
185
+ | `NEUTRINO_WEB_HOST` | Interface to bind, like `--host`; also mounts the UI when `web_console` is off. |
186
+ | `NEUTRINO_NETWORK_ATTEMPTS` | Total tries for a model request that keeps dropping (default 7). |
187
+ | `NEUTRINO_RETRY_DELAY_MS` | Wait before the first retry in ms; each retry doubles it (default 5000). |
188
+ | `NEUTRINO_PLAN` | `off` answers a request in one pass without planning it (overrides `plan_first`); `on` forces planning. |
116
189
 
117
190
  ### Approvals
118
191
 
@@ -210,11 +283,11 @@ Only sessions that were started in the current working directory are listed, so
210
283
 
211
284
  ## Web UI
212
285
 
213
- Neutrino can mount a small website on a port of your choice. It groups every
214
- saved conversation by working directory (showing the five newest per path with
215
- the rest collapsed), folds the work of every finished turn into one summary line
216
- that opens on a click while the reply stays on screen, streams a running
217
- conversation live, and lets you pause or
286
+ Neutrino mounts a small website at <http://127.0.0.1:8765> whenever the
287
+ terminal UI starts. It groups every saved conversation by working directory
288
+ (showing the five newest per path with the rest collapsed), folds the work of
289
+ every finished turn into one summary line that opens on a click while the reply
290
+ stays on screen, streams a running conversation live, and lets you pause or
218
291
  resume it and send new messages from the browser.
219
292
 
220
293
  A saved transcript opens read-only, because nothing is running for it. Press
@@ -225,7 +298,7 @@ the same session file.
225
298
 
226
299
  Use the selectors above a conversation to change its provider, model or
227
300
  intensity, then press **Apply to conversation** (pause an active turn first).
228
- The **⚙ Settings** button in the sidebar edits defaults for new conversations,
301
+ The **Settings** (gear) button in the sidebar edits defaults for new conversations,
229
302
  context compression and approval settings. Conversation choices are saved with
230
303
  the transcript and do not overwrite the defaults. Configured models and keys
231
304
  are shared with the terminal; keys stay on the server.
@@ -238,8 +311,12 @@ to future turns in active conversations. The legacy fixed
238
311
  `context_compress_threshold` setting is no longer used.
239
312
 
240
313
  ```sh
241
- # Mount the web UI while the terminal UI runs
242
- neutrino --web-port 8765
314
+ # The terminal UI now brings the web UI up with it
315
+ neutrino
316
+
317
+ # Pick a different port or interface
318
+ neutrino --port 9000
319
+ neutrino --host 100.64.0.7
243
320
 
244
321
  # Or serve only the saved history, with no terminal
245
322
  neutrino web --port 8765
@@ -248,6 +325,12 @@ neutrino web --port 8765
248
325
  Then open <http://127.0.0.1:8765>. The site itself lives in [`web/`](./web) and
249
326
  needs no build step; see [`web/README.md`](./web/README.md) for the API.
250
327
 
328
+ The console is on by default; turn it off for one run with `--no-web`, or
329
+ permanently in the terminal's **Settings → Web console**. The `--web-port` flag
330
+ is still accepted as an alias for `--port`. If the default port is already taken
331
+ (usually by another Neutrino instance), the terminal UI starts anyway and prints
332
+ how to choose another port.
333
+
251
334
  ### Reaching it from another device
252
335
 
253
336
  The server binds to loopback, so only this machine can open it. To use it from a
@@ -255,11 +338,13 @@ phone, a tablet or another box on your LAN or VPN, bind an interface others can
255
338
  see:
256
339
 
257
340
  ```sh
258
- neutrino web --port 8765 --lan # every interface (0.0.0.0)
259
- neutrino --web-port 8765 --lan # same, while the TUI runs
260
- neutrino web --port 8765 --host 100.64.0.7 # one specific address
341
+ neutrino --lan # TUI + web UI on every interface (0.0.0.0)
342
+ neutrino --host 100.64.0.7 # TUI + web UI on one specific address
343
+ neutrino web --port 8765 --lan # headless web UI on every interface
261
344
  ```
262
345
 
346
+ `--lan` is shorthand for `--host 0.0.0.0`; the two cannot be combined.
347
+
263
348
  Startup prints every address that works, so you can copy the one matching the
264
349
  device you are on:
265
350
 
@@ -274,7 +359,9 @@ Open the address that matches your device:
274
359
  Open the address and sign in with your permanent Web password. The initial
275
360
  password is **`114514`**. Change it in the browser's **Settings → Web access**
276
361
  or in the terminal's **Settings → Web password**. It is saved as a salted hash
277
- in `~/.neutrino/config.json` and survives restarts.
362
+ in `~/.neutrino/config.json` and survives restarts. While the site is running,
363
+ its address is also shown in the terminal header and in the terminal's
364
+ **Settings → Web console** row, which can start or stop it without a restart.
278
365
 
279
366
  For automatic login, open `http://<host>:8765/?pwd=114514`. The page checks the
280
367
  password, remembers the login, and removes `pwd` from the address bar. If it is
@@ -292,8 +379,8 @@ and startup output never contain your password or session credentials.
292
379
  - **Firewall:** the first bind to `0.0.0.0` triggers an allow prompt on Windows
293
380
  and macOS. Allow it for private networks, and check that your WLAN is not
294
381
  marked "public", where incoming connections are blocked anyway.
295
- - **`127.0.0.1` remains the default** because the zero-config case should not be
296
- a network service.
382
+ - **`127.0.0.1` is still the default interface.** The console starts with the
383
+ terminal UI, but it is not a network service until you pass `--host`/`--lan`.
297
384
 
298
385
  ## Built-in tools
299
386
 
@@ -302,7 +389,8 @@ The agent can call the following tools during a conversation:
302
389
  - `run_command` — execute a shell command (120s timeout, output capped).
303
390
  - `read_file` — read a text file (capped at 200KB).
304
391
  - `write_file` — write content to a file.
305
- - `list_files` — list directory contents.
392
+ - `list_files` — list directory contents.
393
+ - `delegate` — hand one self-contained sub-task to a sub-agent and return its report.
306
394
 
307
395
  Every tool takes an additional `reason` argument: one sentence saying what the
308
396
  call is for and why it is needed now. It is what the approval card shows and what
@@ -332,11 +420,14 @@ node scripts/test-render.mjs # fuzz the diff renderer + render the chat scre
332
420
  # asserting no stale/residual cells are left behind
333
421
  node scripts/test-sanitize.mjs # verify ANSI/control codes never reach the screen
334
422
  node scripts/test-drift.mjs # verify the input area self-heals after terminal drift
335
- node scripts/test-fetch.mjs # verify streamed requests release aborts and retry drops
423
+ node scripts/test-fetch.mjs # verify streamed requests release aborts, retry drops and the 7-attempt backoff
336
424
  node scripts/test-compact.mjs # verify context-compression planning
337
425
  node scripts/test-history-fold.mjs # verify full-history retention, folding and resume
338
426
  node scripts/test-web-history.mjs # verify browser history disclosures and streaming state
339
- node scripts/test-turn-fold.mjs # verify folded turns keep their reply and every message
427
+ node scripts/test-web-default.mjs # verify the web console starts and toggles by default
428
+ node scripts/test-turn-fold.mjs # verify folded turns keep their reply and every message
429
+ node scripts/test-plan.mjs # verify plan-first turns, step order, the step card and the fallback
430
+ node scripts/test-subagent.mjs # verify delegation: isolation, approvals, abort, round limits
340
431
  node scripts/test-tools.mjs # verify tool-output folding
341
432
  node scripts/test-web.mjs # verify the web server, API, SSE stream and path-traversal guards
342
433
  node scripts/test-web-settings.mjs # verify model switching, persistence, browser controls and defaults
@@ -359,11 +450,28 @@ A cell's glyph is copied straight into the output stream, so a cell holding a *c
359
450
 
360
451
  ### Network resilience
361
452
 
362
- Model responses arrive over a streamed connection that can drop at any point. A single blip should not end the session, so three cases are handled:
453
+ Model responses arrive over a streamed connection that can drop at any point. A single blip must not end the session — a turn is minutes of work, and a proxy that resets an idle socket says nothing about the user's task — so a failed request is retried before anything is given up:
454
+
455
+ | Attempt | Wait before it |
456
+ | --- | --- |
457
+ | 1 | — |
458
+ | 2 | 5s |
459
+ | 3 | 10s |
460
+ | 4 | 20s |
461
+ | 5 | 40s |
462
+ | 6 | 80s |
463
+ | 7 | 2m 40s |
464
+
465
+ That is seven tries and roughly five minutes in total, doubling every time: a link that is merely flaky recovers long before the budget is spent, while a link that is genuinely down is not hammered. `NEUTRINO_NETWORK_ATTEMPTS` and `NEUTRINO_RETRY_DELAY_MS` tune both numbers. The policy lives in `src/llm/retry.ts`.
466
+
467
+ What counts as worth retrying:
468
+
469
+ - **Connection failures** — a reset (`ECONNRESET`), a refused or unreachable socket, `EPIPE`, DNS (`EAI_AGAIN`), a connect timeout, and undici's `UND_ERR_*` codes.
470
+ - **A stream that dies before the first byte** — the server or a proxy closed the connection mid-flight (`TypeError: terminated`, `other side closed`, `socket hang up`, …). Nothing has reached the user, so the request is simply re-issued.
471
+ - **A transient HTTP status** — 408/425, 429 and 5xx (502/503/504 from a load balancer, 529 "overloaded"). A bad request is different: 400/401/403/404/422 surface on the first answer, because re-sending them only delays the real message.
472
+ - **A stream that dies after output has started** is *not* retried: replaying it would duplicate the reply, so the error is surfaced with the real reason expanded out of the `error.cause` chain (`fetch failed` alone says nothing useful) and the partial text is kept.
363
473
 
364
- - **The request never goes out** (`fetch` rejects) — retried a couple of times with a short backoff, bounded by a connect timeout.
365
- - **The stream dies before the first byte** — the server or a proxy closed the connection mid-flight (`TypeError: terminated`, `other side closed`, …). Nothing has reached the user, so the request is simply re-issued.
366
- - **The stream dies after output has started** — retrying would duplicate text, so the error is surfaced instead, with the real reason expanded out of the `error.cause` chain (`fetch failed` alone says nothing useful).
474
+ A retry can leave the stream silent for up to 160s, so it is never silent *about* being silent: the terminal status line and the web badge name the attempt and the delay ("Network hiccup — retrying in 40s (attempt 4/7)"). The wait is also polled, so pausing the turn, sending a newer message or quitting drops it immediately instead of sitting out the backoff.
367
475
 
368
476
  Any stream the consumer walks away from (pause, or a newer turn taking over) always has its body cancelled, so sockets are not leaked.
369
477
 
@@ -0,0 +1,35 @@
1
+ You are Neutrino's planning agent.
2
+
3
+ You execute nothing. You are given one user request, the project directory and
4
+ the conversation so far, and you return the plan that a second agent will carry
5
+ out. That agent has file, shell and network tools, and it is handed exactly one
6
+ step at a time, so a step that is vague here becomes vague work there.
7
+
8
+ Split the request into the fewest steps that can each be executed and verified on
9
+ their own:
10
+
11
+ - One step, one deliverable. "Model the case from box.png" is one step;
12
+ "model the case and light the scene" is two.
13
+ - Name the concrete material: which reference image, which file, which command,
14
+ which output. A worker who has to guess guesses differently every run.
15
+ - Order the steps the way the work has to happen: look at what already exists,
16
+ then create, then wire together, then verify (build, run, test, render).
17
+ - End with the step that proves the whole result works, when the request allows
18
+ it.
19
+ - Include only what the user asked for. Do not add features, styling or
20
+ refactors nobody requested, and do not invent files that are neither mentioned
21
+ nor obviously implied.
22
+ - Between 2 and 8 steps. A single-action request returns one step and runs as a
23
+ normal turn.
24
+ - When the user supplied exact wording — camera moves, copy, timings, a script —
25
+ keep it verbatim in that step's detail.
26
+ - Write every title in the same language as the user's request, as an imperative
27
+ action of at most about 80 characters.
28
+ - Add "detail" only when the step needs it: paths, parameters, acceptance
29
+ criteria or the user's own wording. Keep it to one line of about 200
30
+ characters or less.
31
+
32
+ Answer with JSON only. No prose, no explanation, no code fence:
33
+
34
+ {"goal": "one line restating what the user wants",
35
+ "steps": [{"title": "the action", "detail": "optional material for the step"}]}
package/assets/prompt.txt CHANGED
@@ -1,24 +1,59 @@
1
- You are XINGJI Neutrino, a powerful CLI-based AI coding assistant. Your capabilities include:
1
+ You are XINGJI Neutrino, an AI engineering agent working inside the user's
2
+ project on the user's machine. You read and edit files and run commands: the
3
+ work is done when the files are changed and the result is verified, not when it
4
+ is described.
2
5
 
3
- 1. **Code Generation**: Write clean, efficient, and well-structured code in any programming language.
4
- 2. **File Operations**: Read, create, edit, and manage files in the user's project.
5
- 3. **Command Execution**: Run shell commands to build, test, lint, and deploy code.
6
- 4. **Code Review**: Analyze code for bugs, security issues, and performance problems.
7
- 5. **Refactoring**: Suggest and implement code improvements and restructuring.
8
- 6. **Debugging**: Help identify and fix issues in codebases.
9
- 7. **Documentation**: Generate clear and comprehensive documentation.
10
-
11
- Guidelines:
12
- - Always understand the full context before making changes.
13
- - Follow existing code style and conventions in the project.
14
- - Write robust, production-quality code with proper error handling.
15
- - Prefer simple, readable solutions over complex ones.
16
- - When suggesting changes, explain the reasoning.
17
- - Use the tools available to you to explore the codebase before making changes.
18
- - Verify your changes by running tests and linting when possible.
6
+ ## How you work
7
+ - Your work is planned before it starts. When a turn carries a plan you are given
8
+ exactly one step of it: do that step, verify it, and stop. Never start a later
9
+ step, and never redo an earlier one.
10
+ - Without a plan, state the numbered steps you are about to take in your first
11
+ message, then take them in that order.
12
+ - Look before you change: read the files you are about to edit, list the
13
+ directory, run the command that shows the current state. Anything about the
14
+ code you have not read is a guess, and guesses are the main source of bad
15
+ patches.
19
16
  - Every tool call must carry a `reason` argument: one honest sentence saying what
20
- the call is for and why it is needed for the current request. It is what the
21
- user sees when the call needs approval, and a call that needs approval but gives
22
- no reason is refused instead of run.
17
+ the call is for and why the current request needs it. It is what the user sees
18
+ when the call needs approval, and a call that needs approval but gives no
19
+ reason is refused instead of run. Never claim in it that the user already
20
+ approved something.
21
+ - Keep the change minimal and in the project's own style: reuse the existing
22
+ helpers, match the surrounding naming and formatting, and do not reformat or
23
+ restructure code you were not asked to touch.
24
+ - Verify what you did: run the build, the tests, or the command whose output
25
+ proves the result. Do not report success you have not seen, and never invent
26
+ command output.
27
+ - When something cannot be done, say so plainly with the error or the missing
28
+ piece, instead of quietly improvising a different task.
29
+
30
+ ## Sub-agents
31
+ - Delegate work that is independent and whose exploration would crowd this
32
+ conversation: surveying a large codebase, checking how a library or a command
33
+ really behaves, or one self-contained piece of implementation. The `delegate`
34
+ tool runs such work in a fresh context and returns only its report, so the
35
+ files it reads never fill this conversation.
36
+ - Write the task so it stands alone: the sub-agent cannot see this conversation
37
+ and cannot ask a question. Say what to do, where (paths, references), and what
38
+ "done" looks like; do not bury it in a list of steps unless the order matters.
39
+ - Its risky calls still go to the user's approval gate, and it cannot delegate
40
+ further. Treat its report as evidence: quote what it found, and check anything
41
+ the result depends on before you rely on it.
42
+
43
+ ## Quality bar
44
+ - Production quality: real error handling, no placeholders, no TODOs, no "left as
45
+ an exercise", no stubbed-out functions, no fake data standing in for the real
46
+ thing.
47
+ - Prefer the simplest solution that fully works; do not add configuration,
48
+ options or abstraction that nothing asked for.
49
+ - Never print secrets or API keys, and never write them into a file that is not
50
+ meant to hold them.
51
+ - Do not delete or overwrite the user's data without a reason the user can see.
23
52
 
24
- Language: Always respond in the same language the user writes in.
53
+ ## Answering
54
+ - Answer in the language the user writes in.
55
+ - Be specific and short: what changed (paths), what proves it (command, test,
56
+ render), what is still open. Skip the description of the process unless it is
57
+ needed to continue the work.
58
+ - If something is ambiguous in a way that changes the result, ask one precise
59
+ question instead of guessing silently.
@@ -0,0 +1,14 @@
1
+ You are executing step {{index}} of {{total}} of the plan below. Do this step
2
+ and nothing else: no later step, and no rewriting of earlier ones. Stop as soon
3
+ as this step is finished and verified.
4
+
5
+ Plan agreed with the user:
6
+ {{plan}}
7
+
8
+ Current step {{index}}: {{step}}
9
+ {{detail}}
10
+
11
+ End with one short paragraph: what you did, which files changed, and the evidence
12
+ (command output, test result, render) that shows it works. If the step cannot be
13
+ done as written, say precisely what blocks it instead of doing something else —
14
+ the user sees this and can re-plan.
@@ -0,0 +1,20 @@
1
+ You are a sub-agent. Another agent working with the user delegated one
2
+ self-contained task to you; it cannot see your messages, and the only thing it
3
+ receives from you is your final message.
4
+
5
+ Rules:
6
+ - Do the task with the tools you have, in the working directory you are given.
7
+ You cannot ask the user a question: when something is missing, do what can be
8
+ done and say precisely what is missing.
9
+ - You have no sub-agents of your own, and you must not try to delegate.
10
+ - Look before you change: read the files you are about to edit and run the
11
+ command that shows the current state.
12
+ - Risky calls still need the user's approval, exactly as for the parent agent.
13
+ If one is denied, do not retry it unchanged — work around it or report it as
14
+ blocked.
15
+ - Verify what you did with a command whose output you can quote. Do not report
16
+ success you have not seen, and never invent command output.
17
+ - Your final message is your report: what you did, the files you touched, the
18
+ commands you ran and what their output proved, and anything left unfinished or
19
+ blocked. Be factual and short — no preamble, no restating of the task.
20
+ - Answer in the same language as the task.
package/dist/app.js CHANGED
@@ -90,6 +90,8 @@ export class App {
90
90
  selOffset = null;
91
91
  resizeHandler = () => this.onResize();
92
92
  webHandle = null;
93
+ /** Where the web UI lives; kept even while it is switched off. */
94
+ webOptions = { port: 8765, auth: true };
93
95
  constructor() {
94
96
  this.width = process.stdout.columns || 80;
95
97
  this.height = process.stdout.rows || 24;
@@ -119,6 +121,13 @@ export class App {
119
121
  this.state.selectedModelApiKey = String(config.selected_model_api_key ?? this.state.selectedModelApiKey);
120
122
  this.state.selectedModelStreaming = Boolean(config.selected_model_streaming ?? true);
121
123
  }
124
+ /**
125
+ * Remember where the browser UI should listen. Called at startup even when
126
+ * the console is disabled, so Settings can mount it later without guessing.
127
+ */
128
+ configureWeb(port, host, auth) {
129
+ this.webOptions = { port, host, auth };
130
+ }
122
131
  /**
123
132
  * Mount the browser UI on the given port. Safe to call once; subsequent calls
124
133
  * return the already-running server.
@@ -126,8 +135,40 @@ export class App {
126
135
  async enableWeb(port, host, auth = true) {
127
136
  if (this.webHandle)
128
137
  return this.webHandle;
129
- this.webHandle = await startWebServer({ port, host, auth });
130
- return this.webHandle;
138
+ this.webOptions = { port, host, auth };
139
+ const handle = await startWebServer({ port, host, auth });
140
+ this.webHandle = handle;
141
+ this.state.webUrl = handle.url;
142
+ return handle;
143
+ }
144
+ /**
145
+ * Start or stop the browser UI while the terminal keeps running. Returns a
146
+ * result instead of throwing so a failed bind (usually a port already in use)
147
+ * can be reported inline without an unhandled rejection.
148
+ */
149
+ async setWebConsole(enabled) {
150
+ if (enabled) {
151
+ if (this.webHandle)
152
+ return { ok: true };
153
+ try {
154
+ await this.enableWeb(this.webOptions.port, this.webOptions.host, this.webOptions.auth);
155
+ this.showToast(`Web console: ${this.state.webUrl}`);
156
+ return { ok: true };
157
+ }
158
+ catch (err) {
159
+ const error = err instanceof Error ? err.message : String(err);
160
+ this.state.webUrl = null;
161
+ this.showToast(`Web console failed: ${error}`, "top");
162
+ return { ok: false, error };
163
+ }
164
+ }
165
+ const handle = this.webHandle;
166
+ this.webHandle = null;
167
+ this.state.webUrl = null;
168
+ this.render();
169
+ await handle?.close();
170
+ this.showToast("Web console stopped");
171
+ return { ok: true };
131
172
  }
132
173
  async start() {
133
174
  // Installed before anything touches the terminal: from here on, a crash
@@ -752,6 +793,7 @@ export class App {
752
793
  screen.loadedTokens = data.tokens ?? null;
753
794
  screen.loadedContext = data.context ?? null;
754
795
  screen.loadedApprovalMode = data.approval_mode ?? null;
796
+ screen.loadedPlan = data.plan ?? null;
755
797
  if (data.provider)
756
798
  this.state.selectedProvider = data.provider;
757
799
  if (data.model)
package/dist/bin.js CHANGED
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import process from "node:process";
3
3
  import { App } from "./app.js";
4
+ import { loadConfig } from "./config.js";
4
5
  import { installCrashGuard } from "./terminal/restore.js";
5
6
  import { startWebServer } from "./web/server.js";
6
7
  const DEFAULT_WEB_PORT = 8765;
@@ -31,25 +32,28 @@ function printHelp() {
31
32
  process.stdout.write(`Neutrino
32
33
 
33
34
  Usage:
34
- neutrino Start the terminal UI.
35
- neutrino --web-port <port> Start the terminal UI and mount the web UI.
35
+ neutrino Start the terminal UI (with the web UI).
36
+ neutrino --lan Start both, reachable from other devices.
36
37
  neutrino web [--port <port>] Start only the web UI (serves saved history).
37
38
 
38
39
  Options:
39
- -p, --port <port> Web server port (web mode). Default: ${DEFAULT_WEB_PORT}
40
+ -p, --port <port> Web UI port. Default: ${DEFAULT_WEB_PORT}
40
41
  --host <host> Interface to bind. Default: 127.0.0.1
41
42
  --lan Short for --host ${WILDCARD_HOST}: reachable from other
42
43
  devices on your LAN or VPN.
44
+ --no-web Do not start the web UI with the terminal UI.
43
45
  --no-auth Serve without password login.
44
- --web-port <n> Also mount the web UI while the TUI runs.
46
+ --web-port <n> Deprecated alias for --port while the TUI runs.
45
47
  -h, --help Show this help.
46
48
 
47
49
  Environment:
48
- NEUTRINO_WEB_PORT Same as --web-port for the TUI, or the port in web mode.
50
+ NEUTRINO_WEB_PORT Same as --port in either mode.
49
51
  NEUTRINO_WEB_HOST Same as --host.
50
52
 
51
- The web UI can read every saved conversation and drive an agent with shell
52
- access, so it binds to loopback by default. Log in with the permanent password
53
+ The web UI starts by default alongside the terminal UI on loopback. Turn it
54
+ off for one run with --no-web, or permanently in Settings → Web console. It can
55
+ read every saved conversation and drive an agent with shell access, so it binds
56
+ to loopback unless you ask for --host/--lan. Log in with the permanent password
53
57
  (default: 114514). Change it in Settings. --no-auth disables password login.
54
58
  `);
55
59
  }
@@ -62,7 +66,7 @@ function requireValue(argv, index, flag) {
62
66
  return value;
63
67
  }
64
68
  function parseArgs(argv) {
65
- const options = { mode: "tui", port: null, lan: false, noAuth: false };
69
+ const options = { mode: "tui", port: null, lan: false, noWeb: false, noAuth: false };
66
70
  for (let i = 0; i < argv.length; i += 1) {
67
71
  const arg = argv[i];
68
72
  if (arg === "web") {
@@ -89,6 +93,9 @@ function parseArgs(argv) {
89
93
  else if (arg === "--lan") {
90
94
  options.lan = true;
91
95
  }
96
+ else if (arg === "--no-web") {
97
+ options.noWeb = true;
98
+ }
92
99
  else if (arg === "--no-auth") {
93
100
  options.noAuth = true;
94
101
  }
@@ -103,6 +110,12 @@ function parseArgs(argv) {
103
110
  if (options.lan && options.host) {
104
111
  throw new Error("use either --lan or --host, not both");
105
112
  }
113
+ if (options.noWeb && options.mode === "web") {
114
+ throw new Error("--no-web cannot be used with the 'web' command");
115
+ }
116
+ if (options.noWeb && (options.port !== null || options.host !== undefined || options.lan)) {
117
+ throw new Error("use either --no-web or web UI options, not both");
118
+ }
106
119
  return options;
107
120
  }
108
121
  /** Print every address a client can open, plus what it takes to open one. */
@@ -140,10 +153,11 @@ catch (err) {
140
153
  installCrashGuard();
141
154
  const host = options.lan ? WILDCARD_HOST : options.host || process.env.NEUTRINO_WEB_HOST || undefined;
142
155
  const auth = !options.noAuth;
156
+ const portFromEnv = envPort();
143
157
  if (options.mode === "web") {
144
158
  // Headless: serve the saved history (and any live sessions in this process,
145
159
  // which for a standalone server is none).
146
- const port = options.port ?? envPort() ?? DEFAULT_WEB_PORT;
160
+ const port = options.port ?? portFromEnv ?? DEFAULT_WEB_PORT;
147
161
  try {
148
162
  const handle = await startWebServer({ port, host, auth });
149
163
  reportAccess(handle);
@@ -166,15 +180,26 @@ else {
166
180
  process.exit(1);
167
181
  }
168
182
  const app = new App();
169
- const webPort = options.port ?? envPort();
170
- if (webPort !== null) {
183
+ const webPort = options.port ?? portFromEnv ?? DEFAULT_WEB_PORT;
184
+ // Remember where the UI should live even when it is off right now, so the
185
+ // Settings toggle can mount it without restarting the terminal.
186
+ app.configureWeb(webPort, host, auth);
187
+ // Explicit web settings win over the saved preference; otherwise the web UI
188
+ // is part of a normal launch unless the user turned it off in Settings.
189
+ const requested = options.port !== null || options.host !== undefined || options.lan
190
+ || options.noAuth || portFromEnv !== null || Boolean(process.env.NEUTRINO_WEB_HOST);
191
+ const enabledByDefault = loadConfig().web_console !== false;
192
+ if (!options.noWeb && (requested || enabledByDefault)) {
171
193
  try {
172
194
  const handle = await app.enableWeb(webPort, host, auth);
173
195
  reportAccess(handle);
174
196
  }
175
197
  catch (err) {
176
- process.stderr.write(`neutrino: failed to start web server: ${err instanceof Error ? err.message : String(err)}\n`);
177
- process.exit(1);
198
+ // A console that cannot bind (usually a second instance on 8765) must
199
+ // never take the terminal down with it.
200
+ const detail = err instanceof Error ? err.message : String(err);
201
+ process.stderr.write(`neutrino: web UI not started: ${detail}\n`);
202
+ process.stderr.write("The terminal UI will keep running. Pass --port <port> to use another port, or --no-web to skip the web UI.\n");
178
203
  }
179
204
  }
180
205
  await app.start();