@xingjisoft/neutrino 0.5.2 → 0.6.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 +1 -0
- package/assets/fast.txt +18 -0
- package/assets/pro/common.txt +4 -0
- package/assets/pro/main_model.txt +2 -0
- package/assets/pro/plan_model.txt +3 -1
- package/assets/prompt.txt +9 -0
- package/assets/ptc.txt +27 -0
- package/assets/subagent.txt +22 -1
- package/dist/app.js +2 -0
- package/dist/config.js +30 -1
- package/dist/llm/client.js +61 -8
- package/dist/llm/codeExecutor.js +239 -0
- package/dist/llm/codemode.js +164 -0
- package/dist/llm/context.js +97 -0
- package/dist/llm/plan.js +97 -9
- package/dist/llm/risk.js +10 -0
- package/dist/llm/runtime.js +301 -0
- package/dist/llm/search.js +426 -0
- package/dist/llm/searchNetwork.js +129 -0
- package/dist/llm/subagent.js +236 -112
- package/dist/llm/tools.js +101 -4
- package/dist/pro/agents.js +49 -19
- package/dist/pro/preflight.js +24 -2
- package/dist/screens/chat.js +299 -27
- package/dist/web/hub.js +9 -0
- package/dist/web/server.js +57 -8
- package/dist/web/session.js +327 -62
- package/dist/web/terminalBridge.js +10 -1
- package/package.json +10 -5
- package/web/app.js +221 -16
- package/web/i18n.js +20 -0
- package/web/index.html +10 -0
- package/web/style.css +56 -10
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { randomBytes, timingSafeEqual } from "node:crypto";
|
|
2
2
|
import http from "node:http";
|
|
3
|
-
import { resolveModelSelection } from "../config.js";
|
|
3
|
+
import { resolveModelSelection, isAgentMode } from "../config.js";
|
|
4
4
|
import { hub } from "./hub.js";
|
|
5
5
|
function sendJson(res, status, data) {
|
|
6
6
|
res.writeHead(status, { "Content-Type": "application/json", "Cache-Control": "no-store" });
|
|
@@ -82,6 +82,15 @@ export async function startTerminalBridge() {
|
|
|
82
82
|
return;
|
|
83
83
|
}
|
|
84
84
|
}
|
|
85
|
+
else if (action === "agent-mode") {
|
|
86
|
+
if (!isAgentMode(body.mode))
|
|
87
|
+
throw new Error("invalid agent mode");
|
|
88
|
+
const result = hub.setAgentMode(key, body.mode);
|
|
89
|
+
if (!result.ok) {
|
|
90
|
+
sendJson(res, 409, { error: result.error });
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
85
94
|
else if (action === "model") {
|
|
86
95
|
const result = hub.configure(key, resolveModelSelection(body));
|
|
87
96
|
if (!result.ok) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xingjisoft/neutrino",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "XINGJI Neutrino - a CLI coding agent in the terminal.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -14,11 +14,11 @@
|
|
|
14
14
|
"web"
|
|
15
15
|
],
|
|
16
16
|
"engines": {
|
|
17
|
-
"node": ">=18.
|
|
17
|
+
"node": ">=18.17.0"
|
|
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-mobile-history.mjs && node scripts/test-session-migration.mjs && node scripts/test-web-session-migration.mjs && node scripts/test-web-default.mjs && node scripts/test-turn-fold.mjs && node scripts/test-long-turn.mjs && node scripts/test-plan.mjs && node scripts/test-subagent.mjs && node scripts/test-web-settings.mjs && node scripts/test-update.mjs && node scripts/test-update-ui.mjs && node scripts/test-update-web.mjs && node scripts/test-settings-update.mjs && node scripts/test-update-check.mjs && node scripts/test-update-watch.mjs && node scripts/test-model-intensity.mjs && node scripts/test-background.mjs && node scripts/test-background-lifetime.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-web-sidebar-status.mjs && node scripts/test-web-seen.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-lan.mjs && node scripts/test-web-auth.mjs && node scripts/test-web-auth-ui.mjs && node scripts/test-web-pro.mjs && node scripts/test-pro-preflight.mjs && node scripts/test-pro-runtime.mjs && node scripts/test-pro-team.mjs && node scripts/test-pro-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-web-search.mjs && node scripts/test-search-network.mjs && node scripts/test-compact.mjs && node scripts/test-history-fold.mjs && node scripts/test-web-history.mjs && node scripts/test-web-mobile-history.mjs && node scripts/test-session-migration.mjs && node scripts/test-web-session-migration.mjs && node scripts/test-web-default.mjs && node scripts/test-turn-fold.mjs && node scripts/test-long-turn.mjs && node scripts/test-plan.mjs && node scripts/test-agent-modes.mjs && node scripts/test-subagent.mjs && node scripts/test-task-runtime.mjs && node scripts/test-web-settings.mjs && node scripts/test-update.mjs && node scripts/test-update-ui.mjs && node scripts/test-update-web.mjs && node scripts/test-settings-update.mjs && node scripts/test-update-check.mjs && node scripts/test-update-watch.mjs && node scripts/test-model-intensity.mjs && node scripts/test-background.mjs && node scripts/test-background-lifetime.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-web-sidebar-status.mjs && node scripts/test-web-seen.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-lan.mjs && node scripts/test-web-auth.mjs && node scripts/test-web-auth-ui.mjs && node scripts/test-web-pro.mjs && node scripts/test-pro-preflight.mjs && node scripts/test-pro-runtime.mjs && node scripts/test-pro-team.mjs && node scripts/test-pro-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
|
},
|
|
@@ -31,7 +31,12 @@
|
|
|
31
31
|
"coding"
|
|
32
32
|
],
|
|
33
33
|
"devDependencies": {
|
|
34
|
-
"@types/node": "^22.0.0"
|
|
35
|
-
|
|
34
|
+
"@types/node": "^22.0.0"
|
|
35
|
+
},
|
|
36
|
+
"dependencies": {
|
|
37
|
+
"@cloudflare/codemode": "0.5.6",
|
|
38
|
+
"quickjs-emscripten": "0.32.0",
|
|
39
|
+
"typescript": "^5.5.0",
|
|
40
|
+
"undici": "^6.29.1"
|
|
36
41
|
}
|
|
37
42
|
}
|
package/web/app.js
CHANGED
|
@@ -445,6 +445,9 @@ const el = {
|
|
|
445
445
|
messages: document.getElementById("messages"),
|
|
446
446
|
composer: document.getElementById("composer"),
|
|
447
447
|
input: document.getElementById("input"),
|
|
448
|
+
modePicker: document.getElementById("agent-mode-picker"),
|
|
449
|
+
modeLabel: document.getElementById("agent-mode-label"),
|
|
450
|
+
modeFeedback: document.getElementById("agent-mode-feedback"),
|
|
448
451
|
send: document.getElementById("send"),
|
|
449
452
|
approval: document.getElementById("approval"),
|
|
450
453
|
emptyNewChat: document.getElementById("empty-new-chat"),
|
|
@@ -593,6 +596,148 @@ function storedWorkspace() {
|
|
|
593
596
|
return { key: pathKey(path), path };
|
|
594
597
|
}
|
|
595
598
|
|
|
599
|
+
const AGENT_MODE_LABELS = { standard: "Standard mode", ptc: "PTC mode", fast: "Fast mode", pro: "Professional mode" };
|
|
600
|
+
let newAgentMode = readStorage("neutrino.agent-mode");
|
|
601
|
+
if (!["standard", "ptc", "fast"].includes(newAgentMode)) newAgentMode = "standard";
|
|
602
|
+
let agentModePending = Promise.resolve();
|
|
603
|
+
let agentModeChanging = false;
|
|
604
|
+
let workspaceNavigation = false;
|
|
605
|
+
let agentModeError = "";
|
|
606
|
+
|
|
607
|
+
function clearAgentModeFeedback() {
|
|
608
|
+
agentModeError = "";
|
|
609
|
+
agentModePending = Promise.resolve();
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
function currentAgentMode() {
|
|
613
|
+
if (PRO_MODE) return "pro";
|
|
614
|
+
if (state.selection?.kind === "live") return state.snapshot?.agentMode || "standard";
|
|
615
|
+
if (state.selection?.kind === "file") return state.fileData?.agent_mode || "standard";
|
|
616
|
+
return newAgentMode;
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
function renderAgentMode() {
|
|
620
|
+
if (!el.modePicker || !el.modeLabel) return;
|
|
621
|
+
const mode = currentAgentMode();
|
|
622
|
+
el.modeLabel.removeAttribute?.("data-i18n");
|
|
623
|
+
el.modeLabel.textContent = tr(AGENT_MODE_LABELS[mode] || AGENT_MODE_LABELS.standard);
|
|
624
|
+
const nextTurn = state.snapshot?.busy && state.snapshot.activeAgentMode !== state.snapshot.agentMode;
|
|
625
|
+
el.modePicker.classList.toggle("is-pending", Boolean(agentModeChanging || nextTurn));
|
|
626
|
+
for (const button of el.modePicker.querySelectorAll?.("[data-agent-mode]") || []) {
|
|
627
|
+
button.setAttribute("aria-checked", String(button.dataset.agentMode === mode));
|
|
628
|
+
button.disabled = agentModeChanging || workspaceNavigation;
|
|
629
|
+
}
|
|
630
|
+
if (el.modeFeedback) {
|
|
631
|
+
el.modeFeedback.textContent = agentModeError || (agentModeChanging ? tr("Changing mode…") : nextTurn ? tr("Applies to the next turn") : "");
|
|
632
|
+
el.modeFeedback.classList.toggle("error", Boolean(agentModeError));
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
async function chooseAgentMode(mode) {
|
|
637
|
+
if (!Object.hasOwn(AGENT_MODE_LABELS, mode) || agentModeChanging || workspaceNavigation || mode === currentAgentMode()) return;
|
|
638
|
+
const selected = state.selection;
|
|
639
|
+
agentModeChanging = true;
|
|
640
|
+
agentModeError = "";
|
|
641
|
+
if (el.modePicker) renderAgentMode();
|
|
642
|
+
el.modePicker.open = false;
|
|
643
|
+
try {
|
|
644
|
+
if (mode === "pro" || PRO_MODE) {
|
|
645
|
+
await switchWorkspaceMode(mode);
|
|
646
|
+
} else if (selected?.kind === "live") {
|
|
647
|
+
const response = await apiFetch(`/api/live/${encodeURIComponent(selected.key)}/agent-mode`, {
|
|
648
|
+
method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ mode }),
|
|
649
|
+
});
|
|
650
|
+
const body = await response.json();
|
|
651
|
+
if (!response.ok) throw new Error(apiError(body, `HTTP ${response.status}`));
|
|
652
|
+
if (state.selection === selected) { state.snapshot = body.snapshot; renderLive(); }
|
|
653
|
+
} else if (selected?.kind === "file") {
|
|
654
|
+
await continueSession(selected.id, preferences.selectionForFile(selected.id), mode);
|
|
655
|
+
} else {
|
|
656
|
+
newAgentMode = mode;
|
|
657
|
+
}
|
|
658
|
+
if (mode !== "pro") { newAgentMode = mode; writeStorage("neutrino.agent-mode", mode); }
|
|
659
|
+
} catch (error) {
|
|
660
|
+
if (state.selection !== selected) return;
|
|
661
|
+
agentModeError = tr("Could not change mode: {error}", { error: error.message });
|
|
662
|
+
throw error;
|
|
663
|
+
} finally { agentModeChanging = false; renderAgentMode(); }
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
async function switchWorkspaceMode(mode) {
|
|
667
|
+
const cwd = selectionCwd() || state.workspace?.path;
|
|
668
|
+
if (!cwd) throw new Error(tr("Select a working directory first."));
|
|
669
|
+
const targetPro = mode === "pro";
|
|
670
|
+
const response = await fetch(targetPro ? "/pro/api/conversations" : "/api/conversations", {
|
|
671
|
+
method: "POST", credentials: "same-origin", headers: { "Content-Type": "application/json" },
|
|
672
|
+
body: JSON.stringify({ cwd, ...(targetPro ? {} : { agent_mode: mode }) }),
|
|
673
|
+
});
|
|
674
|
+
const body = await response.json();
|
|
675
|
+
if (response.status === 401) showGate();
|
|
676
|
+
if (!response.ok) throw new Error(apiError(body, `HTTP ${response.status}`));
|
|
677
|
+
workspaceNavigation = true;
|
|
678
|
+
if (el.modePicker) renderAgentMode();
|
|
679
|
+
try { sessionStorage.setItem(`neutrino.workspace-draft.${body.key}`, el.input.value); } catch { /* Navigation still works without storage. */ }
|
|
680
|
+
const target = `${targetPro ? "/pro" : "/"}?live=${encodeURIComponent(body.key)}`;
|
|
681
|
+
if (targetPro) startProTransition(target);
|
|
682
|
+
else { try { location.assign(target); } catch { location.href = target; } }
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
/** A transferred chat remains reloadable until the user chooses another view. */
|
|
686
|
+
function clearLinkedConversation(keepKey) {
|
|
687
|
+
const params = new URLSearchParams(location.search);
|
|
688
|
+
if (!params.has("live") || params.get("live") === keepKey) return;
|
|
689
|
+
params.delete("live");
|
|
690
|
+
const query = params.toString();
|
|
691
|
+
try { history.replaceState(history.state, "", location.pathname + (query ? `?${query}` : "") + location.hash); } catch { /* History is optional. */ }
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
async function openLinkedConversation() {
|
|
695
|
+
const key = new URLSearchParams(location.search).get("live");
|
|
696
|
+
if (!key || state.selection) return false;
|
|
697
|
+
try {
|
|
698
|
+
const response = await apiFetch(`/api/live/${encodeURIComponent(key)}`, { cache: "no-store" });
|
|
699
|
+
const body = await response.json();
|
|
700
|
+
if (!response.ok) throw new Error(apiError(body, `HTTP ${response.status}`));
|
|
701
|
+
if (state.selection) return false;
|
|
702
|
+
setWorkspace({ path: body.cwd });
|
|
703
|
+
await selectLive(key);
|
|
704
|
+
state.snapshot = body;
|
|
705
|
+
renderLive();
|
|
706
|
+
setConversationView(true);
|
|
707
|
+
try {
|
|
708
|
+
const storageKey = `neutrino.workspace-draft.${key}`;
|
|
709
|
+
el.input.value = sessionStorage.getItem(storageKey) || "";
|
|
710
|
+
sessionStorage.removeItem(storageKey);
|
|
711
|
+
} catch { /* Storage is optional. */ }
|
|
712
|
+
autosize(); el.input.focus();
|
|
713
|
+
return true;
|
|
714
|
+
} catch (error) { el.sub.textContent = error.message; return false; }
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
function initializeAgentMode() {
|
|
718
|
+
if (!el.modePicker?.querySelectorAll) return;
|
|
719
|
+
for (const button of el.modePicker.querySelectorAll("[data-agent-mode]")) {
|
|
720
|
+
button.addEventListener("click", () => {
|
|
721
|
+
agentModePending = chooseAgentMode(button.dataset.agentMode);
|
|
722
|
+
// Keep the rejection available to sendMessage while avoiding an unhandled promise.
|
|
723
|
+
void agentModePending.catch(() => {});
|
|
724
|
+
});
|
|
725
|
+
}
|
|
726
|
+
el.modePicker.addEventListener("keydown", event => {
|
|
727
|
+
if (event.key === "Escape") { el.modePicker.open = false; el.modePicker.querySelector("summary")?.focus(); event.stopPropagation(); }
|
|
728
|
+
if (!["ArrowDown", "ArrowUp", "Home", "End"].includes(event.key)) return;
|
|
729
|
+
event.preventDefault(); el.modePicker.open = true;
|
|
730
|
+
const buttons = [...el.modePicker.querySelectorAll("[data-agent-mode]")];
|
|
731
|
+
let index = buttons.indexOf(document.activeElement);
|
|
732
|
+
index = event.key === "Home" ? 0 : event.key === "End" ? buttons.length - 1 : (index + (event.key === "ArrowDown" ? 1 : -1) + buttons.length) % buttons.length;
|
|
733
|
+
buttons[index]?.focus();
|
|
734
|
+
});
|
|
735
|
+
document.addEventListener("pointerdown", event => {
|
|
736
|
+
if (el.modePicker.open && !el.modePicker.contains(event.target)) el.modePicker.open = false;
|
|
737
|
+
});
|
|
738
|
+
if (el.modePicker) renderAgentMode();
|
|
739
|
+
}
|
|
740
|
+
|
|
596
741
|
let proUI = null;
|
|
597
742
|
const preferences = createSettingsUI({
|
|
598
743
|
apiFetch,
|
|
@@ -909,20 +1054,42 @@ function renderGroup(group, options = {}) {
|
|
|
909
1054
|
wrap.className = "group";
|
|
910
1055
|
|
|
911
1056
|
const collapsed = !options.forceOpen && state.collapsed.has(group.key);
|
|
1057
|
+
// The row answers "start something here", not "how much is already here": a
|
|
1058
|
+
// plus beside the folder opens a fresh conversation in it, the way Codex puts
|
|
1059
|
+
// a project's own new-chat control next to the project. Two actions cannot
|
|
1060
|
+
// share one button, so the row holds the folder and the plus side by side.
|
|
1061
|
+
const head = document.createElement("div");
|
|
1062
|
+
head.className = "group-head";
|
|
1063
|
+
|
|
912
1064
|
const header = document.createElement("button");
|
|
913
1065
|
header.className = "group-header";
|
|
1066
|
+
header.type = "button";
|
|
1067
|
+
header.setAttribute("aria-expanded", String(!collapsed));
|
|
914
1068
|
header.innerHTML =
|
|
915
1069
|
`<span class="chevron${collapsed ? " collapsed" : ""}">` +
|
|
916
1070
|
'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m9 18 6-6-6-6"/></svg>' +
|
|
917
1071
|
"</span>" +
|
|
918
|
-
`<span class="path" title="${escapeHtml(group.cwd)}">${escapeHtml(shortenPath(group.cwd))}</span
|
|
919
|
-
`<span class="count">${group.sessions.length}</span>`;
|
|
1072
|
+
`<span class="path" title="${escapeHtml(group.cwd)}">${escapeHtml(shortenPath(group.cwd))}</span>`;
|
|
920
1073
|
header.addEventListener("click", () => {
|
|
921
1074
|
if (state.collapsed.has(group.key)) state.collapsed.delete(group.key);
|
|
922
1075
|
else state.collapsed.add(group.key);
|
|
923
1076
|
renderSidebar();
|
|
924
1077
|
});
|
|
925
|
-
|
|
1078
|
+
head.appendChild(header);
|
|
1079
|
+
|
|
1080
|
+
if (group.cwd) {
|
|
1081
|
+
const start = document.createElement("button");
|
|
1082
|
+
start.className = "icon-btn group-new";
|
|
1083
|
+
start.type = "button";
|
|
1084
|
+
start.title = tr("New conversation in this folder");
|
|
1085
|
+
start.setAttribute("aria-label", tr("New conversation in this folder"));
|
|
1086
|
+
start.innerHTML =
|
|
1087
|
+
'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 5v14M5 12h14"/></svg>';
|
|
1088
|
+
start.addEventListener("click", () => startConversationIn(group.cwd));
|
|
1089
|
+
head.appendChild(start);
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
wrap.appendChild(head);
|
|
926
1093
|
|
|
927
1094
|
if (collapsed) return wrap;
|
|
928
1095
|
|
|
@@ -956,6 +1123,19 @@ function renderGroup(group, options = {}) {
|
|
|
956
1123
|
return wrap;
|
|
957
1124
|
}
|
|
958
1125
|
|
|
1126
|
+
/**
|
|
1127
|
+
* The plus on a folder header. It opens that folder's landing view, where the
|
|
1128
|
+
* next message becomes a new conversation in that directory, so starting one
|
|
1129
|
+
* costs a click instead of a dialog.
|
|
1130
|
+
*/
|
|
1131
|
+
function startConversationIn(cwd) {
|
|
1132
|
+
const path = String(cwd ?? "").trim();
|
|
1133
|
+
if (!path) return;
|
|
1134
|
+
renderWorkspaceLanding(path);
|
|
1135
|
+
setConversationView(true);
|
|
1136
|
+
el.input?.focus?.();
|
|
1137
|
+
}
|
|
1138
|
+
|
|
959
1139
|
function isSelected(session) {
|
|
960
1140
|
const selection = state.selection;
|
|
961
1141
|
if (!selection) return false;
|
|
@@ -1243,6 +1423,7 @@ function resetMessagePane(text) {
|
|
|
1243
1423
|
}
|
|
1244
1424
|
|
|
1245
1425
|
async function selectLive(key) {
|
|
1426
|
+
if (el.modePicker) { clearAgentModeFeedback(); clearLinkedConversation(key); }
|
|
1246
1427
|
preferences.clear();
|
|
1247
1428
|
closeStream();
|
|
1248
1429
|
state.selection = { kind: "live", key };
|
|
@@ -1319,6 +1500,7 @@ function closeStream() {
|
|
|
1319
1500
|
}
|
|
1320
1501
|
|
|
1321
1502
|
async function selectFile(id) {
|
|
1503
|
+
if (el.modePicker) { clearAgentModeFeedback(); clearLinkedConversation(); }
|
|
1322
1504
|
preferences.clear();
|
|
1323
1505
|
closeStream();
|
|
1324
1506
|
state.selection = { kind: "file", id };
|
|
@@ -1364,6 +1546,7 @@ function renderFile(data) {
|
|
|
1364
1546
|
renderFileControls(data);
|
|
1365
1547
|
maybeOpenPlan(data.plan);
|
|
1366
1548
|
renderPlanCard(data);
|
|
1549
|
+
if (el.modePicker) renderAgentMode();
|
|
1367
1550
|
}
|
|
1368
1551
|
|
|
1369
1552
|
/**
|
|
@@ -1410,8 +1593,10 @@ function renderFileControls(data) {
|
|
|
1410
1593
|
}
|
|
1411
1594
|
|
|
1412
1595
|
/** Attach to a resumed conversation, creating it server-side when needed. */
|
|
1413
|
-
async function continueSession(id, selection = preferences.selectionForFile(id)) {
|
|
1414
|
-
const payload = PRO_MODE ? { pro_preset: currentProPreset() } :
|
|
1596
|
+
async function continueSession(id, selection = preferences.selectionForFile(id), agentMode) {
|
|
1597
|
+
const payload = PRO_MODE ? { pro_preset: currentProPreset() } : {
|
|
1598
|
+
...(selection ? { selection } : {}), ...(agentMode ? { agent_mode: agentMode } : {}),
|
|
1599
|
+
};
|
|
1415
1600
|
const response = await apiFetch(`/api/session/${encodeURIComponent(id)}/continue`, {
|
|
1416
1601
|
method: "POST",
|
|
1417
1602
|
headers: payload ? { "Content-Type": "application/json" } : undefined,
|
|
@@ -1425,6 +1610,7 @@ async function continueSession(id, selection = preferences.selectionForFile(id))
|
|
|
1425
1610
|
|
|
1426
1611
|
function setComposerMode(mode) {
|
|
1427
1612
|
state.composerMode = mode;
|
|
1613
|
+
if (el.modePicker) renderAgentMode();
|
|
1428
1614
|
el.agentActions.classList.add("hidden");
|
|
1429
1615
|
el.composer.classList.remove("hidden");
|
|
1430
1616
|
el.input.placeholder = mode === "resume"
|
|
@@ -1626,6 +1812,7 @@ function renderLive() {
|
|
|
1626
1812
|
});
|
|
1627
1813
|
maybeOpenPlan(snapshot.plan);
|
|
1628
1814
|
renderPlanCard(snapshot);
|
|
1815
|
+
if (el.modePicker) renderAgentMode();
|
|
1629
1816
|
}
|
|
1630
1817
|
|
|
1631
1818
|
async function changeApprovalMode(key, mode) {
|
|
@@ -1687,7 +1874,7 @@ function renderControls(snapshot) {
|
|
|
1687
1874
|
// review callback is still winding down.
|
|
1688
1875
|
const reviewing = snapshot.approval ? null : snapshot.reviewing;
|
|
1689
1876
|
const subagent = snapshot.subagent;
|
|
1690
|
-
badge.textContent = snapshot.paused
|
|
1877
|
+
badge.textContent = snapshot.task?.state === "cancelling" ? tr("Stopping tasks…") : snapshot.paused
|
|
1691
1878
|
? tr("paused")
|
|
1692
1879
|
: snapshot.busy
|
|
1693
1880
|
? snapshot.compressing
|
|
@@ -1702,10 +1889,10 @@ function renderControls(snapshot) {
|
|
|
1702
1889
|
: tr("idle");
|
|
1703
1890
|
el.controls.appendChild(badge);
|
|
1704
1891
|
|
|
1705
|
-
if (snapshot.saveError) {
|
|
1892
|
+
if (snapshot.saveError || snapshot.task?.persistenceError) {
|
|
1706
1893
|
const warning = document.createElement("span");
|
|
1707
1894
|
warning.className = "badge paused";
|
|
1708
|
-
warning.textContent = tr("History save failed: {error}", { error: snapshot.saveError });
|
|
1895
|
+
warning.textContent = tr("History save failed: {error}", { error: snapshot.saveError || snapshot.task?.persistenceError });
|
|
1709
1896
|
el.controls.appendChild(warning);
|
|
1710
1897
|
}
|
|
1711
1898
|
|
|
@@ -1842,6 +2029,12 @@ function renderMessage(message, index) {
|
|
|
1842
2029
|
`<span class="tool-args">${escapeHtml(truncate(safeJson(message.arguments), 120))}</span>`;
|
|
1843
2030
|
details.appendChild(summary);
|
|
1844
2031
|
|
|
2032
|
+
if (message.name === "execute_ts" && typeof message.arguments?.code === "string") {
|
|
2033
|
+
const program = document.createElement("pre");
|
|
2034
|
+
program.className = "tool-result program-source";
|
|
2035
|
+
program.textContent = message.arguments.code;
|
|
2036
|
+
details.appendChild(program);
|
|
2037
|
+
}
|
|
1845
2038
|
const result = document.createElement("pre");
|
|
1846
2039
|
result.className = "tool-result";
|
|
1847
2040
|
result.textContent = message.result || "(no output)";
|
|
@@ -1981,6 +2174,7 @@ function activateComposer() {
|
|
|
1981
2174
|
}
|
|
1982
2175
|
|
|
1983
2176
|
async function sendMessage() {
|
|
2177
|
+
try { await agentModePending; } catch (error) { el.sub.textContent = error.message; return; }
|
|
1984
2178
|
const selection = state.selection;
|
|
1985
2179
|
if (!selection) return;
|
|
1986
2180
|
const text = el.input.value;
|
|
@@ -2119,6 +2313,7 @@ function showHome({ menu = true } = {}) {
|
|
|
2119
2313
|
|
|
2120
2314
|
/** Show a folder as an empty workspace ready for its first message. */
|
|
2121
2315
|
function renderWorkspaceLanding(path) {
|
|
2316
|
+
if (el.modePicker) { clearAgentModeFeedback(); clearLinkedConversation(); }
|
|
2122
2317
|
const sameLanding = state.selection?.kind === "new" && state.selection.cwd === path;
|
|
2123
2318
|
state.selection = { kind: "new", cwd: path };
|
|
2124
2319
|
state.snapshot = null;
|
|
@@ -2394,11 +2589,14 @@ async function submitNewChat() {
|
|
|
2394
2589
|
}
|
|
2395
2590
|
}
|
|
2396
2591
|
|
|
2397
|
-
/** Create a browser-owned conversation in `cwd` and
|
|
2592
|
+
/** Create a browser-owned conversation in `cwd` and show all sidebar directories. */
|
|
2398
2593
|
async function createConversation(cwd, selection, approvalMode, proPreset = currentProPreset()) {
|
|
2399
2594
|
const payload = { cwd };
|
|
2400
2595
|
if (PRO_MODE) payload.pro_preset = proPreset;
|
|
2401
|
-
else
|
|
2596
|
+
else {
|
|
2597
|
+
payload.agent_mode = currentAgentMode();
|
|
2598
|
+
if (selection && selection.provider && selection.model) payload.selection = selection;
|
|
2599
|
+
}
|
|
2402
2600
|
if (approvalMode === "request" || approvalMode === "auto") payload.approval_mode = approvalMode;
|
|
2403
2601
|
const response = await apiFetch("/api/conversations", {
|
|
2404
2602
|
method: "POST",
|
|
@@ -2407,7 +2605,7 @@ async function createConversation(cwd, selection, approvalMode, proPreset = curr
|
|
|
2407
2605
|
});
|
|
2408
2606
|
const data = await response.json().catch(() => ({}));
|
|
2409
2607
|
if (!response.ok) throw new Error(apiError(data, `HTTP ${response.status}`));
|
|
2410
|
-
setWorkspace(
|
|
2608
|
+
setWorkspace(null);
|
|
2411
2609
|
await loadOverview({ silent: true });
|
|
2412
2610
|
return data;
|
|
2413
2611
|
}
|
|
@@ -2588,6 +2786,9 @@ function planStatusLabel(status) {
|
|
|
2588
2786
|
if (status === "done") return tr("Done");
|
|
2589
2787
|
if (status === "active") return tr("Active");
|
|
2590
2788
|
if (status === "failed") return tr("Failed");
|
|
2789
|
+
if (status === "blocked") return tr("Blocked");
|
|
2790
|
+
if (status === "cancelled") return tr("Cancelled");
|
|
2791
|
+
if (status === "skipped") return tr("Skipped");
|
|
2591
2792
|
return tr("Pending");
|
|
2592
2793
|
}
|
|
2593
2794
|
|
|
@@ -2632,7 +2833,7 @@ function renderPlanCard(source) {
|
|
|
2632
2833
|
steps.className = "plan-steps";
|
|
2633
2834
|
let done = 0;
|
|
2634
2835
|
for (const step of plan.steps) {
|
|
2635
|
-
const status = ["done", "active", "failed", "pending"].includes(step.status) ? step.status : "pending";
|
|
2836
|
+
const status = ["done", "active", "failed", "pending", "blocked", "cancelled", "skipped"].includes(step.status) ? step.status : "pending";
|
|
2636
2837
|
if (status === "done") done += 1;
|
|
2637
2838
|
const row = document.createElement("div");
|
|
2638
2839
|
row.className = `plan-step ${status}`;
|
|
@@ -2990,7 +3191,7 @@ function proDelay(callback, ms) {
|
|
|
2990
3191
|
if (typeof setTimeout === "function") setTimeout(callback, ms);
|
|
2991
3192
|
}
|
|
2992
3193
|
|
|
2993
|
-
function startProTransition() {
|
|
3194
|
+
function startProTransition(target = "/pro") {
|
|
2994
3195
|
if (PRO_MODE || proTransitionStarted) return;
|
|
2995
3196
|
proTransitionStarted = true;
|
|
2996
3197
|
const started = Date.now();
|
|
@@ -3006,7 +3207,7 @@ function startProTransition() {
|
|
|
3006
3207
|
curtain.classList.add("pro-curtain-visible");
|
|
3007
3208
|
|
|
3008
3209
|
proDelay(() => {
|
|
3009
|
-
try { location.assign("/pro"); } catch { location.href = "/pro"; }
|
|
3210
|
+
try { location.assign(typeof target === "string" ? target : "/pro"); } catch { location.href = typeof target === "string" ? target : "/pro"; }
|
|
3010
3211
|
}, PRO_TRANSITION_COVER_MS);
|
|
3011
3212
|
}
|
|
3012
3213
|
|
|
@@ -3027,6 +3228,8 @@ function removeProCurtains() {
|
|
|
3027
3228
|
*/
|
|
3028
3229
|
function resetProTransition({ clearRecord = true } = {}) {
|
|
3029
3230
|
removeProCurtains();
|
|
3231
|
+
if (typeof workspaceNavigation !== "undefined") workspaceNavigation = false;
|
|
3232
|
+
if (typeof el !== "undefined" && el.modePicker) renderAgentMode();
|
|
3030
3233
|
document.documentElement?.classList?.remove?.("pro-transitioning");
|
|
3031
3234
|
proTransitionStarted = false;
|
|
3032
3235
|
if (!clearRecord) return;
|
|
@@ -3133,7 +3336,7 @@ function initializeProWorkspace() {
|
|
|
3133
3336
|
// the record and the pre-paint cover behind. Neither may ever affect the
|
|
3134
3337
|
// regular console, so clear them before wiring the badge.
|
|
3135
3338
|
resetProTransition();
|
|
3136
|
-
el.brandLogo?.addEventListener("click", startProTransition);
|
|
3339
|
+
el.brandLogo?.addEventListener("click", () => startProTransition());
|
|
3137
3340
|
initializeProTip();
|
|
3138
3341
|
}
|
|
3139
3342
|
|
|
@@ -3250,6 +3453,7 @@ forgetStaleConversationEntry();
|
|
|
3250
3453
|
|
|
3251
3454
|
initializeLanguage();
|
|
3252
3455
|
initializeProWorkspace();
|
|
3456
|
+
initializeAgentMode();
|
|
3253
3457
|
preferences.load()
|
|
3254
3458
|
.then(async (config) => {
|
|
3255
3459
|
const language = config?.settings?.language;
|
|
@@ -3264,7 +3468,8 @@ preferences.load()
|
|
|
3264
3468
|
maybePromptBackground(config);
|
|
3265
3469
|
})
|
|
3266
3470
|
.catch(error => console.error(error));
|
|
3267
|
-
loadSeen().then(() => loadOverview()).then(() => {
|
|
3471
|
+
loadSeen().then(() => loadOverview()).then(async () => {
|
|
3472
|
+
if (await openLinkedConversation()) return;
|
|
3268
3473
|
// Reopen the folder the user had open last time, without stealing a click
|
|
3269
3474
|
// that landed on a saved conversation while the overview was loading.
|
|
3270
3475
|
if (state.workspace && !state.selection) renderWorkspaceLanding(state.workspace.path);
|
package/web/i18n.js
CHANGED
|
@@ -79,6 +79,21 @@ export function locale() {
|
|
|
79
79
|
}
|
|
80
80
|
|
|
81
81
|
const ZH = {
|
|
82
|
+
"Agent mode": "Agent 模式",
|
|
83
|
+
"Standard mode": "标准模式",
|
|
84
|
+
"PTC mode": "PTC模式",
|
|
85
|
+
"Fast mode": "快速模式",
|
|
86
|
+
"Professional mode": "专业模式",
|
|
87
|
+
"Full-featured Agent for every scenario": "功能完整的Agent,适用于所有场景",
|
|
88
|
+
"All standard capabilities, with code-based planning to save tokens": "拥有标准模式所有功能,通过代码实现规划,节省Token",
|
|
89
|
+
"Less thinking and fewer calls for quick answers": "减少思考和调用,迅速回答问题",
|
|
90
|
+
"Assign tasks to the best-suited models so multiple models complement each other": "适合需要多个模型取长补短的任务,将任务分给最适合他的模型",
|
|
91
|
+
"Applies to the next turn": "从下一轮开始生效",
|
|
92
|
+
"Changing mode…": "正在切换模式…",
|
|
93
|
+
"Could not change mode: {error}": "无法切换模式:{error}",
|
|
94
|
+
"Select a working directory first.": "请先选择工作目录。",
|
|
95
|
+
"Program": "程序",
|
|
96
|
+
|
|
82
97
|
"Pro preset": "Pro 预设",
|
|
83
98
|
"Preset": "预设",
|
|
84
99
|
"Neutrino Pro": "Neutrino Pro",
|
|
@@ -218,6 +233,7 @@ const ZH = {
|
|
|
218
233
|
"Choose a conversation from the sidebar to view it.": "从侧边栏选择一个对话查看。",
|
|
219
234
|
"No conversations yet.": "暂无对话。",
|
|
220
235
|
"No conversations in this folder yet.": "此文件夹中暂无对话。",
|
|
236
|
+
"New conversation in this folder": "在此文件夹中新建对话",
|
|
221
237
|
"Type a message below to start a conversation in this folder.": "在下方输入消息,即可在此文件夹中开始对话。",
|
|
222
238
|
"Send a message to get started.": "发送一条消息开始对话。",
|
|
223
239
|
"This conversation is empty.": "此对话为空。",
|
|
@@ -268,6 +284,10 @@ const ZH = {
|
|
|
268
284
|
|
|
269
285
|
// -- live status --------------------------------------------------------
|
|
270
286
|
"paused": "已暂停",
|
|
287
|
+
"Stopping tasks…": "正在停止任务…",
|
|
288
|
+
"Blocked": "受阻",
|
|
289
|
+
"Cancelled": "已取消",
|
|
290
|
+
"Skipped": "已跳过",
|
|
271
291
|
"working": "处理中",
|
|
272
292
|
"idle": "空闲",
|
|
273
293
|
"compressing": "正在压缩上下文",
|
package/web/index.html
CHANGED
|
@@ -129,6 +129,16 @@
|
|
|
129
129
|
<div class="composer-inner">
|
|
130
130
|
<div class="composer-card">
|
|
131
131
|
<textarea id="input" rows="1" placeholder="Message Neutrino…" aria-label="Message" data-i18n-placeholder="Message Neutrino…"></textarea>
|
|
132
|
+
<details id="agent-mode-picker" class="agent-mode-picker">
|
|
133
|
+
<summary aria-label="Agent mode" data-i18n-aria-label="Agent mode"><strong id="agent-mode-label" data-i18n="Standard mode">Standard mode</strong><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" aria-hidden="true"><path d="m7 10 5 5 5-5" /></svg></summary>
|
|
134
|
+
<div class="agent-mode-menu" role="menu" aria-label="Agent mode" data-i18n-aria-label="Agent mode">
|
|
135
|
+
<button type="button" role="menuitemradio" aria-checked="true" data-agent-mode="standard"><span><strong data-i18n="Standard mode">Standard mode</strong><small data-i18n="Full-featured Agent for every scenario">Full-featured Agent for every scenario</small></span><svg class="agent-mode-check" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="m5 12 4 4L19 6" /></svg></button>
|
|
136
|
+
<button type="button" role="menuitemradio" aria-checked="false" data-agent-mode="ptc"><span><strong data-i18n="PTC mode">PTC mode</strong><small data-i18n="All standard capabilities, with code-based planning to save tokens">All standard capabilities, with code-based planning to save tokens</small></span><svg class="agent-mode-check" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="m5 12 4 4L19 6" /></svg></button>
|
|
137
|
+
<button type="button" role="menuitemradio" aria-checked="false" data-agent-mode="fast"><span><strong data-i18n="Fast mode">Fast mode</strong><small data-i18n="Less thinking and fewer calls for quick answers">Less thinking and fewer calls for quick answers</small></span><svg class="agent-mode-check" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="m5 12 4 4L19 6" /></svg></button>
|
|
138
|
+
<button type="button" role="menuitemradio" aria-checked="false" data-agent-mode="pro"><span><strong data-i18n="Professional mode">Professional mode</strong><small data-i18n="Assign tasks to the best-suited models so multiple models complement each other">Assign tasks to the best-suited models so multiple models complement each other</small></span><svg class="agent-mode-check" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="m5 12 4 4L19 6" /></svg></button>
|
|
139
|
+
</div>
|
|
140
|
+
<p id="agent-mode-feedback" class="agent-mode-feedback" role="status" aria-live="polite"></p>
|
|
141
|
+
</details>
|
|
132
142
|
<details id="model-picker" class="model-picker pro-preset-control hidden">
|
|
133
143
|
<summary id="model-picker-summary" aria-label="Conversation model" data-i18n-aria-label="Conversation model"><span id="model-picker-label"></span><svg class="pro-preset-chevron" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m7 10 5 5 5-5" /></svg></summary>
|
|
134
144
|
<div class="pro-preset-card model-picker-card">
|
package/web/style.css
CHANGED
|
@@ -413,11 +413,20 @@ svg {
|
|
|
413
413
|
margin-bottom: 4px;
|
|
414
414
|
}
|
|
415
415
|
|
|
416
|
+
/* A folder row is two controls: the name folds the list, the plus beside it
|
|
417
|
+
opens a new conversation in that folder. */
|
|
418
|
+
.group-head {
|
|
419
|
+
display: flex;
|
|
420
|
+
align-items: center;
|
|
421
|
+
gap: 2px;
|
|
422
|
+
}
|
|
423
|
+
|
|
416
424
|
.group-header {
|
|
417
425
|
display: flex;
|
|
418
426
|
align-items: center;
|
|
419
427
|
gap: 7px;
|
|
420
|
-
|
|
428
|
+
flex: 1;
|
|
429
|
+
min-width: 0;
|
|
421
430
|
height: 30px;
|
|
422
431
|
padding: 0 8px;
|
|
423
432
|
background: transparent;
|
|
@@ -461,16 +470,22 @@ svg {
|
|
|
461
470
|
font-size: 12px;
|
|
462
471
|
}
|
|
463
472
|
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
background: var(--surface-1);
|
|
473
|
+
/* Small enough to sit on the folder row without competing with the name, and
|
|
474
|
+
dimmed so a long list of folders never reads as a wall of plus signs. */
|
|
475
|
+
.group-new {
|
|
476
|
+
width: 22px;
|
|
477
|
+
height: 22px;
|
|
470
478
|
color: var(--text-caption);
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
.group-new svg {
|
|
482
|
+
width: 13px;
|
|
483
|
+
height: 13px;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
.group-new:hover {
|
|
487
|
+
background: var(--surface-1);
|
|
488
|
+
color: var(--text-secondary);
|
|
474
489
|
}
|
|
475
490
|
|
|
476
491
|
.group-items {
|
|
@@ -1474,6 +1489,26 @@ svg {
|
|
|
1474
1489
|
outline: none;
|
|
1475
1490
|
}
|
|
1476
1491
|
|
|
1492
|
+
.agent-mode-picker { position: relative; align-self: center; margin-right: auto; min-width: 0; }
|
|
1493
|
+
.agent-mode-picker > summary { display: flex; align-items: center; gap: 7px; padding: 7px 9px; border-radius: var(--radius-sm); cursor: pointer; list-style: none; color: var(--text-secondary); font-size: 12px; }
|
|
1494
|
+
.agent-mode-picker > summary::-webkit-details-marker { display: none; }
|
|
1495
|
+
.agent-mode-picker > summary strong { font-weight: 600; }
|
|
1496
|
+
.agent-mode-picker > summary svg { width: 14px; height: 14px; }
|
|
1497
|
+
.agent-mode-picker > summary:hover, .agent-mode-picker[open] > summary { background: var(--surface-3); }
|
|
1498
|
+
.agent-mode-picker.is-pending > summary { color: var(--accent); }
|
|
1499
|
+
.agent-mode-menu { position: absolute; z-index: 35; left: 0; bottom: calc(100% + 10px); width: min(370px, calc(100vw - 48px)); max-height: min(520px, 60dvh); overflow-y: auto; padding: 6px; border: 1px solid var(--border-strong); border-radius: var(--radius-lg); background: var(--surface-1); box-shadow: var(--shadow-strong); }
|
|
1500
|
+
.agent-mode-menu > button { display: flex; align-items: center; gap: 12px; width: 100%; padding: 11px 12px; text-align: left; color: var(--text); background: transparent; border: 0; border-radius: var(--radius-sm); cursor: pointer; font-family: var(--font); }
|
|
1501
|
+
.agent-mode-menu > button:hover, .agent-mode-menu > button:focus-visible, .agent-mode-menu > button[aria-checked="true"] { background: var(--surface-3); }
|
|
1502
|
+
.agent-mode-menu > button > span { flex: 1; min-width: 0; }
|
|
1503
|
+
.agent-mode-menu strong { display: block; margin-bottom: 4px; font-size: 13px; font-weight: 700; }
|
|
1504
|
+
.agent-mode-menu small { display: block; color: var(--text-tertiary); font-size: 12px; font-weight: 400; line-height: 1.55; }
|
|
1505
|
+
.agent-mode-check { flex: none; width: 16px; height: 16px; color: var(--accent); visibility: hidden; }
|
|
1506
|
+
.agent-mode-menu [aria-checked="true"] .agent-mode-check { visibility: visible; }
|
|
1507
|
+
.agent-mode-menu > button:disabled { opacity: .55; cursor: wait; }
|
|
1508
|
+
.agent-mode-feedback { position: absolute; bottom: 100%; left: 0; width: min(370px, calc(100vw - 48px)); margin: 0 0 5px; font-size: 11px; color: var(--text-secondary); }
|
|
1509
|
+
.agent-mode-feedback:empty, .agent-mode-picker[open] .agent-mode-feedback { display: none; }
|
|
1510
|
+
.agent-mode-feedback.error { color: var(--error, #ef8181); }
|
|
1511
|
+
.program-source { border-bottom: 1px solid var(--border-soft); }
|
|
1477
1512
|
.pro-preset-control { position: relative; margin-left: auto; align-self: center; }
|
|
1478
1513
|
.pro-preset-control > summary {
|
|
1479
1514
|
display: flex; align-items: center; gap: 10px; padding: 7px 10px;
|
|
@@ -3308,6 +3343,13 @@ html.pro-transitioning body::before {
|
|
|
3308
3343
|
min-height: 40px;
|
|
3309
3344
|
}
|
|
3310
3345
|
|
|
3346
|
+
/* The plus takes the same 40px target as the other icon buttons here, so
|
|
3347
|
+
its icon grows with it instead of staying at the desktop size. */
|
|
3348
|
+
.group-new svg {
|
|
3349
|
+
width: 16px;
|
|
3350
|
+
height: 16px;
|
|
3351
|
+
}
|
|
3352
|
+
|
|
3311
3353
|
.main-header {
|
|
3312
3354
|
display: grid;
|
|
3313
3355
|
grid-template-columns: 40px minmax(0, 1fr);
|
|
@@ -3533,3 +3575,7 @@ html.pro-transitioning body::before {
|
|
|
3533
3575
|
min-height: 64px;
|
|
3534
3576
|
}
|
|
3535
3577
|
}
|
|
3578
|
+
|
|
3579
|
+
.plan-step.blocked .plan-node,
|
|
3580
|
+
.plan-step.blocked .plan-step-title strong { color: var(--orange, #dca548); }
|
|
3581
|
+
.plan-step.cancelled, .plan-step.skipped { opacity: .65; }
|