@songsid/agend 2.3.5-alpha.3 → 2.3.5-alpha.4
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/dist/agent-runtime-api.d.ts +4 -0
- package/dist/agent-runtime-api.js +59 -0
- package/dist/agent-runtime-api.js.map +1 -0
- package/dist/agent-runtime-change.d.ts +74 -0
- package/dist/agent-runtime-change.js +102 -0
- package/dist/agent-runtime-change.js.map +1 -0
- package/dist/auth-api.js +4 -0
- package/dist/auth-api.js.map +1 -1
- package/dist/backend/codex-footer-hints.d.ts +4 -0
- package/dist/backend/codex-footer-hints.js +27 -0
- package/dist/backend/codex-footer-hints.js.map +1 -0
- package/dist/backend/codex.js +8 -10
- package/dist/backend/codex.js.map +1 -1
- package/dist/fleet-manager.d.ts +3 -0
- package/dist/fleet-manager.js +157 -1
- package/dist/fleet-manager.js.map +1 -1
- package/dist/fleet-windows-runtime.d.ts +6 -1
- package/dist/fleet-windows-runtime.js +70 -1
- package/dist/fleet-windows-runtime.js.map +1 -1
- package/dist/office-share-page.js +1 -1
- package/dist/office-share-page.js.map +1 -1
- package/dist/public-web-link.d.ts +6 -0
- package/dist/public-web-link.js +9 -0
- package/dist/public-web-link.js.map +1 -1
- package/dist/runtime-api.js +84 -9
- package/dist/runtime-api.js.map +1 -1
- package/dist/runtime-build-info.d.ts +9 -0
- package/dist/runtime-build-info.js +57 -0
- package/dist/runtime-build-info.js.map +1 -0
- package/dist/runtime-bundle.d.ts +3 -1
- package/dist/runtime-bundle.js +14 -8
- package/dist/runtime-bundle.js.map +1 -1
- package/dist/runtime-cli.d.ts +6 -1
- package/dist/runtime-cli.js +46 -11
- package/dist/runtime-cli.js.map +1 -1
- package/dist/runtime-client-store.d.ts +20 -1
- package/dist/runtime-client-store.js +56 -4
- package/dist/runtime-client-store.js.map +1 -1
- package/dist/runtime-client.d.ts +12 -0
- package/dist/runtime-client.js +43 -7
- package/dist/runtime-client.js.map +1 -1
- package/dist/runtime-connection.d.ts +22 -0
- package/dist/runtime-connection.js +93 -0
- package/dist/runtime-connection.js.map +1 -0
- package/dist/runtime-coordinator.d.ts +9 -0
- package/dist/runtime-coordinator.js +81 -34
- package/dist/runtime-coordinator.js.map +1 -1
- package/dist/runtime-pane-boot.d.ts +7 -0
- package/dist/runtime-pane-boot.js +23 -0
- package/dist/runtime-pane-boot.js.map +1 -1
- package/dist/runtime-protocol.d.ts +13 -0
- package/dist/runtime-protocol.js +18 -0
- package/dist/runtime-protocol.js.map +1 -1
- package/dist/settings-effect.js +1 -1
- package/dist/settings-effect.js.map +1 -1
- package/dist/settings-http-confirmation.d.ts +3 -0
- package/dist/settings-http-confirmation.js +16 -1
- package/dist/settings-http-confirmation.js.map +1 -1
- package/dist/ui/agent-runtime-settings.js +67 -0
- package/dist/ui/agent-runtime.js +20 -2
- package/dist/ui/backend-signin.js +36 -21
- package/dist/ui/native-instance-settings.js +5 -8
- package/dist/ui/panel-chat.js +3 -2
- package/dist/ui/panel-fleet.js +4 -2
- package/dist/ui/panel-runtimes.js +136 -33
- package/dist/ui/panel-settings.js +224 -139
- package/dist/ui/panel-windows-cli.js +77 -41
- package/dist/ui/settings-agent-filters.js +94 -0
- package/dist/ui/settings-appearance.js +122 -23
- package/dist/ui/settings-dialogs.js +5 -5
- package/dist/ui/settings-guests.js +25 -11
- package/dist/ui/settings-layout.js +245 -0
- package/dist/ui/settings-model.js +8 -0
- package/dist/ui/settings-registry.js +5 -3
- package/dist/ui/settings-shortcuts.js +65 -0
- package/dist/ui/settings-strings.js +93 -0
- package/dist/ui/settings-web-authority.js +14 -11
- package/dist/ui/shared/app-shell.js +17 -7
- package/dist/ui/shared/app.css +1022 -12
- package/dist/ui/shared/instance-nav.js +3 -2
- package/dist/ui/shared/machine-name.js +13 -0
- package/dist/ui/shared/settings-width.js +31 -0
- package/dist/ui/shared/tokens.css +1 -0
- package/dist/ui/shared/ui-dialog.js +2 -2
- package/dist/ui/shared/ui-menu.js +3 -3
- package/dist/ui/windows-cli-catalog.js +41 -12
- package/dist/web-api.d.ts +1 -0
- package/dist/web-api.js +3 -0
- package/dist/web-api.js.map +1 -1
- package/dist/windows-agents.d.ts +3 -1
- package/dist/windows-agents.js +13 -10
- package/dist/windows-agents.js.map +1 -1
- package/dist/windows-cli-api.js +2 -1
- package/dist/windows-cli-api.js.map +1 -1
- package/dist/windows-cli-worker.d.ts +1 -0
- package/dist/windows-cli-worker.js +2 -2
- package/dist/windows-cli-worker.js.map +1 -1
- package/dist/windows-cli.d.ts +1 -0
- package/dist/windows-cli.js +7 -2
- package/dist/windows-cli.js.map +1 -1
- package/package.json +1 -1
|
@@ -5,7 +5,7 @@ import { PanelHeader, setTitle } from "/assets/app-shell.js";
|
|
|
5
5
|
import { Icon } from "/assets/ui-icons.js";
|
|
6
6
|
import { CliPicker, CliProductMark, cliLabel } from "./cli-picker.js";
|
|
7
7
|
import { ModelSelect } from "./settings-dialogs.js";
|
|
8
|
-
import { useWindowsCliCatalog, WindowsCliDiscoveryStatus } from "./windows-cli-catalog.js";
|
|
8
|
+
import { useWindowsCliCatalog, WindowsCliDiscoveryStatus, WindowsCliHostIdentity } from "./windows-cli-catalog.js";
|
|
9
9
|
|
|
10
10
|
register("wincli", {
|
|
11
11
|
"zh-TW": { intro:"在 Windows 上執行一次性任務,使用本機已安裝的 CLI 和登入。", probe:"偵測 Windows CLI", probing:"正在偵測…", newTask:"派送 Windows Codex 任務", folder:"Windows 工作目錄", prompt:"任務內容", access:"檔案存取", readOnly:"唯讀", write:"工作目錄可寫入", limits:"目前支援 Codex 單次任務,同時一個,五分鐘後請求取消。使用 CLI 的沙箱與核准政策;此頁不提供互動核准按鈕。Claude 目前提供偵測。", run:"開始任務", results:"任務與結果", refresh:"更新", signedIn:"已登入", signedOut:"尚未登入", unknown:"登入狀態未知", missing:"未找到執行檔", empty:"還沒有任務。", cancel:"取消任務", starting:"準備啟動", running:"執行中", cancelling:"正在取消", cancelled:"已取消", completed:"已完成", failed:"失敗", waiting:"正在接收 Windows CLI 的輸出…", uncertain:"提交結果不明,請按「更新」確認任務狀態,勿立即重送。" },
|
|
@@ -20,13 +20,24 @@ register("wincli", {
|
|
|
20
20
|
"zh-TW": { newTask:"派送 Windows CLI 任務", taskHint:"選擇本機 CLI 與模型,在指定專案中執行任務。", ready:"CLI 已就緒", unavailable:"請先確認 CLI 已安裝並登入", model:"模型", defaultModel:"使用 CLI 預設模型", write:"使用本機 CLI 權限", writeHint:"依照此 CLI 的本機核准設定", writeHintClaude:"可修改工作目錄內的檔案;目錄外的寫入、會修改檔案的 Shell 指令與網路都會被拒絕(唯讀指令仍會執行)", noReadOnly:"此 CLI 無法保證唯讀,請明確選擇本機 CLI 權限。", noProviders:"沒有找到已安裝的 CLI。安裝後可重新偵測。" },
|
|
21
21
|
en: { newTask:"Run a Windows CLI task", taskHint:"Choose a local CLI and model, then run a task in your project.", ready:"CLI is ready", unavailable:"Check that the CLI is installed and signed in", model:"Model", defaultModel:"Use the CLI default model", write:"Use native CLI permissions", writeHint:"Uses this CLI's local approval settings", writeHintClaude:"Edits files in the working directory; writes outside it, shell commands that change files and network access are denied (read-only commands still run)", noReadOnly:"This CLI cannot guarantee read-only access. Choose native CLI permissions explicitly.", noProviders:"No installed CLIs found. Detect again after installation." },
|
|
22
22
|
});
|
|
23
|
+
register("wincli", {
|
|
24
|
+
en: { remoteIntro:"Inspect CLIs and run a one-off task on the selected Windows computer.", offline:"This computer is offline. Showing its last reported inventory; reconnect its Runtime to detect or run tasks.", removed:"This Runtime is unavailable or no longer approved. Select another computer.", historyLoading:"Loading this computer's task status…", outcomePending:"A task outcome is unconfirmed. Reconnect this Runtime to reconcile before sending another task.", remoteHistory:"Latest 20 tasks on this computer", remoteWorkspace:"Selected Windows computer", remoteHint:"Choose this computer's CLI and model, then run a task in its working directory." },
|
|
25
|
+
"zh-TW": { remoteIntro:"查看所選 Windows 電腦的 CLI,並在該電腦執行一次性任務。", offline:"這台電腦已離線。目前顯示上次回報的清單;請重新連接該 Runtime,才能偵測或派送任務。", removed:"此 Runtime 無法使用或已不再核准,請選擇另一台電腦。", historyLoading:"正在讀取這台電腦的任務狀態…", outcomePending:"有任務的執行結果尚未確認,請重新連接該 Runtime 完成確認後再派工。", remoteHistory:"保留這台電腦最近 20 筆任務", remoteWorkspace:"所選 Windows 電腦", remoteHint:"選擇這台電腦的 CLI 與模型,在該電腦的工作目錄執行任務。" },
|
|
26
|
+
});
|
|
27
|
+
register("wincli", {
|
|
28
|
+
en: { testCli:"Test CLI", testHint:"Optional: send a one-off task to check execution on this computer.", closeKeepsTask:"Closing this window keeps submitted tasks running. Reopen this computer's diagnostics to check their status.", lastDetection:"Last detection: {0}", inventoryTitle:"Detected CLIs", neverDetection:"No detection recorded yet", modelsFound:"Models reported by this CLI", localDisabled:"This computer's Windows CLI access is disabled." },
|
|
29
|
+
"zh-TW": { testCli:"測試 CLI", testHint:"選用:送出一次性任務,確認這台電腦的 CLI 執行能力。", closeKeepsTask:"關閉視窗後,已提交的任務會繼續執行;重開這台電腦的診斷即可查看狀態。", lastDetection:"最近偵測:{0}", inventoryTitle:"偵測到的 CLI", neverDetection:"尚未有偵測紀錄", modelsFound:"此 CLI 回報的模型", localDisabled:"這台電腦的 Windows CLI 存取已停用。" },
|
|
30
|
+
});
|
|
23
31
|
const active = task => ["starting","running","cancelling"].includes(task.status);
|
|
24
32
|
|
|
25
|
-
function CliCard({ cli, probing }) {
|
|
33
|
+
function CliCard({ cli, probing, diagnostic = false }) {
|
|
26
34
|
const signedIn = cli.authenticated === true;
|
|
27
35
|
const checking = probing && !cli.path;
|
|
28
36
|
const authKey = checking ? "checking" : !cli.path ? "missing" : signedIn ? "signedIn" : cli.authenticated === false ? "signedOut" : "unknown";
|
|
29
37
|
const providerLabel = cli.company || ({codex:"OpenAI","claude-code":"Anthropic",antigravity:"Google",grok:"xAI","kiro-cli":"AWS",opencode:"Anomaly",muse:"Meta"})[cli.id];
|
|
38
|
+
if(diagnostic) return html`<details class="windows-cli-diagnostic-row"><summary><${CliProductMark} id=${cli.id} size=${24} /><strong>${cliLabel(cli.id)}</strong><span class="windows-cli-diagnostic-version">${cli.version || "—"}</span><span class=${`windows-cli-auth ${signedIn ? "tone-ready" : "tone-neutral"}`}><span class="windows-cli-dot"></span>${tn(authKey)}</span><${Icon} name="chevron" size=${16} /></summary>
|
|
39
|
+
<div class="windows-cli-diagnostic-facts"><span class=${`windows-cli-badge ${cli.runnable ? "tone-ready" : "tone-neutral"}`}>${tn(cli.runnable ? "available" : cli.supports?.execution === false ? "detectionOnly" : "unavailableBadge")}</span><dl><dt>${tn("path")}</dt><dd><code class="windows-cli-path">${cli.path || tn("missing")}</code></dd>${cli.models?.length ? html`<dt>${tn("modelsFound")}</dt><dd><ul class="windows-cli-model-list">${cli.models.map(model=>html`<li key=${model.id}><code>${model.id}</code>${model.label && model.label!==model.id ? html`<span>${model.label}</span>` : null}</li>`)}</ul></dd>` : null}</dl>${cli.reason ? html`<p class="note">${cli.reason}</p>` : null}</div>
|
|
40
|
+
</details>`;
|
|
30
41
|
return html`<article class=${`windows-cli-provider is-${cli.id}`}>
|
|
31
42
|
<div class="windows-cli-provider-head">
|
|
32
43
|
<span class="windows-cli-brand"><${CliProductMark} id=${cli.id} size=${28} /></span>
|
|
@@ -46,23 +57,35 @@ function TaskResult({ task, latest, cancel }) {
|
|
|
46
57
|
<summary><span class=${`windows-cli-task-state tone-${tone}`}><span class="windows-cli-dot"></span>${tn(task.status)}</span><time dateTime=${new Date(task.startedAt).toISOString()}>${new Date(task.startedAt).toLocaleTimeString([], { hour:"2-digit", minute:"2-digit" })}</time><${Icon} name="chevron" size=${16} /></summary>
|
|
47
58
|
<code class="windows-cli-path windows-cli-task-cwd">${task.cwd}</code>
|
|
48
59
|
<div class="windows-cli-task-detail"><span class="windows-cli-output-label"><${Icon} name="view" size=${14} />${tn("output")}</span><pre class="windows-cli-output" tabIndex="0">${task.output || tn(active(task) ? "waiting" : "noOutput")}</pre>${task.error ? html`<p class="error">${task.error}</p>` : null}
|
|
49
|
-
${["starting","running"].includes(task.status) ? html`<button type="button" class="btn btn-sm btn-ghost danger" onClick=${() => cancel(task.id)}><${Icon} name="stop" size=${14} />${tn("cancel")}</button>` : null}
|
|
60
|
+
${cancel && ["starting","running"].includes(task.status) ? html`<button type="button" class="btn btn-sm btn-ghost danger" onClick=${() => cancel(task.id)}><${Icon} name="stop" size=${14} />${tn("cancel")}</button>` : null}
|
|
50
61
|
</div>
|
|
51
62
|
</details>`;
|
|
52
63
|
}
|
|
53
64
|
|
|
54
|
-
export function WindowsCliPanel({ navKey, embedded = false }) {
|
|
65
|
+
export function WindowsCliPanel({ navKey, embedded = false, machineId = "", machine, drafts, onInventoryChanged, diagnostic = false, enabled = true, session, taskLease }) {
|
|
55
66
|
const lease = useLease(navKey);
|
|
56
|
-
const
|
|
67
|
+
const approved = enabled && (!machineId || ["online","offline"].includes(machine?.status));
|
|
68
|
+
const online = enabled && (!machineId || machine?.status === "online");
|
|
69
|
+
const discovery = useWindowsCliCatalog(approved,machineId);
|
|
70
|
+
const apiBase = machineId ? `/ui/runtimes/machines/${encodeURIComponent(machineId)}` : "/ui/windows-cli";
|
|
71
|
+
const initialDraft = drafts?.get(machineId) || {};
|
|
57
72
|
const clis = discovery.data?.clis || [];
|
|
58
73
|
const probing = discovery.loading;
|
|
59
|
-
const [tasks,setTasks] = useState([]), [cwd,setCwd] = useState("");
|
|
60
|
-
const [prompt,setPrompt] = useState(""), [
|
|
61
|
-
const [selected,setSelected] = useState("codex"), [model,setModel] = useState("");
|
|
62
|
-
|
|
63
|
-
const
|
|
64
|
-
const
|
|
65
|
-
|
|
74
|
+
const [tasks,setTasks] = useState([]), [cwd,setCwd] = useState(initialDraft.cwd || "");
|
|
75
|
+
const [prompt,setPrompt] = useState(initialDraft.prompt || ""), [access,setAccess] = useState(initialDraft.access || "read-only");
|
|
76
|
+
const [selected,setSelected] = useState(initialDraft.selected || "codex"), [model,setModel] = useState(initialDraft.model || "");
|
|
77
|
+
// The page owns a send across diagnostic-dialog close/reopen. Closing never aborts a submitted POST.
|
|
78
|
+
const ownSession = useRef({requestId:null,pending:false,error:""});
|
|
79
|
+
const operation = session || ownSession.current;
|
|
80
|
+
const [submitting,setSubmitting] = useState(!!operation.requestId), [error,setError] = useState(operation.error || "");
|
|
81
|
+
const [testOpen,setTestOpen] = useState(initialDraft.testOpen || false);
|
|
82
|
+
const readGeneration = useRef(0), polling = useRef(false);
|
|
83
|
+
const [historyReady,setHistoryReady] = useState(false);
|
|
84
|
+
const latestDraft = useRef(null), directoryInitialized = useRef(!!initialDraft.cwd);
|
|
85
|
+
latestDraft.current = {cwd,prompt,access,selected,model,testOpen};
|
|
86
|
+
useEffect(() => () => { drafts?.set(machineId,latestDraft.current); },[drafts,machineId]);
|
|
87
|
+
const api = async (path,body,owner = lease) => {
|
|
88
|
+
const r = await owner.fetch(path, { credentials:"same-origin", ...(body === undefined ? {} : { method:"POST", headers:{ "Content-Type":"application/json" }, body:JSON.stringify(body) }) });
|
|
66
89
|
const data = await r.json();
|
|
67
90
|
if (!r.ok) { const e = new Error(data.error || `HTTP ${r.status}`); e.rejected = true; throw e; }
|
|
68
91
|
return data;
|
|
@@ -70,38 +93,45 @@ export function WindowsCliPanel({ navKey, embedded = false }) {
|
|
|
70
93
|
const refresh = async () => {
|
|
71
94
|
const generation = ++readGeneration.current;
|
|
72
95
|
try {
|
|
73
|
-
const data = await api(
|
|
96
|
+
const data = await api(`${apiBase}/tasks`);
|
|
74
97
|
if (!lease.current() || generation !== readGeneration.current) return;
|
|
75
|
-
setTasks(data.tasks);
|
|
76
|
-
if (
|
|
77
|
-
|
|
98
|
+
setTasks(data.tasks); setHistoryReady(true);
|
|
99
|
+
if (operation.requestId && data.tasks.some(task => task.id === operation.requestId)) { operation.requestId = null; operation.pending = false; operation.error = ""; }
|
|
100
|
+
setSubmitting(!!operation.requestId);
|
|
101
|
+
if(operation.error) setError(operation.error);
|
|
102
|
+
} catch (e) { if (lease.current() && generation === readGeneration.current) setError(e.message); }
|
|
78
103
|
};
|
|
79
|
-
const probe = () => discovery.refresh();
|
|
80
|
-
useEffect(() => { if (!embedded) setTitle("Windows CLI"); void refresh(); },[lease]);
|
|
81
|
-
useEffect(() => { if (discovery.data)
|
|
104
|
+
const probe = async () => { if (!online || !approved) return; const result = await discovery.refresh(); if (result && lease.current()) onInventoryChanged?.(); };
|
|
105
|
+
useEffect(() => { if (!embedded) setTitle("Windows CLI"); if (approved) void refresh(); },[lease,approved,online]);
|
|
106
|
+
useEffect(() => { if (discovery.data?.home && !directoryInitialized.current) { directoryInitialized.current = true; setCwd(discovery.data.home); } },[discovery.data]);
|
|
82
107
|
const running = tasks.some(active);
|
|
108
|
+
const uncertain = tasks.some(task => task.outcomeConfirmed === false);
|
|
83
109
|
useEffect(() => {
|
|
84
|
-
if (!running) return;
|
|
110
|
+
if ((!running && !uncertain && !operation.requestId) || !online || !approved) return;
|
|
85
111
|
const timer = lease.interval(async () => { if (polling.current) return; polling.current = true; try { await refresh(); } finally { polling.current = false; } },1000);
|
|
86
112
|
return () => lease.clear(timer);
|
|
87
|
-
},[lease,running]);
|
|
113
|
+
},[lease,running,uncertain,online,approved,submitting]);
|
|
88
114
|
const run = async () => {
|
|
89
|
-
if (
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
try { await api(
|
|
115
|
+
if (operation.requestId || !online || !approved || !historyReady || running || uncertain) return;
|
|
116
|
+
const input = { requestId:crypto.randomUUID(), cli:selected, ...(model.trim() ? {model:model.trim()} : {}), cwd:cwd.trim(), prompt, sandbox:access };
|
|
117
|
+
operation.requestId = input.requestId; operation.pending = true; operation.error = "";
|
|
118
|
+
setSubmitting(true);setError("");
|
|
119
|
+
try { await api(`${apiBase}/tasks`,input,taskLease || lease); }
|
|
94
120
|
catch (e) {
|
|
95
|
-
if
|
|
96
|
-
|
|
97
|
-
|
|
121
|
+
if(operation.requestId === input.requestId) {
|
|
122
|
+
operation.error = e.rejected ? e.message : `${e.message}\n${tn("uncertain")}`;
|
|
123
|
+
if(e.rejected) operation.requestId = null;
|
|
124
|
+
if (lease.current()) setError(operation.error || e.message);
|
|
125
|
+
}
|
|
126
|
+
} finally {
|
|
127
|
+
if(operation.requestId === input.requestId) operation.pending = false;
|
|
98
128
|
}
|
|
99
129
|
if (lease.current()) await refresh();
|
|
100
130
|
};
|
|
101
|
-
const cancel = async id => { try { await api(
|
|
131
|
+
const cancel = async id => { if (!online || !approved) return; try { await api(`${apiBase}/tasks/${id}/cancel`,{}); if (lease.current()) await refresh(); } catch(e) { if (lease.current()) setError(e.message); } };
|
|
102
132
|
const current = clis.find(cli => cli.id === selected);
|
|
103
|
-
const permissionReady =
|
|
104
|
-
const runnable = current?.runnable === true && permissionReady;
|
|
133
|
+
const permissionReady = access !== "read-only" || current?.readOnlySupport !== false;
|
|
134
|
+
const runnable = current?.runnable === true && permissionReady && online && approved && historyReady && !uncertain;
|
|
105
135
|
const providers = clis.filter(cli => cli.path);
|
|
106
136
|
const choices = providers.filter(cli => cli.supports?.execution !== false).map(cli => ({value:cli.id,label:cliLabel(cli.id),disabled:!cli.runnable,description:cli.authenticated === false ? tn("signedOut") : cli.version || "Windows"}));
|
|
107
137
|
useEffect(() => {
|
|
@@ -112,32 +142,38 @@ export function WindowsCliPanel({ navKey, embedded = false }) {
|
|
|
112
142
|
},[clis]);
|
|
113
143
|
return html`<div class=${embedded ? "p-windows-cli windows-cli-settings" : "panel p-windows-cli"}>${!embedded ? html`<${PanelHeader} title="Windows CLI" />` : null}
|
|
114
144
|
<div class=${embedded ? "windows-cli-settings-body" : "panel-body"}><div class="windows-cli-content">
|
|
115
|
-
<header class="windows-cli-intro"><div
|
|
116
|
-
<button type="button" class="btn windows-cli-probe" disabled=${probing} onClick=${probe}><${Icon} name="restart" size=${16} cls=${probing ? "windows-cli-spin" : ""} />${tn(probing ? "probing" : "probe")}</button>
|
|
145
|
+
<header class="windows-cli-intro"><div>${diagnostic ? html`<p>${discovery.data?.probedAt ? t("wincli.lastDetection",new Date(discovery.data.probedAt).toLocaleString()) : tn("neverDetection")}</p>` : html`<span class="windows-cli-eyebrow"><${Icon} name="view" size=${14} />${tn("native")}</span><h2>${tn(machineId ? "remoteWorkspace" : "workspace")}</h2><p>${tn(machineId ? "remoteIntro" : "intro")}</p>`}</div>
|
|
146
|
+
<button type="button" class="btn windows-cli-probe" disabled=${probing || !online || !approved} onClick=${probe}><${Icon} name="restart" size=${16} cls=${probing ? "windows-cli-spin" : ""} />${tn(probing ? "probing" : "probe")}</button>
|
|
117
147
|
</header>
|
|
118
|
-
|
|
119
|
-
|
|
148
|
+
${!diagnostic ? html`<${WindowsCliHostIdentity} inventory=${discovery.data} machineId=${machineId} machine=${machine} />` : null}
|
|
149
|
+
${!approved || !online ? html`<p class="note" role="status">${tn(!enabled ? "localDisabled" : approved ? "offline" : "removed")}</p>` : null}
|
|
150
|
+
${approved ? html`<${WindowsCliDiscoveryStatus} state=${{...discovery,refresh:probe}} disabled=${!online} />` : null}
|
|
151
|
+
<section class=${diagnostic ? "windows-cli-diagnostic-list" : "windows-cli-cards"} aria-label=${tn(diagnostic ? "inventoryTitle" : "providers")}>${providers.map(cli => html`<${CliCard} key=${cli.id} cli=${cli} probing=${probing} diagnostic=${diagnostic} />`)}${discovery.data && !providers.length && !probing && !discovery.error ? html`<p class="note">${tn("noProviders")}</p>` : null}</section>
|
|
120
152
|
${error ? html`<p class="error windows-cli-error" role="alert"><${Icon} name="alert" size=${18} /><span>${error}</span></p>` : null}
|
|
153
|
+
<details class=${diagnostic ? "windows-cli-test-disclosure" : "windows-cli-test-disclosure is-standalone"} open=${!diagnostic || testOpen || running || uncertain || submitting} onToggle=${event=>{if(event.target===event.currentTarget)setTestOpen(event.currentTarget.open);}}>
|
|
154
|
+
${diagnostic ? html`<summary><${Icon} name="view" size=${19} /><span><strong>${tn("testCli")}</strong><small>${tn("testHint")}</small></span><${Icon} name="chevron" size=${16} /></summary>` : null}
|
|
121
155
|
<div class="windows-cli-workbench">
|
|
122
|
-
<section class="windows-cli-card windows-cli-composer" aria-labelledby="windows-task-title"><div class="windows-cli-section-head"><span class="windows-cli-section-icon"><${Icon} name="edit" size=${19} /></span><div><h2 id="windows-task-title">${tn("newTask")}</h2><p>${tn("taskHint")}</p></div></div>
|
|
156
|
+
<section class="windows-cli-card windows-cli-composer" aria-labelledby="windows-task-title"><div class="windows-cli-section-head"><span class="windows-cli-section-icon"><${Icon} name="edit" size=${19} /></span><div><h2 id="windows-task-title">${tn("newTask")}</h2><p>${tn(machineId ? "remoteHint" : "taskHint")}</p></div></div>
|
|
123
157
|
<form onSubmit=${e => { e.preventDefault(); if (runnable && !submitting && !running && cwd.trim() && prompt.trim()) void run(); }}>
|
|
124
158
|
<div class="windows-cli-field"><label>CLI</label><${CliPicker} label="CLI" value=${selected} choices=${choices} disabled=${!choices.length || submitting || running} onChange=${value => { setSelected(value); setModel(""); }} /></div>
|
|
125
159
|
<div class="windows-cli-field"><label for="windows-model">${tn("model")}</label><${ModelSelect} key=${selected} id="windows-model" value=${model} onChange=${setModel} catalogOverride=${current?.models || []} inherit=${tn("defaultModel")} /></div>
|
|
126
160
|
<div class="windows-cli-field"><label for="windows-cwd">${tn("folder")}</label><input id="windows-cwd" class="windows-cli-directory" value=${cwd} placeholder="J:\\Project\\Game" aria-describedby="windows-cwd-hint" required spellcheck=${false} onInput=${e => setCwd(e.target.value)} /><p id="windows-cwd-hint">${tn("folderHint")}</p></div>
|
|
127
161
|
<div class="windows-cli-field"><label for="windows-prompt">${tn("prompt")}</label><textarea id="windows-prompt" rows="5" value=${prompt} placeholder=${tn("promptPlaceholder")} required maxLength=${16384} onInput=${e => setPrompt(e.target.value)} /></div>
|
|
128
162
|
<fieldset class="windows-cli-access"><legend>${tn("access")}</legend><div class="windows-cli-access-options">
|
|
129
|
-
${[["read-only","readOnly","readHint","eye"],["workspace-write","write","writeHint","edit"]].map(([value,label,hint,icon]) => html`<label key=${value} class=${`windows-cli-access-option${
|
|
163
|
+
${[["read-only","readOnly","readHint","eye"],["workspace-write","write","writeHint","edit"]].map(([value,label,hint,icon]) => html`<label key=${value} class=${`windows-cli-access-option${access === value ? " selected" : ""}`}><input type="radio" name="windows-sandbox" value=${value} checked=${access === value} disabled=${value === "read-only" && current?.readOnlySupport === false} onChange=${() => setAccess(value)} /><span><strong><${Icon} name=${icon} size=${15} />${tn(label)}</strong><small>${tn(value === "workspace-write" && current?.id === "claude-code" ? "writeHintClaude" : hint)}</small></span></label>`)}
|
|
130
164
|
</div></fieldset>
|
|
131
165
|
${!permissionReady ? html`<p class="note">${tn("noReadOnly")}</p>` : null}
|
|
132
166
|
<details class="windows-cli-policy"><summary><${Icon} name="info" size=${14} />${tn("policy")}<${Icon} name="chevron" size=${14} /></summary><p>${tn("policyText")}</p></details>
|
|
133
|
-
<div class="windows-cli-submit"><span class=${`windows-cli-readiness ${runnable && !running ? "tone-ready" : "tone-neutral"}`} role="status"><span class="windows-cli-dot"></span>${tn(running ? "busy" : runnable ? "ready" : "unavailable")}</span><button type="submit" class="btn btn-primary" disabled=${!runnable || submitting || running || !cwd.trim() || !prompt.trim()}><${Icon} name=${submitting ? "clock" : "play"} size=${16} />${tn(submitting ? "submitting" : "run")}</button></div>
|
|
167
|
+
<div class="windows-cli-submit"><span class=${`windows-cli-readiness ${runnable && !running ? "tone-ready" : "tone-neutral"}`} role="status"><span class="windows-cli-dot"></span>${tn(uncertain ? "outcomePending" : !historyReady ? "historyLoading" : running ? "busy" : runnable ? "ready" : "unavailable")}</span><button type="submit" class="btn btn-primary" disabled=${!runnable || submitting || running || !cwd.trim() || !prompt.trim()}><${Icon} name=${submitting ? "clock" : "play"} size=${16} />${tn(submitting ? "submitting" : "run")}</button></div>
|
|
134
168
|
<p class="windows-cli-limits"><${Icon} name="clock" size=${13} />${tn("taskLimits")}</p>
|
|
135
169
|
</form>
|
|
136
170
|
</section>
|
|
137
171
|
<section class="windows-cli-card windows-cli-history" aria-labelledby="windows-history-title"><div class="windows-cli-section-head"><span class="windows-cli-section-icon"><${Icon} name="clock" size=${19} /></span><div><h2 id="windows-history-title">${tn("results")} <span class="windows-cli-count">${tasks.length}</span></h2><p>${tn("historyHint")}</p></div><button type="button" class="icon-btn" aria-label=${tn("refresh")} title=${tn("refresh")} onClick=${refresh}><${Icon} name="restart" size=${16} /></button></div>
|
|
138
|
-
<div class="windows-cli-task-list">${!tasks.length ? html`<div class="windows-cli-empty"><span><${Icon} name="view" size=${28} /></span><h3>${tn("empty")}</h3><p>${tn("emptyHint")}</p></div>` : tasks.map((task,index) => html`<${TaskResult} key=${task.id} task=${task} cancel=${cancel} latest=${index === 0} />`)}</div>
|
|
139
|
-
<p class="windows-cli-history-note">${tn("historyNote")}</p>
|
|
172
|
+
<div class="windows-cli-task-list">${!tasks.length ? html`<div class="windows-cli-empty"><span><${Icon} name="view" size=${28} /></span><h3>${tn("empty")}</h3><p>${tn("emptyHint")}</p></div>` : tasks.map((task,index) => html`<${TaskResult} key=${task.id} task=${task} cancel=${online && approved ? cancel : null} latest=${index === 0} />`)}</div>
|
|
173
|
+
<p class="windows-cli-history-note">${tn(machineId ? "remoteHistory" : "historyNote")}</p>
|
|
140
174
|
</section>
|
|
141
175
|
</div>
|
|
176
|
+
${diagnostic ? html`<p class="windows-cli-close-note"><${Icon} name="info" size=${15} />${tn("closeKeepsTask")}</p>` : null}
|
|
177
|
+
</details>
|
|
142
178
|
</div></div></div>`;
|
|
143
179
|
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
// View-only Agent list filters. They never stage settings or read another endpoint.
|
|
2
|
+
import { html } from "/assets/app-html.js";
|
|
3
|
+
import { register, t } from "/assets/app-i18n.js";
|
|
4
|
+
import { instanceMachineId } from "./runtime-catalog.js";
|
|
5
|
+
|
|
6
|
+
register("agentFilters", {
|
|
7
|
+
en: {
|
|
8
|
+
title: "Filter Agents", runtime: "Runtime", status: "Status", allRuntimes: "All Runtimes", allStatuses: "All statuses",
|
|
9
|
+
wsl: "WSL / Linux", local: "Windows local", remote: "Windows remote", machine: "{0} · Windows remote", unknownRuntime: "Unknown Runtime",
|
|
10
|
+
revokedMachine: "{0} · revoked", revokedComputer: "Revoked computer · {0}", unknownComputer: "Unknown computer · {0}",
|
|
11
|
+
machineGone: "This computer is no longer connected. Open the Agent's settings and change its Runtime. ({0})",
|
|
12
|
+
running: "Running", paused: "Paused", stopped: "Stopped", crashed: "Crashed", unknown: "Unknown status", offline: "Runtime offline",
|
|
13
|
+
results: "Showing {0} of {1} Agents", clear: "Clear filters", noMatch: "No matching Agents", noMatchHint: "Try a different search, Runtime, or status.",
|
|
14
|
+
},
|
|
15
|
+
"zh-TW": {
|
|
16
|
+
title: "篩選 Agent", runtime: "Runtime", status: "狀態", allRuntimes: "所有 Runtime", allStatuses: "所有狀態",
|
|
17
|
+
wsl: "WSL / Linux", local: "Windows 本機", remote: "Windows 遠端", machine: "{0} · Windows 遠端", unknownRuntime: "未知 Runtime",
|
|
18
|
+
revokedMachine: "{0} · 已撤銷", revokedComputer: "已撤銷的電腦 · {0}", unknownComputer: "未知電腦 · {0}",
|
|
19
|
+
machineGone: "這台電腦已不再連線。請開啟 Agent 設定,變更 Runtime。({0})",
|
|
20
|
+
running: "執行中", paused: "已暫停", stopped: "已停止", crashed: "已當掉", unknown: "未知狀態", offline: "Runtime 離線",
|
|
21
|
+
results: "顯示 {0} / {1} 個 Agent", clear: "清除篩選", noMatch: "沒有符合條件的 Agent", noMatchHint: "請調整搜尋、Runtime 或狀態。",
|
|
22
|
+
},
|
|
23
|
+
});
|
|
24
|
+
export const agentFilterText = (key, ...values) => t(`agentFilters.${key}`, ...values);
|
|
25
|
+
const STATUSES = ["running", "paused", "stopped", "crashed"];
|
|
26
|
+
|
|
27
|
+
/** A computer is named by what the Runtime page shows; only an id that names no computer is shown as (the start of) the id. */
|
|
28
|
+
const shortId = (id) => String(id).slice(0, 8);
|
|
29
|
+
/**
|
|
30
|
+
* One computer's label. `found` is false once the computer list has loaded and has no such computer; until then (or when
|
|
31
|
+
* the list cannot be read) an unnamed one is simply "Windows remote" rather than a claim that it is gone.
|
|
32
|
+
*/
|
|
33
|
+
export function machineLabel(id, name, status, found = true) {
|
|
34
|
+
if (name) return { label: agentFilterText(status === "revoked" ? "revokedMachine" : "machine", name), title: status === "revoked" ? agentFilterText("machineGone", id) : name, gone: status === "revoked" };
|
|
35
|
+
if (status === "revoked") return { label: agentFilterText("revokedComputer", shortId(id)), title: agentFilterText("machineGone", id), gone: true };
|
|
36
|
+
if (!found) return { label: agentFilterText("unknownComputer", shortId(id)), title: agentFilterText("machineGone", id), gone: true };
|
|
37
|
+
return { label: agentFilterText("remote"), title: id, gone: false };
|
|
38
|
+
}
|
|
39
|
+
/** Missing runtime is the configured WSL default; invalid explicit values are not WSL. `machines` (id → {name, status}) is the loaded computer list, or null. */
|
|
40
|
+
export function agentRuntime(inst, live, machines = null) {
|
|
41
|
+
if (inst?.runtime === undefined || inst?.runtime === "wsl") return { kind: "wsl", machineId: "", label: agentFilterText("wsl") };
|
|
42
|
+
if (inst?.runtime !== "windows") return { kind: "unknown", machineId: "", label: agentFilterText("unknownRuntime") };
|
|
43
|
+
const id = instanceMachineId(inst);
|
|
44
|
+
if (id && typeof id !== "string") return { kind: "unknown", machineId: "", label: agentFilterText("unknownRuntime") };
|
|
45
|
+
if (!id) return { kind: "windows-local", machineId: "", label: agentFilterText("local") };
|
|
46
|
+
const sameLive = instanceMachineId(live) === id, row = machines?.get(id);
|
|
47
|
+
const name = inst.machine_name || (sameLive ? live?.machine_name : "") || row?.name || "";
|
|
48
|
+
const status = (sameLive ? live?.machine_status : "") === "revoked" || row?.status === "revoked" ? "revoked" : row?.status || (sameLive ? live?.machine_status : "");
|
|
49
|
+
return { kind: "windows-remote", machineId: id, ...machineLabel(id, name, status, !machines || !!row || !!name) };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Unknown/unreadable snapshots must not be reported as stopped. */
|
|
53
|
+
export function agentStatus(live) {
|
|
54
|
+
return STATUSES.includes(live?.status) ? live.status : "unknown";
|
|
55
|
+
}
|
|
56
|
+
export const agentRuntimeOffline = (inst, live) => agentRuntime(inst, live).kind === "windows-remote" && live?.machine_status === "offline";
|
|
57
|
+
|
|
58
|
+
/** Search and grouping have already run; preserve their ordering and group identities. */
|
|
59
|
+
export function filterAgentGroups(groups, instances, live, { runtime = "", status = "" } = {}) {
|
|
60
|
+
return groups.map((group) => ({ ...group, names: group.names.filter((name) => {
|
|
61
|
+
const target = agentRuntime(instances[name], live[name]);
|
|
62
|
+
const matchesRuntime = !runtime || (runtime.startsWith("machine:") ? target.machineId === runtime.slice(8) && target.kind === "windows-remote" : target.kind === runtime);
|
|
63
|
+
const matchesStatus = !status || (status === "runtime-offline" ? agentRuntimeOffline(instances[name], live[name]) : agentStatus(live[name]) === status);
|
|
64
|
+
return matchesRuntime && matchesStatus;
|
|
65
|
+
}) })).filter((group) => group.names.length);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function AgentFilters({ instances, live, machines = null, runtime, status, matched, total, active, onRuntime, onStatus, onClear }) {
|
|
69
|
+
const labels = new Map();
|
|
70
|
+
let unknownRuntime = runtime === "unknown", offline = status === "runtime-offline";
|
|
71
|
+
for (const [name, inst] of Object.entries(instances)) {
|
|
72
|
+
const target = agentRuntime(inst, live[name], machines);
|
|
73
|
+
if (target.machineId) labels.set(target.machineId, target.label);
|
|
74
|
+
if (target.kind === "unknown") unknownRuntime = true;
|
|
75
|
+
if (agentRuntimeOffline(inst, live[name])) offline = true;
|
|
76
|
+
}
|
|
77
|
+
// Preserve a selected machine across a snapshot/config refresh instead of silently selecting another Runtime.
|
|
78
|
+
if (runtime.startsWith("machine:") && !labels.has(runtime.slice(8))) labels.set(runtime.slice(8), machineLabel(runtime.slice(8), "", "", false).label);
|
|
79
|
+
return html`<div class="s-agent-filters" role="group" aria-label=${agentFilterText("title")}>
|
|
80
|
+
<div class="s-agent-filter-controls">
|
|
81
|
+
<label><span>${agentFilterText("runtime")}</span><select value=${runtime} onChange=${(event) => onRuntime(event.target.value)}>
|
|
82
|
+
<option value="">${agentFilterText("allRuntimes")}</option><option value="wsl">${agentFilterText("wsl")}</option><option value="windows-local">${agentFilterText("local")}</option><option value="windows-remote">${agentFilterText("remote")}</option>
|
|
83
|
+
${[...labels].sort((a, b) => a[1].localeCompare(b[1])).map(([id, label]) => html`<option key=${id} value=${`machine:${id}`}>${label}</option>`)}
|
|
84
|
+
${unknownRuntime ? html`<option value="unknown">${agentFilterText("unknownRuntime")}</option>` : null}
|
|
85
|
+
</select></label>
|
|
86
|
+
<label><span>${agentFilterText("status")}</span><select value=${status} onChange=${(event) => onStatus(event.target.value)}>
|
|
87
|
+
<option value="">${agentFilterText("allStatuses")}</option>${[...STATUSES, "unknown"].map((value) => html`<option key=${value} value=${value}>${agentFilterText(value)}</option>`)}
|
|
88
|
+
${offline ? html`<option value="runtime-offline">${agentFilterText("offline")}</option>` : null}
|
|
89
|
+
</select></label>
|
|
90
|
+
<button type="button" class="btn btn-sm s-agent-filter-clear" disabled=${!active} onClick=${onClear}>${agentFilterText("clear")}</button>
|
|
91
|
+
</div>
|
|
92
|
+
<p class="s-agent-filter-count" role="status" aria-live="polite">${agentFilterText("results", matched, total)}</p>
|
|
93
|
+
</div>`;
|
|
94
|
+
}
|
|
@@ -5,6 +5,8 @@ import { html, useEffect, useRef, useState } from "/assets/app-html.js";
|
|
|
5
5
|
import { register, t } from "/assets/app-i18n.js";
|
|
6
6
|
import { recordEdit } from "/assets/text-undo.js";
|
|
7
7
|
import { appStore, useStore } from "/assets/app-store.js";
|
|
8
|
+
// #2978 merge: from their own module, never from the long-lived shell (a stale tab's shell lacks them).
|
|
9
|
+
import { DEFAULT_SETTINGS_CONTENT_WIDTH, SETTINGS_CONTENT_WIDTHS, settingsContentWidthStore, setSettingsContentWidth } from "/assets/settings-width.js";
|
|
8
10
|
import { ModsCard } from "./settings-mods.js";
|
|
9
11
|
import { Icon } from "/assets/ui-icons.js";
|
|
10
12
|
import { toast } from "/assets/ui-toast.js";
|
|
@@ -22,6 +24,22 @@ import { CardTitle } from "./settings-learn.js";
|
|
|
22
24
|
import "./settings-strings.js";
|
|
23
25
|
|
|
24
26
|
const tn = (k, ...v) => t(`settings.${k}`, ...v);
|
|
27
|
+
register("settingsContentWidth", {
|
|
28
|
+
en: {
|
|
29
|
+
label: "Settings content width",
|
|
30
|
+
device: "This device · saves immediately",
|
|
31
|
+
hint: "Applies to every Settings page; adapts to narrow windows.",
|
|
32
|
+
option: "{0} px",
|
|
33
|
+
defaultOption: "{0} px (default)",
|
|
34
|
+
},
|
|
35
|
+
"zh-TW": {
|
|
36
|
+
label: "設定頁內容寬度",
|
|
37
|
+
device: "此裝置・立即儲存",
|
|
38
|
+
hint: "套用到所有設定頁;窄視窗會自動配合可用空間。",
|
|
39
|
+
option: "{0} px",
|
|
40
|
+
defaultOption: "{0} px(預設)",
|
|
41
|
+
},
|
|
42
|
+
});
|
|
25
43
|
/** #2390: a refusal's detail in the page's language when the host named it (detail_key), else its English as sent. */
|
|
26
44
|
export function appIconDetail(body) {
|
|
27
45
|
const key = body && typeof body.detail_key === "string" ? `appIconDetail_${body.detail_key}` : null;
|
|
@@ -61,16 +79,85 @@ export function AppearanceSection(props = {}) {
|
|
|
61
79
|
const part = props.part || "look";
|
|
62
80
|
const publicLink = typeof document !== "undefined" && document.body && document.body.dataset.publicLink === "1";
|
|
63
81
|
// #2347 regrouping: Look (the person's own), this device's switches (in Preferences) and the host-wide cards (in Web
|
|
64
|
-
// dashboard) are drawn where each belongs.
|
|
65
|
-
if (publicLink) return part === "look" ? html`<
|
|
82
|
+
// dashboard) are drawn where each belongs. Public Look keeps the existing browser-only theme choice; customization stays unavailable there.
|
|
83
|
+
if (publicLink) return part === "look" ? html`<div class="s-look-workspace s-look-public"><div class="s-look-main">
|
|
84
|
+
<${ThemeModeCard} publicLink=${true} />
|
|
85
|
+
<section class="card s-appearance"><h3>${tn("cssTitle")}</h3><p class="sub">${tn("cssPublicLink")}</p></section>
|
|
86
|
+
</div></div>` : null;
|
|
66
87
|
if (part === "device") return html`<${MotionCard} ...${props} /><${DeviceCard} ...${props} />`;
|
|
67
|
-
if (part === "dashboard") return html`<${AppIconCard}
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
88
|
+
if (part === "dashboard") return html`<${AppIconCard} /><details class="s-look-disclosure s-dashboard-limits"><summary><${Icon} name="sliders" size=${17} /><span>${tn("cardLimitsTitle")}</span><${Icon} name="chevron" size=${16} /></summary><div class="s-look-disclosure-body"><${CardLimitsCard} /></div></details>`;
|
|
89
|
+
return html`<div class="s-look-workspace">
|
|
90
|
+
<div class="s-look-basics">
|
|
91
|
+
<div class="s-look-main">
|
|
92
|
+
<${ThemeModeCard} />
|
|
93
|
+
<section class="card s-appearance s-look-identity"><h3>${tn("lookIdentityGroup")}</h3>
|
|
94
|
+
<div class="s-look-identity-controls"><${CardSizeCard} /><${IdentityColorCard} /></div>
|
|
95
|
+
</section>
|
|
96
|
+
<section class="card s-appearance s-look-motion"><${CardTitle} title=${tn("lookMotionGroup")} topic="themes" />
|
|
97
|
+
<${ThemesCard} compact=${true} ...${props} /><${MotionCard} compact=${true} ...${props} />
|
|
98
|
+
</section>
|
|
99
|
+
</div>
|
|
100
|
+
<${AppearancePreview} />
|
|
101
|
+
</div>
|
|
102
|
+
<div class="s-look-advanced">
|
|
103
|
+
<${AppearanceDisclosure} title=${tn("widgetTitle")} icon="sliders"><${SidebarWidgetCard} /></${AppearanceDisclosure}>
|
|
104
|
+
<${AppearanceDisclosure} title=${tn("cssTitle")} icon="code"><${OwnerCssCard} /><${DeviceCard} ...${props} /></${AppearanceDisclosure}>
|
|
105
|
+
<${AppearanceDisclosure} title=${tn("modsTitle")} icon="plug"><${ModsCard} /></${AppearanceDisclosure}>
|
|
106
|
+
<${AppearanceDisclosure} title=${tn("restoreTitle")} icon="restart" danger=${true}><${RestoreDefaultsCard} ...${props} /></${AppearanceDisclosure}>
|
|
107
|
+
</div>
|
|
108
|
+
</div>`;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Theme remains a normal personal preference, including the public dashboard's existing browser-only choice. */
|
|
112
|
+
function ThemeModeCard({ publicLink = false }) {
|
|
113
|
+
const [theme, setTheme] = useState(typeof AgendTheme !== "undefined" ? AgendTheme.get() : "system");
|
|
114
|
+
useEffect(() => (typeof AgendTheme !== "undefined" && typeof AgendTheme.onChange === "function" ? AgendTheme.onChange(setTheme) : undefined), []);
|
|
115
|
+
const choose = (value) => { setTheme(value); if (typeof AgendTheme !== "undefined") AgendTheme.set(value); savePref("theme", value); };
|
|
116
|
+
return html`<section class="card s-appearance s-look-display"><h3>${tn("lookDisplayGroup")}</h3>
|
|
117
|
+
<fieldset id="pref-theme" class="s-theme-picker"><legend>${t("app.theme")}</legend><div class="s-theme-options">
|
|
118
|
+
${[["light", "themeLight"], ["dark", "themeDark"], ["system", "themeSystem"]].map(([value, key]) => html`<label key=${value} class=${`s-theme-choice${theme === value ? " selected" : ""}`}>
|
|
119
|
+
<input type="radio" name="pref-theme-choice" value=${value} checked=${theme === value} onChange=${(event) => { if (event.currentTarget.checked) choose(value); }} />
|
|
120
|
+
<span class=${`s-theme-miniature is-${value}`} aria-hidden="true"><span class="s-theme-mini-side"><i></i><i></i><i></i></span><span class="s-theme-mini-main"><i></i><i></i><i></i></span></span>
|
|
121
|
+
<span class="s-theme-choice-label">${t(`app.${key}`)}</span>
|
|
122
|
+
</label>`)}
|
|
123
|
+
</div></fieldset>
|
|
124
|
+
<${SettingsContentWidth} />
|
|
125
|
+
${publicLink ? html`<p class="sub">${tn("syncedPublicLink")}</p>` : null}
|
|
126
|
+
</section>`;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** This device's layout choice is independent of the Settings Save/Discard bar. */
|
|
130
|
+
function SettingsContentWidth() {
|
|
131
|
+
const { width } = useStore(settingsContentWidthStore);
|
|
132
|
+
return html`<div class="s-settings-content-width">
|
|
133
|
+
<label for="settings-content-width">${t("settingsContentWidth.label")}<span class="s-scope">${t("settingsContentWidth.device")}</span></label>
|
|
134
|
+
<select id="settings-content-width" value=${width} aria-describedby="settings-content-width-hint" onChange=${(event) => setSettingsContentWidth(Number(event.currentTarget.value))}>
|
|
135
|
+
${SETTINGS_CONTENT_WIDTHS.map((value) => html`<option key=${value} value=${value}>${t(`settingsContentWidth.${value === DEFAULT_SETTINGS_CONTENT_WIDTH ? "defaultOption" : "option"}`, value)}</option>`)}
|
|
136
|
+
</select>
|
|
137
|
+
<p id="settings-content-width-hint" class="sub">${t("settingsContentWidth.hint")}</p>
|
|
138
|
+
</div>`;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** The panels stay mounted inside native details, so drafts, CSS trials and deep links survive disclosure changes. */
|
|
142
|
+
function AppearanceDisclosure({ title, icon, danger = false, children }) {
|
|
143
|
+
return html`<details class=${`s-look-disclosure${danger ? " is-restore" : ""}`}>
|
|
144
|
+
<summary><${Icon} name=${icon} size=${17} /><span>${title}</span><${Icon} name="chevron" size=${16} /></summary>
|
|
145
|
+
<div class="s-look-disclosure-body">${children}</div>
|
|
146
|
+
</details>`;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** This preview uses the same live colour and default-size preferences as the existing controls. */
|
|
150
|
+
function AppearancePreview() {
|
|
151
|
+
useIdentityColors();
|
|
152
|
+
useStore(cardSizeStore);
|
|
153
|
+
const size = cardSizeDefault();
|
|
154
|
+
return html`<aside class="card s-look-preview" aria-labelledby="look-preview-title">
|
|
155
|
+
<h3 id="look-preview-title">${tn("lookPreview")}</h3>
|
|
156
|
+
<p class="sub">${tn("lookPreviewHint")}</p>
|
|
157
|
+
<div class="s-look-sidebar-sample"><${ColorSample} places=${defaultPlaces()} off=${colorsOff()} /></div>
|
|
158
|
+
<div class=${`s-look-card-sample is-${size}`} aria-hidden="true"><span class="s-look-preview-avatar">A</span>
|
|
159
|
+
<strong>${tn("colorSample")}</strong><span>${tn("cardSizeTitle")} · ${t(`card.size${size.toUpperCase()}Name`)}</span></div>
|
|
160
|
+
</aside>`;
|
|
74
161
|
}
|
|
75
162
|
|
|
76
163
|
/** #2347: the staged keys of this device's own switches (Restore defaults sets them, so their staged change goes). */
|
|
@@ -106,17 +193,23 @@ function IdentityColorCard() {
|
|
|
106
193
|
useIdentityColors();
|
|
107
194
|
const places = defaultPlaces(), off = colorsOff();
|
|
108
195
|
const toggle = (p) => setDefaultPlaces(places.includes(p) ? places.filter((x) => x !== p) : [...places, p]);
|
|
109
|
-
return html`<
|
|
110
|
-
|
|
111
|
-
<
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
196
|
+
return html`<details class="card s-appearance s-synced s-colors"><summary class="s-color-summary">
|
|
197
|
+
<${CardTitle} title=${tn("colorTitle")} topic="colors" />
|
|
198
|
+
<span class="s-color-summary-places">${off ? html`<span>${tn("colorOff")}</span>` : places.map((p) => html`<span key=${p}>${t(`color.place_${p}`)}</span>`)}</span>
|
|
199
|
+
<${Icon} name="chevron" size=${16} />
|
|
200
|
+
</summary>
|
|
201
|
+
<div class="s-color-settings">
|
|
202
|
+
<p class="sub">${tn("colorLead")}</p>
|
|
203
|
+
<div class="s-colors-row">
|
|
204
|
+
<fieldset class="idc-places" disabled=${off}><legend>${tn("colorPlaces")}</legend>
|
|
205
|
+
${PLACEMENTS.map((p) => html`<label key=${p} class="check"><input type="checkbox" checked=${places.includes(p)} onChange=${() => toggle(p)} /> ${t(`color.place_${p}`)}</label>`)}
|
|
206
|
+
</fieldset>
|
|
207
|
+
<${ColorSample} places=${places} off=${off} />
|
|
208
|
+
</div>
|
|
209
|
+
<label class="check"><input type="checkbox" class="s-colors-off" checked=${off} onChange=${(e) => setColorsOff(e.target.checked)} /> <span>${tn("colorOff")}</span></label>
|
|
210
|
+
<p class="sub">${tn("colorOffLead")}</p>
|
|
116
211
|
</div>
|
|
117
|
-
|
|
118
|
-
<p class="sub">${tn("colorOffLead")}</p>
|
|
119
|
-
</section>`;
|
|
212
|
+
</details>`;
|
|
120
213
|
}
|
|
121
214
|
|
|
122
215
|
/**
|
|
@@ -233,7 +326,7 @@ function AppIconCard() {
|
|
|
233
326
|
if (auth && auth.canWrite) Promise.resolve(auth.canWrite()).then((v) => setCanWrite(v === true), () => {});
|
|
234
327
|
}, []);
|
|
235
328
|
useEffect(() => () => { if (picked) URL.revokeObjectURL(picked.url); }, [picked]);
|
|
236
|
-
const title = html
|
|
329
|
+
const title = html`<div class="s-app-icon-heading"><h3 class="s-brand-title">${tn("lookBrandGroup")}</h3><${CardTitle} title=${tn("appIconTitle")} topic="app-icon" /></div>`;
|
|
237
330
|
if (!state) return html`<section class="card s-appearance s-app-icon">${title}<p class="sub">…</p></section>`;
|
|
238
331
|
if (state.failed) return html`<section class="card s-appearance s-app-icon">${title}<p class="err">${tn("appIconLoadFailed")}</p></section>`;
|
|
239
332
|
|
|
@@ -290,7 +383,7 @@ function AppIconCard() {
|
|
|
290
383
|
${state.custom && !picked ? html`<button type="button" class="btn" disabled=${busy} onClick=${reset}>${tn("appIconReset")}</button>` : null}
|
|
291
384
|
</div>` : html`<p class="sub">${tn("appIconReadOnly")}</p>`}
|
|
292
385
|
<p class="sub">${tn("appIconWho")}</p>
|
|
293
|
-
<p class="sub">${tn("appIconIos")}</p>
|
|
386
|
+
<details class="s-app-icon-help"><summary>${tn("lookIconMore")}</summary><p class="sub">${tn("appIconIos")}</p></details>
|
|
294
387
|
</section>`;
|
|
295
388
|
}
|
|
296
389
|
|
|
@@ -335,7 +428,7 @@ function CardLimitsCard() {
|
|
|
335
428
|
if (r.ok) { setMsg(tn("cardLimitsSaved")); load(); } else setMsg(tn("cardLimitsFailed", (r.body && r.body.error) || r.status));
|
|
336
429
|
};
|
|
337
430
|
const warnings = Array.isArray(view.warnings) ? view.warnings : [];
|
|
338
|
-
return html`<section class="card s-appearance"><${CardTitle} title=${tn("cardLimitsTitle")} topic="limits" />
|
|
431
|
+
return html`<section class="card s-appearance s-card-limits"><${CardTitle} title=${tn("cardLimitsTitle")} topic="limits" />
|
|
339
432
|
<p class="sub">${tn("cardLimitsLead")}</p>
|
|
340
433
|
<fieldset class="form" disabled=${busy}>
|
|
341
434
|
${["portrait", "banner", "instance_quota"].map((g) => html`<div key=${g} class="grid3">
|
|
@@ -355,8 +448,11 @@ function CardLimitsCard() {
|
|
|
355
448
|
}
|
|
356
449
|
|
|
357
450
|
/** #1629 r2 (d): whether this person's screens show agents' themes (and, as they come, effects) — synced. */
|
|
358
|
-
function ThemesCard() {
|
|
451
|
+
function ThemesCard({ compact = false } = {}) {
|
|
359
452
|
const s = useStore(themesStore);
|
|
453
|
+
if (compact) return html`<div class="s-look-toggle-line s-themes"><label for="fx-show-themes">${tn("themesShow")}</label>
|
|
454
|
+
<input id="fx-show-themes" class="s-ui-toggle" type="checkbox" checked=${s.shown} disabled=${safeMode()} onChange=${(e) => setThemesShown(e.target.checked)} />
|
|
455
|
+
<p class="sub">${safeMode() ? tn("themesSafeMode") : tn("themesShowLead")}</p></div>`;
|
|
360
456
|
return html`<section class="card s-appearance s-synced s-themes"><${CardTitle} title=${tn("themesTitle")} topic="themes" />
|
|
361
457
|
<label class="check"><input id="fx-show-themes" type="checkbox" checked=${s.shown} disabled=${safeMode()} onChange=${(e) => setThemesShown(e.target.checked)} />
|
|
362
458
|
<span>${tn("themesShow")}</span></label>
|
|
@@ -510,6 +606,9 @@ function MotionCard(props = {}) {
|
|
|
510
606
|
const [follow, setFollow] = useState(followsReducedMotion);
|
|
511
607
|
useEffect(() => onMotionChange(() => setFollow(followsReducedMotion())), []);
|
|
512
608
|
const setFollowHere = deviceChoice(props, DEVICE_CHOICES[0], tn("themesFollowMotion"), followsReducedMotion, setFollowsReducedMotion);
|
|
609
|
+
if (props.compact) return html`<div class="s-look-toggle-line s-motion"><label for="fx-follow-reduced-motion">${tn("themesFollowMotion")}<span class="s-scope">${tn("cssDeviceTitle")}</span></label>
|
|
610
|
+
<input type="checkbox" class="s-ui-toggle" id="fx-follow-reduced-motion" checked=${follow} onChange=${(e) => setFollowHere(e.target.checked)} />
|
|
611
|
+
<p class="sub">${tn("themesFollowMotionLead")}</p></div>`;
|
|
513
612
|
return html`<section class="card s-appearance s-local s-motion"><h3>${tn("motionTitle")}</h3>
|
|
514
613
|
<label class="check"><input type="checkbox" id="fx-follow-reduced-motion" checked=${follow} onChange=${(e) => setFollowHere(e.target.checked)} />
|
|
515
614
|
<span>${tn("themesFollowMotion")}</span></label>
|
|
@@ -17,6 +17,7 @@ import { startOperation } from "./settings-apply.js";
|
|
|
17
17
|
import { goToTarget } from "./settings-registry.js";
|
|
18
18
|
import { sectionPath } from "/assets/app-route.js";
|
|
19
19
|
import { backendLabel } from "/assets/instance-nav.js";
|
|
20
|
+
import {AgentRuntimeSettings} from "./agent-runtime-settings.js";
|
|
20
21
|
import "./settings-strings.js"; // its words, wherever a picker of this file is used (#2072: Fleet's create dialog too)
|
|
21
22
|
import { backendChoices, modelChoices, OTHER_MODEL, useBackendRows, useModelCatalogState } from "./settings-catalog.js";
|
|
22
23
|
import {
|
|
@@ -146,9 +147,9 @@ export function ChipList({ items, onChange, placeholder, label }) {
|
|
|
146
147
|
const replyHint = (backend, mode, kiroUi) => (replyGuardSupported(backend, mode, kiroUi) ? tn("replyGuardSupported") : tn("replyGuardUnsupported", backend, mode));
|
|
147
148
|
|
|
148
149
|
/** The dialog every editor uses: Cancel / Stage change; the footer says what the changes made so far will cost (#2346: only the fields that changed). */
|
|
149
|
-
function EditDialog({ title, subtitle, onClose, onStage, impacts, schema, children, ready = true, wide = true }) {
|
|
150
|
+
function EditDialog({ title, subtitle, onClose, onStage, impacts, schema, children, ready = true, wide = true, drawer = false }) {
|
|
150
151
|
const kinds = [...new Set((impacts || []).map((f) => impactOf(schema, f)))];
|
|
151
|
-
return html`<${Dialog} title=${title} label=${subtitle ? `${title} — ${subtitle}` : title} onClose=${onClose} wide=${wide}
|
|
152
|
+
return html`<${Dialog} title=${title} label=${subtitle ? `${title} — ${subtitle}` : title} onClose=${onClose} wide=${wide} drawer=${drawer}
|
|
152
153
|
actions=${html`${kinds.length ? html`<span class="note dlg-impact">${kinds.map(impactText).join(" · ")}</span>` : null}
|
|
153
154
|
<button type="button" class="btn" onClick=${onClose}>${tn("cancel")}</button>
|
|
154
155
|
<button type="button" class="btn btn-primary" disabled=${!ready} onClick=${onStage}>${tn("modalDone")}</button>`}>
|
|
@@ -302,14 +303,13 @@ export function AgentDialog({ name, inst, ctx, onClose }) {
|
|
|
302
303
|
const inh = (k, value, target, kind, set = hasOwn(defaults, k)) => ({ text: valueText(value, kind), builtin: !set, from: target ? inAgentDefaults(target) : null });
|
|
303
304
|
const inheritedBackend = !inst.backend && f.backend === (defaults.backend || "claude-code");
|
|
304
305
|
const L = (text, field, id) => html`<${Label} text=${text} schema=${schema} field=${field} htmlFor=${id} />`;
|
|
305
|
-
return html`<${EditDialog} title=${ctx.shortName(name, inst)} subtitle=${tn("idLine", name)} onClose=${onClose} onStage=${stage} ready=${ready} impacts=${Object.keys(pending()).map((k) => `instance.${k}`)} schema=${schema}>
|
|
306
|
+
return html`<${EditDialog} title=${ctx.shortName(name, inst)} subtitle=${tn("idLine", name)} onClose=${onClose} onStage=${stage} ready=${ready} drawer=${true} impacts=${Object.keys(pending()).map((k) => `instance.${k}`)} schema=${schema}>
|
|
307
|
+
<${AgentRuntimeSettings} name=${name} inst=${{...inst,backend:f.backend}} inherited=${inheritedBackend ? html`<${Inherited} ...${inh("backend", f.backend, "#g-be", "backend")} />` : null} disabled=${Object.keys(pending()).length>0||ctx.staged?.size>0} onChanged=${()=>{ctx.reload();onClose();}}/>
|
|
306
308
|
<div class="grid2">
|
|
307
309
|
<div class="field">${L(tn("displayName"), "instance.display_name", "ag-dn")}
|
|
308
310
|
<${Override} inherit=${f.inherit.display_name} onInherit=${setInherit("display_name")} inherited=${inh("display_name", defaults.display_name || name, null, "", !!defaults.display_name)}>
|
|
309
311
|
<input id="ag-dn" type="text" placeholder=${name} disabled=${f.inherit.display_name} value=${shown("display_name", f.display_name, defaults.display_name || "")} onInput=${(e) => set("display_name")(e.target.value)} /></${Override}></div>
|
|
310
312
|
<div class="field">${L(tn("description"), "instance.description", "ag-desc")}<input id="ag-desc" type="text" value=${f.description} onInput=${(e) => set("description")(e.target.value)} /></div>
|
|
311
|
-
<div class="field">${L(tn("backend"), "instance.backend", "ag-be")}<${BackendSelect} id="ag-be" value=${f.backend} onChange=${set("backend")} />
|
|
312
|
-
${inheritedBackend ? html`<${Inherited} ...${inh("backend", f.backend, "#g-be", "backend")} />` : null}</div>
|
|
313
313
|
<div class="field">${L(tn("model"), "instance.model", "ag-model")}<${ModelSelect} id="ag-model" value=${f.model} onChange=${set("model")}
|
|
314
314
|
backend=${f.backend || defaults.backend || "claude-code"} instance=${name} inherit=${tn("inheritValue", defaults.model || tn("modelCliDefault"))} /></div>
|
|
315
315
|
</div>
|