@xingjisoft/neutrino 0.4.0 → 0.4.1
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 +33 -397
- package/assets/neutrino.ico +0 -0
- package/assets/neutrino.svg +7 -0
- package/dist/agents.js +22 -3
- package/dist/app.js +163 -26
- package/dist/background.js +358 -0
- package/dist/bin.js +179 -42
- package/dist/config.js +7 -3
- package/dist/header.js +2 -1
- package/dist/llm/tools.js +3 -1
- package/dist/migration.js +33 -0
- package/dist/screens/agentConversation.js +48 -5
- package/dist/screens/agentHistory.js +4 -2
- package/dist/screens/settings.js +43 -2
- package/dist/state.js +1 -0
- package/dist/terminal/restore.js +3 -12
- package/dist/tray.js +138 -0
- package/dist/web/hub.js +4 -0
- package/dist/web/server.js +243 -35
- package/dist/web/service.js +135 -0
- package/dist/web/session.js +23 -6
- package/dist/web/settings.js +6 -0
- package/dist/web/terminalBridge.js +250 -0
- package/package.json +2 -2
- package/web/README.md +74 -14
- package/web/app.js +186 -11
- package/web/i18n.js +14 -0
- package/web/index.html +30 -1
- package/web/settings.js +53 -2
- package/web/style.css +101 -0
package/web/app.js
CHANGED
|
@@ -147,6 +147,7 @@ function showGate(message = "") {
|
|
|
147
147
|
closeStream();
|
|
148
148
|
setStatus("locked", "offline");
|
|
149
149
|
document.getElementById("settings-dialog").close();
|
|
150
|
+
document.getElementById("background-dialog")?.close?.();
|
|
150
151
|
|
|
151
152
|
// A modal dialog stays above streaming repaints and traps keyboard focus.
|
|
152
153
|
const gate = document.createElement("dialog");
|
|
@@ -184,6 +185,78 @@ function showGate(message = "") {
|
|
|
184
185
|
gate.showModal();
|
|
185
186
|
}
|
|
186
187
|
|
|
188
|
+
// -------------------------------------------------------- background invite
|
|
189
|
+
//
|
|
190
|
+
// Accepting changes the running console's lifetime immediately, including
|
|
191
|
+
// conversations already in flight. A resident console never offers the prompt.
|
|
192
|
+
|
|
193
|
+
let backgroundPromptAnswered = false;
|
|
194
|
+
|
|
195
|
+
function showBackgroundPrompt() {
|
|
196
|
+
if (backgroundPromptAnswered || !el.backgroundDialog?.showModal) return;
|
|
197
|
+
if (el.backgroundDialog.open) return;
|
|
198
|
+
if (el.backgroundFeedback) {
|
|
199
|
+
el.backgroundFeedback.textContent = "";
|
|
200
|
+
el.backgroundFeedback.classList.remove("error");
|
|
201
|
+
}
|
|
202
|
+
try {
|
|
203
|
+
el.backgroundDialog.showModal();
|
|
204
|
+
} catch {
|
|
205
|
+
// An embedder without dialog support still gets the setting in Settings.
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function maybePromptBackground(config) {
|
|
210
|
+
if (!config?.settings?.background_prompt) return;
|
|
211
|
+
if (config.settings.background === true) return;
|
|
212
|
+
showBackgroundPrompt();
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
async function answerBackgroundPrompt(enable) {
|
|
216
|
+
if (backgroundPromptAnswered) return;
|
|
217
|
+
backgroundPromptAnswered = true;
|
|
218
|
+
const feedback = el.backgroundFeedback;
|
|
219
|
+
const cancel = el.backgroundCancel;
|
|
220
|
+
const confirm = el.backgroundEnable;
|
|
221
|
+
if (cancel) cancel.disabled = true;
|
|
222
|
+
if (confirm) confirm.disabled = true;
|
|
223
|
+
if (feedback) {
|
|
224
|
+
feedback.textContent = "";
|
|
225
|
+
feedback.classList.remove("error");
|
|
226
|
+
}
|
|
227
|
+
try {
|
|
228
|
+
if (enable) {
|
|
229
|
+
const response = await apiFetch("/api/settings", {
|
|
230
|
+
method: "POST",
|
|
231
|
+
headers: { "Content-Type": "application/json" },
|
|
232
|
+
body: JSON.stringify({ settings: { background: true } }),
|
|
233
|
+
});
|
|
234
|
+
const body = await response.json();
|
|
235
|
+
if (!response.ok) throw new Error(body.error || `HTTP ${response.status}`);
|
|
236
|
+
if (body.settings?.background_running !== true) {
|
|
237
|
+
throw new Error(tr("Background residency could not be applied. Restart the console and try again."));
|
|
238
|
+
}
|
|
239
|
+
try { await preferences.load(); } catch { /* Residency is already active. */ }
|
|
240
|
+
} else {
|
|
241
|
+
// Consume the invitation for this CLI run. A failed dismissal is
|
|
242
|
+
// harmless: the prompt simply returns on the next page load.
|
|
243
|
+
try {
|
|
244
|
+
await apiFetch("/api/background/dismiss", { method: "POST" });
|
|
245
|
+
} catch { /* Keep the close local. */ }
|
|
246
|
+
}
|
|
247
|
+
el.backgroundDialog?.close?.();
|
|
248
|
+
} catch (error) {
|
|
249
|
+
backgroundPromptAnswered = false;
|
|
250
|
+
if (feedback) {
|
|
251
|
+
feedback.textContent = tr(error.message);
|
|
252
|
+
feedback.classList.add("error");
|
|
253
|
+
}
|
|
254
|
+
} finally {
|
|
255
|
+
if (cancel) cancel.disabled = false;
|
|
256
|
+
if (confirm) confirm.disabled = false;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
187
260
|
const el = {
|
|
188
261
|
app: document.getElementById("app"),
|
|
189
262
|
back: document.getElementById("back-to-menu"),
|
|
@@ -195,6 +268,7 @@ const el = {
|
|
|
195
268
|
proTip: document.getElementById("pro-tip"),
|
|
196
269
|
proTipClose: document.getElementById("pro-tip-close"),
|
|
197
270
|
proExit: document.getElementById("pro-exit"),
|
|
271
|
+
header: document.getElementById("main-header"),
|
|
198
272
|
title: document.getElementById("session-title"),
|
|
199
273
|
sub: document.getElementById("session-sub"),
|
|
200
274
|
controls: document.getElementById("controls"),
|
|
@@ -252,8 +326,28 @@ const el = {
|
|
|
252
326
|
agentsList: document.getElementById("agents-list"),
|
|
253
327
|
agentsFeedback: document.getElementById("agents-feedback"),
|
|
254
328
|
agentsCwd: document.getElementById("agents-cwd"),
|
|
329
|
+
agentActions: document.getElementById("agent-actions"),
|
|
330
|
+
agentMigrate: document.getElementById("agent-migrate"),
|
|
331
|
+
agentMigrateFeedback: document.getElementById("agent-migrate-feedback"),
|
|
332
|
+
backgroundDialog: document.getElementById("background-dialog"),
|
|
333
|
+
backgroundFeedback: document.getElementById("background-feedback"),
|
|
334
|
+
backgroundCancel: document.getElementById("background-cancel"),
|
|
335
|
+
backgroundEnable: document.getElementById("background-enable"),
|
|
255
336
|
};
|
|
256
337
|
|
|
338
|
+
if (el.backgroundCancel) {
|
|
339
|
+
el.backgroundCancel.addEventListener("click", () => { void answerBackgroundPrompt(false); });
|
|
340
|
+
}
|
|
341
|
+
if (el.backgroundEnable) {
|
|
342
|
+
el.backgroundEnable.addEventListener("click", () => { void answerBackgroundPrompt(true); });
|
|
343
|
+
}
|
|
344
|
+
if (el.backgroundDialog) {
|
|
345
|
+
el.backgroundDialog.addEventListener("cancel", (event) => {
|
|
346
|
+
event.preventDefault();
|
|
347
|
+
void answerBackgroundPrompt(false);
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
|
|
257
351
|
const state = {
|
|
258
352
|
overview: null,
|
|
259
353
|
// { kind: "live", key } | { kind: "file", id } | { kind: "new", cwd } |
|
|
@@ -272,6 +366,7 @@ const state = {
|
|
|
272
366
|
fileData: null,
|
|
273
367
|
// Last loaded read-only transcript from another agent.
|
|
274
368
|
agentData: null,
|
|
369
|
+
agentMigration: null,
|
|
275
370
|
// { key, path } | null — the working directory the sidebar is scoped to.
|
|
276
371
|
workspace: storedWorkspace(),
|
|
277
372
|
// Plan / usage card visibility and the plan it was opened for.
|
|
@@ -746,12 +841,14 @@ function openStream(key) {
|
|
|
746
841
|
|
|
747
842
|
source.onopen = () => setStatus("live", "live");
|
|
748
843
|
source.onerror = () => {
|
|
749
|
-
// EventSource retries automatically;
|
|
844
|
+
// EventSource retries automatically; wait for a fresh snapshot to flash.
|
|
845
|
+
el.header?.classList.remove("is-thinking");
|
|
750
846
|
setStatus("reconnecting…", "offline");
|
|
751
847
|
};
|
|
752
848
|
}
|
|
753
849
|
|
|
754
850
|
function closeStream() {
|
|
851
|
+
el.header?.classList.remove("is-thinking");
|
|
755
852
|
if (state.eventSource) {
|
|
756
853
|
state.eventSource.close();
|
|
757
854
|
state.eventSource = null;
|
|
@@ -865,17 +962,37 @@ async function continueSession(id, selection = preferences.selectionForFile(id))
|
|
|
865
962
|
|
|
866
963
|
function setComposerMode(mode) {
|
|
867
964
|
state.composerMode = mode;
|
|
965
|
+
el.agentActions.classList.add("hidden");
|
|
868
966
|
el.composer.classList.remove("hidden");
|
|
869
967
|
el.input.placeholder = mode === "resume"
|
|
870
968
|
? tr("Continue this conversation…")
|
|
871
969
|
: mode === "new"
|
|
872
970
|
? tr("Message Neutrino in this folder…")
|
|
873
971
|
: tr("Message Neutrino…");
|
|
972
|
+
renderComposerAction();
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
function canPauseFromComposer() {
|
|
976
|
+
const selection = state.selection;
|
|
977
|
+
const snapshot = state.snapshot;
|
|
978
|
+
return Boolean(selection?.kind === "live" && snapshot?.key === selection.key
|
|
979
|
+
&& snapshot.busy && !snapshot.paused && !el.input.value.trim());
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
function renderComposerAction() {
|
|
983
|
+
const pause = canPauseFromComposer();
|
|
984
|
+
const label = pause ? "Pause" : "Send message";
|
|
985
|
+
el.send.classList.toggle("is-pause", pause);
|
|
986
|
+
el.send.setAttribute("data-i18n-title", label);
|
|
987
|
+
el.send.setAttribute("data-i18n-aria-label", label);
|
|
988
|
+
el.send.title = tr(label);
|
|
989
|
+
el.send.setAttribute("aria-label", tr(label));
|
|
874
990
|
}
|
|
875
991
|
|
|
876
992
|
function hideComposer() {
|
|
877
993
|
preferences.clear();
|
|
878
994
|
el.composer.classList.add("hidden");
|
|
995
|
+
el.agentActions.classList.add("hidden");
|
|
879
996
|
renderApproval(null);
|
|
880
997
|
}
|
|
881
998
|
|
|
@@ -1027,6 +1144,8 @@ function decideApproval(approved) {
|
|
|
1027
1144
|
function renderLive() {
|
|
1028
1145
|
const snapshot = state.snapshot;
|
|
1029
1146
|
if (!snapshot) return;
|
|
1147
|
+
renderComposerAction();
|
|
1148
|
+
el.header?.classList.toggle("is-thinking", Boolean(snapshot.busy && !snapshot.paused && !snapshot.approval));
|
|
1030
1149
|
state.fileData = null;
|
|
1031
1150
|
state.agentData = null;
|
|
1032
1151
|
el.title.textContent = snapshot.title || tr("New conversation");
|
|
@@ -1393,6 +1512,11 @@ async function postAction(key, action, body) {
|
|
|
1393
1512
|
}
|
|
1394
1513
|
}
|
|
1395
1514
|
|
|
1515
|
+
function activateComposer() {
|
|
1516
|
+
if (canPauseFromComposer()) return postAction(state.selection.key, "pause");
|
|
1517
|
+
return sendMessage();
|
|
1518
|
+
}
|
|
1519
|
+
|
|
1396
1520
|
async function sendMessage() {
|
|
1397
1521
|
const selection = state.selection;
|
|
1398
1522
|
if (!selection) return;
|
|
@@ -1444,6 +1568,7 @@ async function sendMessage() {
|
|
|
1444
1568
|
function autosize() {
|
|
1445
1569
|
el.input.style.height = "auto";
|
|
1446
1570
|
el.input.style.height = `${Math.min(el.input.scrollHeight, 220)}px`;
|
|
1571
|
+
renderComposerAction();
|
|
1447
1572
|
}
|
|
1448
1573
|
|
|
1449
1574
|
// ------------------------------------------------------- workspaces & dialogs
|
|
@@ -2192,7 +2317,7 @@ function renderAgentList(groups) {
|
|
|
2192
2317
|
"</span>";
|
|
2193
2318
|
item.addEventListener("click", () => {
|
|
2194
2319
|
el.agentsDialog.close();
|
|
2195
|
-
void selectAgentSession(group.id, session.id, session.title || "");
|
|
2320
|
+
void selectAgentSession(group.id, session.id, session.title || "", group.cwd);
|
|
2196
2321
|
});
|
|
2197
2322
|
section.appendChild(item);
|
|
2198
2323
|
}
|
|
@@ -2211,7 +2336,7 @@ async function openAgentsDialog() {
|
|
|
2211
2336
|
const response = await apiFetch(`/api/agents/${agent.id}/sessions?cwd=${encodeURIComponent(cwd)}`, { cache: "no-store" });
|
|
2212
2337
|
const data = await response.json().catch(() => ({}));
|
|
2213
2338
|
if (!response.ok) throw new Error(apiError(data, `HTTP ${response.status}`));
|
|
2214
|
-
return { ...agent, sessions: Array.isArray(data.sessions) ? data.sessions : [] };
|
|
2339
|
+
return { ...agent, cwd: data.cwd || cwd, sessions: Array.isArray(data.sessions) ? data.sessions : [] };
|
|
2215
2340
|
}));
|
|
2216
2341
|
el.agentsFeedback.textContent = "";
|
|
2217
2342
|
renderAgentList(groups);
|
|
@@ -2238,11 +2363,53 @@ function renderAgentView(data) {
|
|
|
2238
2363
|
renderApproval(null);
|
|
2239
2364
|
renderPlanCard(null);
|
|
2240
2365
|
hideComposer();
|
|
2366
|
+
renderAgentMigration(data);
|
|
2241
2367
|
}
|
|
2242
2368
|
|
|
2243
|
-
|
|
2369
|
+
function renderAgentMigration(data) {
|
|
2370
|
+
el.agentActions.classList.remove("hidden");
|
|
2371
|
+
el.agentMigrate.disabled = Boolean(state.agentMigration) || !data.cwd || !data.messages?.length;
|
|
2372
|
+
el.agentMigrate.textContent = state.agentMigration === state.selection ? tr("Migrating…") : tr("Migrate session");
|
|
2373
|
+
}
|
|
2374
|
+
|
|
2375
|
+
async function migrateSelectedAgentSession() {
|
|
2376
|
+
const selection = state.selection;
|
|
2377
|
+
const source = state.agentData;
|
|
2378
|
+
if (selection?.kind !== "agent" || !source?.cwd || !source.messages?.length || state.agentMigration) return;
|
|
2379
|
+
state.agentMigration = selection;
|
|
2380
|
+
el.agentMigrateFeedback.textContent = "";
|
|
2381
|
+
renderAgentMigration(source);
|
|
2382
|
+
try {
|
|
2383
|
+
const payload = { cwd: source.cwd };
|
|
2384
|
+
if (PRO_MODE) payload.pro_preset = currentProPreset();
|
|
2385
|
+
const response = await apiFetch(`/api/agents/${encodeURIComponent(selection.agent)}/sessions/${encodeURIComponent(selection.id)}/migrate`, {
|
|
2386
|
+
method: "POST",
|
|
2387
|
+
headers: { "Content-Type": "application/json" },
|
|
2388
|
+
body: JSON.stringify(payload),
|
|
2389
|
+
});
|
|
2390
|
+
const data = await response.json().catch(() => ({}));
|
|
2391
|
+
if (!response.ok) throw new Error(apiError(data, `HTTP ${response.status}`));
|
|
2392
|
+
if (state.selection === selection) {
|
|
2393
|
+
setWorkspace({ path: data.cwd });
|
|
2394
|
+
await selectLive(data.key);
|
|
2395
|
+
el.input.focus();
|
|
2396
|
+
}
|
|
2397
|
+
await loadOverview({ silent: true });
|
|
2398
|
+
} catch (error) {
|
|
2399
|
+
if (state.selection === selection) {
|
|
2400
|
+
el.agentMigrateFeedback.textContent = tr("Could not migrate this session: {error}", { error: error.message });
|
|
2401
|
+
}
|
|
2402
|
+
} finally {
|
|
2403
|
+
state.agentMigration = null;
|
|
2404
|
+
if (state.selection?.kind === "agent" && state.agentData) renderAgentMigration(state.agentData);
|
|
2405
|
+
}
|
|
2406
|
+
}
|
|
2407
|
+
|
|
2408
|
+
async function selectAgentSession(agent, id, title, cwd) {
|
|
2409
|
+
const scope = cwd || await agentsScopeCwd();
|
|
2244
2410
|
closeStream();
|
|
2245
|
-
|
|
2411
|
+
const selection = { kind: "agent", agent, id, title, cwd: scope };
|
|
2412
|
+
state.selection = selection;
|
|
2246
2413
|
state.snapshot = null;
|
|
2247
2414
|
state.fileData = null;
|
|
2248
2415
|
state.agentData = null;
|
|
@@ -2254,17 +2421,20 @@ async function selectAgentSession(agent, id, title) {
|
|
|
2254
2421
|
renderApproval(null);
|
|
2255
2422
|
renderPlanCard(null);
|
|
2256
2423
|
hideComposer();
|
|
2424
|
+
el.agentMigrateFeedback.textContent = "";
|
|
2257
2425
|
el.title.textContent = tr("Loading…");
|
|
2258
2426
|
el.sub.textContent = title || "";
|
|
2259
2427
|
resetMessagePane(tr("Loading…"));
|
|
2260
2428
|
setConversationView(true);
|
|
2261
2429
|
try {
|
|
2262
|
-
const response = await apiFetch(`/api/agents/${encodeURIComponent(agent)}/sessions/${encodeURIComponent(id)}`, { cache: "no-store" });
|
|
2430
|
+
const response = await apiFetch(`/api/agents/${encodeURIComponent(agent)}/sessions/${encodeURIComponent(id)}?cwd=${encodeURIComponent(scope)}`, { cache: "no-store" });
|
|
2263
2431
|
const data = await response.json().catch(() => ({}));
|
|
2432
|
+
if (state.selection !== selection) return;
|
|
2264
2433
|
if (!response.ok) throw new Error(apiError(data, `HTTP ${response.status}`));
|
|
2265
|
-
state.agentData = { ...data, agent, cwd:
|
|
2434
|
+
state.agentData = { ...data, agent, cwd: data.cwd || scope };
|
|
2266
2435
|
renderAgentView(state.agentData);
|
|
2267
2436
|
} catch (error) {
|
|
2437
|
+
if (state.selection !== selection) return;
|
|
2268
2438
|
el.title.textContent = title || tr("Conversation");
|
|
2269
2439
|
el.sub.textContent = tr("Could not load this session: {error}", { error: error.message });
|
|
2270
2440
|
resetMessagePane(tr("Could not load this session: {error}", { error: error.message }));
|
|
@@ -2505,7 +2675,7 @@ function initializeProWorkspace() {
|
|
|
2505
2675
|
|
|
2506
2676
|
el.refresh.addEventListener("click", () => loadOverview());
|
|
2507
2677
|
el.back.addEventListener("click", () => setConversationView(false));
|
|
2508
|
-
el.send.addEventListener("click",
|
|
2678
|
+
el.send.addEventListener("click", activateComposer);
|
|
2509
2679
|
el.input.addEventListener("input", autosize);
|
|
2510
2680
|
el.input.addEventListener("keydown", (event) => {
|
|
2511
2681
|
if (event.key === "Enter" && !event.shiftKey) {
|
|
@@ -2528,6 +2698,7 @@ el.workspaceAdd.addEventListener("click", () => {
|
|
|
2528
2698
|
openFolderDialog({ start: state.workspace?.path || state.workspaces?.cwd || "", onPick: (path) => openWorkspace(path) });
|
|
2529
2699
|
});
|
|
2530
2700
|
el.agentsOpen.addEventListener("click", () => openAgentsDialog());
|
|
2701
|
+
el.agentMigrate.addEventListener("click", () => migrateSelectedAgentSession());
|
|
2531
2702
|
el.agentsClose.addEventListener("click", () => el.agentsDialog.close());
|
|
2532
2703
|
el.agentsDialog.addEventListener("cancel", (event) => {
|
|
2533
2704
|
event.preventDefault();
|
|
@@ -2609,10 +2780,14 @@ window.addEventListener("beforeunload", closeStream);
|
|
|
2609
2780
|
initializeLanguage();
|
|
2610
2781
|
initializeProWorkspace();
|
|
2611
2782
|
preferences.load()
|
|
2612
|
-
.then((config) => {
|
|
2783
|
+
.then(async (config) => {
|
|
2613
2784
|
const language = config?.settings?.language;
|
|
2614
|
-
if (language && language !== uiLanguage())
|
|
2615
|
-
|
|
2785
|
+
if (language && language !== uiLanguage()) {
|
|
2786
|
+
await changeInterfaceLanguage(language, { persist: false });
|
|
2787
|
+
}
|
|
2788
|
+
// Offer background residency only after the interface language is settled,
|
|
2789
|
+
// so the invitation is never briefly shown in the wrong language.
|
|
2790
|
+
maybePromptBackground(config);
|
|
2616
2791
|
})
|
|
2617
2792
|
.catch(error => console.error(error));
|
|
2618
2793
|
loadOverview().then(() => {
|
package/web/i18n.js
CHANGED
|
@@ -132,6 +132,12 @@ const ZH = {
|
|
|
132
132
|
"Working directory": "工作目录",
|
|
133
133
|
"Working directories": "工作目录",
|
|
134
134
|
"Other agents": "其他 Agent",
|
|
135
|
+
"Migrate session": "迁移会话",
|
|
136
|
+
"Migrating…": "迁移中…",
|
|
137
|
+
"Copy this history into a new Neutrino conversation in the same folder": "将历史复制为同一工作目录下的新 Neutrino 会话",
|
|
138
|
+
"Could not migrate this session: {error}": "无法迁移此会话:{error}",
|
|
139
|
+
"session not found in this working directory": "此工作目录中未找到该会话",
|
|
140
|
+
"this session has no messages to migrate": "此会话没有可迁移的消息",
|
|
135
141
|
"Read-only sessions recorded by other coding agents in this working directory.": "其他编码 Agent 在此工作目录中记录的只读会话。",
|
|
136
142
|
"No saved sessions from other agents in this folder.": "此文件夹中暂无其他 Agent 保存的会话。",
|
|
137
143
|
"Claude Code": "Claude Code",
|
|
@@ -298,6 +304,14 @@ const ZH = {
|
|
|
298
304
|
"Extreme-risk auto-approval (del class, 3 separate review rounds)": "极高风险自动审批(删除类命令,独立审查 3 轮)",
|
|
299
305
|
"Sound alerts (turn finished, approval waiting)": "声音提醒(回合结束、等待审批)",
|
|
300
306
|
"Start the Web console with the terminal": "随终端启动 Web 控制台(下次启动生效)",
|
|
307
|
+
"Background residency": "后台常驻",
|
|
308
|
+
"Run the Web console in the background and keep browser tasks alive after the CLI exits": "让 Web 控制台在后台运行,CLI 退出后浏览器中的任务继续执行",
|
|
309
|
+
"Off by default. Changes take effect immediately, including for running browser tasks. The regular and Pro workspaces share this setting. Terminal conversations still end when the terminal closes.": "默认关闭。切换立即生效,正在运行的浏览器任务也会受到保护;普通模式与 Pro 模式共用此设置。终端内的对话仍会在终端关闭时结束。",
|
|
310
|
+
"Enable background residency to avoid accidentally interrupting tasks": "建议开启后台常驻,避免误触中断任务",
|
|
311
|
+
"Enable": "开启",
|
|
312
|
+
"Background residency is on. Browser tasks keep running after the terminal closes.": "后台常驻已立即开启。关闭终端后,浏览器任务会继续执行。",
|
|
313
|
+
"Background residency is off. Current tasks can finish before the console closes.": "后台常驻已立即关闭。控制台退出前会等待当前任务完成。",
|
|
314
|
+
"Background residency could not be applied. Restart the console and try again.": "后台常驻未能生效,请重启控制台后重试。",
|
|
301
315
|
"Safety determines automatic approval; task relevance only blocks unanimously confirmed, extreme mismatches. Delete-class automatic approval also requires this setting.": "自动审批取决于安全性;任务相关性只有在两位审查者一致确认极端不匹配时才会阻止执行。删除类命令的自动审批还需要开启此设置。",
|
|
302
316
|
"Web access": "Web 访问",
|
|
303
317
|
"Allow access from other devices (LAN, 0.0.0.0)": "允许其他设备访问(局域网,0.0.0.0)",
|
package/web/index.html
CHANGED
|
@@ -128,6 +128,11 @@
|
|
|
128
128
|
|
|
129
129
|
<div id="approval" class="approval hidden" role="alert" aria-live="assertive"></div>
|
|
130
130
|
|
|
131
|
+
<footer id="agent-actions" class="agent-actions hidden">
|
|
132
|
+
<p id="agent-migrate-feedback" class="form-feedback error" role="status" aria-live="polite"></p>
|
|
133
|
+
<button id="agent-migrate" class="control primary" type="button" data-i18n="Migrate session" title="Copy this history into a new Neutrino conversation in the same folder" data-i18n-title="Copy this history into a new Neutrino conversation in the same folder">Migrate session</button>
|
|
134
|
+
</footer>
|
|
135
|
+
|
|
131
136
|
<footer id="composer" class="composer hidden">
|
|
132
137
|
<div class="composer-inner">
|
|
133
138
|
<div class="composer-card">
|
|
@@ -157,7 +162,7 @@
|
|
|
157
162
|
</div>
|
|
158
163
|
</details>
|
|
159
164
|
<button id="send" class="primary" type="button" title="Send message" aria-label="Send message" data-i18n-title="Send message" data-i18n-aria-label="Send message">
|
|
160
|
-
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 19V5M5 12l7-7 7 7" /></svg>
|
|
165
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path class="send-icon" d="M12 19V5M5 12l7-7 7 7" /><rect class="pause-icon" x="5" y="5" width="14" height="14" rx="2" fill="currentColor" stroke="none" /></svg>
|
|
161
166
|
</button>
|
|
162
167
|
</div>
|
|
163
168
|
<div class="composer-hint">
|
|
@@ -222,6 +227,9 @@
|
|
|
222
227
|
<label class="checkbox-label"><input id="settings-sound" type="checkbox" /> <span data-i18n="Sound alerts (turn finished, approval waiting)">Sound alerts (turn finished, approval waiting)</span></label>
|
|
223
228
|
<label class="checkbox-label"><input id="settings-web-console" type="checkbox" /> <span data-i18n="Start the Web console with the terminal">Start the Web console with the terminal</span></label>
|
|
224
229
|
<p class="settings-note" data-i18n="Safety determines automatic approval; task relevance only blocks unanimously confirmed, extreme mismatches. Delete-class automatic approval also requires this setting.">Safety determines automatic approval; task relevance only blocks unanimously confirmed, extreme mismatches. Delete-class automatic approval also requires this setting.</p>
|
|
230
|
+
<h3 data-i18n="Background residency">Background residency</h3>
|
|
231
|
+
<label class="checkbox-label"><input id="settings-background" type="checkbox" /> <span data-i18n="Run the Web console in the background and keep browser tasks alive after the CLI exits">Run the Web console in the background and keep browser tasks alive after the CLI exits</span></label>
|
|
232
|
+
<p class="settings-note" data-i18n="Off by default. Changes take effect immediately, including for running browser tasks. The regular and Pro workspaces share this setting. Terminal conversations still end when the terminal closes.">Off by default. Changes take effect immediately, including for running browser tasks. The regular and Pro workspaces share this setting. Terminal conversations still end when the terminal closes.</p>
|
|
225
233
|
<h3 data-i18n="Web access">Web access</h3>
|
|
226
234
|
<label class="checkbox-label"><input id="settings-web-lan" type="checkbox" /> <span data-i18n="Allow access from other devices (LAN, 0.0.0.0)">Allow access from other devices (LAN, 0.0.0.0)</span></label>
|
|
227
235
|
<p class="settings-note" data-i18n="Binds the Web console to 0.0.0.0 so other devices on your network can open it, like --lan. Takes effect immediately; turning it off returns the console to 127.0.0.1. Keep the Web password on: anyone who can reach this port can drive the agent.">Binds the Web console to 0.0.0.0 so other devices on your network can open it, like --lan. Takes effect immediately; turning it off returns the console to 127.0.0.1. Keep the Web password on: anyone who can reach this port can drive the agent.</p>
|
|
@@ -239,6 +247,27 @@
|
|
|
239
247
|
</form>
|
|
240
248
|
</dialog>
|
|
241
249
|
|
|
250
|
+
<dialog id="background-dialog" class="sheet-dialog background-dialog" aria-labelledby="background-title">
|
|
251
|
+
<div class="sheet background-sheet">
|
|
252
|
+
<header class="sheet-header background-header">
|
|
253
|
+
<span class="background-mark" aria-hidden="true">
|
|
254
|
+
<svg viewBox="0 0 24 24" fill="none" aria-hidden="true">
|
|
255
|
+
<ellipse cx="12" cy="12" rx="9.4" ry="3.8" transform="rotate(-30 12 12)" stroke="currentColor" stroke-width="1.7" />
|
|
256
|
+
<circle cx="12" cy="12" r="3.1" stroke="currentColor" stroke-width="1.7" />
|
|
257
|
+
</svg>
|
|
258
|
+
</span>
|
|
259
|
+
<div class="background-copy">
|
|
260
|
+
<h2 id="background-title" data-i18n="Enable background residency to avoid accidentally interrupting tasks">Enable background residency to avoid accidentally interrupting tasks</h2>
|
|
261
|
+
</div>
|
|
262
|
+
</header>
|
|
263
|
+
<p id="background-feedback" class="form-feedback background-feedback" role="status" aria-live="polite"></p>
|
|
264
|
+
<footer class="sheet-actions background-actions">
|
|
265
|
+
<button id="background-cancel" class="control" type="button" data-i18n="Cancel">Cancel</button>
|
|
266
|
+
<button id="background-enable" class="primary" type="button" data-i18n="Enable" autofocus>Enable</button>
|
|
267
|
+
</footer>
|
|
268
|
+
</div>
|
|
269
|
+
</dialog>
|
|
270
|
+
|
|
242
271
|
<dialog id="new-chat-dialog" class="sheet-dialog" aria-labelledby="new-chat-title">
|
|
243
272
|
<form id="new-chat-form" class="sheet">
|
|
244
273
|
<header class="sheet-header">
|
package/web/settings.js
CHANGED
|
@@ -7,7 +7,7 @@ export function createSettingsUI({ apiFetch, applyModel, onLanguageChange, proMo
|
|
|
7
7
|
"settings-open", "settings-dialog", "settings-form", "settings-close", "settings-cancel", "settings-save", "settings-feedback",
|
|
8
8
|
"default-provider", "default-model", "default-intensity", "settings-context", "settings-compress-note", "settings-reviewer",
|
|
9
9
|
"settings-extreme", "settings-sound", "settings-password", "settings-password-confirm",
|
|
10
|
-
"settings-language", "settings-plan", "settings-approval", "settings-web-console", "settings-web-lan",
|
|
10
|
+
"settings-language", "settings-plan", "settings-approval", "settings-web-console", "settings-web-lan", "settings-background",
|
|
11
11
|
"settings-pro", "settings-pro-preset", "settings-pro-depth"];
|
|
12
12
|
const el = Object.fromEntries(ids.map(id => [id, document.getElementById(id)]));
|
|
13
13
|
let config = null;
|
|
@@ -21,6 +21,10 @@ export function createSettingsUI({ apiFetch, applyModel, onLanguageChange, proMo
|
|
|
21
21
|
// saved, so editing another setting never pins an interface that came from
|
|
22
22
|
// --lan/--host on the command line.
|
|
23
23
|
let lanInitial = false;
|
|
24
|
+
// Same idea for background residency: the dialog must not overwrite a value
|
|
25
|
+
// changed from the terminal while it was open.
|
|
26
|
+
let backgroundInitial = false;
|
|
27
|
+
let backgroundChanging = false;
|
|
24
28
|
|
|
25
29
|
async function json(path, body) {
|
|
26
30
|
const response = await apiFetch(path, body === undefined ? { cache: "no-store" } : {
|
|
@@ -178,8 +182,45 @@ export function createSettingsUI({ apiFetch, applyModel, onLanguageChange, proMo
|
|
|
178
182
|
el["settings-sound"].checked = config.settings.sound_alerts !== false;
|
|
179
183
|
el["settings-web-console"].checked = config.settings.web_console !== false;
|
|
180
184
|
el["settings-web-lan"].checked = config.settings.web_lan === true;
|
|
185
|
+
el["settings-background"].checked = config.settings.background_running === true;
|
|
186
|
+
backgroundInitial = el["settings-background"].checked;
|
|
181
187
|
lanInitial = el["settings-web-lan"].checked;
|
|
182
188
|
}
|
|
189
|
+
el["settings-background"].addEventListener("change", async () => {
|
|
190
|
+
if (!config || backgroundChanging) return;
|
|
191
|
+
const enabled = el["settings-background"].checked;
|
|
192
|
+
backgroundChanging = true;
|
|
193
|
+
el["settings-background"].disabled = true;
|
|
194
|
+
el["settings-save"].disabled = true;
|
|
195
|
+
message(el["settings-feedback"], t("Applying…"));
|
|
196
|
+
try {
|
|
197
|
+
const updated = await json("/api/settings", { settings: { background: enabled } });
|
|
198
|
+
if (updated.settings.background_running !== enabled) {
|
|
199
|
+
throw new Error(t("Background residency could not be applied. Restart the console and try again."));
|
|
200
|
+
}
|
|
201
|
+
config = updated;
|
|
202
|
+
backgroundInitial = config.settings.background_running === true;
|
|
203
|
+
el["settings-background"].checked = backgroundInitial;
|
|
204
|
+
onCatalogChange?.(config);
|
|
205
|
+
message(el["settings-feedback"], t(enabled
|
|
206
|
+
? "Background residency is on. Browser tasks keep running after the terminal closes."
|
|
207
|
+
: "Background residency is off. Current tasks can finish before the console closes."));
|
|
208
|
+
} catch (error) {
|
|
209
|
+
// A response can be lost after the service applied the change. Ask the
|
|
210
|
+
// service for its actual state before putting the switch back.
|
|
211
|
+
try {
|
|
212
|
+
config = await json("/api/settings");
|
|
213
|
+
backgroundInitial = config.settings.background_running === true;
|
|
214
|
+
} catch { /* Keep the last confirmed value and show the error. */ }
|
|
215
|
+
el["settings-background"].checked = backgroundInitial;
|
|
216
|
+
message(el["settings-feedback"], t(error.message), true);
|
|
217
|
+
} finally {
|
|
218
|
+
backgroundChanging = false;
|
|
219
|
+
el["settings-background"].disabled = false;
|
|
220
|
+
el["settings-save"].disabled = false;
|
|
221
|
+
}
|
|
222
|
+
});
|
|
223
|
+
|
|
183
224
|
function renderContextWindow() {
|
|
184
225
|
const provider = config.providers.find(entry => entry.name === el["default-provider"].value);
|
|
185
226
|
const model = provider?.models.find(entry => entry.name === el["default-model"].value);
|
|
@@ -260,6 +301,7 @@ export function createSettingsUI({ apiFetch, applyModel, onLanguageChange, proMo
|
|
|
260
301
|
el["settings-save"].disabled = true;
|
|
261
302
|
const wasLan = config?.settings?.web_lan === true;
|
|
262
303
|
const lanChanged = el["settings-web-lan"].checked !== lanInitial;
|
|
304
|
+
const backgroundChanged = el["settings-background"].checked !== backgroundInitial;
|
|
263
305
|
const selection = { provider: el["default-provider"].value, model: el["default-model"].value, intensity: el["default-intensity"].value };
|
|
264
306
|
const settings = {
|
|
265
307
|
...(!proMode ? {
|
|
@@ -275,6 +317,7 @@ export function createSettingsUI({ apiFetch, applyModel, onLanguageChange, proMo
|
|
|
275
317
|
sound_alerts: el["settings-sound"].checked,
|
|
276
318
|
web_console: el["settings-web-console"].checked,
|
|
277
319
|
...(lanChanged ? { web_lan: el["settings-web-lan"].checked } : {}),
|
|
320
|
+
...(backgroundChanged ? { background: el["settings-background"].checked } : {}),
|
|
278
321
|
language: el["settings-language"].value,
|
|
279
322
|
approval_mode: el["settings-approval"].value,
|
|
280
323
|
};
|
|
@@ -289,16 +332,24 @@ export function createSettingsUI({ apiFetch, applyModel, onLanguageChange, proMo
|
|
|
289
332
|
// server bound and (when available) the address to open elsewhere.
|
|
290
333
|
el["settings-web-lan"].checked = config.settings.web_lan === true;
|
|
291
334
|
lanInitial = el["settings-web-lan"].checked;
|
|
335
|
+
el["settings-background"].checked = config.settings.background_running === true;
|
|
336
|
+
backgroundInitial = el["settings-background"].checked;
|
|
292
337
|
const urls = Array.isArray(config.settings.web_urls) ? config.settings.web_urls : [];
|
|
293
338
|
const lanNote = config.settings.web_lan === wasLan ? "" : config.settings.web_lan
|
|
294
339
|
? urls.length
|
|
295
340
|
? t("LAN access is on. Open {urls} from another device.", { urls: urls.join(" · ") })
|
|
296
341
|
: t("LAN access is on, but no network address was found. Only this machine can connect.")
|
|
297
342
|
: t("LAN access is off. Only this machine can connect.");
|
|
343
|
+
const backgroundNote = !backgroundChanged
|
|
344
|
+
? ""
|
|
345
|
+
: config.settings.background
|
|
346
|
+
? t("Background residency is on. Browser tasks keep running after the terminal closes.")
|
|
347
|
+
: t("Background residency is off. Current tasks can finish before the console closes.");
|
|
298
348
|
const savedText = password
|
|
299
349
|
? t("Settings saved. Web password updated permanently.")
|
|
300
350
|
: t("Settings saved.");
|
|
301
|
-
|
|
351
|
+
const notes = [lanNote, backgroundNote].filter(Boolean).join(" ");
|
|
352
|
+
message(el["settings-feedback"], notes ? `${savedText} ${notes}` : savedText);
|
|
302
353
|
} catch (error) { message(el["settings-feedback"], t(error.message), true); }
|
|
303
354
|
finally { el["settings-save"].disabled = false; }
|
|
304
355
|
});
|
package/web/style.css
CHANGED
|
@@ -597,6 +597,16 @@ svg {
|
|
|
597
597
|
border-bottom: 1px solid var(--border-soft);
|
|
598
598
|
}
|
|
599
599
|
|
|
600
|
+
/* Match the CLI's 500ms thinking phases across the full title bar. */
|
|
601
|
+
.main-header.is-thinking {
|
|
602
|
+
background-color: #28393d;
|
|
603
|
+
animation: header-thinking 1s steps(1, end) infinite;
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
.main-header.is-thinking .main-title h1 {
|
|
607
|
+
color: #99d9ea;
|
|
608
|
+
}
|
|
609
|
+
|
|
600
610
|
.main-title {
|
|
601
611
|
flex: 1;
|
|
602
612
|
min-width: 0;
|
|
@@ -1372,6 +1382,26 @@ svg {
|
|
|
1372
1382
|
|
|
1373
1383
|
/* --------------------------------------------------------------- composer */
|
|
1374
1384
|
|
|
1385
|
+
.agent-actions {
|
|
1386
|
+
display: flex;
|
|
1387
|
+
flex-shrink: 0;
|
|
1388
|
+
align-items: center;
|
|
1389
|
+
justify-content: flex-end;
|
|
1390
|
+
gap: 12px;
|
|
1391
|
+
padding: 12px clamp(16px, 4vw, 40px) max(18px, env(safe-area-inset-bottom));
|
|
1392
|
+
}
|
|
1393
|
+
|
|
1394
|
+
.agent-actions .form-feedback {
|
|
1395
|
+
flex: 1;
|
|
1396
|
+
min-width: 0;
|
|
1397
|
+
margin: 0;
|
|
1398
|
+
overflow-wrap: anywhere;
|
|
1399
|
+
}
|
|
1400
|
+
|
|
1401
|
+
.agent-actions button {
|
|
1402
|
+
flex-shrink: 0;
|
|
1403
|
+
}
|
|
1404
|
+
|
|
1375
1405
|
.composer {
|
|
1376
1406
|
flex-shrink: 0;
|
|
1377
1407
|
padding: 0 clamp(16px, 4vw, 40px) 18px;
|
|
@@ -1554,6 +1584,15 @@ svg {
|
|
|
1554
1584
|
height: 18px;
|
|
1555
1585
|
}
|
|
1556
1586
|
|
|
1587
|
+
#send .pause-icon,
|
|
1588
|
+
#send.is-pause .send-icon {
|
|
1589
|
+
display: none;
|
|
1590
|
+
}
|
|
1591
|
+
|
|
1592
|
+
#send.is-pause .pause-icon {
|
|
1593
|
+
display: block;
|
|
1594
|
+
}
|
|
1595
|
+
|
|
1557
1596
|
#send:hover {
|
|
1558
1597
|
background: var(--primary-hover);
|
|
1559
1598
|
}
|
|
@@ -2659,6 +2698,56 @@ select option {
|
|
|
2659
2698
|
flex: 1;
|
|
2660
2699
|
}
|
|
2661
2700
|
|
|
2701
|
+
/* One-time background-residency invitation. The orbit badge is the regular
|
|
2702
|
+
console's own mark, so the offer reads as part of the same product. */
|
|
2703
|
+
.background-dialog {
|
|
2704
|
+
width: min(460px, calc(100vw - 24px));
|
|
2705
|
+
}
|
|
2706
|
+
|
|
2707
|
+
.background-header {
|
|
2708
|
+
align-items: center;
|
|
2709
|
+
justify-content: flex-start;
|
|
2710
|
+
gap: 14px;
|
|
2711
|
+
border-bottom: 0;
|
|
2712
|
+
padding-bottom: 18px;
|
|
2713
|
+
}
|
|
2714
|
+
|
|
2715
|
+
.background-mark {
|
|
2716
|
+
display: flex;
|
|
2717
|
+
flex: none;
|
|
2718
|
+
align-items: center;
|
|
2719
|
+
justify-content: center;
|
|
2720
|
+
width: 46px;
|
|
2721
|
+
height: 46px;
|
|
2722
|
+
color: var(--accent);
|
|
2723
|
+
background: var(--surface-1);
|
|
2724
|
+
border: 1px solid var(--border-soft);
|
|
2725
|
+
border-radius: 13px;
|
|
2726
|
+
}
|
|
2727
|
+
|
|
2728
|
+
.background-mark svg {
|
|
2729
|
+
width: 24px;
|
|
2730
|
+
height: 24px;
|
|
2731
|
+
}
|
|
2732
|
+
|
|
2733
|
+
.background-copy {
|
|
2734
|
+
min-width: 0;
|
|
2735
|
+
}
|
|
2736
|
+
|
|
2737
|
+
.background-copy h2 {
|
|
2738
|
+
font-size: 15px;
|
|
2739
|
+
line-height: 1.55;
|
|
2740
|
+
font-weight: 600;
|
|
2741
|
+
}
|
|
2742
|
+
|
|
2743
|
+
.background-feedback:empty {
|
|
2744
|
+
display: none;
|
|
2745
|
+
}
|
|
2746
|
+
|
|
2747
|
+
.background-actions {
|
|
2748
|
+
border-top: 1px solid var(--border-soft);
|
|
2749
|
+
}
|
|
2750
|
+
|
|
2662
2751
|
/* Folder picker */
|
|
2663
2752
|
.folder-sheet {
|
|
2664
2753
|
max-height: calc(100vh - 32px);
|
|
@@ -3032,6 +3121,18 @@ html.pro-transitioning body::before {
|
|
|
3032
3121
|
|
|
3033
3122
|
/* -------------------------------------------------------------- animations */
|
|
3034
3123
|
|
|
3124
|
+
@keyframes header-thinking {
|
|
3125
|
+
0%,
|
|
3126
|
+
100% {
|
|
3127
|
+
background-color: #28393d;
|
|
3128
|
+
border-bottom-color: rgba(153, 217, 234, 0.45);
|
|
3129
|
+
}
|
|
3130
|
+
50% {
|
|
3131
|
+
background-color: var(--bg);
|
|
3132
|
+
border-bottom-color: var(--border-soft);
|
|
3133
|
+
}
|
|
3134
|
+
}
|
|
3135
|
+
|
|
3035
3136
|
@keyframes blink {
|
|
3036
3137
|
0%,
|
|
3037
3138
|
49% {
|