@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 +136 -28
- package/assets/planner.txt +35 -0
- package/assets/prompt.txt +56 -21
- package/assets/step.txt +14 -0
- package/assets/subagent.txt +20 -0
- package/dist/app.js +44 -2
- package/dist/bin.js +38 -13
- package/dist/config.js +45 -1
- package/dist/header.js +5 -0
- package/dist/llm/client.js +64 -140
- package/dist/llm/plan.js +403 -0
- package/dist/llm/retry.js +209 -0
- package/dist/llm/subagent.js +134 -0
- package/dist/llm/tools.js +43 -1
- package/dist/screens/chat.js +369 -51
- package/dist/screens/settings.js +30 -1
- package/dist/state.js +1 -0
- package/dist/web/session.js +211 -22
- package/package.json +2 -2
- package/web/README.md +46 -10
- package/web/app.js +52 -16
- package/web/index.html +55 -17
- package/web/style.css +2070 -1093
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
|
-
- **
|
|
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
|
|
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
|
|
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 `--
|
|
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
|
|
214
|
-
|
|
215
|
-
the rest collapsed), folds the work of
|
|
216
|
-
that opens on a click while the reply
|
|
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
|
|
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
|
-
#
|
|
242
|
-
neutrino
|
|
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
|
|
259
|
-
neutrino --
|
|
260
|
-
neutrino web --port 8765 --
|
|
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`
|
|
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
|
|
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-
|
|
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
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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
|
|
21
|
-
|
|
22
|
-
|
|
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
|
-
|
|
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.
|
package/assets/step.txt
ADDED
|
@@ -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.
|
|
130
|
-
|
|
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 --
|
|
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
|
|
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>
|
|
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 --
|
|
50
|
+
NEUTRINO_WEB_PORT Same as --port in either mode.
|
|
49
51
|
NEUTRINO_WEB_HOST Same as --host.
|
|
50
52
|
|
|
51
|
-
The web UI
|
|
52
|
-
|
|
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 ??
|
|
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 ??
|
|
170
|
-
|
|
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
|
-
|
|
177
|
-
|
|
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();
|