@songsid/agend 2.1.6-beta.1 → 2.1.6-beta.3
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/apply-job.d.ts +114 -0
- package/dist/apply-job.js +214 -0
- package/dist/apply-job.js.map +1 -0
- package/dist/backend/codex.d.ts +24 -0
- package/dist/backend/codex.js +102 -0
- package/dist/backend/codex.js.map +1 -1
- package/dist/backend/credential-profile.d.ts +159 -0
- package/dist/backend/credential-profile.js +340 -0
- package/dist/backend/credential-profile.js.map +1 -0
- package/dist/backend/kiro-auth-store.d.ts +25 -0
- package/dist/backend/kiro-auth-store.js +61 -0
- package/dist/backend/kiro-auth-store.js.map +1 -0
- package/dist/backend/kiro.d.ts +8 -0
- package/dist/backend/kiro.js +31 -2
- package/dist/backend/kiro.js.map +1 -1
- package/dist/cli.js +113 -0
- package/dist/cli.js.map +1 -1
- package/dist/config-validator.js +21 -0
- package/dist/config-validator.js.map +1 -1
- package/dist/daemon.d.ts +10 -0
- package/dist/daemon.js +27 -1
- package/dist/daemon.js.map +1 -1
- package/dist/fleet-level-config.d.ts +42 -0
- package/dist/fleet-level-config.js +78 -0
- package/dist/fleet-level-config.js.map +1 -0
- package/dist/fleet-lock.d.ts +21 -0
- package/dist/fleet-lock.js +42 -8
- package/dist/fleet-lock.js.map +1 -1
- package/dist/fleet-manager.d.ts +122 -1
- package/dist/fleet-manager.js +537 -83
- package/dist/fleet-manager.js.map +1 -1
- package/dist/general-knowledge/skills/credential-profiles/SKILL.md +145 -0
- package/dist/instance-config-impact.d.ts +55 -0
- package/dist/instance-config-impact.js +152 -0
- package/dist/instance-config-impact.js.map +1 -0
- package/dist/locale.js +2 -0
- package/dist/locale.js.map +1 -1
- package/dist/outbound-handlers.d.ts +5 -1
- package/dist/outbound-handlers.js +199 -1
- package/dist/outbound-handlers.js.map +1 -1
- package/dist/outbound-schemas.d.ts +6 -5
- package/dist/outbound-schemas.js +1 -0
- package/dist/outbound-schemas.js.map +1 -1
- package/dist/provider-probe.d.ts +45 -0
- package/dist/provider-probe.js +111 -0
- package/dist/provider-probe.js.map +1 -0
- package/dist/quickstart-api.d.ts +161 -0
- package/dist/quickstart-api.js +340 -0
- package/dist/quickstart-api.js.map +1 -0
- package/dist/quickstart.js +23 -50
- package/dist/quickstart.js.map +1 -1
- package/dist/self-restart-limit.d.ts +21 -0
- package/dist/self-restart-limit.js +129 -0
- package/dist/self-restart-limit.js.map +1 -0
- package/dist/settings-api.d.ts +15 -0
- package/dist/settings-api.js +126 -1
- package/dist/settings-api.js.map +1 -1
- package/dist/setup-auth.d.ts +113 -0
- package/dist/setup-auth.js +181 -0
- package/dist/setup-auth.js.map +1 -0
- package/dist/setup-form.d.ts +18 -0
- package/dist/setup-form.js +315 -0
- package/dist/setup-form.js.map +1 -0
- package/dist/setup-host.d.ts +162 -0
- package/dist/setup-host.js +496 -0
- package/dist/setup-host.js.map +1 -0
- package/dist/setup-marker.d.ts +8 -0
- package/dist/setup-marker.js +39 -0
- package/dist/setup-marker.js.map +1 -0
- package/dist/setup-tunnel-consent.d.ts +53 -0
- package/dist/setup-tunnel-consent.js +82 -0
- package/dist/setup-tunnel-consent.js.map +1 -0
- package/dist/tips.js +1 -1
- package/dist/tips.js.map +1 -1
- package/dist/transcript-sources.d.ts +12 -1
- package/dist/transcript-sources.js +19 -3
- package/dist/transcript-sources.js.map +1 -1
- package/dist/tunnel/cloudflared.d.ts +71 -0
- package/dist/tunnel/cloudflared.js +447 -0
- package/dist/tunnel/cloudflared.js.map +1 -0
- package/dist/tunnel/lease.d.ts +75 -0
- package/dist/tunnel/lease.js +209 -0
- package/dist/tunnel/lease.js.map +1 -0
- package/dist/tunnel/manager.d.ts +56 -0
- package/dist/tunnel/manager.js +167 -0
- package/dist/tunnel/manager.js.map +1 -0
- package/dist/tunnel/types.d.ts +119 -0
- package/dist/tunnel/types.js +33 -0
- package/dist/tunnel/types.js.map +1 -0
- package/dist/ui/dashboard.html +4 -3
- package/dist/ui/settings.html +640 -153
- package/dist/usage/providers.d.ts +17 -14
- package/dist/usage/providers.js +87 -55
- package/dist/usage/providers.js.map +1 -1
- package/dist/web-api.js +6 -4
- package/dist/web-api.js.map +1 -1
- package/dist/web-auth.d.ts +75 -0
- package/dist/web-auth.js +208 -2
- package/dist/web-auth.js.map +1 -1
- package/package.json +1 -1
package/dist/ui/settings.html
CHANGED
|
@@ -78,8 +78,24 @@
|
|
|
78
78
|
.inherit-row { display:flex; gap:8px; align-items:center; margin-top:6px; color:var(--muted); font-size:12px; }
|
|
79
79
|
.adv-body { display: none; padding: 14px 16px; }
|
|
80
80
|
.adv-body.open { display: block; }
|
|
81
|
-
|
|
82
|
-
|
|
81
|
+
/* One expandable drawer instead of a hidden third level: nothing is removed,
|
|
82
|
+
everything is one click away. */
|
|
83
|
+
.drawer { border:1px solid var(--border); border-radius:var(--radius-sm); margin-top:12px; background:var(--card); }
|
|
84
|
+
.drawer > summary { cursor:pointer; padding:10px 12px; font-weight:600; font-size:13px; list-style:none; }
|
|
85
|
+
.drawer > summary::-webkit-details-marker { display:none; }
|
|
86
|
+
.drawer > summary::before { content:"▶ "; color:var(--muted); }
|
|
87
|
+
.drawer[open] > summary::before { content:"▼ "; }
|
|
88
|
+
.drawer > .drawer-body { padding:0 12px 12px; }
|
|
89
|
+
.modal-backdrop { position:fixed; inset:0; background:rgba(17,24,39,.45); z-index:40; display:none; }
|
|
90
|
+
.modal-backdrop.open { display:block; }
|
|
91
|
+
.modal { position:fixed; z-index:41; inset:0; display:none; align-items:center; justify-content:center; padding:16px; }
|
|
92
|
+
.modal.open { display:flex; }
|
|
93
|
+
.modal-panel { background:var(--bg); border-radius:var(--radius); width:min(720px, 100%); max-height:88vh; display:flex; flex-direction:column; box-shadow:0 18px 48px rgba(0,0,0,.22); }
|
|
94
|
+
.modal-head { display:flex; align-items:baseline; gap:10px; padding:16px 20px 8px; border-bottom:1px solid var(--border); }
|
|
95
|
+
.modal-head h3 { font-size:16px; }
|
|
96
|
+
.modal-body { padding:16px 20px; overflow:auto; }
|
|
97
|
+
.modal-foot { display:flex; align-items:center; gap:10px; padding:12px 20px; border-top:1px solid var(--border); flex-wrap:wrap; }
|
|
98
|
+
.modal-foot .grow { flex:1; }
|
|
83
99
|
.more-menu { position:relative; }
|
|
84
100
|
.more-menu summary { list-style:none; cursor:pointer; padding:4px 9px; color:var(--muted); }
|
|
85
101
|
.more-menu summary::-webkit-details-marker { display:none; }
|
|
@@ -87,6 +103,13 @@
|
|
|
87
103
|
.more-menu .menu button { width:100%; text-align:left; border:0; }
|
|
88
104
|
.pending-bar { position:fixed; left:0; right:0; bottom:0; display:none; align-items:center; gap:12px; padding:12px max(16px, calc((100vw - 860px) / 2 + 24px)); background:#111827; color:#fff; z-index:35; box-shadow:0 -4px 18px rgba(0,0,0,.18); }
|
|
89
105
|
.pending-bar.open { display:flex; }
|
|
106
|
+
.apply-panel { position:fixed; left:0; right:0; bottom:0; display:none; flex-direction:column; gap:8px; padding:14px max(16px, calc((100vw - 860px) / 2 + 24px)); background:#111827; color:#fff; z-index:36; box-shadow:0 -4px 18px rgba(0,0,0,.18); max-height:60vh; overflow:auto; }
|
|
107
|
+
.apply-panel.open { display:flex; }
|
|
108
|
+
.apply-panel .apply-head { display:flex; align-items:baseline; gap:10px; }
|
|
109
|
+
.apply-panel .sub { color:#9ca3af; }
|
|
110
|
+
.apply-row { display:flex; align-items:center; gap:10px; font-size:13px; }
|
|
111
|
+
.apply-row .name { flex:1; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
|
|
112
|
+
.apply-row .err { color:#fca5a5; }
|
|
90
113
|
.pending-bar .grow { flex:1; }
|
|
91
114
|
.pending-bar button.primary { background:#fff; color:#111827; border-color:#fff; }
|
|
92
115
|
.level-label { font-size:11px; color:var(--muted); text-transform:uppercase; letter-spacing:.4px; margin-bottom:8px; }
|
|
@@ -131,7 +154,8 @@
|
|
|
131
154
|
<div class="grow"></div>
|
|
132
155
|
<button class="help-btn" id="langBtn" data-i18n-title="languageButton" title="Language">🌐</button>
|
|
133
156
|
<button class="help-btn" id="helpBtn" data-i18n-title="helpButton" title="Help">ℹ️</button>
|
|
134
|
-
<button id="
|
|
157
|
+
<button class="sm" id="wizardBtn" data-i18n="wizardButton">🧭 Setup wizard</button>
|
|
158
|
+
<input type="text" id="agentSearch" data-i18n-ph="searchPh" placeholder="🔍 Search agents, bots, channels…" style="width:220px;margin-right:8px">
|
|
135
159
|
</header>
|
|
136
160
|
<div class="help-backdrop" id="helpBackdrop"></div>
|
|
137
161
|
<aside class="help-panel" id="helpPanel">
|
|
@@ -139,9 +163,23 @@
|
|
|
139
163
|
<div id="helpContent"></div>
|
|
140
164
|
</aside>
|
|
141
165
|
<div class="banner" id="banner"></div>
|
|
166
|
+
<div class="modal-backdrop" id="modalBackdrop"></div>
|
|
167
|
+
<div class="modal" id="modal" role="dialog" aria-modal="true">
|
|
168
|
+
<div class="modal-panel">
|
|
169
|
+
<div class="modal-head"><h3 id="modalTitle"></h3><span class="sub" id="modalSubtitle"></span></div>
|
|
170
|
+
<div class="modal-body" id="modalBody"></div>
|
|
171
|
+
<div class="modal-foot">
|
|
172
|
+
<span class="sub" id="modalImpact"></span>
|
|
173
|
+
<span class="grow"></span>
|
|
174
|
+
<span id="modalActions"></span>
|
|
175
|
+
<button id="modalCancel" data-i18n="cancel">Cancel</button>
|
|
176
|
+
<button class="primary" id="modalDone" data-i18n="modalDone">Stage change</button>
|
|
177
|
+
</div>
|
|
178
|
+
</div>
|
|
179
|
+
</div>
|
|
142
180
|
<main>
|
|
143
181
|
<section>
|
|
144
|
-
<div class="sec-head"><h2 data-i18n="myAgents">🤖 My Agents</h2><div class="grow"></div><
|
|
182
|
+
<div class="sec-head"><h2 data-i18n="myAgents">🤖 My Agents</h2><div class="grow"></div><button class="sm" id="newAgentBtn" data-i18n="new">+ New</button></div>
|
|
145
183
|
<div id="newAgent"></div>
|
|
146
184
|
<div class="card" id="agents"></div>
|
|
147
185
|
</section>
|
|
@@ -152,7 +190,7 @@
|
|
|
152
190
|
</section>
|
|
153
191
|
|
|
154
192
|
<section>
|
|
155
|
-
<div class="sec-head"><h2>🔌 Connections & Bots</h2><div class="grow"></div><button class="sm
|
|
193
|
+
<div class="sec-head"><h2>🔌 Connections & Bots</h2><div class="grow"></div><button class="sm" id="newBotBtn" data-i18n="add">+ Add</button></div>
|
|
156
194
|
<div id="newBot"></div>
|
|
157
195
|
<div class="card" id="bots"></div>
|
|
158
196
|
</section>
|
|
@@ -168,6 +206,10 @@
|
|
|
168
206
|
<div class="card"><div class="adv-body" id="advBody"></div></div>
|
|
169
207
|
</section>
|
|
170
208
|
</main>
|
|
209
|
+
<div class="apply-panel" id="applyPanel">
|
|
210
|
+
<div class="apply-head"><strong id="applyTitle">Applying changes…</strong><span id="applyNote" class="sub"></span></div>
|
|
211
|
+
<div id="applyRows"></div>
|
|
212
|
+
</div>
|
|
171
213
|
<div class="pending-bar" id="pendingBar">
|
|
172
214
|
<strong id="pendingCount">0 changes not applied</strong>
|
|
173
215
|
<span id="pendingImpact">⚡ Immediately · 🔄 Restart this Agent · 🔄🔄 Restart AgEnD</span>
|
|
@@ -178,7 +220,8 @@
|
|
|
178
220
|
<script>
|
|
179
221
|
(() => {
|
|
180
222
|
"use strict";
|
|
181
|
-
|
|
223
|
+
// No token in the URL: the gate redeemed it for an HttpOnly session cookie,
|
|
224
|
+
// which the browser attaches to these same-origin requests on its own.
|
|
182
225
|
const $ = (id) => document.getElementById(id);
|
|
183
226
|
const el = (tag, attrs = {}, ...kids) => { const e = document.createElement(tag); for (const k in attrs) { if (k === "class") e.className = attrs[k]; else if (k === "html") e.innerHTML = attrs[k]; else if (k.startsWith("on")) e.addEventListener(k.slice(2), attrs[k]); else e.setAttribute(k, attrs[k]); } for (const c of kids) if (c != null) e.append(c); return e; };
|
|
184
227
|
const esc = (s) => String(s).replace(/[&<>]/g, c => ({ "&":"&","<":"<",">":">" }[c]));
|
|
@@ -204,7 +247,36 @@
|
|
|
204
247
|
helpTitle: "📖 Help", tipReload: "💡 Click [↻ Reload Fleet] after changes to apply them.",
|
|
205
248
|
wnIntro: "Recent highlights:", releaseNotes: "📄 View full Release Notes →", languageButton: "Language", helpButton: "Help", showAdditional: "Show additional controls", hideAdvanced: "Hide advanced", showAdvanced: "Show advanced",
|
|
206
249
|
saved: "Saved", failed: "Failed", actionFailed: "{0} failed", noAgentMatch: "No agents match \"{0}\".", deleteAgent: "Delete agent \"{0}\"? This will be applied only after you confirm the pending changes.", nameRequired: "name required", tokenEnvRequired: "token env required", removeBot: "Remove bot \"{0}\"? (the token in .env is left untouched)",
|
|
207
|
-
|
|
250
|
+
applying: "Applying changes…", applyDone: "✓ Changes applied", applyPartial: "⚠️ Some changes failed", applyClose: "Close",
|
|
251
|
+
settingsButton: "Settings", modalDone: "Stage change", advancedSection: "Advanced",
|
|
252
|
+
wizardButton: "🧭 Setup wizard", wizardTitle: "Set up AgEnD", wizardStep: "Step {0} of {1}",
|
|
253
|
+
wizardRerun: "A fleet is already configured — finishing replaces the connection that uses the same token variable.",
|
|
254
|
+
wizardBack: "Back", wizardNext: "Next", wizardFinish: "Create and start",
|
|
255
|
+
wizardBackend: "Backend", wizardBackendFound: "Found on this host: {0}", wizardBackendNone: "No backend CLI found on this host — install one first.",
|
|
256
|
+
wizardAgentName: "First agent name", wizardNeedDir: "Enter an absolute working directory.",
|
|
257
|
+
wizardTelegramHint: "Create a bot with @BotFather, then add it to a group.",
|
|
258
|
+
wizardDiscordHint: "Create an application in the Discord developer portal and invite the bot to your server.",
|
|
259
|
+
wizardToken: "Bot token", wizardTokenTelegram: "From @BotFather. It is written to ~/.agend/.env and never shown again.",
|
|
260
|
+
wizardTokenDiscord: "From the Bot tab of your application. It is written to ~/.agend/.env and never shown again.",
|
|
261
|
+
wizardVerify: "Verify", wizardVerifying: "Asking the provider…",
|
|
262
|
+
wizardDetect: "Detect from a message", wizardDetectHint: "Or post any message in the group and press Detect.",
|
|
263
|
+
wizardWaitingStart: "Waiting for a message in the group…", wizardNoStartYet: "No group message yet — post one and press Detect again.",
|
|
264
|
+
wizardGeneralChannel: "General channel id", wizardAdminUser: "Your user id (becomes the first allowed user)",
|
|
265
|
+
wizardNeedVerify: "Verify the token before continuing.",
|
|
266
|
+
wizardPlanLoading: "Building the plan…", wizardWillWrite: "This will be written:", wizardEnvKeys: "And into ~/.agend/.env: {0}",
|
|
267
|
+
wizardEnvPerms: "⚠️ ~/.agend/.env is readable by other users on this host.",
|
|
268
|
+
guildIdField: "Guild id", groupIdField: "Group id", notSet: "(not set)", deleteBot: "Delete this bot", noAllowedUsers: "no allowed users",
|
|
269
|
+
searchAll: "🔍 Search agents, bots, channels…", accessMovedHint: "Who may use the bot is set on the connection itself — open Settings on \"{0}\" under Connections & Bots.",
|
|
270
|
+
applyRestartNeeded: "Saved — restart AgEnD to apply", applyNoChange: "no change needed",
|
|
271
|
+
applyRestartHint: "This change needs AgEnD itself restarted. You can restart it here, or run /restart in your chat channel.",
|
|
272
|
+
applyBusy: "AgEnD is already reloading its configuration. Your changes are saved — try Apply again in a moment.",
|
|
273
|
+
restartFleetButton: "Restart AgEnD", restartFleetConfirm: "Restart AgEnD now?\n\nEvery agent stops and starts again, work in progress is interrupted, and the panel is unreachable for up to a minute. A notice is posted to your chat channel.",
|
|
274
|
+
restartFleetStarted: "Restarting AgEnD — this page keeps checking.", restartFleetRefused: "Restart refused: {0}",
|
|
275
|
+
restartFleetRateLimited: "Restart refused: {0} (try again in about {1} minutes)",
|
|
276
|
+
signatureMismatch: "fleet.yaml and the running configuration disagree on: {0}. Restarting will not clear this — check fleet.log first.",
|
|
277
|
+
applyStillWorking: "Still restarting ({0}s) — AgEnD is still working; this page keeps checking.",
|
|
278
|
+
applyLostJob: "Lost track of this apply (the fleet may have restarted). Reload the page to see the current state.",
|
|
279
|
+
changesPending: "{0} change(s) not applied", impactNow: "⚡ Immediately", impactAgent: "🔄 Restart this Agent", impactFleet: "🔄🔄 Restart AgEnD", queued: "Queued: {0}", applyFailed: "Apply failed", changesApplied: "✓ Changes applied. AgEnD is reconciling affected Agents.", reviewChanges: "Review changes", readyApply: "Ready to apply", copied: "Copied", expectedMapping: "expected a mapping", configLoadFailed: "Failed to load config — open the Settings link again.",
|
|
208
280
|
confirmOpenAccess: "Open access allows anyone in this channel to operate the bot. Continue?", confirmLockedEmpty: "Locked access has no allowed users. This may leave nobody able to administer the bot. Continue?", accessLockedEmpty: "Locked mode has no allowed users; add at least one administrator to avoid lockout.", accessChangeCancelled: "Access mode change cancelled",
|
|
209
281
|
mustNonNegative: "Must be 0 or greater", mustPositive: "Must be greater than 0", nonnegativeInteger: "Must be a non-negative integer", failoverEmpty: "Empty list disables failover", concurrencyRange: "Must be an integer from 1 to 20", concurrencyWarning: "May exceed CPU capacity on smaller hosts", staggerRange: "Must be between 0 and 30000", staggerWarning: "Very low values can cause startup spikes",
|
|
210
282
|
},
|
|
@@ -226,7 +298,36 @@
|
|
|
226
298
|
helpTitle: "📖 使用說明", tipReload: "💡 修改後點 [↻ 重新載入] 讓設定生效。",
|
|
227
299
|
wnIntro: "近期更新重點:", releaseNotes: "📄 查看完整 Release Notes →", languageButton: "語言", helpButton: "使用說明", showAdditional: "顯示更多控制項", hideAdvanced: "隱藏進階設定", showAdvanced: "顯示進階設定",
|
|
228
300
|
saved: "已儲存", failed: "失敗", actionFailed: "{0}失敗", noAgentMatch: "沒有符合「{0}」的 Agent。", deleteAgent: "確定刪除 Agent「{0}」嗎?只有在確認套用待處理變更後才會生效。", nameRequired: "必須填寫名稱", tokenEnvRequired: "必須填寫 token 環境變數", removeBot: "確定移除機器人「{0}」嗎?(.env 中的 token 會保留)",
|
|
229
|
-
|
|
301
|
+
applying: "正在套用變更…", applyDone: "✓ 變更已套用", applyPartial: "⚠️ 有部分變更失敗", applyClose: "關閉",
|
|
302
|
+
settingsButton: "設定", modalDone: "加入待套用", advancedSection: "進階",
|
|
303
|
+
wizardButton: "🧭 設定精靈", wizardTitle: "設定 AgEnD", wizardStep: "第 {0} 步,共 {1} 步",
|
|
304
|
+
wizardRerun: "已經有設定好的 fleet —— 完成後會取代使用相同 token 變數的那個連線。",
|
|
305
|
+
wizardBack: "上一步", wizardNext: "下一步", wizardFinish: "建立並啟動",
|
|
306
|
+
wizardBackend: "後端", wizardBackendFound: "在這台主機上找到:{0}", wizardBackendNone: "這台主機上找不到任何後端 CLI —— 請先安裝一個。",
|
|
307
|
+
wizardAgentName: "第一個 agent 名稱", wizardNeedDir: "請輸入絕對路徑的工作目錄。",
|
|
308
|
+
wizardTelegramHint: "用 @BotFather 建立 bot,然後把它加進一個群組。",
|
|
309
|
+
wizardDiscordHint: "在 Discord 開發者後台建立應用程式,並把 bot 邀請到你的伺服器。",
|
|
310
|
+
wizardToken: "Bot token", wizardTokenTelegram: "來自 @BotFather。會寫入 ~/.agend/.env,之後不會再顯示。",
|
|
311
|
+
wizardTokenDiscord: "來自應用程式的 Bot 分頁。會寫入 ~/.agend/.env,之後不會再顯示。",
|
|
312
|
+
wizardVerify: "驗證", wizardVerifying: "正在向平台確認…",
|
|
313
|
+
wizardDetect: "從訊息偵測", wizardDetectHint: "或在群組裡發任一則訊息,然後按「從訊息偵測」。",
|
|
314
|
+
wizardWaitingStart: "等待群組訊息中…", wizardNoStartYet: "還沒收到群組訊息 —— 發一則之後再按一次。",
|
|
315
|
+
wizardGeneralChannel: "General 頻道 id", wizardAdminUser: "你的 user id(會成為第一個允許使用者)",
|
|
316
|
+
wizardNeedVerify: "請先驗證 token 再繼續。",
|
|
317
|
+
wizardPlanLoading: "正在產生方案…", wizardWillWrite: "將寫入:", wizardEnvKeys: "以及寫入 ~/.agend/.env:{0}",
|
|
318
|
+
wizardEnvPerms: "⚠️ ~/.agend/.env 目前其他使用者也讀得到。",
|
|
319
|
+
guildIdField: "Guild id", groupIdField: "群組 id", notSet: "(未設定)", deleteBot: "刪除這個機器人", noAllowedUsers: "沒有允許的使用者",
|
|
320
|
+
searchAll: "🔍 搜尋 agent、機器人、頻道…", accessMovedHint: "誰可以使用 bot 是在連線本身設定 —— 請到「連線與機器人」開啟「{0}」的設定。",
|
|
321
|
+
applyRestartNeeded: "已儲存 —— 需要重新啟動 AgEnD 才會生效", applyNoChange: "無需變更",
|
|
322
|
+
applyRestartHint: "此變更需要重新啟動 AgEnD 本身。可以在這裡重新啟動,或在聊天頻道執行 /restart。",
|
|
323
|
+
applyBusy: "AgEnD 正在重新載入設定。你的變更已儲存,請稍候再按一次套用。",
|
|
324
|
+
restartFleetButton: "重新啟動 AgEnD", restartFleetConfirm: "現在重新啟動 AgEnD?\n\n所有 agent 都會停止再啟動,進行中的工作會被中斷,面板會有最多約一分鐘無法連線。聊天頻道會收到一則通知。",
|
|
325
|
+
restartFleetStarted: "AgEnD 重新啟動中 —— 此頁面會持續確認。", restartFleetRefused: "重新啟動被拒絕:{0}",
|
|
326
|
+
restartFleetRateLimited: "重新啟動被拒絕:{0}(約 {1} 分鐘後可再試)",
|
|
327
|
+
signatureMismatch: "fleet.yaml 與執行中的設定在這些項目上不一致:{0}。重新啟動無法解決,請先查看 fleet.log。",
|
|
328
|
+
applyStillWorking: "仍在重啟中({0} 秒)—— AgEnD 還在處理,此頁面會持續確認。",
|
|
329
|
+
applyLostJob: "已追蹤不到這次套用(AgEnD 可能重啟過)。請重新整理頁面查看目前狀態。",
|
|
330
|
+
changesPending: "有 {0} 項變更尚未套用", impactNow: "⚡ 立即生效", impactAgent: "🔄 重新啟動此 Agent", impactFleet: "🔄🔄 重新啟動 AgEnD", queued: "已加入待套用:{0}", applyFailed: "套用失敗", changesApplied: "✓ 變更已套用,AgEnD 正在協調受影響的 Agent。", reviewChanges: "檢查變更", readyApply: "可套用變更", copied: "已複製", expectedMapping: "預期為 mapping", configLoadFailed: "載入設定失敗 — 請重新開啟 Settings 連結。",
|
|
230
331
|
confirmOpenAccess: "Open 模式會允許此頻道中的所有人操作 bot,確定要繼續嗎?", confirmLockedEmpty: "Locked 模式目前沒有允許的使用者,可能導致沒有人能管理 bot,確定要繼續嗎?", accessLockedEmpty: "Locked 模式沒有允許的使用者;建議至少加入一位管理員,避免無法操作。", accessChangeCancelled: "已取消 Access Mode 變更",
|
|
231
332
|
mustNonNegative: "必須為 0 或更大的數值", mustPositive: "必須大於 0", nonnegativeInteger: "必須是非負整數", failoverEmpty: "空白清單會停用模型備援", concurrencyRange: "必須是 1 到 20 的整數", concurrencyWarning: "在較小的主機上可能超出 CPU 負荷", staggerRange: "必須介於 0 到 30000", staggerWarning: "數值太低可能造成啟動尖峰",
|
|
232
333
|
},
|
|
@@ -292,7 +393,7 @@
|
|
|
292
393
|
|
|
293
394
|
// ── API ──
|
|
294
395
|
async function api(path, opts = {}) {
|
|
295
|
-
const headers = Object.assign({
|
|
396
|
+
const headers = Object.assign({}, opts.headers || {});
|
|
296
397
|
if (opts.body) headers["Content-Type"] = "application/json";
|
|
297
398
|
const r = await fetch(path, Object.assign({}, opts, { headers }));
|
|
298
399
|
let body = null; try { body = await r.json(); } catch {}
|
|
@@ -316,7 +417,7 @@
|
|
|
316
417
|
}
|
|
317
418
|
|
|
318
419
|
// ── State ──
|
|
319
|
-
const state = { fleet: {}, classic: {}, classicStatus: {}, classicLive: {}, live: {}, fleetUp: false, advFmt: "yaml", advEdit: false, collapsedGroups: new Set(), version: "", newAgentOpen: false, newBotOpen: false, pending: new Map() };
|
|
420
|
+
const state = { schema: { impacts: {}, order: ["now", "instance", "fleet"] }, fleet: {}, classic: {}, classicStatus: {}, classicLive: {}, live: {}, fleetUp: false, advFmt: "yaml", advEdit: false, collapsedGroups: new Set(), version: "", newAgentOpen: false, newBotOpen: false, pending: new Map() };
|
|
320
421
|
const channels = () => state.fleet.channels || (state.fleet.channel ? [state.fleet.channel] : []);
|
|
321
422
|
const channelIds = () => channels().map(c => c.id || c.type).filter(Boolean);
|
|
322
423
|
const chLabel = (i) => i === 0 ? "primary" : "persona";
|
|
@@ -333,7 +434,250 @@
|
|
|
333
434
|
}
|
|
334
435
|
function select(value, options) { const s = el("select"); for (const o of options) { const val = typeof o === "string" ? o : o.value; const label = typeof o === "string" ? o : o.label; const opt = el("option", {}, label); opt.value = val; if (val === value) opt.selected = true; s.append(opt); } return s; }
|
|
335
436
|
const impactText = (kind) => t(kind === "now" ? "impactNow" : kind === "fleet" ? "impactFleet" : "impactAgent");
|
|
336
|
-
|
|
437
|
+
// The server derives every field's impact from HOT_INSTANCE_CONFIG_KEYS and
|
|
438
|
+
// hands it over at /api/settings/schema, so this page never decides it.
|
|
439
|
+
const impactOf = (field) => state.schema.impacts[field] || "instance";
|
|
440
|
+
const impact = (field) => el("span", { class: "impact" }, impactText(impactOf(field)));
|
|
441
|
+
// A batch costs as much as its worst member, in the order the server ships.
|
|
442
|
+
const batchImpact = (keys, impactFor) => {
|
|
443
|
+
const order = state.schema.order;
|
|
444
|
+
return order[keys.reduce((worst, k) => Math.max(worst, order.indexOf(impactFor(k))), 0)];
|
|
445
|
+
};
|
|
446
|
+
/** One expandable drawer. Advanced settings are collapsed, never removed. */
|
|
447
|
+
function drawer(title, ...children) {
|
|
448
|
+
const box = el("details", { class: "drawer" });
|
|
449
|
+
box.append(el("summary", {}, title), el("div", { class: "drawer-body" }, ...children));
|
|
450
|
+
return box;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
/**
|
|
454
|
+
* One modal edits one object, and closing it stages a change — it never
|
|
455
|
+
* writes. Applying stays where it was: the Apply button on the pending bar.
|
|
456
|
+
*/
|
|
457
|
+
let modalState = null;
|
|
458
|
+
function openModal({ title, subtitle, body, impacts, stage, doneLabel, actions }) {
|
|
459
|
+
modalState = { stage };
|
|
460
|
+
$("modalActions").innerHTML = "";
|
|
461
|
+
// A wizard drives its own footer; an editor uses the standard Cancel/Done.
|
|
462
|
+
$("modalDone").style.display = actions ? "none" : "";
|
|
463
|
+
if (actions) $("modalActions").append(...actions);
|
|
464
|
+
$("modalTitle").textContent = title;
|
|
465
|
+
$("modalSubtitle").textContent = subtitle || "";
|
|
466
|
+
$("modalBody").innerHTML = "";
|
|
467
|
+
$("modalBody").append(body);
|
|
468
|
+
// The footer says what the whole modal will cost, so the impact per field
|
|
469
|
+
// does not have to be added up by eye.
|
|
470
|
+
const kinds = [...new Set((impacts || []).map(impactOf))];
|
|
471
|
+
$("modalImpact").textContent = kinds.length ? kinds.map(impactText).join(" · ") : "";
|
|
472
|
+
$("modalDone").textContent = doneLabel || t("modalDone");
|
|
473
|
+
$("modal").classList.add("open");
|
|
474
|
+
$("modalBackdrop").classList.add("open");
|
|
475
|
+
}
|
|
476
|
+
function closeModal() {
|
|
477
|
+
modalState = null;
|
|
478
|
+
$("modal").classList.remove("open");
|
|
479
|
+
$("modalBackdrop").classList.remove("open");
|
|
480
|
+
$("modalBody").innerHTML = "";
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
// ── Setup wizard ──────────────────────────────────────────────────────────
|
|
484
|
+
//
|
|
485
|
+
// The same four steps as `agend quickstart`. There is no second flow for
|
|
486
|
+
// "a fleet already exists": the wizard is one modal that pre-fills from the
|
|
487
|
+
// running configuration and, when it finishes, hands over to the ordinary
|
|
488
|
+
// apply job — the same progress, deadline and restart recovery as every other
|
|
489
|
+
// change the panel makes.
|
|
490
|
+
const WIZARD_STEPS = 4;
|
|
491
|
+
let wiz = null;
|
|
492
|
+
|
|
493
|
+
function wizardField(labelText, input, hint) {
|
|
494
|
+
return el("div", { class: "field" }, el("label", {}, labelText), input,
|
|
495
|
+
hint ? el("div", { class: "sub" }, hint) : null);
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
async function openWizard() {
|
|
499
|
+
const envRes = await api("/api/settings/quickstart/environment");
|
|
500
|
+
wiz = {
|
|
501
|
+
step: 1,
|
|
502
|
+
env: envRes.body || { backends: [], channels: [], has_fleet: false },
|
|
503
|
+
backend: (envRes.body?.backends || [])[0] || "claude-code",
|
|
504
|
+
working_directory: "",
|
|
505
|
+
instance_name: "agent-1",
|
|
506
|
+
platform: "telegram",
|
|
507
|
+
token: "",
|
|
508
|
+
token_env: "AGEND_BOT_TOKEN",
|
|
509
|
+
group_id: "",
|
|
510
|
+
guild_id: "",
|
|
511
|
+
general_channel_id: "",
|
|
512
|
+
admin_user_id: "",
|
|
513
|
+
identity: null,
|
|
514
|
+
guilds: [],
|
|
515
|
+
plan: null,
|
|
516
|
+
busy: "",
|
|
517
|
+
};
|
|
518
|
+
renderWizard();
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
function renderWizard() {
|
|
522
|
+
const body = el("div", {});
|
|
523
|
+
body.append(el("div", { class: "sub" }, tf("wizardStep", wiz.step, WIZARD_STEPS)));
|
|
524
|
+
body.append(wizardStepBody());
|
|
525
|
+
if (wiz.busy) body.append(el("div", { class: "sub" }, wiz.busy));
|
|
526
|
+
|
|
527
|
+
const back = el("button", { onclick: () => { wiz.step--; renderWizard(); } }, t("wizardBack"));
|
|
528
|
+
back.disabled = wiz.step === 1;
|
|
529
|
+
const next = el("button", { class: "primary", onclick: () => advanceWizard() },
|
|
530
|
+
wiz.step === WIZARD_STEPS ? t("wizardFinish") : t("wizardNext"));
|
|
531
|
+
openModal({
|
|
532
|
+
title: t("wizardTitle"),
|
|
533
|
+
subtitle: wiz.env.has_fleet ? t("wizardRerun") : "",
|
|
534
|
+
body,
|
|
535
|
+
actions: [back, next],
|
|
536
|
+
});
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
function wizardStepBody() {
|
|
540
|
+
if (wiz.step === 1) {
|
|
541
|
+
const fBackend = select(wiz.backend, wiz.env.backends.length ? wiz.env.backends : BACKENDS);
|
|
542
|
+
fBackend.onchange = () => { wiz.backend = fBackend.value; };
|
|
543
|
+
const fDir = el("input", { type: "text", value: wiz.working_directory, placeholder: "/home/you/projects/app" });
|
|
544
|
+
fDir.oninput = () => { wiz.working_directory = fDir.value.trim(); };
|
|
545
|
+
const fName = el("input", { type: "text", value: wiz.instance_name });
|
|
546
|
+
fName.oninput = () => { wiz.instance_name = fName.value.trim(); };
|
|
547
|
+
return el("div", {},
|
|
548
|
+
wizardField(t("wizardBackend"), fBackend,
|
|
549
|
+
wiz.env.backends.length ? tf("wizardBackendFound", wiz.env.backends.join(", ")) : t("wizardBackendNone")),
|
|
550
|
+
wizardField(t("workingDir"), fDir),
|
|
551
|
+
wizardField(t("wizardAgentName"), fName));
|
|
552
|
+
}
|
|
553
|
+
if (wiz.step === 2) {
|
|
554
|
+
const pick = (platform) => el("button", { class: wiz.platform === platform ? "primary" : "", onclick: () => { wiz.platform = platform; renderWizard(); } },
|
|
555
|
+
platform === "telegram" ? "Telegram" : "Discord");
|
|
556
|
+
return el("div", {}, el("div", { class: "actions" }, pick("telegram"), pick("discord")),
|
|
557
|
+
el("div", { class: "sub" }, wiz.platform === "telegram" ? t("wizardTelegramHint") : t("wizardDiscordHint")));
|
|
558
|
+
}
|
|
559
|
+
if (wiz.step === 3) return wizardCredentialsStep();
|
|
560
|
+
return wizardReviewStep();
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
function wizardCredentialsStep() {
|
|
564
|
+
const fToken = el("input", { type: "password", value: wiz.token, placeholder: "123456:ABC-DEF…" });
|
|
565
|
+
fToken.oninput = () => { wiz.token = fToken.value.trim(); wiz.identity = null; };
|
|
566
|
+
const fEnv = el("input", { type: "text", value: wiz.token_env });
|
|
567
|
+
fEnv.oninput = () => { wiz.token_env = fEnv.value.trim(); };
|
|
568
|
+
const verify = el("button", { onclick: async () => {
|
|
569
|
+
wiz.busy = t("wizardVerifying"); renderWizard();
|
|
570
|
+
const res = await api("/api/settings/quickstart/probe", { method: "POST", body: JSON.stringify({ action: "verify", platform: wiz.platform, token: wiz.token }) });
|
|
571
|
+
wiz.identity = res.body?.identity || { valid: false };
|
|
572
|
+
wiz.busy = "";
|
|
573
|
+
if (wiz.identity.valid && wiz.platform === "discord") {
|
|
574
|
+
const guilds = await api("/api/settings/quickstart/probe", { method: "POST", body: JSON.stringify({ action: "guilds", token: wiz.token }) });
|
|
575
|
+
wiz.guilds = guilds.body?.guilds || [];
|
|
576
|
+
}
|
|
577
|
+
renderWizard();
|
|
578
|
+
} }, t("wizardVerify"));
|
|
579
|
+
|
|
580
|
+
const rows = [
|
|
581
|
+
wizardField(t("wizardToken"), fToken, wiz.platform === "telegram" ? t("wizardTokenTelegram") : t("wizardTokenDiscord")),
|
|
582
|
+
wizardField(t("tokenEnv"), fEnv),
|
|
583
|
+
el("div", { class: "actions" }, verify,
|
|
584
|
+
wiz.identity ? el("span", { class: wiz.identity.valid ? "s" : "e" },
|
|
585
|
+
wiz.identity.valid ? `✓ ${wiz.identity.username || ""}` : `✗ ${wiz.identity.reason || ""}`) : null),
|
|
586
|
+
];
|
|
587
|
+
|
|
588
|
+
// Only the ids that platform actually has.
|
|
589
|
+
if (wiz.platform === "discord") {
|
|
590
|
+
const fGuild = select(wiz.guild_id, ["", ...wiz.guilds.map(g => ({ value: g.id, label: `${g.name} (${g.id})` }))]);
|
|
591
|
+
fGuild.onchange = () => { wiz.guild_id = fGuild.value; };
|
|
592
|
+
const fGeneral = el("input", { type: "text", value: wiz.general_channel_id, placeholder: "General channel id" });
|
|
593
|
+
fGeneral.oninput = () => { wiz.general_channel_id = fGeneral.value.trim(); };
|
|
594
|
+
const fUser = el("input", { type: "text", value: wiz.admin_user_id, placeholder: "your Discord user id" });
|
|
595
|
+
fUser.oninput = () => { wiz.admin_user_id = fUser.value.trim(); };
|
|
596
|
+
rows.push(wizardField(t("guildIdField"), fGuild), wizardField(t("wizardGeneralChannel"), fGeneral), wizardField(t("wizardAdminUser"), fUser));
|
|
597
|
+
} else {
|
|
598
|
+
const fGroup = el("input", { type: "text", value: wiz.group_id, placeholder: "-1001234567890" });
|
|
599
|
+
fGroup.oninput = () => { wiz.group_id = fGroup.value.trim(); };
|
|
600
|
+
const detect = el("button", { onclick: async () => {
|
|
601
|
+
wiz.busy = t("wizardWaitingStart"); renderWizard();
|
|
602
|
+
const res = await api("/api/settings/quickstart/probe", { method: "POST", body: JSON.stringify({ action: "await-telegram-start", token: wiz.token, offset: wiz.offset || 0 }) });
|
|
603
|
+
wiz.busy = "";
|
|
604
|
+
if (!res.ok) { showBanner(res.body?.error || t("applyFailed"), true); renderWizard(); return; }
|
|
605
|
+
wiz.offset = res.body.offset;
|
|
606
|
+
if (res.body.found) {
|
|
607
|
+
wiz.group_id = String(res.body.found.groupId);
|
|
608
|
+
wiz.admin_user_id = String(res.body.found.userId);
|
|
609
|
+
} else showBanner(t("wizardNoStartYet"), true);
|
|
610
|
+
renderWizard();
|
|
611
|
+
} }, t("wizardDetect"));
|
|
612
|
+
const fUser = el("input", { type: "text", value: wiz.admin_user_id, placeholder: "your Telegram user id" });
|
|
613
|
+
fUser.oninput = () => { wiz.admin_user_id = fUser.value.trim(); };
|
|
614
|
+
rows.push(wizardField(t("groupIdField"), fGroup, t("wizardDetectHint")),
|
|
615
|
+
el("div", { class: "actions" }, detect), wizardField(t("wizardAdminUser"), fUser));
|
|
616
|
+
}
|
|
617
|
+
return el("div", {}, ...rows);
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
function wizardReviewStep() {
|
|
621
|
+
const box = el("div", {});
|
|
622
|
+
if (!wiz.plan) { box.append(el("div", { class: "sub" }, t("wizardPlanLoading"))); return box; }
|
|
623
|
+
box.append(
|
|
624
|
+
el("div", { class: "field" }, el("strong", {}, t("wizardWillWrite"))),
|
|
625
|
+
el("pre", { class: "yaml" }, toYaml({ channels: [wiz.plan.channel], instances: { [wiz.plan.instance.name]: { working_directory: wiz.plan.instance.working_directory, backend: wiz.plan.instance.backend } } })),
|
|
626
|
+
el("div", { class: "field" }, el("div", { class: "sub" }, tf("wizardEnvKeys", wiz.plan.env_keys.join(", ")))),
|
|
627
|
+
);
|
|
628
|
+
for (const warning of wiz.plan.warnings || []) box.append(el("div", { class: "field hint" }, "⚠️ " + warning));
|
|
629
|
+
return box;
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
async function advanceWizard() {
|
|
633
|
+
if (wiz.step === 1) {
|
|
634
|
+
if (!wiz.working_directory.startsWith("/")) { showBanner(t("wizardNeedDir"), true); return; }
|
|
635
|
+
wiz.step = 2; renderWizard(); return;
|
|
636
|
+
}
|
|
637
|
+
if (wiz.step === 2) { wiz.step = 3; renderWizard(); return; }
|
|
638
|
+
if (wiz.step === 3) {
|
|
639
|
+
if (!wiz.identity?.valid) { showBanner(t("wizardNeedVerify"), true); return; }
|
|
640
|
+
const res = await api("/api/settings/quickstart/plan", { method: "POST", body: JSON.stringify(wizardInput()) });
|
|
641
|
+
if (!res.ok) { showBanner(res.body?.error || t("applyFailed"), true); return; }
|
|
642
|
+
wiz.plan = res.body; wiz.step = 4; renderWizard(); return;
|
|
643
|
+
}
|
|
644
|
+
await finishWizard();
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
function wizardInput() {
|
|
648
|
+
return {
|
|
649
|
+
platform: wiz.platform, token_env: wiz.token_env, backend: wiz.backend,
|
|
650
|
+
working_directory: wiz.working_directory, instance_name: wiz.instance_name,
|
|
651
|
+
group_id: wiz.group_id || undefined, guild_id: wiz.guild_id || undefined,
|
|
652
|
+
general_channel_id: wiz.general_channel_id || undefined,
|
|
653
|
+
admin_user_id: wiz.admin_user_id || undefined,
|
|
654
|
+
};
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
/**
|
|
658
|
+
* Write the files, then start an ordinary apply job.
|
|
659
|
+
*
|
|
660
|
+
* The wizard does not apply anything itself — "create and start" is the same
|
|
661
|
+
* job the Apply button produces, so it has progress, a deadline and recovery
|
|
662
|
+
* across the restart it may cause instead of a silent spinner.
|
|
663
|
+
*/
|
|
664
|
+
async function finishWizard() {
|
|
665
|
+
const commit = await api("/api/settings/quickstart/commit", { method: "POST", body: JSON.stringify({ ...wizardInput(), token: wiz.token }) });
|
|
666
|
+
if (!commit.ok) { showBanner(commit.body?.error || t("applyFailed"), true); return; }
|
|
667
|
+
if (commit.body?.secret_mode_ok === false) showBanner(t("wizardEnvPerms"), true);
|
|
668
|
+
closeModal();
|
|
669
|
+
const key = (crypto.randomUUID ? crypto.randomUUID() : `w-${Date.now()}-${Math.random().toString(36).slice(2)}`);
|
|
670
|
+
const started = await api("/api/settings/apply", {
|
|
671
|
+
method: "POST",
|
|
672
|
+
headers: { "Idempotency-Key": key },
|
|
673
|
+
body: JSON.stringify({ idempotency_key: key }),
|
|
674
|
+
});
|
|
675
|
+
if (started.status === 409) { showBanner(t("applyBusy"), true); return; }
|
|
676
|
+
if (!started.ok || !started.body?.id) { showBanner(started.body?.error || t("applyFailed"), true); return; }
|
|
677
|
+
await watchApplyJob(started.body);
|
|
678
|
+
await reload();
|
|
679
|
+
}
|
|
680
|
+
|
|
337
681
|
function updatePendingBar() {
|
|
338
682
|
const n = state.pending.size;
|
|
339
683
|
$("pendingBar").classList.toggle("open", n > 0);
|
|
@@ -367,18 +711,126 @@
|
|
|
367
711
|
}
|
|
368
712
|
state.pending.delete(key); updatePendingBar();
|
|
369
713
|
}
|
|
370
|
-
|
|
714
|
+
// The key is generated here, before the first attempt, and reused by every
|
|
715
|
+
// retry: a POST whose response is lost on a phone must rejoin the same job
|
|
716
|
+
// instead of applying everything twice.
|
|
717
|
+
const key = (crypto.randomUUID ? crypto.randomUUID() : `k-${Date.now()}-${Math.random().toString(36).slice(2)}`);
|
|
718
|
+
const started = await api("/api/settings/apply", {
|
|
719
|
+
method: "POST",
|
|
720
|
+
headers: { "Idempotency-Key": key },
|
|
721
|
+
body: JSON.stringify({ idempotency_key: key }),
|
|
722
|
+
});
|
|
723
|
+
if (started.status === 409) {
|
|
724
|
+
// One reconcile at a time. The writes above already landed, so re-applying
|
|
725
|
+
// later is safe and idempotent.
|
|
726
|
+
showBanner(t("applyBusy"), true);
|
|
727
|
+
button.disabled = false;
|
|
728
|
+
return;
|
|
729
|
+
}
|
|
730
|
+
if (!started.ok || !started.body?.id) {
|
|
731
|
+
showBanner(started.body?.error || t("applyFailed"), true);
|
|
732
|
+
button.disabled = false;
|
|
733
|
+
return;
|
|
734
|
+
}
|
|
735
|
+
await watchApplyJob(started.body);
|
|
371
736
|
await reload();
|
|
372
|
-
showBanner(t("changesApplied"));
|
|
373
737
|
button.disabled = false;
|
|
374
738
|
}
|
|
739
|
+
|
|
740
|
+
function applyRow(row) {
|
|
741
|
+
const icon = row.status === "done" ? "✓"
|
|
742
|
+
: row.status === "failed" ? "✗"
|
|
743
|
+
: row.status === "restart-required" ? "🔄🔄"
|
|
744
|
+
: row.status === "running" ? "…" : "·";
|
|
745
|
+
// restart-required is terminal and is NOT success: the value is saved and a
|
|
746
|
+
// running AgEnD cannot adopt it.
|
|
747
|
+
const note = row.status === "restart-required" ? t("applyRestartNeeded")
|
|
748
|
+
: row.settled_by === "no-change" ? t("applyNoChange")
|
|
749
|
+
: row.settled_by === "fleet-restart" ? "(restart)"
|
|
750
|
+
: row.kind === "hot" ? t("impactNow") : t("impactAgent");
|
|
751
|
+
return el("div", { class: "apply-row" },
|
|
752
|
+
el("span", {}, icon),
|
|
753
|
+
el("span", { class: "name" }, row.target === "fleet" ? "AgEnD" : shortName(row.target, {})),
|
|
754
|
+
el("span", { class: "sub" }, note),
|
|
755
|
+
row.error ? el("span", { class: "err" }, row.error) : null);
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
async function restartFleet(job) {
|
|
759
|
+
// Its own confirmation, its own key. A single Apply must never be able to
|
|
760
|
+
// carry a full-fleet restart along with it.
|
|
761
|
+
if (!confirm(t("restartFleetConfirm"))) return;
|
|
762
|
+
const key = (crypto.randomUUID ? crypto.randomUUID() : `r-${Date.now()}-${Math.random().toString(36).slice(2)}`);
|
|
763
|
+
const res = await api("/api/settings/restart-fleet", {
|
|
764
|
+
method: "POST",
|
|
765
|
+
headers: { "Idempotency-Key": key },
|
|
766
|
+
body: JSON.stringify({ job_id: job.id, confirm: "restart-agend", idempotency_key: key }),
|
|
767
|
+
});
|
|
768
|
+
if (!res.ok) {
|
|
769
|
+
const secs = res.body?.retry_after_seconds;
|
|
770
|
+
showBanner(secs
|
|
771
|
+
? tf("restartFleetRateLimited", res.body?.error || "", Math.ceil(secs / 60))
|
|
772
|
+
: tf("restartFleetRefused", res.body?.error || t("applyFailed")), true);
|
|
773
|
+
return;
|
|
774
|
+
}
|
|
775
|
+
showBanner(t("restartFleetStarted"));
|
|
776
|
+
await watchApplyJob(job);
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
function renderApplyJob(job) {
|
|
780
|
+
$("applyPanel").classList.add("open");
|
|
781
|
+
$("applyTitle").textContent = job.status === "done"
|
|
782
|
+
? (job.targets.some(r => r.status === "failed") ? t("applyPartial") : t("applyDone"))
|
|
783
|
+
: job.status === "failed" ? t("applyPartial") : t("applying");
|
|
784
|
+
const needsRestart = job.targets.some(row => row.status === "restart-required");
|
|
785
|
+
const mismatch = state.schema.fleet_signature_mismatch;
|
|
786
|
+
$("applyNote").textContent = job.overdue
|
|
787
|
+
? tf("applyStillWorking", Math.round(job.elapsed_ms / 1000))
|
|
788
|
+
: mismatch && needsRestart ? tf("signatureMismatch", mismatch.join(", "))
|
|
789
|
+
: needsRestart ? t("applyRestartHint") : "";
|
|
790
|
+
const rows = $("applyRows"); rows.innerHTML = "";
|
|
791
|
+
for (const row of job.targets) rows.append(applyRow(row));
|
|
792
|
+
// A restart that cannot clear the mismatch is worse than no button at all.
|
|
793
|
+
if (needsRestart && !mismatch) {
|
|
794
|
+
rows.append(el("div", { class: "apply-row" },
|
|
795
|
+
el("button", { class: "primary", onclick: () => restartFleet(job) }, t("restartFleetButton"))));
|
|
796
|
+
}
|
|
797
|
+
if (job.error) rows.append(el("div", { class: "apply-row err" }, job.error));
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
/**
|
|
801
|
+
* GET is the authority. The SSE stream carries no event id, so a reconnect
|
|
802
|
+
* cannot replay missed frames; it only shortens the wait between polls.
|
|
803
|
+
*/
|
|
804
|
+
async function watchApplyJob(job) {
|
|
805
|
+
renderApplyJob(job);
|
|
806
|
+
const deadline = Date.now() + 10 * 60_000;
|
|
807
|
+
let current = job;
|
|
808
|
+
while (current.status === "running" && Date.now() < deadline) {
|
|
809
|
+
await new Promise(r => setTimeout(r, 1000));
|
|
810
|
+
const res = await api(`/api/settings/apply/${encodeURIComponent(job.id)}`);
|
|
811
|
+
if (res.status === 404) {
|
|
812
|
+
// The job outlives the process that made it, so a 404 means it aged out
|
|
813
|
+
// of the store — not that it never happened.
|
|
814
|
+
$("applyNote").textContent = t("applyLostJob");
|
|
815
|
+
return;
|
|
816
|
+
}
|
|
817
|
+
if (!res.ok || !res.body?.id) continue;
|
|
818
|
+
current = res.body;
|
|
819
|
+
renderApplyJob(current);
|
|
820
|
+
}
|
|
821
|
+
const failed = current.targets.some(r => r.status === "failed");
|
|
822
|
+
const needsRestart = current.targets.some(r => r.status === "restart-required");
|
|
823
|
+
showBanner(failed ? t("applyPartial") : needsRestart ? t("applyRestartNeeded") : t("changesApplied"),
|
|
824
|
+
failed || needsRestart);
|
|
825
|
+
// A restart reminder has to stay on screen; a clean apply can fade.
|
|
826
|
+
if (!needsRestart) setTimeout(() => $("applyPanel").classList.remove("open"), 4000);
|
|
827
|
+
}
|
|
375
828
|
const hasOwn = (obj, key) => Object.prototype.hasOwnProperty.call(obj || {}, key);
|
|
376
|
-
const HOT_FIELDS = new Set(["tool_progress", "reply_completion_guard", "mcp_proxy_reply", "auto_pause_after", "warm_cap", "display_name", "description", "tags", "log_level"]);
|
|
377
829
|
const sameValue = (a, b) => JSON.stringify(a) === JSON.stringify(b);
|
|
378
830
|
function changedFields(next, baseline) {
|
|
379
831
|
return Object.fromEntries(Object.entries(next).filter(([key, value]) => !sameValue(value, baseline[key])));
|
|
380
832
|
}
|
|
381
|
-
function overrideControl(inst, key, input, inheritedValue,
|
|
833
|
+
function overrideControl(inst, key, input, inheritedValue, impactField = `instance.${key}`) {
|
|
382
834
|
const inherited = !hasOwn(inst, key);
|
|
383
835
|
const toggle = el("input", { type: "checkbox" }); toggle.checked = inherited;
|
|
384
836
|
input.disabled = inherited;
|
|
@@ -387,7 +839,7 @@
|
|
|
387
839
|
else input.value = inheritedValue;
|
|
388
840
|
}
|
|
389
841
|
toggle.onchange = () => { input.disabled = toggle.checked; };
|
|
390
|
-
return { input, toggle, row: el("div", {}, input, el("label", { class: "inherit-row" }, toggle, el("span", {}, t("inheritDefault")), impact(
|
|
842
|
+
return { input, toggle, row: el("div", {}, input, el("label", { class: "inherit-row" }, toggle, el("span", {}, t("inheritDefault")), impact(impactField))) };
|
|
391
843
|
}
|
|
392
844
|
const replyGuardSupported = (backend, mode) => backend === "claude-code" && mode === "mcp";
|
|
393
845
|
const replyGuardHint = (backend, mode) => el(
|
|
@@ -412,7 +864,6 @@
|
|
|
412
864
|
}
|
|
413
865
|
|
|
414
866
|
// ── My Agents ──
|
|
415
|
-
let editing = null;
|
|
416
867
|
function agentChannelLabel(inst) {
|
|
417
868
|
if (inst.general_topic) return "general";
|
|
418
869
|
if (!inst.channel_id) return "";
|
|
@@ -453,7 +904,10 @@
|
|
|
453
904
|
el("span", { class: "tag" }, `effort: ${summary.effort}`),
|
|
454
905
|
chTag ? el("span", { class: "tag" + (chTag.includes("persona") ? " persona" : chTag.includes("telegram") ? " tg" : "") }, chTag) : null,
|
|
455
906
|
el("span", { class: "spacer" }),
|
|
456
|
-
el("button", { class: "sm ghost", onclick: () => {
|
|
907
|
+
el("button", { class: "sm ghost", onclick: () => {
|
|
908
|
+
const form = agentEditForm(name, inst);
|
|
909
|
+
openModal({ title: shortName(name, inst), subtitle: "ID: " + name, body: form.box, impacts: form.impacts, stage: form.stage });
|
|
910
|
+
} }, t("settingsButton")),
|
|
457
911
|
running ? el("button", { class: "sm", onclick: () => pauseWakeAgent(name, "pause") }, t("pause")) : null,
|
|
458
912
|
paused ? el("button", { class: "sm", onclick: () => pauseWakeAgent(name, "wake") }, t("wake")) : null,
|
|
459
913
|
!paused ? el("button", { class: "sm", onclick: () => toggleAgent(name, running) }, running ? t("stop") : t("start")) : null,
|
|
@@ -461,7 +915,6 @@
|
|
|
461
915
|
el("summary", { title: "More actions" }, "⋯"),
|
|
462
916
|
el("div", { class: "menu" }, el("button", { class: "sm danger", onclick: () => delAgent(name) }, "Delete Agent"))),
|
|
463
917
|
));
|
|
464
|
-
if (editing === name) item.append(agentEditForm(name, inst));
|
|
465
918
|
return item;
|
|
466
919
|
}
|
|
467
920
|
function renderAgents() {
|
|
@@ -504,13 +957,13 @@
|
|
|
504
957
|
const auto = overrideControl(inst, "auto_pause_after", el("input", { type: "number", min: "0", step: "1", value: inst.auto_pause_after ?? defaults.auto_pause_after ?? 0, placeholder: "0 = disabled" }), defaults.auto_pause_after ?? 0);
|
|
505
958
|
const hangRaw = inst.hang_detector || {};
|
|
506
959
|
const hangHolder = hasOwn(hangRaw, "timeout_minutes") ? { value: hangRaw.timeout_minutes } : {};
|
|
507
|
-
const hang = overrideControl(hangHolder, "value", el("input", { type: "number", min: "0.1", step: "0.5", value: hangRaw.timeout_minutes ?? defaults.hang_detector?.timeout_minutes ?? 15 }), defaults.hang_detector?.timeout_minutes ?? 15);
|
|
960
|
+
const hang = overrideControl(hangHolder, "value", el("input", { type: "number", min: "0.1", step: "0.5", value: hangRaw.timeout_minutes ?? defaults.hang_detector?.timeout_minutes ?? 15 }), defaults.hang_detector?.timeout_minutes ?? 15, "instance.hang_detector");
|
|
508
961
|
const agentMode = overrideControl(inst, "agent_mode", select(inst.agent_mode || defaults.agent_mode || "mcp", ["mcp", "cli"]), defaults.agent_mode || "mcp");
|
|
509
962
|
const toolSet = overrideControl(inst, "tool_set", select(inst.tool_set || defaults.tool_set || "full", ["full", "standard", "minimal"]), defaults.tool_set || "full");
|
|
510
|
-
const toolProgress = overrideControl(inst, "tool_progress", select(inst.tool_progress ?? defaults.tool_progress ?? "off", ["off", "standard", "verbose"]), defaults.tool_progress ?? "off"
|
|
963
|
+
const toolProgress = overrideControl(inst, "tool_progress", select(inst.tool_progress ?? defaults.tool_progress ?? "off", ["off", "standard", "verbose"]), defaults.tool_progress ?? "off");
|
|
511
964
|
const fReplyGuard = el("input", { type: "checkbox" });
|
|
512
965
|
fReplyGuard.checked = inst.reply_completion_guard ?? defaults.reply_completion_guard ?? true;
|
|
513
|
-
const replyGuard = overrideControl(inst, "reply_completion_guard", fReplyGuard, defaults.reply_completion_guard ?? true
|
|
966
|
+
const replyGuard = overrideControl(inst, "reply_completion_guard", fReplyGuard, defaults.reply_completion_guard ?? true);
|
|
514
967
|
const replyHint = replyGuardHint(fBackend.value, agentMode.input.value);
|
|
515
968
|
const refreshReplyHint = () => {
|
|
516
969
|
const supported = replyGuardSupported(fBackend.value, agentMode.input.value);
|
|
@@ -528,7 +981,10 @@
|
|
|
528
981
|
const hangFeedback = el("div", { class: "feedback" });
|
|
529
982
|
const failoverFeedback = el("div", { class: "feedback" });
|
|
530
983
|
const msg = el("span", { class: "msg" });
|
|
531
|
-
|
|
984
|
+
// Never rendered: it carries the validation state and the submit handler
|
|
985
|
+
// that the modal footer triggers through stage(). The form has no button
|
|
986
|
+
// of its own, so nothing here can write on its own.
|
|
987
|
+
const saveBtn = el("button", { hidden: "hidden" });
|
|
532
988
|
const validate = () => {
|
|
533
989
|
const autoValue = Number(auto.input.value), hangValue = Number(hang.input.value);
|
|
534
990
|
const validAuto = setValidation(auto.input, autoFeedback, !auto.toggle.checked && (!Number.isFinite(autoValue) || autoValue < 0) ? t("mustNonNegative") : "");
|
|
@@ -586,44 +1042,56 @@
|
|
|
586
1042
|
}
|
|
587
1043
|
stageChange(`agent:${name}`, {
|
|
588
1044
|
label: `Update ${shortName(name, state.fleet.instances[name])}`,
|
|
589
|
-
impact: Object.keys(patch)
|
|
1045
|
+
impact: batchImpact(Object.keys(patch), key => impactOf(`instance.${key}`)),
|
|
590
1046
|
apply: () => api(`/api/settings/fleet/instances/${encodeURIComponent(name)}`, { method: "PATCH", body: JSON.stringify(patch) }),
|
|
591
1047
|
});
|
|
592
|
-
|
|
1048
|
+
renderAgents();
|
|
593
1049
|
};
|
|
594
1050
|
box.append(
|
|
595
1051
|
el("div", { class: "sub", style: "margin:0 0 8px" }, "ID: " + name),
|
|
596
1052
|
el("div", { class: "grid2" },
|
|
597
|
-
el("div", {}, el("label", {}, t("displayName"), impact("
|
|
598
|
-
el("div", {}, el("label", {}, t("description"), impact("
|
|
599
|
-
el("div", {}, el("label", {}, t("backend"), impact("instance")), fBackend),
|
|
600
|
-
el("div", {}, el("label", {}, t("model"), impact("instance")), fModel)),
|
|
601
|
-
el("label", {}, t("workingDir"), impact("instance")), fWd,
|
|
1053
|
+
el("div", {}, el("label", {}, t("displayName"), impact("instance.display_name")), displayName.row),
|
|
1054
|
+
el("div", {}, el("label", {}, t("description"), impact("instance.description")), fDesc),
|
|
1055
|
+
el("div", {}, el("label", {}, t("backend"), impact("instance.backend")), fBackend),
|
|
1056
|
+
el("div", {}, el("label", {}, t("model"), impact("instance.model")), fModel)),
|
|
1057
|
+
el("label", {}, t("workingDir"), impact("instance.working_directory")), fWd,
|
|
602
1058
|
el("div", { class: "grid2" },
|
|
603
|
-
el("div", {}, el("label", {}, `${t("autoPause")} (${t("minutes")})`, impact("
|
|
604
|
-
|
|
605
|
-
el("div", { class: "sub", style: "margin:14px 0 6px;font-weight:600" }, "Advanced Agent settings"),
|
|
1059
|
+
el("div", {}, el("label", {}, `${t("autoPause")} (${t("minutes")})`, impact("instance.auto_pause_after")), auto.row, autoFeedback)),
|
|
1060
|
+
drawer(t("advancedSection"),
|
|
606
1061
|
el("div", { class: "grid2" },
|
|
607
|
-
el("div", {}, el("label", {}, t("channelBinding"), impact("
|
|
608
|
-
el("div", {}, el("label", {}, `${t("hangTimeout")} (${t("minutes")})
|
|
609
|
-
el("div", {}, el("label", {}, t("agentMode")), agentMode.row),
|
|
610
|
-
el("div", {}, el("label", {}, t("toolSet")), toolSet.row),
|
|
611
|
-
el("div", {}, el("label", {}, t("toolProgress"), impact("
|
|
612
|
-
el("div", {}, el("label", {}, t("replyCompletionGuard"), impact("
|
|
613
|
-
el("div", {}, el("label", {}, t("logLevel"), impact("
|
|
614
|
-
el("div", {}, el("label", {}, t("lightweight")), lightweight.row),
|
|
615
|
-
el("div", {}, el("label", {}, t("modelFailover")), failover.row, failoverFeedback)),
|
|
616
|
-
el("label", {}, t("systemPrompt"), impact("instance")), fSys,
|
|
617
|
-
el("label", {}, t("tags"), impact("
|
|
618
|
-
el("label", { style: "display:flex;align-items:center;gap:8px;cursor:pointer" }, fGeneral, el("span", {}, t("generalTopicField")), impact("
|
|
619
|
-
el("div", { class: "actions" },
|
|
620
|
-
saveBtn,
|
|
621
|
-
el("button", { class: "sm ghost", onclick: () => { editing = null; renderAgents(); } }, t("cancel")),
|
|
622
|
-
msg),
|
|
1062
|
+
el("div", {}, el("label", {}, t("channelBinding"), impact("instance.topic_id")), fChan),
|
|
1063
|
+
el("div", {}, el("label", {}, `${t("hangTimeout")} (${t("minutes")})`, impact("instance.hang_detector")), hang.row, hangFeedback),
|
|
1064
|
+
el("div", {}, el("label", {}, t("agentMode"), impact("instance.agent_mode")), agentMode.row),
|
|
1065
|
+
el("div", {}, el("label", {}, t("toolSet"), impact("instance.tool_set")), toolSet.row),
|
|
1066
|
+
el("div", {}, el("label", {}, t("toolProgress"), impact("instance.tool_progress")), toolProgress.row),
|
|
1067
|
+
el("div", {}, el("label", {}, t("replyCompletionGuard"), impact("instance.reply_completion_guard")), replyGuard.row, replyHint),
|
|
1068
|
+
el("div", {}, el("label", {}, t("logLevel"), impact("instance.log_level")), logLevel.row),
|
|
1069
|
+
el("div", {}, el("label", {}, t("lightweight"), impact("instance.lightweight")), lightweight.row),
|
|
1070
|
+
el("div", {}, el("label", {}, t("modelFailover"), impact("instance.model_failover")), failover.row, failoverFeedback)),
|
|
1071
|
+
el("label", {}, t("systemPrompt"), impact("instance.systemPrompt")), fSys,
|
|
1072
|
+
el("label", {}, t("tags"), impact("instance.tags")), chipList(tags, null, "tag"),
|
|
1073
|
+
el("label", { style: "display:flex;align-items:center;gap:8px;cursor:pointer" }, fGeneral, el("span", {}, t("generalTopicField")), impact("instance.general_topic"))),
|
|
1074
|
+
el("div", { class: "actions" }, msg),
|
|
623
1075
|
);
|
|
624
1076
|
validate();
|
|
625
|
-
return
|
|
1077
|
+
return {
|
|
1078
|
+
box,
|
|
1079
|
+
// The modal's footer drives this; nothing in the form submits by itself.
|
|
1080
|
+
stage: () => { if (saveBtn.disabled) return false; saveBtn.onclick(); return true; },
|
|
1081
|
+
impacts: AGENT_MODAL_FIELDS,
|
|
1082
|
+
};
|
|
626
1083
|
}
|
|
1084
|
+
|
|
1085
|
+
/** Every field the agent modal shows, for the footer's impact summary. */
|
|
1086
|
+
const AGENT_MODAL_FIELDS = [
|
|
1087
|
+
"instance.display_name", "instance.description", "instance.backend",
|
|
1088
|
+
"instance.model", "instance.working_directory", "instance.auto_pause_after",
|
|
1089
|
+
"instance.topic_id", "instance.hang_detector", "instance.agent_mode",
|
|
1090
|
+
"instance.tool_set", "instance.tool_progress",
|
|
1091
|
+
"instance.reply_completion_guard", "instance.log_level",
|
|
1092
|
+
"instance.lightweight", "instance.model_failover", "instance.systemPrompt",
|
|
1093
|
+
"instance.tags", "instance.general_topic",
|
|
1094
|
+
];
|
|
627
1095
|
async function toggleAgent(name, running) {
|
|
628
1096
|
if (running) await api(`/stop/${encodeURIComponent(name)}`, { method: "POST" });
|
|
629
1097
|
else await api(`/api/instance/${encodeURIComponent(name)}/start`, { method: "POST" });
|
|
@@ -638,7 +1106,7 @@
|
|
|
638
1106
|
if (!confirm(tf("deleteAgent", name))) return;
|
|
639
1107
|
delete state.fleet.instances[name];
|
|
640
1108
|
stageChange(`agent:${name}`, {
|
|
641
|
-
label: `Delete ${name}`, impact: "
|
|
1109
|
+
label: `Delete ${name}`, impact: impactOf("instance.delete"),
|
|
642
1110
|
apply: () => api(`/api/settings/fleet/instances/${encodeURIComponent(name)}`, { method: "DELETE" }),
|
|
643
1111
|
});
|
|
644
1112
|
renderAgents();
|
|
@@ -666,10 +1134,13 @@
|
|
|
666
1134
|
}
|
|
667
1135
|
|
|
668
1136
|
// ── Bots ──
|
|
669
|
-
let editingBot = null;
|
|
670
1137
|
function renderBots() {
|
|
671
|
-
const host = $("bots"); host.innerHTML = "";
|
|
672
|
-
|
|
1138
|
+
const host = $("bots"); host.innerHTML = ""; let chs = channels();
|
|
1139
|
+
// One search box for the whole page.
|
|
1140
|
+
const filter = ($("agentSearch")?.value || "").trim().toLowerCase();
|
|
1141
|
+
if (filter) chs = chs.filter(ch => [ch.id, ch.type, ch.group_id, ch.bot_token_env]
|
|
1142
|
+
.some(v => String(v ?? "").toLowerCase().includes(filter)));
|
|
1143
|
+
if (!chs.length) { host.className = "card empty"; host.textContent = filter ? tf("noAgentMatch", filter) : t("noBots"); return; }
|
|
673
1144
|
host.className = "card";
|
|
674
1145
|
chs.forEach((ch, i) => {
|
|
675
1146
|
const item = el("div", { class: "item" });
|
|
@@ -677,13 +1148,20 @@
|
|
|
677
1148
|
item.append(el("div", { class: "item-row" },
|
|
678
1149
|
el("span", { class: "name" }, type === "telegram" ? "Telegram" : "Discord"),
|
|
679
1150
|
el("span", { class: "tag" + (i === 0 ? "" : " persona") + (type === "telegram" ? " tg" : "") }, ch.id || chLabel(i)),
|
|
680
|
-
el("span", { class: "sub
|
|
1151
|
+
el("span", { class: "sub", style: "margin:0" }, ch.bot_token_env || "(no token env)"),
|
|
1152
|
+
ch.group_id ? el("span", { class: "tag" }, `${type === "telegram" ? "group" : "guild"} ${ch.group_id}`) : null,
|
|
1153
|
+
el("span", { class: "tag" }, `${t("accessMode")}: ${ch.access?.mode || "locked"}`),
|
|
1154
|
+
el("span", { class: "sub", style: "margin:0" },
|
|
1155
|
+
(ch.access?.allowed_users || []).length
|
|
1156
|
+
? (ch.access.allowed_users).slice(0, 3).join(", ") + ((ch.access.allowed_users).length > 3 ? ` +${(ch.access.allowed_users).length - 3}` : "")
|
|
1157
|
+
: t("noAllowedUsers")),
|
|
681
1158
|
el("span", { class: "spacer" }),
|
|
682
1159
|
el("span", { style: `color:${state.fleetUp ? "var(--green)" : "var(--red)"};font-size:13px` }, state.fleetUp ? "Connected" : "Problem"),
|
|
683
|
-
el("button", { class: "sm ghost
|
|
684
|
-
|
|
1160
|
+
el("button", { class: "sm ghost", onclick: () => {
|
|
1161
|
+
const form = botEditForm(ch, i);
|
|
1162
|
+
openModal({ title: type === "telegram" ? "Telegram" : "Discord", subtitle: ch.id || chLabel(i), body: form.box, impacts: form.impacts, stage: form.stage });
|
|
1163
|
+
} }, t("settingsButton")),
|
|
685
1164
|
));
|
|
686
|
-
if (editingBot === i) item.append(botEditForm(ch, i));
|
|
687
1165
|
host.append(item);
|
|
688
1166
|
});
|
|
689
1167
|
}
|
|
@@ -692,37 +1170,54 @@
|
|
|
692
1170
|
ch.access = ch.access || { mode: "locked", allowed_users: [] };
|
|
693
1171
|
ch.access.allowed_users = ch.access.allowed_users || [];
|
|
694
1172
|
const previousAccess = structuredClone(ch.access);
|
|
1173
|
+
const type = ch.type || "telegram";
|
|
695
1174
|
const box = el("div", { class: "edit" });
|
|
696
1175
|
const fMode = select(ch.access.mode || "locked", ["open", "locked", "pairing"]);
|
|
697
1176
|
const users = [...ch.access.allowed_users];
|
|
698
|
-
const msg = el("span", { class: "msg" });
|
|
699
1177
|
const accessFeedback = el("div", { class: "feedback" });
|
|
700
1178
|
const validateAccess = () => setValidation(fMode, accessFeedback, "", fMode.value === "locked" && users.length === 0 ? t("accessLockedEmpty") : "");
|
|
701
1179
|
fMode.addEventListener("change", validateAccess);
|
|
702
1180
|
box.append(
|
|
703
1181
|
el("div", { class: "grid2" },
|
|
704
|
-
el("div", {}, el("label", {}, t("accessMode"), impact("fleet")), fMode, accessFeedback),
|
|
705
|
-
el("div", {}, el("label", {}, t("allowedUsers"), impact("fleet")), chipList(users, validateAccess, "user id"))),
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
editingBot = null; await reload();
|
|
715
|
-
showBanner(`✓ ${t("affected")}: ${affected.length ? affected.join(", ") : chLabel(i) + " bot"} — 🔄🔄 restart fleet`);
|
|
716
|
-
}
|
|
717
|
-
} }, t("save")),
|
|
718
|
-
el("button", { class: "sm ghost", onclick: () => { editingBot = null; renderBots(); } }, t("cancel")), msg),
|
|
1182
|
+
el("div", {}, el("label", {}, t("accessMode"), impact("fleet.channel.access.mode")), fMode, accessFeedback),
|
|
1183
|
+
el("div", {}, el("label", {}, t("allowedUsers"), impact("fleet.channel.access.allowed_users")), chipList(users, validateAccess, "user id"))),
|
|
1184
|
+
// Only the field that platform actually has, the way quickstart asks.
|
|
1185
|
+
el("div", { class: "field" },
|
|
1186
|
+
el("label", {}, type === "discord" ? t("guildIdField") : t("groupIdField"), impact("fleet.channels")),
|
|
1187
|
+
el("div", { class: "sub" }, ch.group_id ? String(ch.group_id) : t("notSet"))),
|
|
1188
|
+
drawer(t("advancedSection"),
|
|
1189
|
+
el("div", { class: "field" }, el("label", {}, t("tokenEnv")), el("div", { class: "sub" }, ch.bot_token_env || "(no token env)")),
|
|
1190
|
+
el("div", { class: "actions" },
|
|
1191
|
+
el("button", { class: "sm danger", onclick: () => { closeModal(); delBot(i); } }, t("deleteBot")))),
|
|
719
1192
|
);
|
|
720
1193
|
validateAccess();
|
|
721
|
-
return
|
|
1194
|
+
return {
|
|
1195
|
+
box,
|
|
1196
|
+
impacts: ["fleet.channel.access.mode", "fleet.channel.access.allowed_users", "fleet.channels"],
|
|
1197
|
+
// Staged, not saved: the old form wrote channels straight to the API from
|
|
1198
|
+
// inside the editor, which is the one thing a modal must not do.
|
|
1199
|
+
stage: () => {
|
|
1200
|
+
const originalChannels = structuredClone(channels());
|
|
1201
|
+
const stagedAccessMode = fMode.value;
|
|
1202
|
+
const stagedAllowedUsers = [...users];
|
|
1203
|
+
const nextChannels = structuredClone(originalChannels);
|
|
1204
|
+
nextChannels[i] = { ...nextChannels[i], access: { ...nextChannels[i].access, mode: stagedAccessMode, allowed_users: stagedAllowedUsers } };
|
|
1205
|
+
if (sameValue(nextChannels, originalChannels)) return true;
|
|
1206
|
+
stageChange(`connections:${ch.id || chLabel(i)}`, {
|
|
1207
|
+
label: `Update ${ch.id || chLabel(i)} access`,
|
|
1208
|
+
impact: impactOf("fleet.channels"),
|
|
1209
|
+
// Confirm the immutable staged snapshot, not live form controls
|
|
1210
|
+
// that the user may edit again before pressing global Apply.
|
|
1211
|
+
confirm: () => confirmAccessChange(previousAccess, stagedAccessMode, stagedAllowedUsers),
|
|
1212
|
+
cancelMessage: t("accessChangeCancelled"),
|
|
1213
|
+
apply: () => api("/api/settings/fleet/channels", { method: "PUT", body: JSON.stringify(nextChannels) }),
|
|
1214
|
+
});
|
|
1215
|
+
return true;
|
|
1216
|
+
},
|
|
1217
|
+
};
|
|
722
1218
|
}
|
|
723
1219
|
|
|
724
1220
|
// ── ClassicBot Channels ──
|
|
725
|
-
let editingClassic = null;
|
|
726
1221
|
function classicRuntime(instanceName) {
|
|
727
1222
|
const live = state.classicLive[instanceName] || {};
|
|
728
1223
|
const status = live.status || state.classicStatus[instanceName] || "stopped";
|
|
@@ -741,11 +1236,11 @@
|
|
|
741
1236
|
const runtime = classicRuntime(c.instanceName);
|
|
742
1237
|
const fBackend = select(c.backend || inheritedBackend, BACKENDS.filter(value => value !== "gemini-cli"));
|
|
743
1238
|
const fModel = el("input", { type: "text", value: c.model || "", placeholder: inheritedModel || "(inherit default)" });
|
|
744
|
-
const auto = overrideControl(c, "auto_pause_after", el("input", { type: "number", min: "0", step: "1", value: c.auto_pause_after ?? inheritedAutoPause, placeholder: "0 = disabled" }), inheritedAutoPause);
|
|
745
|
-
const toolProgress = overrideControl(c, "tool_progress", select(c.tool_progress ?? inheritedToolProgress, ["off", "standard", "verbose"]), inheritedToolProgress, "
|
|
1239
|
+
const auto = overrideControl(c, "auto_pause_after", el("input", { type: "number", min: "0", step: "1", value: c.auto_pause_after ?? inheritedAutoPause, placeholder: "0 = disabled" }), inheritedAutoPause, "classic.auto_pause_after");
|
|
1240
|
+
const toolProgress = overrideControl(c, "tool_progress", select(c.tool_progress ?? inheritedToolProgress, ["off", "standard", "verbose"]), inheritedToolProgress, "classic.tool_progress");
|
|
746
1241
|
const fReplyGuard = el("input", { type: "checkbox" });
|
|
747
1242
|
fReplyGuard.checked = c.reply_completion_guard ?? inheritedReplyGuard;
|
|
748
|
-
const replyGuard = overrideControl(c, "reply_completion_guard", fReplyGuard, inheritedReplyGuard, "
|
|
1243
|
+
const replyGuard = overrideControl(c, "reply_completion_guard", fReplyGuard, inheritedReplyGuard, "classic.reply_completion_guard");
|
|
749
1244
|
const classicMode = fleetDefaults.agent_mode ?? "mcp";
|
|
750
1245
|
const replyHint = replyGuardHint(fBackend.value, classicMode);
|
|
751
1246
|
fBackend.addEventListener("change", () => {
|
|
@@ -756,7 +1251,10 @@
|
|
|
756
1251
|
const fContext = el("input", { type: "number", min: "0", step: "1", value: c.context_lines ?? classicDefaults.context_lines ?? 5 });
|
|
757
1252
|
const autoFeedback = el("div", { class: "feedback" });
|
|
758
1253
|
const contextFeedback = el("div", { class: "feedback" });
|
|
759
|
-
|
|
1254
|
+
// Never rendered: it carries the validation state and the submit handler
|
|
1255
|
+
// that the modal footer triggers through stage(). The form has no button
|
|
1256
|
+
// of its own, so nothing here can write on its own.
|
|
1257
|
+
const saveBtn = el("button", { hidden: "hidden" });
|
|
760
1258
|
const validate = () => {
|
|
761
1259
|
const autoValue = Number(auto.input.value);
|
|
762
1260
|
const contextValue = Number(fContext.value);
|
|
@@ -788,40 +1286,45 @@
|
|
|
788
1286
|
collab: !!c.collab,
|
|
789
1287
|
context_lines: c.context_lines ?? classicDefaults.context_lines ?? 5,
|
|
790
1288
|
});
|
|
791
|
-
if (Object.keys(patch).length === 0) {
|
|
1289
|
+
if (Object.keys(patch).length === 0) { renderClassic(); return; }
|
|
792
1290
|
state.classic.channels[c.key] = { ...state.classic.channels[c.key], ...patch };
|
|
793
1291
|
if (patch.model === null) delete state.classic.channels[c.key].model;
|
|
794
1292
|
if (patch.auto_pause_after === null) delete state.classic.channels[c.key].auto_pause_after;
|
|
795
1293
|
if (patch.tool_progress === null) delete state.classic.channels[c.key].tool_progress;
|
|
796
1294
|
if (patch.reply_completion_guard === null) delete state.classic.channels[c.key].reply_completion_guard;
|
|
797
|
-
const hotOnly = Object.keys(patch).every(key => key === "
|
|
1295
|
+
const hotOnly = Object.keys(patch).every(key => impactOf(`classic.${key}`) === "now");
|
|
798
1296
|
stageChange(`classic:${c.key}`, {
|
|
799
1297
|
label: `Update ${c.name || c.instanceName}`,
|
|
800
1298
|
impact: hotOnly ? "now" : "instance",
|
|
801
1299
|
apply: () => api(`/api/settings/classic/channels/${encodeURIComponent(c.key)}`, { method: "PATCH", body: JSON.stringify(patch) }),
|
|
802
1300
|
});
|
|
803
|
-
editingClassic = null;
|
|
804
1301
|
renderClassic();
|
|
805
1302
|
};
|
|
806
1303
|
box.append(
|
|
807
1304
|
el("div", { class: "sub", style: "margin:0 0 8px" }, `ID: ${c.instanceName} · ${EXECUTION_TEXT[runtime.execution] || runtime.execution}`),
|
|
808
1305
|
el("div", { class: "grid2" },
|
|
809
|
-
el("div", {}, el("label", {}, t("backend"), impact("
|
|
810
|
-
el("div", {}, el("label", {}, t("model"), impact("
|
|
811
|
-
el("div", {}, el("label", {}, `${t("autoPause")} (${t("minutes")})`, impact("
|
|
812
|
-
el("div", {}, el("label", {}, t("toolProgress"), impact("
|
|
813
|
-
el("div", {}, el("label", {}, t("replyCompletionGuard"), impact("
|
|
814
|
-
el("div", {}, el("label", { style: "display:flex;align-items:center;gap:8px;cursor:pointer;margin-top:28px" }, fCollab, el("span", {}, "Collaboration mode"), impact("
|
|
815
|
-
|
|
816
|
-
el("label", {}, "Context lines", impact("
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
runtime.status === "paused" ? el("button", { class: "sm", onclick: () => pauseWakeAgent(c.instanceName, "wake") }, t("wake")) : null,
|
|
821
|
-
el("button", { class: "sm ghost", onclick: () => { editingClassic = null; renderClassic(); } }, t("cancel"))),
|
|
1306
|
+
el("div", {}, el("label", {}, t("backend"), impact("classic.backend")), fBackend),
|
|
1307
|
+
el("div", {}, el("label", {}, t("model"), impact("classic.model")), fModel),
|
|
1308
|
+
el("div", {}, el("label", {}, `${t("autoPause")} (${t("minutes")})`, impact("classic.auto_pause_after")), auto.row, autoFeedback),
|
|
1309
|
+
el("div", {}, el("label", {}, t("toolProgress"), impact("classic.tool_progress")), toolProgress.row),
|
|
1310
|
+
el("div", {}, el("label", {}, t("replyCompletionGuard"), impact("classic.reply_completion_guard")), replyGuard.row, replyHint),
|
|
1311
|
+
el("div", {}, el("label", { style: "display:flex;align-items:center;gap:8px;cursor:pointer;margin-top:28px" }, fCollab, el("span", {}, "Collaboration mode"), impact("classic.collab")))),
|
|
1312
|
+
drawer(t("advancedSection"),
|
|
1313
|
+
el("label", {}, "Context lines", impact("classic.context_lines")), fContext, contextFeedback,
|
|
1314
|
+
el("div", { class: "actions" },
|
|
1315
|
+
runtime.status === "running" ? el("button", { class: "sm", onclick: () => { closeModal(); pauseWakeAgent(c.instanceName, "pause"); } }, t("pause")) : null,
|
|
1316
|
+
runtime.status === "paused" ? el("button", { class: "sm", onclick: () => { closeModal(); pauseWakeAgent(c.instanceName, "wake"); } }, t("wake")) : null)),
|
|
822
1317
|
);
|
|
823
1318
|
validate();
|
|
824
|
-
return
|
|
1319
|
+
return {
|
|
1320
|
+
box,
|
|
1321
|
+
stage: () => { if (saveBtn.disabled) return false; saveBtn.onclick(); return true; },
|
|
1322
|
+
impacts: [
|
|
1323
|
+
"classic.backend", "classic.model", "classic.auto_pause_after",
|
|
1324
|
+
"classic.tool_progress", "classic.reply_completion_guard",
|
|
1325
|
+
"classic.collab", "classic.context_lines",
|
|
1326
|
+
],
|
|
1327
|
+
};
|
|
825
1328
|
}
|
|
826
1329
|
function renderClassic() {
|
|
827
1330
|
const host = $("classicList"); if (!host) return; host.innerHTML = "";
|
|
@@ -853,11 +1356,13 @@
|
|
|
853
1356
|
c.collab ? el("span", { class: "tag", style: "background:#ecfdf5;color:#065f46" }, "collab") : null,
|
|
854
1357
|
el("span", { class: "spacer" }),
|
|
855
1358
|
el("span", { class: "tag", title: STATUS_TEXT[st] || st }, EXECUTION_TEXT[runtime.execution] || runtime.execution),
|
|
856
|
-
el("button", { class: "sm ghost", onclick: () => {
|
|
1359
|
+
el("button", { class: "sm ghost", onclick: () => {
|
|
1360
|
+
const form = classicEditForm(c);
|
|
1361
|
+
openModal({ title: c.name || c.instanceName, subtitle: c.instanceName, body: form.box, impacts: form.impacts, stage: form.stage });
|
|
1362
|
+
} }, t("settingsButton")),
|
|
857
1363
|
st === "running" ? el("button", { class: "sm", onclick: () => pauseWakeAgent(c.instanceName, "pause") }, t("pause")) : null,
|
|
858
1364
|
st === "paused" ? el("button", { class: "sm", onclick: () => pauseWakeAgent(c.instanceName, "wake") }, t("wake")) : null,
|
|
859
1365
|
));
|
|
860
|
-
if (editingClassic === c.key) item.append(classicEditForm(c));
|
|
861
1366
|
host.append(item);
|
|
862
1367
|
}
|
|
863
1368
|
}
|
|
@@ -922,9 +1427,9 @@
|
|
|
922
1427
|
const fToolSet = select(d.tool_set || "full", ["full", "standard", "minimal"]);
|
|
923
1428
|
const fToolProgress = select(d.tool_progress ?? "off", ["off", "standard", "verbose"]);
|
|
924
1429
|
const fReplyGuard = el("input", { type: "checkbox" }); fReplyGuard.checked = d.reply_completion_guard ?? true;
|
|
925
|
-
const fClassicToolProgress = overrideControl(c, "tool_progress", select(c.tool_progress ?? d.tool_progress ?? "off", ["off", "standard", "verbose"]), d.tool_progress ?? "off", "
|
|
1430
|
+
const fClassicToolProgress = overrideControl(c, "tool_progress", select(c.tool_progress ?? d.tool_progress ?? "off", ["off", "standard", "verbose"]), d.tool_progress ?? "off", "classic_defaults.tool_progress");
|
|
926
1431
|
const fClassicReplyInput = el("input", { type: "checkbox" }); fClassicReplyInput.checked = c.reply_completion_guard ?? d.reply_completion_guard ?? true;
|
|
927
|
-
const fClassicReplyGuard = overrideControl(c, "reply_completion_guard", fClassicReplyInput, d.reply_completion_guard ?? true, "
|
|
1432
|
+
const fClassicReplyGuard = overrideControl(c, "reply_completion_guard", fClassicReplyInput, d.reply_completion_guard ?? true, "classic_defaults.reply_completion_guard");
|
|
928
1433
|
const fAccessMode = select(originalPrimaryAccess.mode || "locked", ["locked", "open", "pairing"]);
|
|
929
1434
|
const fLogLevel = select(d.log_level || "info", ["trace", "debug", "info", "warn", "error"]);
|
|
930
1435
|
const fLocale = el("select");
|
|
@@ -1016,27 +1521,10 @@
|
|
|
1016
1521
|
state.classic.defaults = { ...c, ...classicPatch };
|
|
1017
1522
|
for (const [key, value] of Object.entries(classicPatch)) if (value === null) delete state.classic.defaults[key];
|
|
1018
1523
|
if (Object.keys(defaultsPatch).length > 0) {
|
|
1019
|
-
stageChange("defaults:fleet", { label: "Update Agent defaults", impact: Object.keys(defaultsPatch)
|
|
1020
|
-
}
|
|
1021
|
-
if (primary) {
|
|
1022
|
-
const stagedAccessMode = fAccessMode.value;
|
|
1023
|
-
const stagedAllowedUsers = [...allowedUsers];
|
|
1024
|
-
const nextChannels = structuredClone(originalChannels);
|
|
1025
|
-
nextChannels[0] = { ...nextChannels[0], access: { ...nextChannels[0].access, mode: stagedAccessMode, allowed_users: stagedAllowedUsers } };
|
|
1026
|
-
if (!sameValue(nextChannels, originalChannels)) {
|
|
1027
|
-
stageChange("connections:primary", {
|
|
1028
|
-
label: "Update primary connection access",
|
|
1029
|
-
impact: "fleet",
|
|
1030
|
-
// Confirm the immutable staged snapshot, not live form controls
|
|
1031
|
-
// that the user may edit again before pressing global Apply.
|
|
1032
|
-
confirm: () => confirmAccessChange(originalPrimaryAccess, stagedAccessMode, stagedAllowedUsers),
|
|
1033
|
-
cancelMessage: t("accessChangeCancelled"),
|
|
1034
|
-
apply: () => api("/api/settings/fleet/channels", { method: "PUT", body: JSON.stringify(nextChannels) }),
|
|
1035
|
-
});
|
|
1036
|
-
}
|
|
1524
|
+
stageChange("defaults:fleet", { label: "Update Agent defaults", impact: batchImpact(Object.keys(defaultsPatch), key => impactOf(`defaults.${key}`)), apply: () => api("/api/settings/fleet/defaults", { method: "PUT", body: JSON.stringify(defaultsPatch) }) });
|
|
1037
1525
|
}
|
|
1038
1526
|
if (Object.keys(classicPatch).length > 0) {
|
|
1039
|
-
const classicHotOnly = Object.keys(classicPatch).every(key => key === "
|
|
1527
|
+
const classicHotOnly = Object.keys(classicPatch).every(key => impactOf(`classic_defaults.${key}`) === "now");
|
|
1040
1528
|
stageChange("defaults:classic", { label: "Update ClassicBot defaults and access", impact: classicHotOnly ? "now" : "fleet", apply: () => api("/api/settings/classic/defaults", { method: "PUT", body: JSON.stringify(classicPatch) }) });
|
|
1041
1529
|
}
|
|
1042
1530
|
msg.innerHTML = `<span class="s">${esc(t("readyApply"))}</span>`;
|
|
@@ -1050,40 +1538,40 @@
|
|
|
1050
1538
|
host.append(
|
|
1051
1539
|
section("Defaults", true,
|
|
1052
1540
|
el("div", { class: "grid2" },
|
|
1053
|
-
el("div", { class: "field" }, el("label", {}, t("language"), impact("
|
|
1054
|
-
el("div", { class: "field" }, el("label", {}, t("defaultBackend"), impact("
|
|
1055
|
-
el("div", { class: "field" }, el("label", {}, t("defaultModel"), impact("
|
|
1056
|
-
el("div", { class: "field" }, el("label", {}, `${t("autoPause")} (${t("minutes")})`, impact("
|
|
1057
|
-
el("div", { class: "field" }, el("label", {}, t("toolProgress"), impact("
|
|
1058
|
-
el("div", { class: "field" }, el("label", { style: "display:flex;align-items:center;gap:8px" }, fReplyGuard, el("span", {}, t("replyCompletionGuard")), impact("
|
|
1541
|
+
el("div", { class: "field" }, el("label", {}, t("language"), impact("defaults.locale")), fLocale),
|
|
1542
|
+
el("div", { class: "field" }, el("label", {}, t("defaultBackend"), impact("defaults.backend")), fBackend),
|
|
1543
|
+
el("div", { class: "field" }, el("label", {}, t("defaultModel"), impact("defaults.model")), fModel),
|
|
1544
|
+
el("div", { class: "field" }, el("label", {}, `${t("autoPause")} (${t("minutes")})`, impact("defaults.auto_pause_after")), fAutoPause, autoFeedback),
|
|
1545
|
+
el("div", { class: "field" }, el("label", {}, t("toolProgress"), impact("defaults.tool_progress")), fToolProgress),
|
|
1546
|
+
el("div", { class: "field" }, el("label", { style: "display:flex;align-items:center;gap:8px" }, fReplyGuard, el("span", {}, t("replyCompletionGuard")), impact("defaults.reply_completion_guard")), defaultReplyHint))),
|
|
1059
1547
|
section("ClassicBot", true,
|
|
1060
1548
|
el("div", { class: "grid2" },
|
|
1061
|
-
el("div", { class: "field" }, el("label", {}, "Classic default backend", impact("
|
|
1062
|
-
el("div", { class: "field" }, el("label", {}, t("toolProgress"), impact("
|
|
1063
|
-
el("div", { class: "field" }, el("label", {}, t("replyCompletionGuard"), impact("
|
|
1549
|
+
el("div", { class: "field" }, el("label", {}, "Classic default backend", impact("classic_defaults.backend")), fClassicBackend),
|
|
1550
|
+
el("div", { class: "field" }, el("label", {}, t("toolProgress"), impact("classic_defaults.tool_progress")), fClassicToolProgress.row),
|
|
1551
|
+
el("div", { class: "field" }, el("label", {}, t("replyCompletionGuard"), impact("classic_defaults.reply_completion_guard")), fClassicReplyGuard.row, classicReplyHint)),
|
|
1064
1552
|
el("div", { class: "field" }, el("strong", {}, "Who can use ClassicBot"),
|
|
1065
1553
|
el("div", { class: "grid2" },
|
|
1066
|
-
el("div", {}, el("label", {}, t("classicAdmins"), impact("
|
|
1067
|
-
el("div", {}, el("label", {}, t("allowedGuilds"), impact("
|
|
1068
|
-
|
|
1554
|
+
el("div", {}, el("label", {}, t("classicAdmins"), impact("classic.admin_users")), chipList(c.admin_users, null, "user id")),
|
|
1555
|
+
el("div", {}, el("label", {}, t("allowedGuilds"), impact("classic.allowed_guilds")), chipList(c.allowed_guilds, null, "guild id"))))),
|
|
1556
|
+
drawer(t("advancedSection"),
|
|
1069
1557
|
section(t("runtimeResources"), false,
|
|
1070
1558
|
el("div", { class: "grid2" },
|
|
1071
|
-
el("div", { class: "field" }, el("label", {}, t("toolSet"), impact("
|
|
1072
|
-
el("div", { class: "field" }, el("label", {}, t("logLevel"), impact("
|
|
1073
|
-
el("div", { class: "field" }, el("label", {}, t("agentMode"), impact("
|
|
1559
|
+
el("div", { class: "field" }, el("label", {}, t("toolSet"), impact("defaults.tool_set")), fToolSet),
|
|
1560
|
+
el("div", { class: "field" }, el("label", {}, t("logLevel"), impact("defaults.log_level")), fLogLevel),
|
|
1561
|
+
el("div", { class: "field" }, el("label", {}, t("agentMode"), impact("defaults.agent_mode")), fAgentMode))),
|
|
1074
1562
|
section(t("detection"), false,
|
|
1075
1563
|
el("div", { class: "grid2" },
|
|
1076
|
-
el("div", { class: "field" }, el("label", { style: "display:flex;align-items:center;gap:8px" }, fHangEnabled, el("span", {}, t("hangEnabled")), impact("
|
|
1077
|
-
el("div", { class: "field" }, el("label", {}, `${t("hangTimeout")} (${t("minutes")})`, impact("
|
|
1564
|
+
el("div", { class: "field" }, el("label", { style: "display:flex;align-items:center;gap:8px" }, fHangEnabled, el("span", {}, t("hangEnabled")), impact("defaults.hang_detector"))),
|
|
1565
|
+
el("div", { class: "field" }, el("label", {}, `${t("hangTimeout")} (${t("minutes")})`, impact("defaults.hang_detector")), fHangTimeout, hangFeedback))),
|
|
1078
1566
|
section(t("startup"), false,
|
|
1079
1567
|
el("div", { class: "grid2" },
|
|
1080
|
-
el("div", { class: "field" }, el("label", {}, t("concurrency"), impact("fleet")), fConcurrency, concurrencyFeedback),
|
|
1081
|
-
el("div", { class: "field" }, el("label", {}, `${t("staggerDelay")} (ms)`, impact("fleet")), fStagger, staggerFeedback))),
|
|
1082
|
-
section("
|
|
1568
|
+
el("div", { class: "field" }, el("label", {}, t("concurrency"), impact("fleet.spawn_concurrency")), fConcurrency, concurrencyFeedback),
|
|
1569
|
+
el("div", { class: "field" }, el("label", {}, `${t("staggerDelay")} (ms)`, impact("fleet.spawn_stagger_ms")), fStagger, staggerFeedback))),
|
|
1570
|
+
section("Access", false,
|
|
1571
|
+
// Access belongs to a connection, and one modal edits one object — so
|
|
1572
|
+
// it is edited there, and this is only a pointer to it.
|
|
1083
1573
|
primary
|
|
1084
|
-
? el("div", { class: "
|
|
1085
|
-
el("div", { class: "field" }, el("label", {}, t("accessMode"), impact("fleet")), fAccessMode, accessFeedback),
|
|
1086
|
-
el("div", { class: "field" }, el("label", {}, t("allowedUsers"), impact("fleet")), chipList(allowedUsers, validate, "user id")))
|
|
1574
|
+
? el("div", { class: "field hint" }, tf("accessMovedHint", primary.id || chLabel(0)))
|
|
1087
1575
|
: el("div", { class: "field hint" }, t("noBots")))),
|
|
1088
1576
|
el("div", { class: "field" }, el("div", { class: "actions" }, saveBtn, msg)),
|
|
1089
1577
|
);
|
|
@@ -1177,6 +1665,8 @@
|
|
|
1177
1665
|
}
|
|
1178
1666
|
async function reload() {
|
|
1179
1667
|
// Forms edit the user-authored config, not defaults-expanded runtime values.
|
|
1668
|
+
const schema = await api("/api/settings/schema");
|
|
1669
|
+
if (schema.ok && schema.body) state.schema = schema.body;
|
|
1180
1670
|
const f = await api("/api/settings/fleet/raw");
|
|
1181
1671
|
if (!f.ok) { $("banner").style.display = "block"; $("banner").style.background = "#fef2f2"; $("banner").style.color = "var(--red)"; $("banner").textContent = t("configLoadFailed"); return; }
|
|
1182
1672
|
state.fleet = f.body || {};
|
|
@@ -1191,19 +1681,16 @@
|
|
|
1191
1681
|
$("helpBackdrop").onclick = () => setHelp(false);
|
|
1192
1682
|
$("langBtn").onclick = () => { lang = lang === "en" ? "zh-TW" : "en"; localStorage.setItem("agend_lang", lang); renderAll(); };
|
|
1193
1683
|
$("verBadge").onclick = () => window.open("https://github.com/songsid/AgEnD/releases/latest", "_blank", "noopener");
|
|
1194
|
-
$("agentSearch").oninput = () => { renderAgents(); renderClassic(); };
|
|
1684
|
+
$("agentSearch").oninput = () => { renderAgents(); renderClassic(); renderBots(); };
|
|
1195
1685
|
$("newAgentBtn").onclick = () => { state.newAgentOpen = !state.newAgentOpen; $("newAgentBtn").textContent = state.newAgentOpen ? t("cancel") : t("new"); renderNewAgent(state.newAgentOpen); };
|
|
1196
1686
|
$("newBotBtn").onclick = () => { state.newBotOpen = !state.newBotOpen; $("newBotBtn").textContent = state.newBotOpen ? t("cancel") : t("add"); renderNewBot(state.newBotOpen); };
|
|
1197
1687
|
$("advToggle").onclick = () => { const b = $("advBody"); const open = !b.classList.contains("open"); b.classList.toggle("open", open); $("advToggle").textContent = open ? t("collapse") : t("expand"); };
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
$("
|
|
1201
|
-
$("
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
localStorage.setItem("agend_settings_advanced", String(enabled));
|
|
1205
|
-
$("advancedToggle").textContent = enabled ? t("hideAdvanced") : t("showAdvanced");
|
|
1206
|
-
};
|
|
1688
|
+
// Closing stages; cancelling discards. Neither writes anything.
|
|
1689
|
+
$("modalDone").onclick = () => { if (modalState?.stage() !== false) closeModal(); };
|
|
1690
|
+
$("modalCancel").onclick = () => closeModal();
|
|
1691
|
+
$("modalBackdrop").onclick = () => closeModal();
|
|
1692
|
+
document.addEventListener("keydown", e => { if (e.key === "Escape" && modalState) closeModal(); });
|
|
1693
|
+
$("wizardBtn").onclick = () => openWizard();
|
|
1207
1694
|
$("applyChanges").onclick = applyPendingChanges;
|
|
1208
1695
|
$("discardChanges").onclick = async () => { state.pending.clear(); updatePendingBar(); await reload(); };
|
|
1209
1696
|
|