@xingjisoft/neutrino 0.3.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 -549
- package/assets/neutrino.ico +0 -0
- package/assets/neutrino.svg +7 -0
- package/assets/pro/backend_output_model.txt +5 -0
- package/assets/pro/claude-module-prompt.txt +65 -0
- package/assets/pro/common.txt +9 -0
- package/assets/pro/confirm_model.txt +5 -0
- package/assets/pro/front_output_model.txt +9 -0
- package/assets/pro/genimage_model.txt +1 -0
- package/assets/pro/main_model.txt +5 -0
- package/assets/pro/model-template.json +376 -0
- package/assets/pro/modeling_model.txt +5 -0
- package/assets/pro/plan_model.txt +8 -0
- package/assets/pro/thinking_model.txt +3 -0
- package/dist/agents.js +22 -3
- package/dist/app.js +199 -26
- package/dist/background.js +358 -0
- package/dist/bin.js +184 -44
- package/dist/browser.js +57 -0
- package/dist/config.js +174 -26
- package/dist/header.js +2 -1
- package/dist/index.js +1 -1
- package/dist/llm/approval.js +6 -2
- package/dist/llm/client.js +197 -28
- package/dist/llm/subagent.js +37 -1
- package/dist/llm/tools.js +3 -1
- package/dist/migration.js +33 -0
- package/dist/pro/agents.js +104 -0
- package/dist/pro/config.js +88 -0
- package/dist/pro/preflight.js +172 -0
- package/dist/screens/agentConversation.js +48 -5
- package/dist/screens/agentHistory.js +4 -2
- package/dist/screens/chat.js +14 -8
- package/dist/screens/modelSelect.js +15 -4
- package/dist/screens/settings.js +52 -9
- package/dist/screens/start.js +48 -15
- package/dist/state.js +1 -0
- package/dist/terminal/restore.js +3 -12
- package/dist/tray.js +138 -0
- package/dist/ui.js +58 -0
- package/dist/web/hub.js +26 -4
- package/dist/web/server.js +495 -74
- package/dist/web/service.js +135 -0
- package/dist/web/session.js +278 -25
- package/dist/web/settings.js +97 -24
- package/dist/web/terminalBridge.js +250 -0
- package/package.json +37 -37
- package/web/README.md +150 -16
- package/web/app.js +490 -25
- package/web/i18n.js +396 -345
- package/web/index.html +120 -15
- package/web/neutrino-pro.gif +0 -0
- package/web/neutrino.png +0 -0
- package/web/pro.js +184 -0
- package/web/settings.js +373 -247
- package/web/style.css +398 -0
package/web/app.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createSettingsUI } from "./settings.js";
|
|
2
|
+
import { createProUI } from "./pro.js";
|
|
2
3
|
import { renderMarkdown as renderMarkdownHtml } from "./markdown.js";
|
|
3
4
|
import { turnAnswerIndex, turnFolds, turnPreview, turnRanges } from "./turns.js";
|
|
4
5
|
import { applyTranslations, getLanguage as currentLanguage, locale as currentLocale, setLanguage as setI18nLanguage, t as translate } from "./i18n.js";
|
|
@@ -24,6 +25,37 @@ const REFRESH_MS = 4000;
|
|
|
24
25
|
// Remove credentials left behind by older versions of the UI.
|
|
25
26
|
try { localStorage.removeItem("neutrino.token"); } catch { /* Storage may be blocked. */ }
|
|
26
27
|
|
|
28
|
+
// ------------------------------------------------------------------ pro mode
|
|
29
|
+
//
|
|
30
|
+
// The "Pro" workspace is served at /pro and owns an isolated API under
|
|
31
|
+
// /pro/api, while this page and the login cookie are shared. Detect the mode
|
|
32
|
+
// before the request helpers below so no call can leak between the workspaces.
|
|
33
|
+
const PRO_MODE = (() => {
|
|
34
|
+
try {
|
|
35
|
+
const pathname = String(location?.pathname ?? "");
|
|
36
|
+
return pathname === "/pro" || pathname.startsWith("/pro/");
|
|
37
|
+
} catch {
|
|
38
|
+
return false; // Test sandboxes and unusual embedders have no location.
|
|
39
|
+
}
|
|
40
|
+
})();
|
|
41
|
+
|
|
42
|
+
const API_BASE = PRO_MODE ? "/pro/api" : "/api";
|
|
43
|
+
// Lets Pro-specific chrome (the wider sidebar needed by the longer brand name)
|
|
44
|
+
// apply before the first paint of this module.
|
|
45
|
+
if (PRO_MODE) document.body?.classList.add("pro-mode");
|
|
46
|
+
|
|
47
|
+
/** Point an /api request at the active workspace; other paths are unchanged. */
|
|
48
|
+
function apiPath(path) {
|
|
49
|
+
if (!PRO_MODE) return path;
|
|
50
|
+
const text = String(path);
|
|
51
|
+
return text.startsWith("/api/") ? `${API_BASE}${text.slice(4)}` : path;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Namespace a remembered preference so both workspaces keep their own copy. */
|
|
55
|
+
function modeStorageKey(key) {
|
|
56
|
+
return PRO_MODE ? `neutrino.pro.${key}` : key;
|
|
57
|
+
}
|
|
58
|
+
|
|
27
59
|
// Translation lookup. `translate` is an ES-module import; the browser tests
|
|
28
60
|
// that evaluate this file in a sandbox strip imports, so the helper degrades to
|
|
29
61
|
// the English source text instead of throwing. Interpolation is kept here too.
|
|
@@ -92,7 +124,7 @@ const initialLogin = (() => {
|
|
|
92
124
|
|
|
93
125
|
async function apiFetch(path, init = {}) {
|
|
94
126
|
if (!await initialLogin) throw new Error("Password login required.");
|
|
95
|
-
const response = await fetch(path, { ...init, credentials: "same-origin" });
|
|
127
|
+
const response = await fetch(apiPath(path), { ...init, credentials: "same-origin" });
|
|
96
128
|
if (response.status === 401) {
|
|
97
129
|
showGate();
|
|
98
130
|
throw new Error("Password login required.");
|
|
@@ -115,6 +147,7 @@ function showGate(message = "") {
|
|
|
115
147
|
closeStream();
|
|
116
148
|
setStatus("locked", "offline");
|
|
117
149
|
document.getElementById("settings-dialog").close();
|
|
150
|
+
document.getElementById("background-dialog")?.close?.();
|
|
118
151
|
|
|
119
152
|
// A modal dialog stays above streaming repaints and traps keyboard focus.
|
|
120
153
|
const gate = document.createElement("dialog");
|
|
@@ -152,12 +185,90 @@ function showGate(message = "") {
|
|
|
152
185
|
gate.showModal();
|
|
153
186
|
}
|
|
154
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
|
+
|
|
155
260
|
const el = {
|
|
156
261
|
app: document.getElementById("app"),
|
|
157
262
|
back: document.getElementById("back-to-menu"),
|
|
158
263
|
sessions: document.getElementById("sessions"),
|
|
159
264
|
status: document.getElementById("status"),
|
|
160
265
|
refresh: document.getElementById("refresh"),
|
|
266
|
+
brandLogo: document.getElementById("brand-logo"),
|
|
267
|
+
brandName: document.getElementById("brand-name"),
|
|
268
|
+
proTip: document.getElementById("pro-tip"),
|
|
269
|
+
proTipClose: document.getElementById("pro-tip-close"),
|
|
270
|
+
proExit: document.getElementById("pro-exit"),
|
|
271
|
+
header: document.getElementById("main-header"),
|
|
161
272
|
title: document.getElementById("session-title"),
|
|
162
273
|
sub: document.getElementById("session-sub"),
|
|
163
274
|
controls: document.getElementById("controls"),
|
|
@@ -215,8 +326,28 @@ const el = {
|
|
|
215
326
|
agentsList: document.getElementById("agents-list"),
|
|
216
327
|
agentsFeedback: document.getElementById("agents-feedback"),
|
|
217
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"),
|
|
218
336
|
};
|
|
219
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
|
+
|
|
220
351
|
const state = {
|
|
221
352
|
overview: null,
|
|
222
353
|
// { kind: "live", key } | { kind: "file", id } | { kind: "new", cwd } |
|
|
@@ -235,6 +366,7 @@ const state = {
|
|
|
235
366
|
fileData: null,
|
|
236
367
|
// Last loaded read-only transcript from another agent.
|
|
237
368
|
agentData: null,
|
|
369
|
+
agentMigration: null,
|
|
238
370
|
// { key, path } | null — the working directory the sidebar is scoped to.
|
|
239
371
|
workspace: storedWorkspace(),
|
|
240
372
|
// Plan / usage card visibility and the plan it was opened for.
|
|
@@ -249,13 +381,16 @@ const state = {
|
|
|
249
381
|
};
|
|
250
382
|
|
|
251
383
|
function storedWorkspace() {
|
|
252
|
-
const path = readStorage("neutrino.workspace");
|
|
384
|
+
const path = readStorage(modeStorageKey("neutrino.workspace"));
|
|
253
385
|
if (!path) return null;
|
|
254
386
|
return { key: pathKey(path), path };
|
|
255
387
|
}
|
|
256
388
|
|
|
389
|
+
let proUI = null;
|
|
257
390
|
const preferences = createSettingsUI({
|
|
258
391
|
apiFetch,
|
|
392
|
+
proMode: PRO_MODE,
|
|
393
|
+
onCatalogChange: (catalog) => proUI?.load(catalog),
|
|
259
394
|
onLanguageChange: (language) => changeInterfaceLanguage(language),
|
|
260
395
|
async applyModel(selection) {
|
|
261
396
|
const current = state.selection;
|
|
@@ -276,6 +411,37 @@ const preferences = createSettingsUI({
|
|
|
276
411
|
},
|
|
277
412
|
});
|
|
278
413
|
|
|
414
|
+
if (PRO_MODE && typeof createProUI === "function") {
|
|
415
|
+
proUI = createProUI({
|
|
416
|
+
async changePreset(preset) {
|
|
417
|
+
const current = state.selection;
|
|
418
|
+
if (current?.kind === "live") {
|
|
419
|
+
const response = await apiFetch(`/api/live/${encodeURIComponent(current.key)}/pro-preset`, {
|
|
420
|
+
method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ preset }),
|
|
421
|
+
});
|
|
422
|
+
const body = await response.json();
|
|
423
|
+
if (!response.ok) throw new Error(body.error || `HTTP ${response.status}`);
|
|
424
|
+
if (state.selection?.kind === "live" && state.selection.key === current.key) {
|
|
425
|
+
state.snapshot = body.snapshot;
|
|
426
|
+
renderLive();
|
|
427
|
+
}
|
|
428
|
+
} else if (current?.kind !== "file") {
|
|
429
|
+
const response = await apiFetch("/api/settings", {
|
|
430
|
+
method: "POST", headers: { "Content-Type": "application/json" },
|
|
431
|
+
body: JSON.stringify({ settings: { pro_preset: preset } }),
|
|
432
|
+
});
|
|
433
|
+
const body = await response.json();
|
|
434
|
+
if (!response.ok) throw new Error(body.error || `HTTP ${response.status}`);
|
|
435
|
+
await preferences.load();
|
|
436
|
+
}
|
|
437
|
+
},
|
|
438
|
+
});
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
function currentProPreset() {
|
|
442
|
+
return proUI?.preset() || preferences.catalog?.()?.pro?.preset;
|
|
443
|
+
}
|
|
444
|
+
|
|
279
445
|
// ------------------------------------------------------------------- sounds
|
|
280
446
|
//
|
|
281
447
|
// A turn that ends while the tab is in the background is invisible, so finished
|
|
@@ -395,7 +561,7 @@ function workspaceName(value) {
|
|
|
395
561
|
|
|
396
562
|
function setWorkspace(workspace, { persist = true } = {}) {
|
|
397
563
|
state.workspace = workspace ? { key: pathKey(workspace.path), path: workspace.path } : null;
|
|
398
|
-
if (persist) writeStorage("neutrino.workspace", state.workspace?.path ?? "");
|
|
564
|
+
if (persist) writeStorage(modeStorageKey("neutrino.workspace"), state.workspace?.path ?? "");
|
|
399
565
|
renderWorkspaceChrome();
|
|
400
566
|
renderSidebar();
|
|
401
567
|
}
|
|
@@ -649,7 +815,7 @@ async function selectLive(key) {
|
|
|
649
815
|
|
|
650
816
|
function openStream(key) {
|
|
651
817
|
closeStream();
|
|
652
|
-
const source = new EventSource(`/api/live/${encodeURIComponent(key)}/stream`);
|
|
818
|
+
const source = new EventSource(apiPath(`/api/live/${encodeURIComponent(key)}/stream`));
|
|
653
819
|
state.eventSource = source;
|
|
654
820
|
|
|
655
821
|
source.addEventListener("snapshot", (event) => {
|
|
@@ -675,12 +841,14 @@ function openStream(key) {
|
|
|
675
841
|
|
|
676
842
|
source.onopen = () => setStatus("live", "live");
|
|
677
843
|
source.onerror = () => {
|
|
678
|
-
// EventSource retries automatically;
|
|
844
|
+
// EventSource retries automatically; wait for a fresh snapshot to flash.
|
|
845
|
+
el.header?.classList.remove("is-thinking");
|
|
679
846
|
setStatus("reconnecting…", "offline");
|
|
680
847
|
};
|
|
681
848
|
}
|
|
682
849
|
|
|
683
850
|
function closeStream() {
|
|
851
|
+
el.header?.classList.remove("is-thinking");
|
|
684
852
|
if (state.eventSource) {
|
|
685
853
|
state.eventSource.close();
|
|
686
854
|
state.eventSource = null;
|
|
@@ -728,6 +896,7 @@ function renderFile(data) {
|
|
|
728
896
|
el.title.textContent = firstUserMessage(messages) || tr("Conversation");
|
|
729
897
|
el.sub.textContent = [data.cwd, data.model, data.provider].filter(Boolean).join(" · ");
|
|
730
898
|
preferences.showConversation(data, `file:${data.id}`);
|
|
899
|
+
proUI?.show(data, `file:${data.id}`);
|
|
731
900
|
renderMessageList(messages, null, { readOnly: true, context: data.context, active: false });
|
|
732
901
|
renderFileControls(data);
|
|
733
902
|
maybeOpenPlan(data.plan);
|
|
@@ -779,10 +948,11 @@ function renderFileControls(data) {
|
|
|
779
948
|
|
|
780
949
|
/** Attach to a resumed conversation, creating it server-side when needed. */
|
|
781
950
|
async function continueSession(id, selection = preferences.selectionForFile(id)) {
|
|
951
|
+
const payload = PRO_MODE ? { pro_preset: currentProPreset() } : selection ? { selection } : undefined;
|
|
782
952
|
const response = await apiFetch(`/api/session/${encodeURIComponent(id)}/continue`, {
|
|
783
953
|
method: "POST",
|
|
784
|
-
headers:
|
|
785
|
-
body:
|
|
954
|
+
headers: payload ? { "Content-Type": "application/json" } : undefined,
|
|
955
|
+
body: payload ? JSON.stringify(payload) : undefined,
|
|
786
956
|
});
|
|
787
957
|
const body = await response.json().catch(() => ({}));
|
|
788
958
|
if (!response.ok) throw new Error(body.error || `HTTP ${response.status}`);
|
|
@@ -792,17 +962,37 @@ async function continueSession(id, selection = preferences.selectionForFile(id))
|
|
|
792
962
|
|
|
793
963
|
function setComposerMode(mode) {
|
|
794
964
|
state.composerMode = mode;
|
|
965
|
+
el.agentActions.classList.add("hidden");
|
|
795
966
|
el.composer.classList.remove("hidden");
|
|
796
967
|
el.input.placeholder = mode === "resume"
|
|
797
968
|
? tr("Continue this conversation…")
|
|
798
969
|
: mode === "new"
|
|
799
970
|
? tr("Message Neutrino in this folder…")
|
|
800
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));
|
|
801
990
|
}
|
|
802
991
|
|
|
803
992
|
function hideComposer() {
|
|
804
993
|
preferences.clear();
|
|
805
994
|
el.composer.classList.add("hidden");
|
|
995
|
+
el.agentActions.classList.add("hidden");
|
|
806
996
|
renderApproval(null);
|
|
807
997
|
}
|
|
808
998
|
|
|
@@ -954,11 +1144,14 @@ function decideApproval(approved) {
|
|
|
954
1144
|
function renderLive() {
|
|
955
1145
|
const snapshot = state.snapshot;
|
|
956
1146
|
if (!snapshot) return;
|
|
1147
|
+
renderComposerAction();
|
|
1148
|
+
el.header?.classList.toggle("is-thinking", Boolean(snapshot.busy && !snapshot.paused && !snapshot.approval));
|
|
957
1149
|
state.fileData = null;
|
|
958
1150
|
state.agentData = null;
|
|
959
1151
|
el.title.textContent = snapshot.title || tr("New conversation");
|
|
960
1152
|
el.sub.textContent = [snapshot.cwd, snapshot.model, snapshot.provider].filter(Boolean).join(" · ");
|
|
961
1153
|
preferences.showConversation(snapshot, `live:${snapshot.key}`);
|
|
1154
|
+
proUI?.show(snapshot, `live:${snapshot.key}`);
|
|
962
1155
|
renderControls(snapshot);
|
|
963
1156
|
renderApproval(snapshot);
|
|
964
1157
|
renderMessageList(snapshot.messages ?? [], snapshot.streamingContent, {
|
|
@@ -1319,11 +1512,20 @@ async function postAction(key, action, body) {
|
|
|
1319
1512
|
}
|
|
1320
1513
|
}
|
|
1321
1514
|
|
|
1515
|
+
function activateComposer() {
|
|
1516
|
+
if (canPauseFromComposer()) return postAction(state.selection.key, "pause");
|
|
1517
|
+
return sendMessage();
|
|
1518
|
+
}
|
|
1519
|
+
|
|
1322
1520
|
async function sendMessage() {
|
|
1323
1521
|
const selection = state.selection;
|
|
1324
1522
|
if (!selection) return;
|
|
1325
1523
|
const text = el.input.value;
|
|
1326
1524
|
if (!text.trim()) return;
|
|
1525
|
+
if (PRO_MODE) {
|
|
1526
|
+
try { await proUI?.flush(); }
|
|
1527
|
+
catch (error) { el.sub.textContent = error.message; return; }
|
|
1528
|
+
}
|
|
1327
1529
|
|
|
1328
1530
|
if (selection.kind === "live") {
|
|
1329
1531
|
el.input.value = "";
|
|
@@ -1366,6 +1568,7 @@ async function sendMessage() {
|
|
|
1366
1568
|
function autosize() {
|
|
1367
1569
|
el.input.style.height = "auto";
|
|
1368
1570
|
el.input.style.height = `${Math.min(el.input.scrollHeight, 220)}px`;
|
|
1571
|
+
renderComposerAction();
|
|
1369
1572
|
}
|
|
1370
1573
|
|
|
1371
1574
|
// ------------------------------------------------------- workspaces & dialogs
|
|
@@ -1467,6 +1670,7 @@ function renderWorkspaceLanding(path) {
|
|
|
1467
1670
|
el.title.textContent = workspaceName(path) || tr("Working directory");
|
|
1468
1671
|
el.sub.textContent = path;
|
|
1469
1672
|
setComposerMode("new");
|
|
1673
|
+
proUI?.show(null, `new:${path}`);
|
|
1470
1674
|
el.messages.innerHTML = "";
|
|
1471
1675
|
|
|
1472
1676
|
const empty = document.createElement("div");
|
|
@@ -1616,6 +1820,19 @@ function fillNewChatModels(catalog, selectedModel) {
|
|
|
1616
1820
|
})), selectedModel);
|
|
1617
1821
|
}
|
|
1618
1822
|
|
|
1823
|
+
/** Offer only the levels the chosen model declares, keeping the draft if valid. */
|
|
1824
|
+
function fillNewChatIntensities(catalog, selected) {
|
|
1825
|
+
const provider = catalog.providers.find((entry) => entry.name === el.newChatProvider.value) ?? catalog.providers[0];
|
|
1826
|
+
const model = provider?.models.find((entry) => entry.name === el.newChatModel.value);
|
|
1827
|
+
const levels = Array.isArray(model?.intensities) && model.intensities.length ? model.intensities : catalog.intensities;
|
|
1828
|
+
let wanted = selected;
|
|
1829
|
+
if (!levels.includes(wanted)) {
|
|
1830
|
+
const fallback = catalog.settings?.default_intensity;
|
|
1831
|
+
wanted = levels.includes(fallback) ? fallback : levels.includes("High") ? "High" : levels[0] ?? "";
|
|
1832
|
+
}
|
|
1833
|
+
fillSelect(el.newChatIntensity, levels.map((value) => ({ value, label: value })), wanted);
|
|
1834
|
+
}
|
|
1835
|
+
|
|
1619
1836
|
function updateNewChatNote(catalog) {
|
|
1620
1837
|
const provider = catalog?.providers.find((entry) => entry.name === el.newChatProvider.value);
|
|
1621
1838
|
const model = provider?.models.find((entry) => entry.name === el.newChatModel.value);
|
|
@@ -1626,6 +1843,15 @@ function updateNewChatNote(catalog) {
|
|
|
1626
1843
|
}
|
|
1627
1844
|
}
|
|
1628
1845
|
|
|
1846
|
+
function renderProNewChatPresets(catalog, selected = catalog.pro?.preset) {
|
|
1847
|
+
if (!PRO_MODE) return;
|
|
1848
|
+
const select = document.getElementById("new-chat-pro-preset");
|
|
1849
|
+
if (!select) return;
|
|
1850
|
+
fillSelect(select, (catalog.pro?.presets || []).map(preset => ({
|
|
1851
|
+
value: preset.name, label: uiLanguage() === "zh" ? preset.chinese_name || preset.name : preset.name,
|
|
1852
|
+
})), selected);
|
|
1853
|
+
}
|
|
1854
|
+
|
|
1629
1855
|
/** Relabel the open new-conversation sheet without losing its selections. */
|
|
1630
1856
|
function refreshNewChatTexts() {
|
|
1631
1857
|
if (!el.newChatDialog?.open) return;
|
|
@@ -1637,9 +1863,10 @@ function refreshNewChatTexts() {
|
|
|
1637
1863
|
const approval = el.newChatApproval.value;
|
|
1638
1864
|
fillSelect(el.newChatProvider, catalog.providers.map((entry) => ({ value: entry.name, label: entry.name })), provider);
|
|
1639
1865
|
fillNewChatModels(catalog, model);
|
|
1640
|
-
|
|
1866
|
+
fillNewChatIntensities(catalog, intensity);
|
|
1641
1867
|
el.newChatApproval.value = approval;
|
|
1642
1868
|
updateNewChatNote(catalog);
|
|
1869
|
+
renderProNewChatPresets(catalog, document.getElementById("new-chat-pro-preset")?.value);
|
|
1643
1870
|
renderNewChatRecents();
|
|
1644
1871
|
}
|
|
1645
1872
|
|
|
@@ -1659,13 +1886,14 @@ async function openNewChatDialog(options = {}) {
|
|
|
1659
1886
|
const defaults = catalog.selection;
|
|
1660
1887
|
fillSelect(el.newChatProvider, catalog.providers.map((provider) => ({ value: provider.name, label: provider.name })), defaults.provider);
|
|
1661
1888
|
fillNewChatModels(catalog, defaults.model);
|
|
1662
|
-
|
|
1889
|
+
fillNewChatIntensities(catalog, defaults.intensity);
|
|
1663
1890
|
el.newChatApproval.value = catalog.settings.approval_mode === "auto" ? "auto" : "request";
|
|
1664
1891
|
|
|
1665
1892
|
const start = String(options.cwd || selectionCwd() || state.workspace?.path || state.workspaces?.cwd || "").trim();
|
|
1666
1893
|
el.newChatCwd.value = start;
|
|
1667
1894
|
renderNewChatRecents();
|
|
1668
1895
|
updateNewChatNote(catalog);
|
|
1896
|
+
renderProNewChatPresets(catalog);
|
|
1669
1897
|
if (typeof el.newChatCwd.focus === "function") el.newChatCwd.focus();
|
|
1670
1898
|
}
|
|
1671
1899
|
|
|
@@ -1686,7 +1914,8 @@ async function submitNewChat() {
|
|
|
1686
1914
|
el.newChatStart.textContent = tr("Creating…");
|
|
1687
1915
|
el.newChatFeedback.textContent = "";
|
|
1688
1916
|
try {
|
|
1689
|
-
const created = await createConversation(cwd, selection, el.newChatApproval.value
|
|
1917
|
+
const created = await createConversation(cwd, selection, el.newChatApproval.value,
|
|
1918
|
+
PRO_MODE ? document.getElementById("new-chat-pro-preset")?.value : undefined);
|
|
1690
1919
|
el.newChatDialog.close();
|
|
1691
1920
|
await selectLive(created.key);
|
|
1692
1921
|
setConversationView(true);
|
|
@@ -1700,9 +1929,10 @@ async function submitNewChat() {
|
|
|
1700
1929
|
}
|
|
1701
1930
|
|
|
1702
1931
|
/** Create a browser-owned conversation in `cwd` and reveal its folder. */
|
|
1703
|
-
async function createConversation(cwd, selection, approvalMode) {
|
|
1932
|
+
async function createConversation(cwd, selection, approvalMode, proPreset = currentProPreset()) {
|
|
1704
1933
|
const payload = { cwd };
|
|
1705
|
-
if (
|
|
1934
|
+
if (PRO_MODE) payload.pro_preset = proPreset;
|
|
1935
|
+
else if (selection && selection.provider && selection.model) payload.selection = selection;
|
|
1706
1936
|
if (approvalMode === "request" || approvalMode === "auto") payload.approval_mode = approvalMode;
|
|
1707
1937
|
const response = await apiFetch("/api/conversations", {
|
|
1708
1938
|
method: "POST",
|
|
@@ -2087,7 +2317,7 @@ function renderAgentList(groups) {
|
|
|
2087
2317
|
"</span>";
|
|
2088
2318
|
item.addEventListener("click", () => {
|
|
2089
2319
|
el.agentsDialog.close();
|
|
2090
|
-
void selectAgentSession(group.id, session.id, session.title || "");
|
|
2320
|
+
void selectAgentSession(group.id, session.id, session.title || "", group.cwd);
|
|
2091
2321
|
});
|
|
2092
2322
|
section.appendChild(item);
|
|
2093
2323
|
}
|
|
@@ -2106,7 +2336,7 @@ async function openAgentsDialog() {
|
|
|
2106
2336
|
const response = await apiFetch(`/api/agents/${agent.id}/sessions?cwd=${encodeURIComponent(cwd)}`, { cache: "no-store" });
|
|
2107
2337
|
const data = await response.json().catch(() => ({}));
|
|
2108
2338
|
if (!response.ok) throw new Error(apiError(data, `HTTP ${response.status}`));
|
|
2109
|
-
return { ...agent, sessions: Array.isArray(data.sessions) ? data.sessions : [] };
|
|
2339
|
+
return { ...agent, cwd: data.cwd || cwd, sessions: Array.isArray(data.sessions) ? data.sessions : [] };
|
|
2110
2340
|
}));
|
|
2111
2341
|
el.agentsFeedback.textContent = "";
|
|
2112
2342
|
renderAgentList(groups);
|
|
@@ -2133,11 +2363,53 @@ function renderAgentView(data) {
|
|
|
2133
2363
|
renderApproval(null);
|
|
2134
2364
|
renderPlanCard(null);
|
|
2135
2365
|
hideComposer();
|
|
2366
|
+
renderAgentMigration(data);
|
|
2367
|
+
}
|
|
2368
|
+
|
|
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
|
+
}
|
|
2136
2406
|
}
|
|
2137
2407
|
|
|
2138
|
-
async function selectAgentSession(agent, id, title) {
|
|
2408
|
+
async function selectAgentSession(agent, id, title, cwd) {
|
|
2409
|
+
const scope = cwd || await agentsScopeCwd();
|
|
2139
2410
|
closeStream();
|
|
2140
|
-
|
|
2411
|
+
const selection = { kind: "agent", agent, id, title, cwd: scope };
|
|
2412
|
+
state.selection = selection;
|
|
2141
2413
|
state.snapshot = null;
|
|
2142
2414
|
state.fileData = null;
|
|
2143
2415
|
state.agentData = null;
|
|
@@ -2149,17 +2421,20 @@ async function selectAgentSession(agent, id, title) {
|
|
|
2149
2421
|
renderApproval(null);
|
|
2150
2422
|
renderPlanCard(null);
|
|
2151
2423
|
hideComposer();
|
|
2424
|
+
el.agentMigrateFeedback.textContent = "";
|
|
2152
2425
|
el.title.textContent = tr("Loading…");
|
|
2153
2426
|
el.sub.textContent = title || "";
|
|
2154
2427
|
resetMessagePane(tr("Loading…"));
|
|
2155
2428
|
setConversationView(true);
|
|
2156
2429
|
try {
|
|
2157
|
-
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" });
|
|
2158
2431
|
const data = await response.json().catch(() => ({}));
|
|
2432
|
+
if (state.selection !== selection) return;
|
|
2159
2433
|
if (!response.ok) throw new Error(apiError(data, `HTTP ${response.status}`));
|
|
2160
|
-
state.agentData = { ...data, agent, cwd:
|
|
2434
|
+
state.agentData = { ...data, agent, cwd: data.cwd || scope };
|
|
2161
2435
|
renderAgentView(state.agentData);
|
|
2162
2436
|
} catch (error) {
|
|
2437
|
+
if (state.selection !== selection) return;
|
|
2163
2438
|
el.title.textContent = title || tr("Conversation");
|
|
2164
2439
|
el.sub.textContent = tr("Could not load this session: {error}", { error: error.message });
|
|
2165
2440
|
resetMessagePane(tr("Could not load this session: {error}", { error: error.message }));
|
|
@@ -2177,11 +2452,12 @@ function syncLanguageSelects(language) {
|
|
|
2177
2452
|
async function changeInterfaceLanguage(language, { persist = true } = {}) {
|
|
2178
2453
|
const normalized = String(language ?? "").toLowerCase().startsWith("zh") ? "zh" : "en";
|
|
2179
2454
|
setUiLanguage(normalized);
|
|
2180
|
-
writeStorage("neutrino.language", normalized);
|
|
2455
|
+
writeStorage(modeStorageKey("neutrino.language"), normalized);
|
|
2181
2456
|
applyStaticTranslations();
|
|
2182
2457
|
syncLanguageSelects(normalized);
|
|
2183
2458
|
if (state.status) setStatus(state.status.key, state.status.kind);
|
|
2184
2459
|
preferences.refreshTexts?.();
|
|
2460
|
+
proUI?.refresh();
|
|
2185
2461
|
refreshNewChatTexts();
|
|
2186
2462
|
renderWorkspaceChrome();
|
|
2187
2463
|
renderSidebar();
|
|
@@ -2209,18 +2485,197 @@ async function changeInterfaceLanguage(language, { persist = true } = {}) {
|
|
|
2209
2485
|
}
|
|
2210
2486
|
|
|
2211
2487
|
function initializeLanguage() {
|
|
2212
|
-
const stored = readStorage("neutrino.language");
|
|
2488
|
+
const stored = readStorage(modeStorageKey("neutrino.language"));
|
|
2213
2489
|
if (stored) setUiLanguage(stored);
|
|
2214
2490
|
applyStaticTranslations();
|
|
2215
2491
|
syncLanguageSelects(uiLanguage());
|
|
2216
2492
|
if (state.status) setStatus(state.status.key, state.status.kind);
|
|
2217
2493
|
}
|
|
2218
2494
|
|
|
2495
|
+
// ------------------------------------------------------------------ pro mode
|
|
2496
|
+
//
|
|
2497
|
+
// Both workspaces reuse this console. Clicking the orbit badge on the normal
|
|
2498
|
+
// console starts a short wordmark curtain; the recorded start time lets /pro
|
|
2499
|
+
// finish the same animation instead of flashing the freshly loaded page.
|
|
2500
|
+
|
|
2501
|
+
const PRO_TRANSITION_KEY = "neutrino.pro.transition";
|
|
2502
|
+
const PRO_TIP_KEY = "neutrino.pro.tip.dismissed";
|
|
2503
|
+
const PRO_TRANSITION_FADE_MS = 350; // curtain fade in / fade out
|
|
2504
|
+
const PRO_TRANSITION_COVER_MS = 450; // navigate once the curtain is fully opaque
|
|
2505
|
+
const PRO_TRANSITION_HOLD_MS = 3000; // wordmark stays up until 3s after the switch began
|
|
2506
|
+
const PRO_TRANSITION_RESUME_MS = 8000; // older records are stale and never replayed
|
|
2507
|
+
const PRO_LOGO_MARK = '<img class="pro-logo-mark" src="/neutrino.png" alt="Neutrino Pro">';
|
|
2508
|
+
|
|
2509
|
+
let proTransitionStarted = false;
|
|
2510
|
+
|
|
2511
|
+
function proCurtain() {
|
|
2512
|
+
const curtain = document.createElement("div");
|
|
2513
|
+
curtain.className = "pro-curtain";
|
|
2514
|
+
curtain.setAttribute("aria-hidden", "true");
|
|
2515
|
+
const image = document.createElement("img");
|
|
2516
|
+
image.src = "/neutrino-pro.gif";
|
|
2517
|
+
image.alt = "";
|
|
2518
|
+
curtain.appendChild(image);
|
|
2519
|
+
return curtain;
|
|
2520
|
+
}
|
|
2521
|
+
|
|
2522
|
+
/** Timers exist in the browser; the test sandboxes load this file without them. */
|
|
2523
|
+
function proDelay(callback, ms) {
|
|
2524
|
+
if (typeof setTimeout === "function") setTimeout(callback, ms);
|
|
2525
|
+
}
|
|
2526
|
+
|
|
2527
|
+
function startProTransition() {
|
|
2528
|
+
if (PRO_MODE || proTransitionStarted) return;
|
|
2529
|
+
proTransitionStarted = true;
|
|
2530
|
+
const started = Date.now();
|
|
2531
|
+
try {
|
|
2532
|
+
sessionStorage.setItem(PRO_TRANSITION_KEY, String(started));
|
|
2533
|
+
} catch { /* Storage may be blocked; navigation still works. */ }
|
|
2534
|
+
|
|
2535
|
+
const curtain = proCurtain();
|
|
2536
|
+
curtain.style.transition = `opacity ${PRO_TRANSITION_FADE_MS}ms ease`;
|
|
2537
|
+
document.body.appendChild(curtain);
|
|
2538
|
+
// Commit the transparent first frame, then let the style transition fade in.
|
|
2539
|
+
void curtain.offsetWidth;
|
|
2540
|
+
curtain.classList.add("pro-curtain-visible");
|
|
2541
|
+
|
|
2542
|
+
proDelay(() => {
|
|
2543
|
+
try { location.assign("/pro"); } catch { location.href = "/pro"; }
|
|
2544
|
+
}, PRO_TRANSITION_COVER_MS);
|
|
2545
|
+
}
|
|
2546
|
+
|
|
2547
|
+
/** Remove every switch curtain left in this document. */
|
|
2548
|
+
function removeProCurtains() {
|
|
2549
|
+
if (typeof document.querySelectorAll !== "function") return;
|
|
2550
|
+
for (const curtain of document.querySelectorAll(".pro-curtain")) curtain.remove?.();
|
|
2551
|
+
}
|
|
2552
|
+
|
|
2553
|
+
/**
|
|
2554
|
+
* Drop the one-shot switch chrome.
|
|
2555
|
+
*
|
|
2556
|
+
* The curtain lives in the document that navigated away, so the browser's
|
|
2557
|
+
* back/forward cache can restore it fully opaque — a Back or Forward would
|
|
2558
|
+
* otherwise land on that frozen frame. `clearRecord` stays false while this
|
|
2559
|
+
* document is being hidden for /pro: the destination still needs the recorded
|
|
2560
|
+
* start time to finish the same animation.
|
|
2561
|
+
*/
|
|
2562
|
+
function resetProTransition({ clearRecord = true } = {}) {
|
|
2563
|
+
removeProCurtains();
|
|
2564
|
+
document.documentElement?.classList?.remove?.("pro-transitioning");
|
|
2565
|
+
proTransitionStarted = false;
|
|
2566
|
+
if (!clearRecord) return;
|
|
2567
|
+
try { sessionStorage.removeItem(PRO_TRANSITION_KEY); } catch { /* Storage may be blocked. */ }
|
|
2568
|
+
}
|
|
2569
|
+
|
|
2570
|
+
/** Undo the switch chrome when the browser restores either page from bfcache. */
|
|
2571
|
+
function watchProNavigation() {
|
|
2572
|
+
if (typeof window === "undefined" || typeof window.addEventListener !== "function") return;
|
|
2573
|
+
window.addEventListener("pagehide", (event) => {
|
|
2574
|
+
if (!event?.persisted) return;
|
|
2575
|
+
// The page is entering the back/forward cache: clean the snapshot, but
|
|
2576
|
+
// keep the record for the /pro page that is currently loading.
|
|
2577
|
+
resetProTransition({ clearRecord: false });
|
|
2578
|
+
});
|
|
2579
|
+
window.addEventListener("pageshow", (event) => {
|
|
2580
|
+
if (!event?.persisted) return;
|
|
2581
|
+
// Restored by Back/Forward: any curtain captured in the snapshot is stale.
|
|
2582
|
+
resetProTransition();
|
|
2583
|
+
});
|
|
2584
|
+
}
|
|
2585
|
+
|
|
2586
|
+
/** Consume a just-recorded switch and finish the animation on /pro. */
|
|
2587
|
+
function resumeProTransition() {
|
|
2588
|
+
if (!PRO_MODE) return;
|
|
2589
|
+
let started = 0;
|
|
2590
|
+
try { started = Number(sessionStorage.getItem(PRO_TRANSITION_KEY)) || 0; } catch { /* Storage may be blocked. */ }
|
|
2591
|
+
// Consume the record even when stale: direct visits never show a curtain.
|
|
2592
|
+
if (started) {
|
|
2593
|
+
try { sessionStorage.removeItem(PRO_TRANSITION_KEY); } catch { /* Ignore. */ }
|
|
2594
|
+
}
|
|
2595
|
+
const now = Date.now();
|
|
2596
|
+
const age = now - started;
|
|
2597
|
+
if (!started || age < 0 || age > PRO_TRANSITION_RESUME_MS) {
|
|
2598
|
+
document.documentElement.classList.remove("pro-transitioning");
|
|
2599
|
+
return;
|
|
2600
|
+
}
|
|
2601
|
+
|
|
2602
|
+
const curtain = proCurtain();
|
|
2603
|
+
// Start fully opaque on the first frame so the new page never flashes through.
|
|
2604
|
+
curtain.style.transition = "none";
|
|
2605
|
+
curtain.style.opacity = "1";
|
|
2606
|
+
document.body.appendChild(curtain);
|
|
2607
|
+
// The real curtain is up: retire the pre-paint cover from index.html so it
|
|
2608
|
+
// cannot remain over the page after the fade completes.
|
|
2609
|
+
document.documentElement.classList.remove("pro-transitioning");
|
|
2610
|
+
|
|
2611
|
+
proDelay(() => {
|
|
2612
|
+
curtain.style.transition = `opacity ${PRO_TRANSITION_FADE_MS}ms ease`;
|
|
2613
|
+
curtain.style.opacity = "0";
|
|
2614
|
+
proDelay(() => curtain.remove(), PRO_TRANSITION_FADE_MS);
|
|
2615
|
+
}, Math.max(0, started + PRO_TRANSITION_HOLD_MS - now));
|
|
2616
|
+
}
|
|
2617
|
+
|
|
2618
|
+
/** In Pro mode the badge is a static "you are here" mark, not a control. */
|
|
2619
|
+
function applyProBrand() {
|
|
2620
|
+
document.title = "Neutrino Pro";
|
|
2621
|
+
if (el.brandName) el.brandName.textContent = "Neutrino Pro";
|
|
2622
|
+
const logo = el.brandLogo;
|
|
2623
|
+
if (!logo) return;
|
|
2624
|
+
logo.innerHTML = PRO_LOGO_MARK;
|
|
2625
|
+
logo.classList.add("pro-logo");
|
|
2626
|
+
logo.tabIndex = -1;
|
|
2627
|
+
// Drop the i18n annotations too, or a language switch restores the tooltip.
|
|
2628
|
+
const annotations = ["title", "aria-label", "data-i18n-title", "data-i18n-aria-label"];
|
|
2629
|
+
for (const attribute of annotations) logo.removeAttribute?.(attribute);
|
|
2630
|
+
}
|
|
2631
|
+
|
|
2632
|
+
/** The blue tip explains the badge on the normal console; Pro never shows it. */
|
|
2633
|
+
function initializeProTip() {
|
|
2634
|
+
if (PRO_MODE || !el.proTip) return;
|
|
2635
|
+
if (readStorage(PRO_TIP_KEY) === "1") return;
|
|
2636
|
+
el.proTip.classList.remove("hidden");
|
|
2637
|
+
el.proTipClose?.addEventListener("click", () => {
|
|
2638
|
+
writeStorage(PRO_TIP_KEY, "1");
|
|
2639
|
+
el.proTip.classList.add("hidden");
|
|
2640
|
+
});
|
|
2641
|
+
}
|
|
2642
|
+
|
|
2643
|
+
/** The Pro workspace's top-right escape hatch back to the regular console. */
|
|
2644
|
+
function initializeProExit() {
|
|
2645
|
+
if (!el.proExit) return;
|
|
2646
|
+
el.proExit.classList.remove("hidden");
|
|
2647
|
+
el.proExit.addEventListener("click", () => {
|
|
2648
|
+
el.proExit.disabled = true;
|
|
2649
|
+
try {
|
|
2650
|
+
location.assign("/");
|
|
2651
|
+
} catch {
|
|
2652
|
+
location.href = "/";
|
|
2653
|
+
}
|
|
2654
|
+
});
|
|
2655
|
+
}
|
|
2656
|
+
|
|
2657
|
+
/** Wire the shared brand and either start the switch or resume its animation. */
|
|
2658
|
+
function initializeProWorkspace() {
|
|
2659
|
+
watchProNavigation();
|
|
2660
|
+
if (PRO_MODE) {
|
|
2661
|
+
applyProBrand();
|
|
2662
|
+
initializeProExit();
|
|
2663
|
+
resumeProTransition();
|
|
2664
|
+
return;
|
|
2665
|
+
}
|
|
2666
|
+
// A switch that was interrupted (Stop, Back, a failed navigation) can leave
|
|
2667
|
+
// the record and the pre-paint cover behind. Neither may ever affect the
|
|
2668
|
+
// regular console, so clear them before wiring the badge.
|
|
2669
|
+
resetProTransition();
|
|
2670
|
+
el.brandLogo?.addEventListener("click", startProTransition);
|
|
2671
|
+
initializeProTip();
|
|
2672
|
+
}
|
|
2673
|
+
|
|
2219
2674
|
// --------------------------------------------------------------------- init
|
|
2220
2675
|
|
|
2221
2676
|
el.refresh.addEventListener("click", () => loadOverview());
|
|
2222
2677
|
el.back.addEventListener("click", () => setConversationView(false));
|
|
2223
|
-
el.send.addEventListener("click",
|
|
2678
|
+
el.send.addEventListener("click", activateComposer);
|
|
2224
2679
|
el.input.addEventListener("input", autosize);
|
|
2225
2680
|
el.input.addEventListener("keydown", (event) => {
|
|
2226
2681
|
if (event.key === "Enter" && !event.shiftKey) {
|
|
@@ -2243,6 +2698,7 @@ el.workspaceAdd.addEventListener("click", () => {
|
|
|
2243
2698
|
openFolderDialog({ start: state.workspace?.path || state.workspaces?.cwd || "", onPick: (path) => openWorkspace(path) });
|
|
2244
2699
|
});
|
|
2245
2700
|
el.agentsOpen.addEventListener("click", () => openAgentsDialog());
|
|
2701
|
+
el.agentMigrate.addEventListener("click", () => migrateSelectedAgentSession());
|
|
2246
2702
|
el.agentsClose.addEventListener("click", () => el.agentsDialog.close());
|
|
2247
2703
|
el.agentsDialog.addEventListener("cancel", (event) => {
|
|
2248
2704
|
event.preventDefault();
|
|
@@ -2274,12 +2730,16 @@ el.newChatProvider.addEventListener("change", () => {
|
|
|
2274
2730
|
const catalog = preferences.catalog?.();
|
|
2275
2731
|
if (catalog) {
|
|
2276
2732
|
fillNewChatModels(catalog, "");
|
|
2733
|
+
fillNewChatIntensities(catalog, el.newChatIntensity.value);
|
|
2277
2734
|
updateNewChatNote(catalog);
|
|
2278
2735
|
}
|
|
2279
2736
|
});
|
|
2280
2737
|
el.newChatModel.addEventListener("change", () => {
|
|
2281
2738
|
const catalog = preferences.catalog?.();
|
|
2282
|
-
if (catalog)
|
|
2739
|
+
if (catalog) {
|
|
2740
|
+
fillNewChatIntensities(catalog, el.newChatIntensity.value);
|
|
2741
|
+
updateNewChatNote(catalog);
|
|
2742
|
+
}
|
|
2283
2743
|
});
|
|
2284
2744
|
|
|
2285
2745
|
el.folderClose.addEventListener("click", () => el.folderDialog.close());
|
|
@@ -2318,11 +2778,16 @@ el.folderDialog.addEventListener("cancel", (event) => {
|
|
|
2318
2778
|
window.addEventListener("beforeunload", closeStream);
|
|
2319
2779
|
|
|
2320
2780
|
initializeLanguage();
|
|
2781
|
+
initializeProWorkspace();
|
|
2321
2782
|
preferences.load()
|
|
2322
|
-
.then((config) => {
|
|
2783
|
+
.then(async (config) => {
|
|
2323
2784
|
const language = config?.settings?.language;
|
|
2324
|
-
if (language && language !== uiLanguage())
|
|
2325
|
-
|
|
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);
|
|
2326
2791
|
})
|
|
2327
2792
|
.catch(error => console.error(error));
|
|
2328
2793
|
loadOverview().then(() => {
|