@xingjisoft/neutrino 0.2.1 → 0.3.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 +35 -7
- package/dist/config.js +9 -0
- package/dist/screens/chat.js +40 -30
- package/dist/web/hub.js +9 -0
- package/dist/web/server.js +353 -5
- package/dist/web/session.js +65 -1
- package/dist/web/settings.js +27 -1
- package/package.json +2 -2
- package/web/README.md +83 -15
- package/web/app.js +1309 -124
- package/web/i18n.js +345 -0
- package/web/index.html +210 -34
- package/web/markdown.js +710 -0
- package/web/settings.js +73 -19
- package/web/style.css +1124 -85
package/dist/web/session.js
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
* registers itself in the live hub, so the regular `/api/live/*` endpoints
|
|
11
11
|
* (snapshot, stream, pause, resume, message) work unchanged.
|
|
12
12
|
*/
|
|
13
|
+
import { randomUUID } from "node:crypto";
|
|
13
14
|
import { findModel, contextCompressionThreshold, loadConfig, loadPlannerPrompt, loadStepPrompt, loadSubAgentPrompt, normalizeApprovalMode, planFirstEnabled, loadModels, loadPrompt, saveSession, } from "../config.js";
|
|
14
15
|
import { COMPACT_KEEP, planCompaction, transcriptFor, modelHistory, normalizeContext, } from "../compact.js";
|
|
15
16
|
import { LLMClient, describeError } from "../llm/client.js";
|
|
@@ -31,6 +32,35 @@ const TOOL_PENDING = "…";
|
|
|
31
32
|
const SUB_AGENT_TOOLS = getSubAgentToolDefinitions();
|
|
32
33
|
/** Marker for the model-only summary, separate from the original transcript. */
|
|
33
34
|
const SUMMARY_PREFIX = "Context was compressed. Summary of the earlier part:";
|
|
35
|
+
/**
|
|
36
|
+
* A fresh session id in the same shape `saveSession` generates, so a new
|
|
37
|
+
* browser conversation has a stable identity before its first message exists.
|
|
38
|
+
*/
|
|
39
|
+
export function newSessionId(now = new Date()) {
|
|
40
|
+
return `${now.toISOString().replace(/[:.]/g, "-")}-${randomUUID().slice(0, 8)}`;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Start an empty conversation in a caller-chosen working directory. Nothing is
|
|
44
|
+
* written to disk until the first message, so an abandoned new-chat dialog
|
|
45
|
+
* cannot litter the history with empty transcripts.
|
|
46
|
+
*/
|
|
47
|
+
export function createWebSession(cwd, selection, approvalMode) {
|
|
48
|
+
const now = new Date();
|
|
49
|
+
const source = {
|
|
50
|
+
id: newSessionId(now),
|
|
51
|
+
created_at: now.toISOString(),
|
|
52
|
+
updated_at: now.toISOString(),
|
|
53
|
+
model: selection.model.name,
|
|
54
|
+
provider: selection.provider,
|
|
55
|
+
intensity: selection.intensity,
|
|
56
|
+
cwd,
|
|
57
|
+
title: "",
|
|
58
|
+
messages: [],
|
|
59
|
+
tokens: { prompt: 0, completion: 0 },
|
|
60
|
+
approval_mode: normalizeApprovalMode(approvalMode ?? loadConfig().approval_mode),
|
|
61
|
+
};
|
|
62
|
+
return new WebSession(source, selection.model);
|
|
63
|
+
}
|
|
34
64
|
function parseArguments(raw) {
|
|
35
65
|
const text = raw?.trim() ?? "";
|
|
36
66
|
if (!text)
|
|
@@ -267,6 +297,40 @@ export class WebSession {
|
|
|
267
297
|
this.save();
|
|
268
298
|
this.publishLive();
|
|
269
299
|
}
|
|
300
|
+
/**
|
|
301
|
+
* Start a fresh chat in the same working directory, mirroring the terminal's
|
|
302
|
+
* clear shortcut. The finished transcript is checkpointed first and stays on
|
|
303
|
+
* disk; this session simply moves on under a new id.
|
|
304
|
+
*/
|
|
305
|
+
liveClear() {
|
|
306
|
+
if (this.disposed)
|
|
307
|
+
return;
|
|
308
|
+
if (!this.checkpoint.flush())
|
|
309
|
+
return;
|
|
310
|
+
this.turnToken++;
|
|
311
|
+
if (this.pendingApproval) {
|
|
312
|
+
const pending = this.pendingApproval;
|
|
313
|
+
this.pendingApproval = null;
|
|
314
|
+
pending.resolve(false);
|
|
315
|
+
}
|
|
316
|
+
this.busy = false;
|
|
317
|
+
this.paused = false;
|
|
318
|
+
this.queued = null;
|
|
319
|
+
this.compressing = false;
|
|
320
|
+
this.retrying = null;
|
|
321
|
+
this.subagent = null;
|
|
322
|
+
this.reviewing = null;
|
|
323
|
+
this.streamingContent = null;
|
|
324
|
+
this.plan = null;
|
|
325
|
+
this.planning = false;
|
|
326
|
+
this.messages = [];
|
|
327
|
+
this.context = null;
|
|
328
|
+
this.lastPromptTokens = 0;
|
|
329
|
+
this.saveError = null;
|
|
330
|
+
this.sessionId = newSessionId();
|
|
331
|
+
this.publishLive();
|
|
332
|
+
this.touch();
|
|
333
|
+
}
|
|
270
334
|
// ---- turn driving ----------------------------------------------------
|
|
271
335
|
send(text) {
|
|
272
336
|
const trimmed = text.trim();
|
|
@@ -725,7 +789,7 @@ export class WebSession {
|
|
|
725
789
|
return line.length > 80 ? `${line.slice(0, 77)}…` : line;
|
|
726
790
|
}
|
|
727
791
|
}
|
|
728
|
-
return "
|
|
792
|
+
return "";
|
|
729
793
|
}
|
|
730
794
|
retainStreamingContent() {
|
|
731
795
|
if (this.streamingContent) {
|
package/dist/web/settings.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
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, isContextWindow, loadConfig, loadModels, normalizeApprovalMode, normalizeWebLanguage, resolveModelSelection, saveConfig, saveModels, } from "../config.js";
|
|
2
2
|
import { hashWebPassword, validateWebPassword } from "./auth.js";
|
|
3
3
|
/** Public settings use a whitelist: API keys and cached credentials stay on disk. */
|
|
4
4
|
export function getWebSettings() {
|
|
@@ -15,6 +15,7 @@ export function getWebSettings() {
|
|
|
15
15
|
return {
|
|
16
16
|
providers,
|
|
17
17
|
intensities: INTENSITY_LEVELS,
|
|
18
|
+
languages: WEB_LANGUAGES.map((value) => ({ value, label: value === "zh" ? "简体中文" : "English" })),
|
|
18
19
|
selection: {
|
|
19
20
|
provider: provider?.name ?? "",
|
|
20
21
|
model: model?.name ?? "",
|
|
@@ -26,6 +27,10 @@ export function getWebSettings() {
|
|
|
26
27
|
approval_model: String(config.approval_model ?? ""),
|
|
27
28
|
auto_approve_extreme_risk: config.auto_approve_extreme_risk === true,
|
|
28
29
|
sound_alerts: config.sound_alerts !== false,
|
|
30
|
+
language: normalizeWebLanguage(config.language),
|
|
31
|
+
plan_first: config.plan_first !== false,
|
|
32
|
+
approval_mode: normalizeApprovalMode(config.approval_mode),
|
|
33
|
+
web_console: config.web_console !== false,
|
|
29
34
|
},
|
|
30
35
|
};
|
|
31
36
|
}
|
|
@@ -67,6 +72,27 @@ export function updateWebSettings(value) {
|
|
|
67
72
|
throw new Error("auto-approval must be a boolean");
|
|
68
73
|
config[key] = val;
|
|
69
74
|
}
|
|
75
|
+
else if (key === "language") {
|
|
76
|
+
if (typeof val !== "string" || !WEB_LANGUAGES.includes(val)) {
|
|
77
|
+
throw new Error("invalid language");
|
|
78
|
+
}
|
|
79
|
+
config.language = val;
|
|
80
|
+
}
|
|
81
|
+
else if (key === "plan_first") {
|
|
82
|
+
if (typeof val !== "boolean")
|
|
83
|
+
throw new Error("invalid plan setting");
|
|
84
|
+
config.plan_first = val;
|
|
85
|
+
}
|
|
86
|
+
else if (key === "approval_mode") {
|
|
87
|
+
if (val !== "request" && val !== "auto")
|
|
88
|
+
throw new Error("invalid approval mode");
|
|
89
|
+
config.approval_mode = val;
|
|
90
|
+
}
|
|
91
|
+
else if (key === "web_console") {
|
|
92
|
+
if (typeof val !== "boolean")
|
|
93
|
+
throw new Error("web console must be a boolean");
|
|
94
|
+
config.web_console = val;
|
|
95
|
+
}
|
|
70
96
|
else if (key === "web_password") {
|
|
71
97
|
validateWebPassword(val);
|
|
72
98
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xingjisoft/neutrino",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "XINGJI Neutrino - a CLI coding agent in the terminal.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
},
|
|
19
19
|
"scripts": {
|
|
20
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",
|
|
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-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-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
22
|
"prepare": "npm run build",
|
|
23
23
|
"web": "node dist/bin.js web"
|
|
24
24
|
},
|
package/web/README.md
CHANGED
|
@@ -8,8 +8,17 @@ A small, dependency-free single page app that is served by the Neutrino CLI at
|
|
|
8
8
|
- watch a running terminal conversation stream in real time: the work behind every
|
|
9
9
|
finished turn folds into one line that opens on a click, while the reply itself
|
|
10
10
|
stays on screen (the turn being answered is open, and a pause is not a fold);
|
|
11
|
+
- read replies as rendered markdown: headings, nested/task lists, tables, block
|
|
12
|
+
quotes, code fences, emphasis, safe links and images; raw HTML is escaped;
|
|
13
|
+
- start a new conversation in a working directory you choose, including a folder
|
|
14
|
+
created from the built-in folder picker;
|
|
15
|
+
- switch the workspace between every known folder and **All directories**;
|
|
11
16
|
- switch the provider, model and intensity for the next turn;
|
|
12
|
-
- edit
|
|
17
|
+
- edit every terminal setting — defaults, interface language, plan-first turns,
|
|
18
|
+
default approval mode, context compression, reviewer and Web password;
|
|
19
|
+
- switch the interface between English and 简体中文, instantly and persistently;
|
|
20
|
+
- open the same plan / token card as the terminal's `ⓘ`, clear a conversation,
|
|
21
|
+
delete a saved transcript, and review other agents' read-only history;
|
|
13
22
|
- drive the running conversation: pause / resume the current turn and send new
|
|
14
23
|
messages (they queue while the agent is busy);
|
|
15
24
|
- pick up a saved conversation in the browser and keep chatting, even when no
|
|
@@ -112,13 +121,42 @@ selection. Changing the selectors and then using **Continue here** or sending a
|
|
|
112
121
|
message also uses the draft selection, so a transcript whose old model was
|
|
113
122
|
removed can still be continued.
|
|
114
123
|
|
|
115
|
-
The **Settings** (gear) button in the sidebar opens
|
|
116
|
-
the
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
124
|
+
The **Settings** (gear) button in the sidebar opens the interface language,
|
|
125
|
+
plan-first turns, the default approval mode, defaults for new conversations, the
|
|
126
|
+
context compression threshold, the approval reviewer model, sound alerts, the
|
|
127
|
+
extreme-risk automatic approval option, and the Web password. Save settings
|
|
128
|
+
explicitly; cancelling leaves the saved values intact. Conversation model changes
|
|
129
|
+
are separate from these defaults. Models come from the same catalog as the
|
|
130
|
+
terminal, and API keys stay on the server. Configure missing keys in the terminal
|
|
131
|
+
model screen first. Intensity uses the same Low–Ultra sampling settings as the
|
|
132
|
+
terminal.
|
|
133
|
+
|
|
134
|
+
## New conversations and working directories
|
|
135
|
+
|
|
136
|
+
**New conversation** starts a chat that has no transcript yet. The dialog takes a
|
|
137
|
+
working directory plus provider, model, thinking level and approval mode. The
|
|
138
|
+
conversation is browser-owned from that moment: it appears in the sidebar under
|
|
139
|
+
its folder and is written to its own session file once the first message exists.
|
|
140
|
+
|
|
141
|
+
**Browse…** opens the server-side folder picker. It lists folders only, offers
|
|
142
|
+
the existing drive roots (on Windows) or `/`, moves up through the directory
|
|
143
|
+
tree, and can create a folder in place. **Use this folder** fills the working
|
|
144
|
+
directory of the new conversation.
|
|
145
|
+
|
|
146
|
+
**Working directories** (the folder button under New conversation) shows every
|
|
147
|
+
folder the console has opened or that already holds conversations. Choosing one
|
|
148
|
+
scopes the sidebar to that folder and shows a landing page where typing starts a
|
|
149
|
+
conversation there. **All directories** returns to the grouped view. Opened
|
|
150
|
+
folders are stored in `config.json` under `web_workspaces`, so an empty folder
|
|
151
|
+
that has no conversations yet is still listed after a restart.
|
|
152
|
+
|
|
153
|
+
## Interface language
|
|
154
|
+
|
|
155
|
+
The sidebar footer and **Settings → Interface** both offer English and 简体中文.
|
|
156
|
+
The change is applied to the already-open page (labels, messages, status line,
|
|
157
|
+
plan card and dialogs) without a reload, is stored in `config.json`, and is
|
|
158
|
+
shared by every browser. The terminal UI stays in English. New conversation
|
|
159
|
+
content, of course, remains in the language the model writes.
|
|
122
160
|
|
|
123
161
|
## History vs. live sessions
|
|
124
162
|
|
|
@@ -144,6 +182,22 @@ Browser-owned sessions are dropped after 30 idle minutes, or immediately through
|
|
|
144
182
|
`POST /api/live/:key/close`. Sessions owned by the terminal cannot be closed from
|
|
145
183
|
the web.
|
|
146
184
|
|
|
185
|
+
**Clear** empties the running conversation in place (the previous transcript is
|
|
186
|
+
checkpointed first and stays on disk), mirroring `Ctrl+L` in the terminal. A
|
|
187
|
+
read-only transcript gets **Delete** instead, which removes the saved JSON file
|
|
188
|
+
after confirmation; a conversation that is currently live cannot be deleted.
|
|
189
|
+
Sessions from the terminal's `Ctrl+L` also appear as fresh live conversations in
|
|
190
|
+
the sidebar.
|
|
191
|
+
|
|
192
|
+
## Other agents' history
|
|
193
|
+
|
|
194
|
+
**Other agents** in the sidebar lists read-only sessions recorded by Claude Code,
|
|
195
|
+
Codex and OpenCode for the current working directory (the scoped workspace, the
|
|
196
|
+
open conversation's folder, or the server's launch directory). Clicking one shows
|
|
197
|
+
the complete transcript in the conversation pane with a `read-only` badge. The
|
|
198
|
+
sources are the same as the terminal's history browser; OpenCode requires Node.js
|
|
199
|
+
22.13+ because it reads its SQLite database through `node:sqlite`.
|
|
200
|
+
|
|
147
201
|
## Plan-first turns
|
|
148
202
|
|
|
149
203
|
A browser-owned turn is planned before it runs, exactly like a turn in the
|
|
@@ -169,8 +223,10 @@ needed by a client:
|
|
|
169
223
|
|
|
170
224
|
`status` is `pending`, `active`, `done` or `failed`; `stopped: true` marks a plan
|
|
171
225
|
whose turn was paused before the last step. The terminal draws this in its `ⓘ`
|
|
172
|
-
card
|
|
173
|
-
|
|
226
|
+
card, and the web console renders the same timeline in its `ⓘ` plan card: the
|
|
227
|
+
active step is highlighted, finished steps are ticked, failed steps are red, and
|
|
228
|
+
the footer counts the steps completed. The card also shows token usage when no
|
|
229
|
+
plan is running.
|
|
174
230
|
|
|
175
231
|
Planning is the default for both loops. It can be turned off with
|
|
176
232
|
`"plan_first": false` in `~/.neutrino/config.json` or `NEUTRINO_PLAN=off`; a
|
|
@@ -193,21 +249,31 @@ to know whether a conversation is driven by the terminal or by the server.
|
|
|
193
249
|
| --- | --- | --- |
|
|
194
250
|
| GET | `/api/overview` | Sessions grouped by cwd + live sessions |
|
|
195
251
|
| GET | `/api/sessions` | Flat list of saved sessions |
|
|
196
|
-
| GET | `/api/
|
|
197
|
-
| POST | `/api/
|
|
252
|
+
| GET | `/api/workspaces` | Recent working directories, launch cwd and home |
|
|
253
|
+
| POST | `/api/workspaces` | Validate and remember `{ "path": "…" }` |
|
|
254
|
+
| GET | `/api/fs?path=…` | Folder listing for the picker (folders only, roots, parent) |
|
|
255
|
+
| POST | `/api/fs/mkdir` | Create `{ "path": "…" }` and return the new path |
|
|
256
|
+
| POST | `/api/conversations` | Start `{ cwd, selection?, approval_mode? }`, returns `{ key, snapshot }` |
|
|
257
|
+
| GET | `/api/settings` | Model catalog, languages and public settings; no credentials |
|
|
258
|
+
| POST | `/api/settings` | Save validated `{ selection?, settings? }`, including `language`, `plan_first`, `approval_mode` and `web_password` |
|
|
198
259
|
| GET | `/api/session/:id` | One saved transcript |
|
|
199
260
|
| POST | `/api/session/:id/continue` | Resume a transcript with optional `{ selection }`, returns `{ key }` |
|
|
261
|
+
| POST | `/api/session/:id/delete` | Delete a saved, non-running transcript |
|
|
262
|
+
| GET | `/api/agents/:agent/sessions?cwd=…` | Read-only sessions from Claude Code / Codex / OpenCode |
|
|
263
|
+
| GET | `/api/agents/:agent/sessions/:id` | Read-only messages of one other-agent session |
|
|
200
264
|
| GET | `/api/live/:key` | Snapshot of a live session |
|
|
201
265
|
| GET | `/api/live/:key/stream` | SSE stream of a live session |
|
|
202
266
|
| POST | `/api/live/:key/pause` | Pause the running turn |
|
|
203
267
|
| POST | `/api/live/:key/resume` | Resume a paused conversation |
|
|
204
268
|
| POST | `/api/live/:key/message` | Send `{ "text": "…" }` |
|
|
205
269
|
| POST | `/api/live/:key/model` | Apply `{ provider, model, intensity }` while idle or paused |
|
|
270
|
+
| POST | `/api/live/:key/clear` | Empty the conversation in place; keeps the old transcript |
|
|
271
|
+
| POST | `/api/live/:key/approve` | Answer `{ "approved": true|false }` |
|
|
272
|
+
| POST | `/api/live/:key/approval-mode` | Set `{ "mode": "request"|"auto" }` |
|
|
206
273
|
| POST | `/api/live/:key/close` | Dispose a browser-owned session |
|
|
207
274
|
| GET | `/api/health` | Liveness probe |
|
|
208
275
|
| POST | `/api/auth/login` | Sign in with `{ "password": "…" }` |
|
|
209
276
|
| POST | `/api/auth/logout` | Clear the login cookie |
|
|
210
|
-
| GET / POST | `/api/settings` | Read settings or update them, including `settings.web_password` |
|
|
211
277
|
|
|
212
278
|
Conversation and settings routes require the login cookie; see
|
|
213
279
|
[Authentication](#authentication). `OPTIONS` is answered unauthenticated so
|
|
@@ -219,8 +285,10 @@ When a running agent wants to do something risky (a delete-class command, an
|
|
|
219
285
|
install, a path outside the working directory) the browser shows an orange
|
|
220
286
|
**Request Approval** banner above the composer, showing the full command (or
|
|
221
287
|
formatted tool arguments), working directory and risk reasons, plus two buttons.
|
|
222
|
-
|
|
223
|
-
|
|
288
|
+
The banner only appears when a human decision is actually waiting. Long proposals
|
|
289
|
+
remain complete and scroll inside the card.
|
|
290
|
+
Automatic reviews are reported in the conversation's status badge instead
|
|
291
|
+
(`reviewing round 2/3`), so auto approval never looks like a request for approval.
|
|
224
292
|
`Approve` runs the call, `Deny` returns a note to the agent.
|
|
225
293
|
The reviewer uses the current turn's captured user request and earlier user
|
|
226
294
|
messages; queued input belongs to the next turn. Expand a denied tool result
|