@songsid/agend 2.1.5 → 2.1.6-beta.10
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-endpoint.d.ts +18 -0
- package/dist/agent-endpoint.js +53 -2
- package/dist/agent-endpoint.js.map +1 -1
- 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/claude-code.d.ts +25 -0
- package/dist/backend/claude-code.js +127 -0
- package/dist/backend/claude-code.js.map +1 -1
- package/dist/backend/codex.d.ts +57 -0
- package/dist/backend/codex.js +183 -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/factory.js +4 -1
- package/dist/backend/factory.js.map +1 -1
- 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/backend/muse.d.ts +90 -0
- package/dist/backend/muse.js +437 -0
- package/dist/backend/muse.js.map +1 -0
- package/dist/backend/types.d.ts +54 -0
- package/dist/backend/types.js.map +1 -1
- package/dist/channel/access-manager.d.ts +20 -0
- package/dist/channel/access-manager.js +25 -0
- package/dist/channel/access-manager.js.map +1 -1
- package/dist/channel/adapters/discord.d.ts +17 -0
- package/dist/channel/adapters/discord.js +72 -11
- package/dist/channel/adapters/discord.js.map +1 -1
- package/dist/channel/adapters/telegram.d.ts +6 -0
- package/dist/channel/adapters/telegram.js +36 -0
- package/dist/channel/adapters/telegram.js.map +1 -1
- package/dist/channel/markdown-chunk.d.ts +8 -3
- package/dist/channel/markdown-chunk.js +12 -3
- package/dist/channel/markdown-chunk.js.map +1 -1
- package/dist/channel/mcp-server.js +9 -12
- package/dist/channel/mcp-server.js.map +1 -1
- package/dist/channel/mcp-tools.d.ts +6 -2
- package/dist/channel/mcp-tools.js +6 -36
- package/dist/channel/mcp-tools.js.map +1 -1
- package/dist/channel/tool-router.js +32 -3
- package/dist/channel/tool-router.js.map +1 -1
- package/dist/channel/types.d.ts +18 -0
- package/dist/cli.js +113 -0
- package/dist/cli.js.map +1 -1
- package/dist/config-validator.js +45 -5
- package/dist/config-validator.js.map +1 -1
- package/dist/connection-secrets.d.ts +96 -0
- package/dist/connection-secrets.js +25 -0
- package/dist/connection-secrets.js.map +1 -0
- package/dist/daemon.d.ts +76 -0
- package/dist/daemon.js +489 -103
- package/dist/daemon.js.map +1 -1
- package/dist/event-log.d.ts +9 -0
- package/dist/event-log.js +21 -0
- package/dist/event-log.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 +425 -2
- package/dist/fleet-manager.js +2055 -182
- package/dist/fleet-manager.js.map +1 -1
- package/dist/general-knowledge/skills/credential-profiles/SKILL.md +145 -0
- package/dist/general-knowledge/skills/worker-collaboration/SKILL.md +11 -1
- 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/instance-lifecycle.js +8 -4
- package/dist/instance-lifecycle.js.map +1 -1
- package/dist/locale.js +24 -0
- package/dist/locale.js.map +1 -1
- package/dist/logger.js +42 -25
- package/dist/logger.js.map +1 -1
- package/dist/muse-usage-relay.d.ts +72 -0
- package/dist/muse-usage-relay.js +419 -0
- package/dist/muse-usage-relay.js.map +1 -0
- 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 +7 -5
- package/dist/outbound-schemas.js +3 -2
- package/dist/outbound-schemas.js.map +1 -1
- package/dist/provider-probe.d.ts +48 -0
- package/dist/provider-probe.js +121 -0
- package/dist/provider-probe.js.map +1 -0
- package/dist/provider-secret-registry.d.ts +98 -0
- package/dist/provider-secret-registry.js +334 -0
- package/dist/provider-secret-registry.js.map +1 -0
- package/dist/quickstart-api.d.ts +164 -0
- package/dist/quickstart-api.js +351 -0
- package/dist/quickstart-api.js.map +1 -0
- package/dist/quickstart.js +23 -50
- package/dist/quickstart.js.map +1 -1
- package/dist/scheduler/db.js +17 -5
- package/dist/scheduler/db.js.map +1 -1
- package/dist/scheduler/db.test.js +34 -1
- package/dist/scheduler/db.test.js.map +1 -1
- package/dist/scheduler/types.d.ts +4 -0
- package/dist/scheduler/types.js.map +1 -1
- package/dist/secret-store.d.ts +25 -0
- package/dist/secret-store.js +165 -0
- package/dist/secret-store.js.map +1 -0
- 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 +116 -0
- package/dist/settings-api.js +530 -5
- 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/setup-wizard.js +4 -0
- package/dist/setup-wizard.js.map +1 -1
- package/dist/steer-capability.js +4 -1
- package/dist/steer-capability.js.map +1 -1
- package/dist/tips.js +1 -1
- package/dist/tips.js.map +1 -1
- package/dist/tmux-manager.js +29 -4
- package/dist/tmux-manager.js.map +1 -1
- package/dist/tool-permissions-notice.d.ts +38 -0
- package/dist/tool-permissions-notice.js +92 -0
- package/dist/tool-permissions-notice.js.map +1 -0
- package/dist/tool-permissions.d.ts +120 -0
- package/dist/tool-permissions.js +277 -0
- package/dist/tool-permissions.js.map +1 -0
- package/dist/topic-commands.js +1 -0
- package/dist/topic-commands.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/types.d.ts +2 -0
- package/dist/ui/dashboard.html +4 -3
- package/dist/ui/settings.html +808 -156
- package/dist/ui/view.html +2 -2
- package/dist/usage/i18n-keys.d.ts +1 -1
- package/dist/usage/i18n-keys.js +1 -1
- package/dist/usage/i18n-keys.js.map +1 -1
- package/dist/usage/providers.d.ts +23 -14
- package/dist/usage/providers.js +220 -58
- package/dist/usage/providers.js.map +1 -1
- package/dist/usage/usage-api.d.ts +13 -0
- package/dist/usage/usage-api.js +69 -1
- package/dist/usage/usage-api.js.map +1 -1
- package/dist/web-api.js +8 -5
- 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/dist/web-terminal.d.ts +30 -1
- package/dist/web-terminal.js +86 -3
- package/dist/web-terminal.js.map +1 -1
- package/package.json +2 -1
package/dist/ui/settings.html
CHANGED
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
button.danger:hover { background: #fef2f2; }
|
|
50
50
|
button.sm { padding: 4px 9px; font-size: 12px; }
|
|
51
51
|
label { display: block; font-size: 12px; font-weight: 500; color: var(--muted); margin: 10px 0 4px; }
|
|
52
|
-
input[type=text], input[type=number], select { width: 100%; font-family: inherit; font-size: 13px; padding: 8px 10px; border: 1px solid var(--border); border-radius: var(--radius-sm); background: #fff; color: var(--fg); }
|
|
52
|
+
input[type=text], input[type=password], input[type=number], select { width: 100%; font-family: inherit; font-size: 13px; padding: 8px 10px; border: 1px solid var(--border); border-radius: var(--radius-sm); background: #fff; color: var(--fg); }
|
|
53
53
|
input:focus, select:focus { outline: 2px solid #bfdbfe; border-color: var(--accent); }
|
|
54
54
|
.edit { padding: 14px 16px; background: #fff; border-top: 1px solid var(--border); }
|
|
55
55
|
.grid2 { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
|
|
@@ -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,9 +190,10 @@
|
|
|
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>
|
|
196
|
+
<div class="card" id="providerSecrets" style="margin-top:12px"></div>
|
|
158
197
|
</section>
|
|
159
198
|
|
|
160
199
|
<section>
|
|
@@ -168,6 +207,10 @@
|
|
|
168
207
|
<div class="card"><div class="adv-body" id="advBody"></div></div>
|
|
169
208
|
</section>
|
|
170
209
|
</main>
|
|
210
|
+
<div class="apply-panel" id="applyPanel">
|
|
211
|
+
<div class="apply-head"><strong id="applyTitle">Applying changes…</strong><span id="applyNote" class="sub"></span></div>
|
|
212
|
+
<div id="applyRows"></div>
|
|
213
|
+
</div>
|
|
171
214
|
<div class="pending-bar" id="pendingBar">
|
|
172
215
|
<strong id="pendingCount">0 changes not applied</strong>
|
|
173
216
|
<span id="pendingImpact">⚡ Immediately · 🔄 Restart this Agent · 🔄🔄 Restart AgEnD</span>
|
|
@@ -178,7 +221,8 @@
|
|
|
178
221
|
<script>
|
|
179
222
|
(() => {
|
|
180
223
|
"use strict";
|
|
181
|
-
|
|
224
|
+
// No token in the URL: the gate redeemed it for an HttpOnly session cookie,
|
|
225
|
+
// which the browser attaches to these same-origin requests on its own.
|
|
182
226
|
const $ = (id) => document.getElementById(id);
|
|
183
227
|
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
228
|
const esc = (s) => String(s).replace(/[&<>]/g, c => ({ "&":"&","<":"<",">":">" }[c]));
|
|
@@ -204,7 +248,36 @@
|
|
|
204
248
|
helpTitle: "📖 Help", tipReload: "💡 Click [↻ Reload Fleet] after changes to apply them.",
|
|
205
249
|
wnIntro: "Recent highlights:", releaseNotes: "📄 View full Release Notes →", languageButton: "Language", helpButton: "Help", showAdditional: "Show additional controls", hideAdvanced: "Hide advanced", showAdvanced: "Show advanced",
|
|
206
250
|
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
|
-
|
|
251
|
+
applying: "Applying changes…", applyDone: "✓ Changes applied", applyPartial: "⚠️ Some changes failed", applyClose: "Close",
|
|
252
|
+
settingsButton: "Settings", modalDone: "Stage change", advancedSection: "Advanced",
|
|
253
|
+
wizardButton: "🧭 Setup wizard", wizardTitle: "Set up AgEnD", wizardStep: "Step {0} of {1}",
|
|
254
|
+
wizardRerun: "A fleet is already configured — finishing replaces the connection that uses the same token variable.",
|
|
255
|
+
wizardBack: "Back", wizardNext: "Next", wizardFinish: "Create and start",
|
|
256
|
+
wizardBackend: "Backend", wizardBackendFound: "Found on this host: {0}", wizardBackendNone: "No backend CLI found on this host — install one first.",
|
|
257
|
+
wizardAgentName: "First agent name", wizardNeedDir: "Enter an absolute working directory.",
|
|
258
|
+
wizardTelegramHint: "Create a bot with @BotFather, then add it to a group.",
|
|
259
|
+
wizardDiscordHint: "Create an application in the Discord developer portal and invite the bot to your server.",
|
|
260
|
+
wizardToken: "Bot token", wizardTokenTelegram: "From @BotFather. It is written to ~/.agend/.env and never shown again.",
|
|
261
|
+
wizardTokenDiscord: "From the Bot tab of your application. It is written to ~/.agend/.env and never shown again.",
|
|
262
|
+
wizardVerify: "Verify", wizardVerifying: "Asking the provider…",
|
|
263
|
+
wizardDetect: "Detect from a message", wizardDetectHint: "Or post any message in the group and press Detect.",
|
|
264
|
+
wizardWaitingStart: "Waiting for a message in the group…", wizardNoStartYet: "No group message yet — post one and press Detect again.",
|
|
265
|
+
wizardGeneralChannel: "General channel id", wizardAdminUser: "Your user id (becomes the first allowed user)",
|
|
266
|
+
wizardNeedVerify: "Verify the token before continuing.",
|
|
267
|
+
wizardPlanLoading: "Building the plan…", wizardWillWrite: "This will be written:", wizardEnvKeys: "And into ~/.agend/.env: {0}",
|
|
268
|
+
wizardEnvPerms: "⚠️ ~/.agend/.env is readable by other users on this host.",
|
|
269
|
+
guildIdField: "Guild id", groupIdField: "Group id", notSet: "(not set)", deleteBot: "Delete this bot", noAllowedUsers: "no allowed users",
|
|
270
|
+
searchAll: "🔍 Search agents, bots, channels…", accessMovedHint: "Who may use the bot is set on the connection itself — open Settings on \"{0}\" under Connections & Bots.",
|
|
271
|
+
applyRestartNeeded: "Saved — restart AgEnD to apply", applyNoChange: "no change needed",
|
|
272
|
+
applyRestartHint: "This change needs AgEnD itself restarted. You can restart it here, or run /restart in your chat channel.",
|
|
273
|
+
applyBusy: "AgEnD is already reloading its configuration. Your changes are saved — try Apply again in a moment.",
|
|
274
|
+
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.",
|
|
275
|
+
restartFleetStarted: "Restarting AgEnD — this page keeps checking.", restartFleetRefused: "Restart refused: {0}",
|
|
276
|
+
restartFleetRateLimited: "Restart refused: {0} (try again in about {1} minutes)",
|
|
277
|
+
signatureMismatch: "fleet.yaml and the running configuration disagree on: {0}. Restarting will not clear this — check fleet.log first.",
|
|
278
|
+
applyStillWorking: "Still restarting ({0}s) — AgEnD is still working; this page keeps checking.",
|
|
279
|
+
applyLostJob: "Lost track of this apply (the fleet may have restarted). Reload the page to see the current state.",
|
|
280
|
+
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
281
|
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
282
|
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
283
|
},
|
|
@@ -226,7 +299,36 @@
|
|
|
226
299
|
helpTitle: "📖 使用說明", tipReload: "💡 修改後點 [↻ 重新載入] 讓設定生效。",
|
|
227
300
|
wnIntro: "近期更新重點:", releaseNotes: "📄 查看完整 Release Notes →", languageButton: "語言", helpButton: "使用說明", showAdditional: "顯示更多控制項", hideAdvanced: "隱藏進階設定", showAdvanced: "顯示進階設定",
|
|
228
301
|
saved: "已儲存", failed: "失敗", actionFailed: "{0}失敗", noAgentMatch: "沒有符合「{0}」的 Agent。", deleteAgent: "確定刪除 Agent「{0}」嗎?只有在確認套用待處理變更後才會生效。", nameRequired: "必須填寫名稱", tokenEnvRequired: "必須填寫 token 環境變數", removeBot: "確定移除機器人「{0}」嗎?(.env 中的 token 會保留)",
|
|
229
|
-
|
|
302
|
+
applying: "正在套用變更…", applyDone: "✓ 變更已套用", applyPartial: "⚠️ 有部分變更失敗", applyClose: "關閉",
|
|
303
|
+
settingsButton: "設定", modalDone: "加入待套用", advancedSection: "進階",
|
|
304
|
+
wizardButton: "🧭 設定精靈", wizardTitle: "設定 AgEnD", wizardStep: "第 {0} 步,共 {1} 步",
|
|
305
|
+
wizardRerun: "已經有設定好的 fleet —— 完成後會取代使用相同 token 變數的那個連線。",
|
|
306
|
+
wizardBack: "上一步", wizardNext: "下一步", wizardFinish: "建立並啟動",
|
|
307
|
+
wizardBackend: "後端", wizardBackendFound: "在這台主機上找到:{0}", wizardBackendNone: "這台主機上找不到任何後端 CLI —— 請先安裝一個。",
|
|
308
|
+
wizardAgentName: "第一個 agent 名稱", wizardNeedDir: "請輸入絕對路徑的工作目錄。",
|
|
309
|
+
wizardTelegramHint: "用 @BotFather 建立 bot,然後把它加進一個群組。",
|
|
310
|
+
wizardDiscordHint: "在 Discord 開發者後台建立應用程式,並把 bot 邀請到你的伺服器。",
|
|
311
|
+
wizardToken: "Bot token", wizardTokenTelegram: "來自 @BotFather。會寫入 ~/.agend/.env,之後不會再顯示。",
|
|
312
|
+
wizardTokenDiscord: "來自應用程式的 Bot 分頁。會寫入 ~/.agend/.env,之後不會再顯示。",
|
|
313
|
+
wizardVerify: "驗證", wizardVerifying: "正在向平台確認…",
|
|
314
|
+
wizardDetect: "從訊息偵測", wizardDetectHint: "或在群組裡發任一則訊息,然後按「從訊息偵測」。",
|
|
315
|
+
wizardWaitingStart: "等待群組訊息中…", wizardNoStartYet: "還沒收到群組訊息 —— 發一則之後再按一次。",
|
|
316
|
+
wizardGeneralChannel: "General 頻道 id", wizardAdminUser: "你的 user id(會成為第一個允許使用者)",
|
|
317
|
+
wizardNeedVerify: "請先驗證 token 再繼續。",
|
|
318
|
+
wizardPlanLoading: "正在產生方案…", wizardWillWrite: "將寫入:", wizardEnvKeys: "以及寫入 ~/.agend/.env:{0}",
|
|
319
|
+
wizardEnvPerms: "⚠️ ~/.agend/.env 目前其他使用者也讀得到。",
|
|
320
|
+
guildIdField: "Guild id", groupIdField: "群組 id", notSet: "(未設定)", deleteBot: "刪除這個機器人", noAllowedUsers: "沒有允許的使用者",
|
|
321
|
+
searchAll: "🔍 搜尋 agent、機器人、頻道…", accessMovedHint: "誰可以使用 bot 是在連線本身設定 —— 請到「連線與機器人」開啟「{0}」的設定。",
|
|
322
|
+
applyRestartNeeded: "已儲存 —— 需要重新啟動 AgEnD 才會生效", applyNoChange: "無需變更",
|
|
323
|
+
applyRestartHint: "此變更需要重新啟動 AgEnD 本身。可以在這裡重新啟動,或在聊天頻道執行 /restart。",
|
|
324
|
+
applyBusy: "AgEnD 正在重新載入設定。你的變更已儲存,請稍候再按一次套用。",
|
|
325
|
+
restartFleetButton: "重新啟動 AgEnD", restartFleetConfirm: "現在重新啟動 AgEnD?\n\n所有 agent 都會停止再啟動,進行中的工作會被中斷,面板會有最多約一分鐘無法連線。聊天頻道會收到一則通知。",
|
|
326
|
+
restartFleetStarted: "AgEnD 重新啟動中 —— 此頁面會持續確認。", restartFleetRefused: "重新啟動被拒絕:{0}",
|
|
327
|
+
restartFleetRateLimited: "重新啟動被拒絕:{0}(約 {1} 分鐘後可再試)",
|
|
328
|
+
signatureMismatch: "fleet.yaml 與執行中的設定在這些項目上不一致:{0}。重新啟動無法解決,請先查看 fleet.log。",
|
|
329
|
+
applyStillWorking: "仍在重啟中({0} 秒)—— AgEnD 還在處理,此頁面會持續確認。",
|
|
330
|
+
applyLostJob: "已追蹤不到這次套用(AgEnD 可能重啟過)。請重新整理頁面查看目前狀態。",
|
|
331
|
+
changesPending: "有 {0} 項變更尚未套用", impactNow: "⚡ 立即生效", impactAgent: "🔄 重新啟動此 Agent", impactFleet: "🔄🔄 重新啟動 AgEnD", queued: "已加入待套用:{0}", applyFailed: "套用失敗", changesApplied: "✓ 變更已套用,AgEnD 正在協調受影響的 Agent。", reviewChanges: "檢查變更", readyApply: "可套用變更", copied: "已複製", expectedMapping: "預期為 mapping", configLoadFailed: "載入設定失敗 — 請重新開啟 Settings 連結。",
|
|
230
332
|
confirmOpenAccess: "Open 模式會允許此頻道中的所有人操作 bot,確定要繼續嗎?", confirmLockedEmpty: "Locked 模式目前沒有允許的使用者,可能導致沒有人能管理 bot,確定要繼續嗎?", accessLockedEmpty: "Locked 模式沒有允許的使用者;建議至少加入一位管理員,避免無法操作。", accessChangeCancelled: "已取消 Access Mode 變更",
|
|
231
333
|
mustNonNegative: "必須為 0 或更大的數值", mustPositive: "必須大於 0", nonnegativeInteger: "必須是非負整數", failoverEmpty: "空白清單會停用模型備援", concurrencyRange: "必須是 1 到 20 的整數", concurrencyWarning: "在較小的主機上可能超出 CPU 負荷", staggerRange: "必須介於 0 到 30000", staggerWarning: "數值太低可能造成啟動尖峰",
|
|
232
334
|
},
|
|
@@ -292,7 +394,7 @@
|
|
|
292
394
|
|
|
293
395
|
// ── API ──
|
|
294
396
|
async function api(path, opts = {}) {
|
|
295
|
-
const headers = Object.assign({
|
|
397
|
+
const headers = Object.assign({}, opts.headers || {});
|
|
296
398
|
if (opts.body) headers["Content-Type"] = "application/json";
|
|
297
399
|
const r = await fetch(path, Object.assign({}, opts, { headers }));
|
|
298
400
|
let body = null; try { body = await r.json(); } catch {}
|
|
@@ -316,7 +418,7 @@
|
|
|
316
418
|
}
|
|
317
419
|
|
|
318
420
|
// ── 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() };
|
|
421
|
+
const state = { schema: { impacts: {}, order: ["now", "instance", "fleet"] }, fleet: {}, classic: {}, classicStatus: {}, classicLive: {}, live: {}, connections: [], providerSecrets: [], fleetUp: false, advFmt: "yaml", advEdit: false, collapsedGroups: new Set(), version: "", newAgentOpen: false, newBotOpen: false, pending: new Map() };
|
|
320
422
|
const channels = () => state.fleet.channels || (state.fleet.channel ? [state.fleet.channel] : []);
|
|
321
423
|
const channelIds = () => channels().map(c => c.id || c.type).filter(Boolean);
|
|
322
424
|
const chLabel = (i) => i === 0 ? "primary" : "persona";
|
|
@@ -333,7 +435,250 @@
|
|
|
333
435
|
}
|
|
334
436
|
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
437
|
const impactText = (kind) => t(kind === "now" ? "impactNow" : kind === "fleet" ? "impactFleet" : "impactAgent");
|
|
336
|
-
|
|
438
|
+
// The server derives every field's impact from HOT_INSTANCE_CONFIG_KEYS and
|
|
439
|
+
// hands it over at /api/settings/schema, so this page never decides it.
|
|
440
|
+
const impactOf = (field) => state.schema.impacts[field] || "instance";
|
|
441
|
+
const impact = (field) => el("span", { class: "impact" }, impactText(impactOf(field)));
|
|
442
|
+
// A batch costs as much as its worst member, in the order the server ships.
|
|
443
|
+
const batchImpact = (keys, impactFor) => {
|
|
444
|
+
const order = state.schema.order;
|
|
445
|
+
return order[keys.reduce((worst, k) => Math.max(worst, order.indexOf(impactFor(k))), 0)];
|
|
446
|
+
};
|
|
447
|
+
/** One expandable drawer. Advanced settings are collapsed, never removed. */
|
|
448
|
+
function drawer(title, ...children) {
|
|
449
|
+
const box = el("details", { class: "drawer" });
|
|
450
|
+
box.append(el("summary", {}, title), el("div", { class: "drawer-body" }, ...children));
|
|
451
|
+
return box;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
/**
|
|
455
|
+
* One modal edits one object, and closing it stages a change — it never
|
|
456
|
+
* writes. Applying stays where it was: the Apply button on the pending bar.
|
|
457
|
+
*/
|
|
458
|
+
let modalState = null;
|
|
459
|
+
function openModal({ title, subtitle, body, impacts, stage, doneLabel, actions }) {
|
|
460
|
+
modalState = { stage };
|
|
461
|
+
$("modalActions").innerHTML = "";
|
|
462
|
+
// A wizard drives its own footer; an editor uses the standard Cancel/Done.
|
|
463
|
+
$("modalDone").style.display = actions ? "none" : "";
|
|
464
|
+
if (actions) $("modalActions").append(...actions);
|
|
465
|
+
$("modalTitle").textContent = title;
|
|
466
|
+
$("modalSubtitle").textContent = subtitle || "";
|
|
467
|
+
$("modalBody").innerHTML = "";
|
|
468
|
+
$("modalBody").append(body);
|
|
469
|
+
// The footer says what the whole modal will cost, so the impact per field
|
|
470
|
+
// does not have to be added up by eye.
|
|
471
|
+
const kinds = [...new Set((impacts || []).map(impactOf))];
|
|
472
|
+
$("modalImpact").textContent = kinds.length ? kinds.map(impactText).join(" · ") : "";
|
|
473
|
+
$("modalDone").textContent = doneLabel || t("modalDone");
|
|
474
|
+
$("modal").classList.add("open");
|
|
475
|
+
$("modalBackdrop").classList.add("open");
|
|
476
|
+
}
|
|
477
|
+
function closeModal() {
|
|
478
|
+
modalState = null;
|
|
479
|
+
$("modal").classList.remove("open");
|
|
480
|
+
$("modalBackdrop").classList.remove("open");
|
|
481
|
+
$("modalBody").innerHTML = "";
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
// ── Setup wizard ──────────────────────────────────────────────────────────
|
|
485
|
+
//
|
|
486
|
+
// The same four steps as `agend quickstart`. There is no second flow for
|
|
487
|
+
// "a fleet already exists": the wizard is one modal that pre-fills from the
|
|
488
|
+
// running configuration and, when it finishes, hands over to the ordinary
|
|
489
|
+
// apply job — the same progress, deadline and restart recovery as every other
|
|
490
|
+
// change the panel makes.
|
|
491
|
+
const WIZARD_STEPS = 4;
|
|
492
|
+
let wiz = null;
|
|
493
|
+
|
|
494
|
+
function wizardField(labelText, input, hint) {
|
|
495
|
+
return el("div", { class: "field" }, el("label", {}, labelText), input,
|
|
496
|
+
hint ? el("div", { class: "sub" }, hint) : null);
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
async function openWizard() {
|
|
500
|
+
const envRes = await api("/api/settings/quickstart/environment");
|
|
501
|
+
wiz = {
|
|
502
|
+
step: 1,
|
|
503
|
+
env: envRes.body || { backends: [], channels: [], has_fleet: false },
|
|
504
|
+
backend: (envRes.body?.backends || [])[0] || "claude-code",
|
|
505
|
+
working_directory: "",
|
|
506
|
+
instance_name: "agent-1",
|
|
507
|
+
platform: "telegram",
|
|
508
|
+
token: "",
|
|
509
|
+
token_env: "AGEND_BOT_TOKEN",
|
|
510
|
+
group_id: "",
|
|
511
|
+
guild_id: "",
|
|
512
|
+
general_channel_id: "",
|
|
513
|
+
admin_user_id: "",
|
|
514
|
+
identity: null,
|
|
515
|
+
guilds: [],
|
|
516
|
+
plan: null,
|
|
517
|
+
busy: "",
|
|
518
|
+
};
|
|
519
|
+
renderWizard();
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
function renderWizard() {
|
|
523
|
+
const body = el("div", {});
|
|
524
|
+
body.append(el("div", { class: "sub" }, tf("wizardStep", wiz.step, WIZARD_STEPS)));
|
|
525
|
+
body.append(wizardStepBody());
|
|
526
|
+
if (wiz.busy) body.append(el("div", { class: "sub" }, wiz.busy));
|
|
527
|
+
|
|
528
|
+
const back = el("button", { onclick: () => { wiz.step--; renderWizard(); } }, t("wizardBack"));
|
|
529
|
+
back.disabled = wiz.step === 1;
|
|
530
|
+
const next = el("button", { class: "primary", onclick: () => advanceWizard() },
|
|
531
|
+
wiz.step === WIZARD_STEPS ? t("wizardFinish") : t("wizardNext"));
|
|
532
|
+
openModal({
|
|
533
|
+
title: t("wizardTitle"),
|
|
534
|
+
subtitle: wiz.env.has_fleet ? t("wizardRerun") : "",
|
|
535
|
+
body,
|
|
536
|
+
actions: [back, next],
|
|
537
|
+
});
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
function wizardStepBody() {
|
|
541
|
+
if (wiz.step === 1) {
|
|
542
|
+
const fBackend = select(wiz.backend, wiz.env.backends.length ? wiz.env.backends : BACKENDS);
|
|
543
|
+
fBackend.onchange = () => { wiz.backend = fBackend.value; };
|
|
544
|
+
const fDir = el("input", { type: "text", value: wiz.working_directory, placeholder: "/home/you/projects/app" });
|
|
545
|
+
fDir.oninput = () => { wiz.working_directory = fDir.value.trim(); };
|
|
546
|
+
const fName = el("input", { type: "text", value: wiz.instance_name });
|
|
547
|
+
fName.oninput = () => { wiz.instance_name = fName.value.trim(); };
|
|
548
|
+
return el("div", {},
|
|
549
|
+
wizardField(t("wizardBackend"), fBackend,
|
|
550
|
+
wiz.env.backends.length ? tf("wizardBackendFound", wiz.env.backends.join(", ")) : t("wizardBackendNone")),
|
|
551
|
+
wizardField(t("workingDir"), fDir),
|
|
552
|
+
wizardField(t("wizardAgentName"), fName));
|
|
553
|
+
}
|
|
554
|
+
if (wiz.step === 2) {
|
|
555
|
+
const pick = (platform) => el("button", { class: wiz.platform === platform ? "primary" : "", onclick: () => { wiz.platform = platform; renderWizard(); } },
|
|
556
|
+
platform === "telegram" ? "Telegram" : "Discord");
|
|
557
|
+
return el("div", {}, el("div", { class: "actions" }, pick("telegram"), pick("discord")),
|
|
558
|
+
el("div", { class: "sub" }, wiz.platform === "telegram" ? t("wizardTelegramHint") : t("wizardDiscordHint")));
|
|
559
|
+
}
|
|
560
|
+
if (wiz.step === 3) return wizardCredentialsStep();
|
|
561
|
+
return wizardReviewStep();
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
function wizardCredentialsStep() {
|
|
565
|
+
const fToken = el("input", { type: "password", value: wiz.token, placeholder: "123456:ABC-DEF…" });
|
|
566
|
+
fToken.oninput = () => { wiz.token = fToken.value.trim(); wiz.identity = null; };
|
|
567
|
+
const fEnv = el("input", { type: "text", value: wiz.token_env });
|
|
568
|
+
fEnv.oninput = () => { wiz.token_env = fEnv.value.trim(); };
|
|
569
|
+
const verify = el("button", { onclick: async () => {
|
|
570
|
+
wiz.busy = t("wizardVerifying"); renderWizard();
|
|
571
|
+
const res = await api("/api/settings/quickstart/probe", { method: "POST", body: JSON.stringify({ action: "verify", platform: wiz.platform, token: wiz.token }) });
|
|
572
|
+
wiz.identity = res.body?.identity || { valid: false };
|
|
573
|
+
wiz.busy = "";
|
|
574
|
+
if (wiz.identity.valid && wiz.platform === "discord") {
|
|
575
|
+
const guilds = await api("/api/settings/quickstart/probe", { method: "POST", body: JSON.stringify({ action: "guilds", token: wiz.token }) });
|
|
576
|
+
wiz.guilds = guilds.body?.guilds || [];
|
|
577
|
+
}
|
|
578
|
+
renderWizard();
|
|
579
|
+
} }, t("wizardVerify"));
|
|
580
|
+
|
|
581
|
+
const rows = [
|
|
582
|
+
wizardField(t("wizardToken"), fToken, wiz.platform === "telegram" ? t("wizardTokenTelegram") : t("wizardTokenDiscord")),
|
|
583
|
+
wizardField(t("tokenEnv"), fEnv),
|
|
584
|
+
el("div", { class: "actions" }, verify,
|
|
585
|
+
wiz.identity ? el("span", { class: wiz.identity.valid ? "s" : "e" },
|
|
586
|
+
wiz.identity.valid ? `✓ ${wiz.identity.username || ""}` : `✗ ${wiz.identity.reason || ""}`) : null),
|
|
587
|
+
];
|
|
588
|
+
|
|
589
|
+
// Only the ids that platform actually has.
|
|
590
|
+
if (wiz.platform === "discord") {
|
|
591
|
+
const fGuild = select(wiz.guild_id, ["", ...wiz.guilds.map(g => ({ value: g.id, label: `${g.name} (${g.id})` }))]);
|
|
592
|
+
fGuild.onchange = () => { wiz.guild_id = fGuild.value; };
|
|
593
|
+
const fGeneral = el("input", { type: "text", value: wiz.general_channel_id, placeholder: "General channel id" });
|
|
594
|
+
fGeneral.oninput = () => { wiz.general_channel_id = fGeneral.value.trim(); };
|
|
595
|
+
const fUser = el("input", { type: "text", value: wiz.admin_user_id, placeholder: "your Discord user id" });
|
|
596
|
+
fUser.oninput = () => { wiz.admin_user_id = fUser.value.trim(); };
|
|
597
|
+
rows.push(wizardField(t("guildIdField"), fGuild), wizardField(t("wizardGeneralChannel"), fGeneral), wizardField(t("wizardAdminUser"), fUser));
|
|
598
|
+
} else {
|
|
599
|
+
const fGroup = el("input", { type: "text", value: wiz.group_id, placeholder: "-1001234567890" });
|
|
600
|
+
fGroup.oninput = () => { wiz.group_id = fGroup.value.trim(); };
|
|
601
|
+
const detect = el("button", { onclick: async () => {
|
|
602
|
+
wiz.busy = t("wizardWaitingStart"); renderWizard();
|
|
603
|
+
const res = await api("/api/settings/quickstart/probe", { method: "POST", body: JSON.stringify({ action: "await-telegram-start", token: wiz.token, offset: wiz.offset || 0 }) });
|
|
604
|
+
wiz.busy = "";
|
|
605
|
+
if (!res.ok) { showBanner(res.body?.error || t("applyFailed"), true); renderWizard(); return; }
|
|
606
|
+
wiz.offset = res.body.offset;
|
|
607
|
+
if (res.body.found) {
|
|
608
|
+
wiz.group_id = String(res.body.found.groupId);
|
|
609
|
+
wiz.admin_user_id = String(res.body.found.userId);
|
|
610
|
+
} else showBanner(t("wizardNoStartYet"), true);
|
|
611
|
+
renderWizard();
|
|
612
|
+
} }, t("wizardDetect"));
|
|
613
|
+
const fUser = el("input", { type: "text", value: wiz.admin_user_id, placeholder: "your Telegram user id" });
|
|
614
|
+
fUser.oninput = () => { wiz.admin_user_id = fUser.value.trim(); };
|
|
615
|
+
rows.push(wizardField(t("groupIdField"), fGroup, t("wizardDetectHint")),
|
|
616
|
+
el("div", { class: "actions" }, detect), wizardField(t("wizardAdminUser"), fUser));
|
|
617
|
+
}
|
|
618
|
+
return el("div", {}, ...rows);
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
function wizardReviewStep() {
|
|
622
|
+
const box = el("div", {});
|
|
623
|
+
if (!wiz.plan) { box.append(el("div", { class: "sub" }, t("wizardPlanLoading"))); return box; }
|
|
624
|
+
box.append(
|
|
625
|
+
el("div", { class: "field" }, el("strong", {}, t("wizardWillWrite"))),
|
|
626
|
+
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 } } })),
|
|
627
|
+
el("div", { class: "field" }, el("div", { class: "sub" }, tf("wizardEnvKeys", wiz.plan.env_keys.join(", ")))),
|
|
628
|
+
);
|
|
629
|
+
for (const warning of wiz.plan.warnings || []) box.append(el("div", { class: "field hint" }, "⚠️ " + warning));
|
|
630
|
+
return box;
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
async function advanceWizard() {
|
|
634
|
+
if (wiz.step === 1) {
|
|
635
|
+
if (!wiz.working_directory.startsWith("/")) { showBanner(t("wizardNeedDir"), true); return; }
|
|
636
|
+
wiz.step = 2; renderWizard(); return;
|
|
637
|
+
}
|
|
638
|
+
if (wiz.step === 2) { wiz.step = 3; renderWizard(); return; }
|
|
639
|
+
if (wiz.step === 3) {
|
|
640
|
+
if (!wiz.identity?.valid) { showBanner(t("wizardNeedVerify"), true); return; }
|
|
641
|
+
const res = await api("/api/settings/quickstart/plan", { method: "POST", body: JSON.stringify(wizardInput()) });
|
|
642
|
+
if (!res.ok) { showBanner(res.body?.error || t("applyFailed"), true); return; }
|
|
643
|
+
wiz.plan = res.body; wiz.step = 4; renderWizard(); return;
|
|
644
|
+
}
|
|
645
|
+
await finishWizard();
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
function wizardInput() {
|
|
649
|
+
return {
|
|
650
|
+
platform: wiz.platform, token_env: wiz.token_env, backend: wiz.backend,
|
|
651
|
+
working_directory: wiz.working_directory, instance_name: wiz.instance_name,
|
|
652
|
+
group_id: wiz.group_id || undefined, guild_id: wiz.guild_id || undefined,
|
|
653
|
+
general_channel_id: wiz.general_channel_id || undefined,
|
|
654
|
+
admin_user_id: wiz.admin_user_id || undefined,
|
|
655
|
+
};
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
/**
|
|
659
|
+
* Write the files, then start an ordinary apply job.
|
|
660
|
+
*
|
|
661
|
+
* The wizard does not apply anything itself — "create and start" is the same
|
|
662
|
+
* job the Apply button produces, so it has progress, a deadline and recovery
|
|
663
|
+
* across the restart it may cause instead of a silent spinner.
|
|
664
|
+
*/
|
|
665
|
+
async function finishWizard() {
|
|
666
|
+
const commit = await api("/api/settings/quickstart/commit", { method: "POST", body: JSON.stringify({ ...wizardInput(), token: wiz.token }) });
|
|
667
|
+
if (!commit.ok) { showBanner(commit.body?.error || t("applyFailed"), true); return; }
|
|
668
|
+
if (commit.body?.secret_mode_ok === false) showBanner(t("wizardEnvPerms"), true);
|
|
669
|
+
closeModal();
|
|
670
|
+
const key = (crypto.randomUUID ? crypto.randomUUID() : `w-${Date.now()}-${Math.random().toString(36).slice(2)}`);
|
|
671
|
+
const started = await api("/api/settings/apply", {
|
|
672
|
+
method: "POST",
|
|
673
|
+
headers: { "Idempotency-Key": key },
|
|
674
|
+
body: JSON.stringify({ idempotency_key: key }),
|
|
675
|
+
});
|
|
676
|
+
if (started.status === 409) { showBanner(t("applyBusy"), true); return; }
|
|
677
|
+
if (!started.ok || !started.body?.id) { showBanner(started.body?.error || t("applyFailed"), true); return; }
|
|
678
|
+
await watchApplyJob(started.body);
|
|
679
|
+
await reload();
|
|
680
|
+
}
|
|
681
|
+
|
|
337
682
|
function updatePendingBar() {
|
|
338
683
|
const n = state.pending.size;
|
|
339
684
|
$("pendingBar").classList.toggle("open", n > 0);
|
|
@@ -363,22 +708,132 @@
|
|
|
363
708
|
catch (error) { result = { ok: false, body: { error: error.message } }; }
|
|
364
709
|
if (!result?.ok) {
|
|
365
710
|
showBanner(`${change.label}: ${result?.body?.error || t("applyFailed")}`, true);
|
|
711
|
+
change.cleanup?.();
|
|
366
712
|
button.disabled = false; return;
|
|
367
713
|
}
|
|
714
|
+
change.cleanup?.();
|
|
368
715
|
state.pending.delete(key); updatePendingBar();
|
|
369
716
|
}
|
|
370
|
-
|
|
717
|
+
// The key is generated here, before the first attempt, and reused by every
|
|
718
|
+
// retry: a POST whose response is lost on a phone must rejoin the same job
|
|
719
|
+
// instead of applying everything twice.
|
|
720
|
+
const key = (crypto.randomUUID ? crypto.randomUUID() : `k-${Date.now()}-${Math.random().toString(36).slice(2)}`);
|
|
721
|
+
const started = await api("/api/settings/apply", {
|
|
722
|
+
method: "POST",
|
|
723
|
+
headers: { "Idempotency-Key": key },
|
|
724
|
+
body: JSON.stringify({ idempotency_key: key }),
|
|
725
|
+
});
|
|
726
|
+
if (started.status === 409) {
|
|
727
|
+
// One reconcile at a time. The writes above already landed, so re-applying
|
|
728
|
+
// later is safe and idempotent.
|
|
729
|
+
showBanner(t("applyBusy"), true);
|
|
730
|
+
button.disabled = false;
|
|
731
|
+
return;
|
|
732
|
+
}
|
|
733
|
+
if (!started.ok || !started.body?.id) {
|
|
734
|
+
showBanner(started.body?.error || t("applyFailed"), true);
|
|
735
|
+
button.disabled = false;
|
|
736
|
+
return;
|
|
737
|
+
}
|
|
738
|
+
await watchApplyJob(started.body);
|
|
371
739
|
await reload();
|
|
372
|
-
showBanner(t("changesApplied"));
|
|
373
740
|
button.disabled = false;
|
|
374
741
|
}
|
|
742
|
+
|
|
743
|
+
function applyRow(row) {
|
|
744
|
+
const icon = row.status === "done" ? "✓"
|
|
745
|
+
: row.status === "failed" ? "✗"
|
|
746
|
+
: row.status === "restart-required" ? "🔄🔄"
|
|
747
|
+
: row.status === "running" ? "…" : "·";
|
|
748
|
+
// restart-required is terminal and is NOT success: the value is saved and a
|
|
749
|
+
// running AgEnD cannot adopt it.
|
|
750
|
+
const note = row.status === "restart-required" ? t("applyRestartNeeded")
|
|
751
|
+
: row.settled_by === "no-change" ? t("applyNoChange")
|
|
752
|
+
: row.settled_by === "fleet-restart" ? "(restart)"
|
|
753
|
+
: row.kind === "hot" ? t("impactNow") : t("impactAgent");
|
|
754
|
+
return el("div", { class: "apply-row" },
|
|
755
|
+
el("span", {}, icon),
|
|
756
|
+
el("span", { class: "name" }, row.target === "fleet" ? "AgEnD" : shortName(row.target, {})),
|
|
757
|
+
el("span", { class: "sub" }, note),
|
|
758
|
+
row.error ? el("span", { class: "err" }, row.error) : null);
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
async function restartFleet(job) {
|
|
762
|
+
// Its own confirmation, its own key. A single Apply must never be able to
|
|
763
|
+
// carry a full-fleet restart along with it.
|
|
764
|
+
if (!confirm(t("restartFleetConfirm"))) return;
|
|
765
|
+
const key = (crypto.randomUUID ? crypto.randomUUID() : `r-${Date.now()}-${Math.random().toString(36).slice(2)}`);
|
|
766
|
+
const res = await api("/api/settings/restart-fleet", {
|
|
767
|
+
method: "POST",
|
|
768
|
+
headers: { "Idempotency-Key": key },
|
|
769
|
+
body: JSON.stringify({ job_id: job.id, confirm: "restart-agend", idempotency_key: key }),
|
|
770
|
+
});
|
|
771
|
+
if (!res.ok) {
|
|
772
|
+
const secs = res.body?.retry_after_seconds;
|
|
773
|
+
showBanner(secs
|
|
774
|
+
? tf("restartFleetRateLimited", res.body?.error || "", Math.ceil(secs / 60))
|
|
775
|
+
: tf("restartFleetRefused", res.body?.error || t("applyFailed")), true);
|
|
776
|
+
return;
|
|
777
|
+
}
|
|
778
|
+
showBanner(t("restartFleetStarted"));
|
|
779
|
+
await watchApplyJob(job);
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
function renderApplyJob(job) {
|
|
783
|
+
$("applyPanel").classList.add("open");
|
|
784
|
+
$("applyTitle").textContent = job.status === "done"
|
|
785
|
+
? (job.targets.some(r => r.status === "failed") ? t("applyPartial") : t("applyDone"))
|
|
786
|
+
: job.status === "failed" ? t("applyPartial") : t("applying");
|
|
787
|
+
const needsRestart = job.targets.some(row => row.status === "restart-required");
|
|
788
|
+
const mismatch = state.schema.fleet_signature_mismatch;
|
|
789
|
+
$("applyNote").textContent = job.overdue
|
|
790
|
+
? tf("applyStillWorking", Math.round(job.elapsed_ms / 1000))
|
|
791
|
+
: mismatch && needsRestart ? tf("signatureMismatch", mismatch.join(", "))
|
|
792
|
+
: needsRestart ? t("applyRestartHint") : "";
|
|
793
|
+
const rows = $("applyRows"); rows.innerHTML = "";
|
|
794
|
+
for (const row of job.targets) rows.append(applyRow(row));
|
|
795
|
+
// A restart that cannot clear the mismatch is worse than no button at all.
|
|
796
|
+
if (needsRestart && !mismatch) {
|
|
797
|
+
rows.append(el("div", { class: "apply-row" },
|
|
798
|
+
el("button", { class: "primary", onclick: () => restartFleet(job) }, t("restartFleetButton"))));
|
|
799
|
+
}
|
|
800
|
+
if (job.error) rows.append(el("div", { class: "apply-row err" }, job.error));
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
/**
|
|
804
|
+
* GET is the authority. The SSE stream carries no event id, so a reconnect
|
|
805
|
+
* cannot replay missed frames; it only shortens the wait between polls.
|
|
806
|
+
*/
|
|
807
|
+
async function watchApplyJob(job) {
|
|
808
|
+
renderApplyJob(job);
|
|
809
|
+
const deadline = Date.now() + 10 * 60_000;
|
|
810
|
+
let current = job;
|
|
811
|
+
while (current.status === "running" && Date.now() < deadline) {
|
|
812
|
+
await new Promise(r => setTimeout(r, 1000));
|
|
813
|
+
const res = await api(`/api/settings/apply/${encodeURIComponent(job.id)}`);
|
|
814
|
+
if (res.status === 404) {
|
|
815
|
+
// The job outlives the process that made it, so a 404 means it aged out
|
|
816
|
+
// of the store — not that it never happened.
|
|
817
|
+
$("applyNote").textContent = t("applyLostJob");
|
|
818
|
+
return;
|
|
819
|
+
}
|
|
820
|
+
if (!res.ok || !res.body?.id) continue;
|
|
821
|
+
current = res.body;
|
|
822
|
+
renderApplyJob(current);
|
|
823
|
+
}
|
|
824
|
+
const failed = current.targets.some(r => r.status === "failed");
|
|
825
|
+
const needsRestart = current.targets.some(r => r.status === "restart-required");
|
|
826
|
+
showBanner(failed ? t("applyPartial") : needsRestart ? t("applyRestartNeeded") : t("changesApplied"),
|
|
827
|
+
failed || needsRestart);
|
|
828
|
+
// A restart reminder has to stay on screen; a clean apply can fade.
|
|
829
|
+
if (!needsRestart) setTimeout(() => $("applyPanel").classList.remove("open"), 4000);
|
|
830
|
+
}
|
|
375
831
|
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
832
|
const sameValue = (a, b) => JSON.stringify(a) === JSON.stringify(b);
|
|
378
833
|
function changedFields(next, baseline) {
|
|
379
834
|
return Object.fromEntries(Object.entries(next).filter(([key, value]) => !sameValue(value, baseline[key])));
|
|
380
835
|
}
|
|
381
|
-
function overrideControl(inst, key, input, inheritedValue,
|
|
836
|
+
function overrideControl(inst, key, input, inheritedValue, impactField = `instance.${key}`) {
|
|
382
837
|
const inherited = !hasOwn(inst, key);
|
|
383
838
|
const toggle = el("input", { type: "checkbox" }); toggle.checked = inherited;
|
|
384
839
|
input.disabled = inherited;
|
|
@@ -387,7 +842,7 @@
|
|
|
387
842
|
else input.value = inheritedValue;
|
|
388
843
|
}
|
|
389
844
|
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(
|
|
845
|
+
return { input, toggle, row: el("div", {}, input, el("label", { class: "inherit-row" }, toggle, el("span", {}, t("inheritDefault")), impact(impactField))) };
|
|
391
846
|
}
|
|
392
847
|
const replyGuardSupported = (backend, mode) => backend === "claude-code" && mode === "mcp";
|
|
393
848
|
const replyGuardHint = (backend, mode) => el(
|
|
@@ -412,7 +867,6 @@
|
|
|
412
867
|
}
|
|
413
868
|
|
|
414
869
|
// ── My Agents ──
|
|
415
|
-
let editing = null;
|
|
416
870
|
function agentChannelLabel(inst) {
|
|
417
871
|
if (inst.general_topic) return "general";
|
|
418
872
|
if (!inst.channel_id) return "";
|
|
@@ -453,7 +907,10 @@
|
|
|
453
907
|
el("span", { class: "tag" }, `effort: ${summary.effort}`),
|
|
454
908
|
chTag ? el("span", { class: "tag" + (chTag.includes("persona") ? " persona" : chTag.includes("telegram") ? " tg" : "") }, chTag) : null,
|
|
455
909
|
el("span", { class: "spacer" }),
|
|
456
|
-
el("button", { class: "sm ghost", onclick: () => {
|
|
910
|
+
el("button", { class: "sm ghost", onclick: () => {
|
|
911
|
+
const form = agentEditForm(name, inst);
|
|
912
|
+
openModal({ title: shortName(name, inst), subtitle: "ID: " + name, body: form.box, impacts: form.impacts, stage: form.stage });
|
|
913
|
+
} }, t("settingsButton")),
|
|
457
914
|
running ? el("button", { class: "sm", onclick: () => pauseWakeAgent(name, "pause") }, t("pause")) : null,
|
|
458
915
|
paused ? el("button", { class: "sm", onclick: () => pauseWakeAgent(name, "wake") }, t("wake")) : null,
|
|
459
916
|
!paused ? el("button", { class: "sm", onclick: () => toggleAgent(name, running) }, running ? t("stop") : t("start")) : null,
|
|
@@ -461,7 +918,6 @@
|
|
|
461
918
|
el("summary", { title: "More actions" }, "⋯"),
|
|
462
919
|
el("div", { class: "menu" }, el("button", { class: "sm danger", onclick: () => delAgent(name) }, "Delete Agent"))),
|
|
463
920
|
));
|
|
464
|
-
if (editing === name) item.append(agentEditForm(name, inst));
|
|
465
921
|
return item;
|
|
466
922
|
}
|
|
467
923
|
function renderAgents() {
|
|
@@ -504,13 +960,13 @@
|
|
|
504
960
|
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
961
|
const hangRaw = inst.hang_detector || {};
|
|
506
962
|
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);
|
|
963
|
+
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
964
|
const agentMode = overrideControl(inst, "agent_mode", select(inst.agent_mode || defaults.agent_mode || "mcp", ["mcp", "cli"]), defaults.agent_mode || "mcp");
|
|
509
965
|
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"
|
|
966
|
+
const toolProgress = overrideControl(inst, "tool_progress", select(inst.tool_progress ?? defaults.tool_progress ?? "off", ["off", "standard", "verbose"]), defaults.tool_progress ?? "off");
|
|
511
967
|
const fReplyGuard = el("input", { type: "checkbox" });
|
|
512
968
|
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
|
|
969
|
+
const replyGuard = overrideControl(inst, "reply_completion_guard", fReplyGuard, defaults.reply_completion_guard ?? true);
|
|
514
970
|
const replyHint = replyGuardHint(fBackend.value, agentMode.input.value);
|
|
515
971
|
const refreshReplyHint = () => {
|
|
516
972
|
const supported = replyGuardSupported(fBackend.value, agentMode.input.value);
|
|
@@ -528,7 +984,10 @@
|
|
|
528
984
|
const hangFeedback = el("div", { class: "feedback" });
|
|
529
985
|
const failoverFeedback = el("div", { class: "feedback" });
|
|
530
986
|
const msg = el("span", { class: "msg" });
|
|
531
|
-
|
|
987
|
+
// Never rendered: it carries the validation state and the submit handler
|
|
988
|
+
// that the modal footer triggers through stage(). The form has no button
|
|
989
|
+
// of its own, so nothing here can write on its own.
|
|
990
|
+
const saveBtn = el("button", { hidden: "hidden" });
|
|
532
991
|
const validate = () => {
|
|
533
992
|
const autoValue = Number(auto.input.value), hangValue = Number(hang.input.value);
|
|
534
993
|
const validAuto = setValidation(auto.input, autoFeedback, !auto.toggle.checked && (!Number.isFinite(autoValue) || autoValue < 0) ? t("mustNonNegative") : "");
|
|
@@ -586,44 +1045,56 @@
|
|
|
586
1045
|
}
|
|
587
1046
|
stageChange(`agent:${name}`, {
|
|
588
1047
|
label: `Update ${shortName(name, state.fleet.instances[name])}`,
|
|
589
|
-
impact: Object.keys(patch)
|
|
1048
|
+
impact: batchImpact(Object.keys(patch), key => impactOf(`instance.${key}`)),
|
|
590
1049
|
apply: () => api(`/api/settings/fleet/instances/${encodeURIComponent(name)}`, { method: "PATCH", body: JSON.stringify(patch) }),
|
|
591
1050
|
});
|
|
592
|
-
|
|
1051
|
+
renderAgents();
|
|
593
1052
|
};
|
|
594
1053
|
box.append(
|
|
595
1054
|
el("div", { class: "sub", style: "margin:0 0 8px" }, "ID: " + name),
|
|
596
1055
|
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,
|
|
1056
|
+
el("div", {}, el("label", {}, t("displayName"), impact("instance.display_name")), displayName.row),
|
|
1057
|
+
el("div", {}, el("label", {}, t("description"), impact("instance.description")), fDesc),
|
|
1058
|
+
el("div", {}, el("label", {}, t("backend"), impact("instance.backend")), fBackend),
|
|
1059
|
+
el("div", {}, el("label", {}, t("model"), impact("instance.model")), fModel)),
|
|
1060
|
+
el("label", {}, t("workingDir"), impact("instance.working_directory")), fWd,
|
|
602
1061
|
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"),
|
|
1062
|
+
el("div", {}, el("label", {}, `${t("autoPause")} (${t("minutes")})`, impact("instance.auto_pause_after")), auto.row, autoFeedback)),
|
|
1063
|
+
drawer(t("advancedSection"),
|
|
606
1064
|
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),
|
|
1065
|
+
el("div", {}, el("label", {}, t("channelBinding"), impact("instance.topic_id")), fChan),
|
|
1066
|
+
el("div", {}, el("label", {}, `${t("hangTimeout")} (${t("minutes")})`, impact("instance.hang_detector")), hang.row, hangFeedback),
|
|
1067
|
+
el("div", {}, el("label", {}, t("agentMode"), impact("instance.agent_mode")), agentMode.row),
|
|
1068
|
+
el("div", {}, el("label", {}, t("toolSet"), impact("instance.tool_set")), toolSet.row),
|
|
1069
|
+
el("div", {}, el("label", {}, t("toolProgress"), impact("instance.tool_progress")), toolProgress.row),
|
|
1070
|
+
el("div", {}, el("label", {}, t("replyCompletionGuard"), impact("instance.reply_completion_guard")), replyGuard.row, replyHint),
|
|
1071
|
+
el("div", {}, el("label", {}, t("logLevel"), impact("instance.log_level")), logLevel.row),
|
|
1072
|
+
el("div", {}, el("label", {}, t("lightweight"), impact("instance.lightweight")), lightweight.row),
|
|
1073
|
+
el("div", {}, el("label", {}, t("modelFailover"), impact("instance.model_failover")), failover.row, failoverFeedback)),
|
|
1074
|
+
el("label", {}, t("systemPrompt"), impact("instance.systemPrompt")), fSys,
|
|
1075
|
+
el("label", {}, t("tags"), impact("instance.tags")), chipList(tags, null, "tag"),
|
|
1076
|
+
el("label", { style: "display:flex;align-items:center;gap:8px;cursor:pointer" }, fGeneral, el("span", {}, t("generalTopicField")), impact("instance.general_topic"))),
|
|
1077
|
+
el("div", { class: "actions" }, msg),
|
|
623
1078
|
);
|
|
624
1079
|
validate();
|
|
625
|
-
return
|
|
1080
|
+
return {
|
|
1081
|
+
box,
|
|
1082
|
+
// The modal's footer drives this; nothing in the form submits by itself.
|
|
1083
|
+
stage: () => { if (saveBtn.disabled) return false; saveBtn.onclick(); return true; },
|
|
1084
|
+
impacts: AGENT_MODAL_FIELDS,
|
|
1085
|
+
};
|
|
626
1086
|
}
|
|
1087
|
+
|
|
1088
|
+
/** Every field the agent modal shows, for the footer's impact summary. */
|
|
1089
|
+
const AGENT_MODAL_FIELDS = [
|
|
1090
|
+
"instance.display_name", "instance.description", "instance.backend",
|
|
1091
|
+
"instance.model", "instance.working_directory", "instance.auto_pause_after",
|
|
1092
|
+
"instance.topic_id", "instance.hang_detector", "instance.agent_mode",
|
|
1093
|
+
"instance.tool_set", "instance.tool_progress",
|
|
1094
|
+
"instance.reply_completion_guard", "instance.log_level",
|
|
1095
|
+
"instance.lightweight", "instance.model_failover", "instance.systemPrompt",
|
|
1096
|
+
"instance.tags", "instance.general_topic",
|
|
1097
|
+
];
|
|
627
1098
|
async function toggleAgent(name, running) {
|
|
628
1099
|
if (running) await api(`/stop/${encodeURIComponent(name)}`, { method: "POST" });
|
|
629
1100
|
else await api(`/api/instance/${encodeURIComponent(name)}/start`, { method: "POST" });
|
|
@@ -638,7 +1109,7 @@
|
|
|
638
1109
|
if (!confirm(tf("deleteAgent", name))) return;
|
|
639
1110
|
delete state.fleet.instances[name];
|
|
640
1111
|
stageChange(`agent:${name}`, {
|
|
641
|
-
label: `Delete ${name}`, impact: "
|
|
1112
|
+
label: `Delete ${name}`, impact: impactOf("instance.delete"),
|
|
642
1113
|
apply: () => api(`/api/settings/fleet/instances/${encodeURIComponent(name)}`, { method: "DELETE" }),
|
|
643
1114
|
});
|
|
644
1115
|
renderAgents();
|
|
@@ -666,10 +1137,13 @@
|
|
|
666
1137
|
}
|
|
667
1138
|
|
|
668
1139
|
// ── Bots ──
|
|
669
|
-
let editingBot = null;
|
|
670
1140
|
function renderBots() {
|
|
671
|
-
const host = $("bots"); host.innerHTML = "";
|
|
672
|
-
|
|
1141
|
+
const host = $("bots"); host.innerHTML = ""; let chs = channels();
|
|
1142
|
+
// One search box for the whole page.
|
|
1143
|
+
const filter = ($("agentSearch")?.value || "").trim().toLowerCase();
|
|
1144
|
+
if (filter) chs = chs.filter(ch => [ch.id, ch.type, ch.group_id, ch.bot_token_env]
|
|
1145
|
+
.some(v => String(v ?? "").toLowerCase().includes(filter)));
|
|
1146
|
+
if (!chs.length) { host.className = "card empty"; host.textContent = filter ? tf("noAgentMatch", filter) : t("noBots"); return; }
|
|
673
1147
|
host.className = "card";
|
|
674
1148
|
chs.forEach((ch, i) => {
|
|
675
1149
|
const item = el("div", { class: "item" });
|
|
@@ -677,52 +1151,232 @@
|
|
|
677
1151
|
item.append(el("div", { class: "item-row" },
|
|
678
1152
|
el("span", { class: "name" }, type === "telegram" ? "Telegram" : "Discord"),
|
|
679
1153
|
el("span", { class: "tag" + (i === 0 ? "" : " persona") + (type === "telegram" ? " tg" : "") }, ch.id || chLabel(i)),
|
|
680
|
-
el("span", { class: "sub
|
|
1154
|
+
el("span", { class: "sub", style: "margin:0" }, ch.bot_token_env || "(no token env)"),
|
|
1155
|
+
ch.group_id ? el("span", { class: "tag" }, `${type === "telegram" ? "group" : "guild"} ${ch.group_id}`) : null,
|
|
1156
|
+
el("span", { class: "tag" }, `${t("accessMode")}: ${ch.access?.mode || "locked"}`),
|
|
1157
|
+
el("span", { class: "sub", style: "margin:0" },
|
|
1158
|
+
(ch.access?.allowed_users || []).length
|
|
1159
|
+
? (ch.access.allowed_users).slice(0, 3).join(", ") + ((ch.access.allowed_users).length > 3 ? ` +${(ch.access.allowed_users).length - 3}` : "")
|
|
1160
|
+
: t("noAllowedUsers")),
|
|
681
1161
|
el("span", { class: "spacer" }),
|
|
1162
|
+
el("span", { class: "tag", title: "secret status" }, state.connections.find(c => (c.id || c.type) === (ch.id || ch.type))?.token_present ? "Token configured" : "Token missing"),
|
|
682
1163
|
el("span", { style: `color:${state.fleetUp ? "var(--green)" : "var(--red)"};font-size:13px` }, state.fleetUp ? "Connected" : "Problem"),
|
|
683
|
-
el("button", { class: "sm ghost
|
|
684
|
-
|
|
1164
|
+
el("button", { class: "sm ghost", onclick: () => {
|
|
1165
|
+
const form = botEditForm(ch, i);
|
|
1166
|
+
openModal({ title: type === "telegram" ? "Telegram" : "Discord", subtitle: ch.id || chLabel(i), body: form.box, impacts: form.impacts, stage: form.stage });
|
|
1167
|
+
} }, t("settingsButton")),
|
|
685
1168
|
));
|
|
686
|
-
if (editingBot === i) item.append(botEditForm(ch, i));
|
|
687
1169
|
host.append(item);
|
|
688
1170
|
});
|
|
689
1171
|
}
|
|
690
1172
|
|
|
1173
|
+
// Provider API keys use a separate registry endpoint. Unsupported providers
|
|
1174
|
+
// are intentionally rendered without an input: a key that cannot be
|
|
1175
|
+
// positively verified must never be accepted as an "unverified" write.
|
|
1176
|
+
function providerSecretInputAllowed(spec) { return spec?.verifier === "available"; }
|
|
1177
|
+
function renderProviderSecrets() {
|
|
1178
|
+
const host = $("providerSecrets"); if (!host) return;
|
|
1179
|
+
host.innerHTML = "";
|
|
1180
|
+
const rows = Array.isArray(state.providerSecrets) ? state.providerSecrets : [];
|
|
1181
|
+
if (!rows.length) { host.className = "card empty"; host.textContent = "No provider API-key verifiers available."; return; }
|
|
1182
|
+
host.className = "card";
|
|
1183
|
+
rows.forEach(spec => {
|
|
1184
|
+
const row = el("div", { class: "item" });
|
|
1185
|
+
const feedback = el("div", { class: "feedback" });
|
|
1186
|
+
const present = spec.token_present ? "Key configured" : "Not configured";
|
|
1187
|
+
if (!providerSecretInputAllowed(spec)) {
|
|
1188
|
+
row.append(el("div", { class: "item-row" }, el("span", { class: "name" }, spec.display_name), el("span", { class: "tag" }, "Unsupported verifier")), el("div", { class: "hint" }, "This provider cannot be safely verified here; no key input is offered."));
|
|
1189
|
+
host.append(row); return;
|
|
1190
|
+
}
|
|
1191
|
+
const input = el("input", { type: "password", autocomplete: "new-password", placeholder: present });
|
|
1192
|
+
const button = el("button", { class: "sm primary" }, "Verify & apply");
|
|
1193
|
+
button.onclick = async () => {
|
|
1194
|
+
const secret = input.value;
|
|
1195
|
+
if (!secret) { feedback.className = "feedback error"; feedback.textContent = "Enter a key first."; return; }
|
|
1196
|
+
button.disabled = true; feedback.className = "feedback"; feedback.textContent = "Verifying…";
|
|
1197
|
+
const key = crypto.randomUUID ? crypto.randomUUID() : `provider-${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
1198
|
+
try {
|
|
1199
|
+
const verified = await api(`/api/settings/secrets/${encodeURIComponent(spec.id)}/verify`, { method: "POST", headers: { "Idempotency-Key": key }, body: JSON.stringify({ secret, idempotency_key: key }) });
|
|
1200
|
+
if (!verified.ok) { feedback.className = "feedback error"; feedback.textContent = `✗ ${verified.body?.error || "verification failed"}`; return; }
|
|
1201
|
+
feedback.textContent = "Applying…";
|
|
1202
|
+
let result = await api(`/api/settings/secrets/${encodeURIComponent(spec.id)}/apply`, { method: "POST", headers: { "Idempotency-Key": key }, body: JSON.stringify({ verification_id: verified.body?.verification_id, idempotency_key: key }) });
|
|
1203
|
+
let body = result.body || {};
|
|
1204
|
+
for (let n = 0; result.ok && body.job_id && body.result === "applying" && n < 60; n++) {
|
|
1205
|
+
await new Promise(resolve => setTimeout(resolve, 250));
|
|
1206
|
+
const status = await api(`/api/settings/secrets/${encodeURIComponent(spec.id)}/apply/${encodeURIComponent(body.job_id)}`);
|
|
1207
|
+
if (status.ok) body = status.body || body;
|
|
1208
|
+
}
|
|
1209
|
+
const labels = { applied_next_use: "✓ Saved — new processes will use it", reloaded: "✓ Applied", rolled_back: "↩ Rolled back", rollback_failed: "✗ Rollback failed" };
|
|
1210
|
+
const ok = body.result === "applied_next_use" || body.result === "reloaded";
|
|
1211
|
+
feedback.className = `feedback ${ok ? "" : "error"}`;
|
|
1212
|
+
feedback.textContent = labels[body.result] || `✗ ${body.error || "Apply failed"}`;
|
|
1213
|
+
if (ok) { input.value = ""; await reload(); }
|
|
1214
|
+
} finally { input.value = ""; button.disabled = false; }
|
|
1215
|
+
};
|
|
1216
|
+
row.append(el("div", { class: "item-row" }, el("span", { class: "name" }, spec.display_name), el("span", { class: "tag" }, present), el("span", { class: "spacer" }, input), button), feedback);
|
|
1217
|
+
host.append(row);
|
|
1218
|
+
});
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
// Secret rotation is staged through the same Apply bar as every other
|
|
1222
|
+
// Settings mutation. The token is held only in this short-lived closure and
|
|
1223
|
+
// is cleared by the caller after the request settles; it never enters
|
|
1224
|
+
// localStorage, the URL, a label, or the staged change's display text.
|
|
1225
|
+
async function applyConnectionSecret(connectionId, secret, feedback) {
|
|
1226
|
+
const key = crypto.randomUUID ? crypto.randomUUID() : `secret-${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
1227
|
+
feedback.className = "feedback"; feedback.textContent = "Verifying…";
|
|
1228
|
+
const verified = await api(`/api/settings/connections/${encodeURIComponent(connectionId)}/secret/verify`, {
|
|
1229
|
+
method: "POST", headers: { "Idempotency-Key": key }, body: JSON.stringify({ secret, idempotency_key: key }),
|
|
1230
|
+
});
|
|
1231
|
+
if (!verified.ok) { feedback.className = "feedback error"; feedback.textContent = `✗ ${verified.body?.error || "verification failed"}`; return verified; }
|
|
1232
|
+
feedback.textContent = "Applying…";
|
|
1233
|
+
const applied = await api(`/api/settings/connections/${encodeURIComponent(connectionId)}/secret/apply`, {
|
|
1234
|
+
method: "POST", headers: { "Idempotency-Key": key }, body: JSON.stringify({ verification_id: verified.body?.verification_id, idempotency_key: key }),
|
|
1235
|
+
});
|
|
1236
|
+
if (!applied.ok && applied.status !== 409) { feedback.className = "feedback error"; feedback.textContent = `✗ ${applied.body?.error || "apply failed"}`; return applied; }
|
|
1237
|
+
let result = applied.body || {};
|
|
1238
|
+
if (result.job_id && result.result === "applying") {
|
|
1239
|
+
for (let n = 0; n < 60; n++) {
|
|
1240
|
+
await new Promise(resolve => setTimeout(resolve, 250));
|
|
1241
|
+
const status = await api(`/api/settings/connections/${encodeURIComponent(connectionId)}/secret/apply/${encodeURIComponent(result.job_id)}`);
|
|
1242
|
+
if (status.ok) { result = status.body || result; if (result.result !== "applying") break; }
|
|
1243
|
+
}
|
|
1244
|
+
}
|
|
1245
|
+
const labels = { applying: "…Still applying — check status", applied: "✓ Applied", restart_required: "↻ Saved — restart required", rolled_back: "↩ Rolled back", rollback_failed: "✗ Rollback failed" };
|
|
1246
|
+
feedback.className = `feedback ${result.result === "applied" || result.result === "restart_required" ? "" : "error"}`;
|
|
1247
|
+
feedback.textContent = labels[result.result] || "✗ Unknown apply result";
|
|
1248
|
+
return { ok: result.result === "applied" || result.result === "restart_required", status: result.result === "applied" ? 200 : 409, body: result };
|
|
1249
|
+
}
|
|
1250
|
+
|
|
1251
|
+
// Binding changes use a separate verified challenge. They never go through
|
|
1252
|
+
// PUT /fleet/channels: the provider probe proves the target first, and the
|
|
1253
|
+
// apply job updates only group/general coordinates after the replacement
|
|
1254
|
+
// adapter is ready.
|
|
1255
|
+
async function verifyAndApplyConnectionBinding(connectionId, groupId, generalChannelId, feedback) {
|
|
1256
|
+
const key = crypto.randomUUID ? crypto.randomUUID() : `binding-${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
1257
|
+
const binding = { group_id: String(groupId).trim(), general_channel_id: String(generalChannelId || "").trim() || null };
|
|
1258
|
+
feedback.className = "feedback"; feedback.textContent = "Verifying target…";
|
|
1259
|
+
const verified = await api(`/api/settings/connections/${encodeURIComponent(connectionId)}/binding/verify`, {
|
|
1260
|
+
method: "POST", headers: { "Idempotency-Key": key }, body: JSON.stringify({ ...binding, idempotency_key: key }),
|
|
1261
|
+
});
|
|
1262
|
+
if (!verified.ok) { feedback.className = "feedback error"; feedback.textContent = `✗ ${verified.body?.error || "binding verification failed"}`; return verified; }
|
|
1263
|
+
const probe = verified.body?.probe || {};
|
|
1264
|
+
const target = [probe.group_name || binding.group_id, probe.channel_name || probe.channel_id].filter(Boolean).join(" / ");
|
|
1265
|
+
const perms = [probe.can_view ? "view" : null, probe.can_send ? "send" : null, probe.can_manage_topics ? "topics" : null].filter(Boolean).join(", ");
|
|
1266
|
+
feedback.textContent = `✓ Verified ${target}${perms ? ` (${perms})` : ""}`;
|
|
1267
|
+
if (!window.confirm(`Rebind this connection to ${target || binding.group_id}?`)) {
|
|
1268
|
+
feedback.textContent = "Verified; not applied.";
|
|
1269
|
+
return { ok: false, status: 409, body: { result: "verified" } };
|
|
1270
|
+
}
|
|
1271
|
+
feedback.textContent = "Applying binding…";
|
|
1272
|
+
const applied = await api(`/api/settings/connections/${encodeURIComponent(connectionId)}/binding/apply`, {
|
|
1273
|
+
method: "POST", headers: { "Idempotency-Key": key }, body: JSON.stringify({ verification_id: verified.body?.verification_id, idempotency_key: key }),
|
|
1274
|
+
});
|
|
1275
|
+
if (!applied.ok && applied.status !== 409) { feedback.className = "feedback error"; feedback.textContent = `✗ ${applied.body?.error || "binding apply failed"}`; return applied; }
|
|
1276
|
+
let result = applied.body || {};
|
|
1277
|
+
if (result.job_id && result.result === "applying") {
|
|
1278
|
+
for (let n = 0; n < 60; n++) {
|
|
1279
|
+
await new Promise(resolve => setTimeout(resolve, 250));
|
|
1280
|
+
const status = await api(`/api/settings/connections/${encodeURIComponent(connectionId)}/binding/apply/${encodeURIComponent(result.job_id)}`);
|
|
1281
|
+
if (status.ok) { result = status.body || result; if (result.result !== "applying") break; }
|
|
1282
|
+
}
|
|
1283
|
+
}
|
|
1284
|
+
const labels = { applied: "✓ Binding applied", rolled_back: "↩ Binding rolled back", rollback_failed: "✗ Binding rollback failed", applying: "…Still applying — check status" };
|
|
1285
|
+
const ok = result.result === "applied";
|
|
1286
|
+
feedback.className = `feedback ${ok ? "" : "error"}`;
|
|
1287
|
+
feedback.textContent = labels[result.result] || "✗ Unknown binding result";
|
|
1288
|
+
if (ok) await reload();
|
|
1289
|
+
return { ok, status: ok ? 200 : 409, body: result };
|
|
1290
|
+
}
|
|
1291
|
+
|
|
691
1292
|
function botEditForm(ch, i) {
|
|
692
1293
|
ch.access = ch.access || { mode: "locked", allowed_users: [] };
|
|
693
1294
|
ch.access.allowed_users = ch.access.allowed_users || [];
|
|
694
1295
|
const previousAccess = structuredClone(ch.access);
|
|
1296
|
+
const type = ch.type || "telegram";
|
|
695
1297
|
const box = el("div", { class: "edit" });
|
|
696
1298
|
const fMode = select(ch.access.mode || "locked", ["open", "locked", "pairing"]);
|
|
697
1299
|
const users = [...ch.access.allowed_users];
|
|
698
|
-
const msg = el("span", { class: "msg" });
|
|
699
1300
|
const accessFeedback = el("div", { class: "feedback" });
|
|
1301
|
+
const tokenInput = el("input", { type: "password", autocomplete: "new-password", placeholder: "Paste a new bot token to rotate it" });
|
|
1302
|
+
const secretFeedback = el("div", { class: "feedback" });
|
|
1303
|
+
const bindingFeedback = el("div", { class: "feedback" });
|
|
1304
|
+
const fGroup = el("input", { type: "text", value: ch.group_id != null ? String(ch.group_id) : "", autocomplete: "off" });
|
|
1305
|
+
const fGeneral = el("input", { type: "text", value: ch.options?.general_channel_id != null ? String(ch.options.general_channel_id) : "", autocomplete: "off", placeholder: type === "discord" ? "General channel id (optional)" : "Forum topic id (optional)" });
|
|
1306
|
+
const bindingVerify = el("button", { class: "sm primary", onclick: async () => {
|
|
1307
|
+
if (!fGroup.value.trim()) { bindingFeedback.className = "feedback error"; bindingFeedback.textContent = "✗ group/guild id required"; return; }
|
|
1308
|
+
await verifyAndApplyConnectionBinding(connectionId, fGroup.value, fGeneral.value, bindingFeedback);
|
|
1309
|
+
} }, "Verify new binding → Confirm rebind");
|
|
1310
|
+
const connectionId = ch.id || ch.type || chLabel(i);
|
|
1311
|
+
let pendingSecret = "";
|
|
1312
|
+
const stageSecret = () => {
|
|
1313
|
+
const secret = tokenInput.value;
|
|
1314
|
+
if (!secret) { secretFeedback.className = "feedback error"; secretFeedback.textContent = "✗ token required"; return; }
|
|
1315
|
+
pendingSecret = secret;
|
|
1316
|
+
tokenInput.value = "";
|
|
1317
|
+
stageChange(`secret:${connectionId}`, {
|
|
1318
|
+
// Secret rotation is an immediate adapter operation. Use the server's
|
|
1319
|
+
// published impact ordering rather than duplicating an impact literal
|
|
1320
|
+
// in this page (hot/cold parity guards reject such copies).
|
|
1321
|
+
label: `Rotate ${type} token`, impact: state.schema.order[0],
|
|
1322
|
+
apply: async () => {
|
|
1323
|
+
const value = pendingSecret; pendingSecret = "";
|
|
1324
|
+
try { return await applyConnectionSecret(connectionId, value, secretFeedback); }
|
|
1325
|
+
finally { pendingSecret = ""; }
|
|
1326
|
+
},
|
|
1327
|
+
cleanup: () => { pendingSecret = ""; },
|
|
1328
|
+
});
|
|
1329
|
+
secretFeedback.className = "feedback"; secretFeedback.textContent = "Queued — click Apply changes.";
|
|
1330
|
+
};
|
|
700
1331
|
const validateAccess = () => setValidation(fMode, accessFeedback, "", fMode.value === "locked" && users.length === 0 ? t("accessLockedEmpty") : "");
|
|
701
1332
|
fMode.addEventListener("change", validateAccess);
|
|
702
1333
|
box.append(
|
|
703
1334
|
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
|
-
el("div", { class: "
|
|
707
|
-
el("
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
}
|
|
717
|
-
|
|
718
|
-
el("
|
|
1335
|
+
el("div", {}, el("label", {}, t("accessMode"), impact("fleet.channel.access.mode")), fMode, accessFeedback),
|
|
1336
|
+
el("div", {}, el("label", {}, t("allowedUsers"), impact("fleet.channel.access.allowed_users")), chipList(users, validateAccess, "user id"))),
|
|
1337
|
+
el("div", { class: "field" },
|
|
1338
|
+
el("label", {}, type === "discord" ? t("guildIdField") : t("groupIdField"), impact("fleet.channels")),
|
|
1339
|
+
fGroup,
|
|
1340
|
+
el("label", {}, type === "discord" ? "General channel id" : "Forum topic id"), fGeneral,
|
|
1341
|
+
el("div", { class: "hint" }, "The target is checked with the existing bot token before any binding is changed."),
|
|
1342
|
+
el("div", { class: "actions" }, bindingVerify, bindingFeedback)),
|
|
1343
|
+
drawer(t("advancedSection"),
|
|
1344
|
+
el("div", { class: "field" }, el("label", {}, t("tokenEnv")), el("div", { class: "sub" }, ch.bot_token_env || "(no token env)")),
|
|
1345
|
+
el("div", { class: "field" },
|
|
1346
|
+
el("label", {}, "Bot token (never stored in the browser)"), tokenInput,
|
|
1347
|
+
el("div", { class: "hint" }, "The token is verified by AgEnD, then applied by replacing the running adapter. It is never echoed."),
|
|
1348
|
+
el("div", { class: "actions" }, el("button", { class: "sm primary", onclick: stageSecret }, "Verify & apply token"), secretFeedback)),
|
|
1349
|
+
el("div", { class: "actions" },
|
|
1350
|
+
el("button", { class: "sm danger", onclick: () => { closeModal(); delBot(i); } }, t("deleteBot")))),
|
|
719
1351
|
);
|
|
720
1352
|
validateAccess();
|
|
721
|
-
return
|
|
1353
|
+
return {
|
|
1354
|
+
box,
|
|
1355
|
+
impacts: ["fleet.channel.access.mode", "fleet.channel.access.allowed_users", "fleet.channels"],
|
|
1356
|
+
// Staged, not saved: the old form wrote channels straight to the API from
|
|
1357
|
+
// inside the editor, which is the one thing a modal must not do.
|
|
1358
|
+
stage: () => {
|
|
1359
|
+
const originalChannels = structuredClone(channels());
|
|
1360
|
+
const stagedAccessMode = fMode.value;
|
|
1361
|
+
const stagedAllowedUsers = [...users];
|
|
1362
|
+
const nextChannels = structuredClone(originalChannels);
|
|
1363
|
+
nextChannels[i] = { ...nextChannels[i], access: { ...nextChannels[i].access, mode: stagedAccessMode, allowed_users: stagedAllowedUsers } };
|
|
1364
|
+
if (sameValue(nextChannels, originalChannels)) return true;
|
|
1365
|
+
stageChange(`connections:${ch.id || chLabel(i)}`, {
|
|
1366
|
+
label: `Update ${ch.id || chLabel(i)} access`,
|
|
1367
|
+
impact: impactOf("fleet.channels"),
|
|
1368
|
+
// Confirm the immutable staged snapshot, not live form controls
|
|
1369
|
+
// that the user may edit again before pressing global Apply.
|
|
1370
|
+
confirm: () => confirmAccessChange(previousAccess, stagedAccessMode, stagedAllowedUsers),
|
|
1371
|
+
cancelMessage: t("accessChangeCancelled"),
|
|
1372
|
+
apply: () => api("/api/settings/fleet/channels", { method: "PUT", body: JSON.stringify(nextChannels) }),
|
|
1373
|
+
});
|
|
1374
|
+
return true;
|
|
1375
|
+
},
|
|
1376
|
+
};
|
|
722
1377
|
}
|
|
723
1378
|
|
|
724
1379
|
// ── ClassicBot Channels ──
|
|
725
|
-
let editingClassic = null;
|
|
726
1380
|
function classicRuntime(instanceName) {
|
|
727
1381
|
const live = state.classicLive[instanceName] || {};
|
|
728
1382
|
const status = live.status || state.classicStatus[instanceName] || "stopped";
|
|
@@ -741,11 +1395,11 @@
|
|
|
741
1395
|
const runtime = classicRuntime(c.instanceName);
|
|
742
1396
|
const fBackend = select(c.backend || inheritedBackend, BACKENDS.filter(value => value !== "gemini-cli"));
|
|
743
1397
|
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, "
|
|
1398
|
+
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");
|
|
1399
|
+
const toolProgress = overrideControl(c, "tool_progress", select(c.tool_progress ?? inheritedToolProgress, ["off", "standard", "verbose"]), inheritedToolProgress, "classic.tool_progress");
|
|
746
1400
|
const fReplyGuard = el("input", { type: "checkbox" });
|
|
747
1401
|
fReplyGuard.checked = c.reply_completion_guard ?? inheritedReplyGuard;
|
|
748
|
-
const replyGuard = overrideControl(c, "reply_completion_guard", fReplyGuard, inheritedReplyGuard, "
|
|
1402
|
+
const replyGuard = overrideControl(c, "reply_completion_guard", fReplyGuard, inheritedReplyGuard, "classic.reply_completion_guard");
|
|
749
1403
|
const classicMode = fleetDefaults.agent_mode ?? "mcp";
|
|
750
1404
|
const replyHint = replyGuardHint(fBackend.value, classicMode);
|
|
751
1405
|
fBackend.addEventListener("change", () => {
|
|
@@ -756,7 +1410,10 @@
|
|
|
756
1410
|
const fContext = el("input", { type: "number", min: "0", step: "1", value: c.context_lines ?? classicDefaults.context_lines ?? 5 });
|
|
757
1411
|
const autoFeedback = el("div", { class: "feedback" });
|
|
758
1412
|
const contextFeedback = el("div", { class: "feedback" });
|
|
759
|
-
|
|
1413
|
+
// Never rendered: it carries the validation state and the submit handler
|
|
1414
|
+
// that the modal footer triggers through stage(). The form has no button
|
|
1415
|
+
// of its own, so nothing here can write on its own.
|
|
1416
|
+
const saveBtn = el("button", { hidden: "hidden" });
|
|
760
1417
|
const validate = () => {
|
|
761
1418
|
const autoValue = Number(auto.input.value);
|
|
762
1419
|
const contextValue = Number(fContext.value);
|
|
@@ -788,40 +1445,45 @@
|
|
|
788
1445
|
collab: !!c.collab,
|
|
789
1446
|
context_lines: c.context_lines ?? classicDefaults.context_lines ?? 5,
|
|
790
1447
|
});
|
|
791
|
-
if (Object.keys(patch).length === 0) {
|
|
1448
|
+
if (Object.keys(patch).length === 0) { renderClassic(); return; }
|
|
792
1449
|
state.classic.channels[c.key] = { ...state.classic.channels[c.key], ...patch };
|
|
793
1450
|
if (patch.model === null) delete state.classic.channels[c.key].model;
|
|
794
1451
|
if (patch.auto_pause_after === null) delete state.classic.channels[c.key].auto_pause_after;
|
|
795
1452
|
if (patch.tool_progress === null) delete state.classic.channels[c.key].tool_progress;
|
|
796
1453
|
if (patch.reply_completion_guard === null) delete state.classic.channels[c.key].reply_completion_guard;
|
|
797
|
-
const hotOnly = Object.keys(patch).every(key => key === "
|
|
1454
|
+
const hotOnly = Object.keys(patch).every(key => impactOf(`classic.${key}`) === "now");
|
|
798
1455
|
stageChange(`classic:${c.key}`, {
|
|
799
1456
|
label: `Update ${c.name || c.instanceName}`,
|
|
800
1457
|
impact: hotOnly ? "now" : "instance",
|
|
801
1458
|
apply: () => api(`/api/settings/classic/channels/${encodeURIComponent(c.key)}`, { method: "PATCH", body: JSON.stringify(patch) }),
|
|
802
1459
|
});
|
|
803
|
-
editingClassic = null;
|
|
804
1460
|
renderClassic();
|
|
805
1461
|
};
|
|
806
1462
|
box.append(
|
|
807
1463
|
el("div", { class: "sub", style: "margin:0 0 8px" }, `ID: ${c.instanceName} · ${EXECUTION_TEXT[runtime.execution] || runtime.execution}`),
|
|
808
1464
|
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"))),
|
|
1465
|
+
el("div", {}, el("label", {}, t("backend"), impact("classic.backend")), fBackend),
|
|
1466
|
+
el("div", {}, el("label", {}, t("model"), impact("classic.model")), fModel),
|
|
1467
|
+
el("div", {}, el("label", {}, `${t("autoPause")} (${t("minutes")})`, impact("classic.auto_pause_after")), auto.row, autoFeedback),
|
|
1468
|
+
el("div", {}, el("label", {}, t("toolProgress"), impact("classic.tool_progress")), toolProgress.row),
|
|
1469
|
+
el("div", {}, el("label", {}, t("replyCompletionGuard"), impact("classic.reply_completion_guard")), replyGuard.row, replyHint),
|
|
1470
|
+
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")))),
|
|
1471
|
+
drawer(t("advancedSection"),
|
|
1472
|
+
el("label", {}, "Context lines", impact("classic.context_lines")), fContext, contextFeedback,
|
|
1473
|
+
el("div", { class: "actions" },
|
|
1474
|
+
runtime.status === "running" ? el("button", { class: "sm", onclick: () => { closeModal(); pauseWakeAgent(c.instanceName, "pause"); } }, t("pause")) : null,
|
|
1475
|
+
runtime.status === "paused" ? el("button", { class: "sm", onclick: () => { closeModal(); pauseWakeAgent(c.instanceName, "wake"); } }, t("wake")) : null)),
|
|
822
1476
|
);
|
|
823
1477
|
validate();
|
|
824
|
-
return
|
|
1478
|
+
return {
|
|
1479
|
+
box,
|
|
1480
|
+
stage: () => { if (saveBtn.disabled) return false; saveBtn.onclick(); return true; },
|
|
1481
|
+
impacts: [
|
|
1482
|
+
"classic.backend", "classic.model", "classic.auto_pause_after",
|
|
1483
|
+
"classic.tool_progress", "classic.reply_completion_guard",
|
|
1484
|
+
"classic.collab", "classic.context_lines",
|
|
1485
|
+
],
|
|
1486
|
+
};
|
|
825
1487
|
}
|
|
826
1488
|
function renderClassic() {
|
|
827
1489
|
const host = $("classicList"); if (!host) return; host.innerHTML = "";
|
|
@@ -853,11 +1515,13 @@
|
|
|
853
1515
|
c.collab ? el("span", { class: "tag", style: "background:#ecfdf5;color:#065f46" }, "collab") : null,
|
|
854
1516
|
el("span", { class: "spacer" }),
|
|
855
1517
|
el("span", { class: "tag", title: STATUS_TEXT[st] || st }, EXECUTION_TEXT[runtime.execution] || runtime.execution),
|
|
856
|
-
el("button", { class: "sm ghost", onclick: () => {
|
|
1518
|
+
el("button", { class: "sm ghost", onclick: () => {
|
|
1519
|
+
const form = classicEditForm(c);
|
|
1520
|
+
openModal({ title: c.name || c.instanceName, subtitle: c.instanceName, body: form.box, impacts: form.impacts, stage: form.stage });
|
|
1521
|
+
} }, t("settingsButton")),
|
|
857
1522
|
st === "running" ? el("button", { class: "sm", onclick: () => pauseWakeAgent(c.instanceName, "pause") }, t("pause")) : null,
|
|
858
1523
|
st === "paused" ? el("button", { class: "sm", onclick: () => pauseWakeAgent(c.instanceName, "wake") }, t("wake")) : null,
|
|
859
1524
|
));
|
|
860
|
-
if (editingClassic === c.key) item.append(classicEditForm(c));
|
|
861
1525
|
host.append(item);
|
|
862
1526
|
}
|
|
863
1527
|
}
|
|
@@ -922,9 +1586,9 @@
|
|
|
922
1586
|
const fToolSet = select(d.tool_set || "full", ["full", "standard", "minimal"]);
|
|
923
1587
|
const fToolProgress = select(d.tool_progress ?? "off", ["off", "standard", "verbose"]);
|
|
924
1588
|
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", "
|
|
1589
|
+
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
1590
|
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, "
|
|
1591
|
+
const fClassicReplyGuard = overrideControl(c, "reply_completion_guard", fClassicReplyInput, d.reply_completion_guard ?? true, "classic_defaults.reply_completion_guard");
|
|
928
1592
|
const fAccessMode = select(originalPrimaryAccess.mode || "locked", ["locked", "open", "pairing"]);
|
|
929
1593
|
const fLogLevel = select(d.log_level || "info", ["trace", "debug", "info", "warn", "error"]);
|
|
930
1594
|
const fLocale = el("select");
|
|
@@ -1016,27 +1680,10 @@
|
|
|
1016
1680
|
state.classic.defaults = { ...c, ...classicPatch };
|
|
1017
1681
|
for (const [key, value] of Object.entries(classicPatch)) if (value === null) delete state.classic.defaults[key];
|
|
1018
1682
|
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
|
-
}
|
|
1683
|
+
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
1684
|
}
|
|
1038
1685
|
if (Object.keys(classicPatch).length > 0) {
|
|
1039
|
-
const classicHotOnly = Object.keys(classicPatch).every(key => key === "
|
|
1686
|
+
const classicHotOnly = Object.keys(classicPatch).every(key => impactOf(`classic_defaults.${key}`) === "now");
|
|
1040
1687
|
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
1688
|
}
|
|
1042
1689
|
msg.innerHTML = `<span class="s">${esc(t("readyApply"))}</span>`;
|
|
@@ -1050,40 +1697,40 @@
|
|
|
1050
1697
|
host.append(
|
|
1051
1698
|
section("Defaults", true,
|
|
1052
1699
|
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("
|
|
1700
|
+
el("div", { class: "field" }, el("label", {}, t("language"), impact("defaults.locale")), fLocale),
|
|
1701
|
+
el("div", { class: "field" }, el("label", {}, t("defaultBackend"), impact("defaults.backend")), fBackend),
|
|
1702
|
+
el("div", { class: "field" }, el("label", {}, t("defaultModel"), impact("defaults.model")), fModel),
|
|
1703
|
+
el("div", { class: "field" }, el("label", {}, `${t("autoPause")} (${t("minutes")})`, impact("defaults.auto_pause_after")), fAutoPause, autoFeedback),
|
|
1704
|
+
el("div", { class: "field" }, el("label", {}, t("toolProgress"), impact("defaults.tool_progress")), fToolProgress),
|
|
1705
|
+
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
1706
|
section("ClassicBot", true,
|
|
1060
1707
|
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("
|
|
1708
|
+
el("div", { class: "field" }, el("label", {}, "Classic default backend", impact("classic_defaults.backend")), fClassicBackend),
|
|
1709
|
+
el("div", { class: "field" }, el("label", {}, t("toolProgress"), impact("classic_defaults.tool_progress")), fClassicToolProgress.row),
|
|
1710
|
+
el("div", { class: "field" }, el("label", {}, t("replyCompletionGuard"), impact("classic_defaults.reply_completion_guard")), fClassicReplyGuard.row, classicReplyHint)),
|
|
1064
1711
|
el("div", { class: "field" }, el("strong", {}, "Who can use ClassicBot"),
|
|
1065
1712
|
el("div", { class: "grid2" },
|
|
1066
|
-
el("div", {}, el("label", {}, t("classicAdmins"), impact("
|
|
1067
|
-
el("div", {}, el("label", {}, t("allowedGuilds"), impact("
|
|
1068
|
-
|
|
1713
|
+
el("div", {}, el("label", {}, t("classicAdmins"), impact("classic.admin_users")), chipList(c.admin_users, null, "user id")),
|
|
1714
|
+
el("div", {}, el("label", {}, t("allowedGuilds"), impact("classic.allowed_guilds")), chipList(c.allowed_guilds, null, "guild id"))))),
|
|
1715
|
+
drawer(t("advancedSection"),
|
|
1069
1716
|
section(t("runtimeResources"), false,
|
|
1070
1717
|
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("
|
|
1718
|
+
el("div", { class: "field" }, el("label", {}, t("toolSet"), impact("defaults.tool_set")), fToolSet),
|
|
1719
|
+
el("div", { class: "field" }, el("label", {}, t("logLevel"), impact("defaults.log_level")), fLogLevel),
|
|
1720
|
+
el("div", { class: "field" }, el("label", {}, t("agentMode"), impact("defaults.agent_mode")), fAgentMode))),
|
|
1074
1721
|
section(t("detection"), false,
|
|
1075
1722
|
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("
|
|
1723
|
+
el("div", { class: "field" }, el("label", { style: "display:flex;align-items:center;gap:8px" }, fHangEnabled, el("span", {}, t("hangEnabled")), impact("defaults.hang_detector"))),
|
|
1724
|
+
el("div", { class: "field" }, el("label", {}, `${t("hangTimeout")} (${t("minutes")})`, impact("defaults.hang_detector")), fHangTimeout, hangFeedback))),
|
|
1078
1725
|
section(t("startup"), false,
|
|
1079
1726
|
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("
|
|
1727
|
+
el("div", { class: "field" }, el("label", {}, t("concurrency"), impact("fleet.spawn_concurrency")), fConcurrency, concurrencyFeedback),
|
|
1728
|
+
el("div", { class: "field" }, el("label", {}, `${t("staggerDelay")} (ms)`, impact("fleet.spawn_stagger_ms")), fStagger, staggerFeedback))),
|
|
1729
|
+
section("Access", false,
|
|
1730
|
+
// Access belongs to a connection, and one modal edits one object — so
|
|
1731
|
+
// it is edited there, and this is only a pointer to it.
|
|
1083
1732
|
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")))
|
|
1733
|
+
? el("div", { class: "field hint" }, tf("accessMovedHint", primary.id || chLabel(0)))
|
|
1087
1734
|
: el("div", { class: "field hint" }, t("noBots")))),
|
|
1088
1735
|
el("div", { class: "field" }, el("div", { class: "actions" }, saveBtn, msg)),
|
|
1089
1736
|
);
|
|
@@ -1151,7 +1798,7 @@
|
|
|
1151
1798
|
}
|
|
1152
1799
|
|
|
1153
1800
|
// ── Load / render ──
|
|
1154
|
-
function renderAll() { renderAgents(); renderClassic(); renderBots(); renderGeneral(); renderAdvanced(); applyStaticI18n(); }
|
|
1801
|
+
function renderAll() { renderAgents(); renderClassic(); renderBots(); renderProviderSecrets(); renderGeneral(); renderAdvanced(); applyStaticI18n(); }
|
|
1155
1802
|
async function reloadLive() {
|
|
1156
1803
|
const f = await api("/api/fleet");
|
|
1157
1804
|
state.fleetUp = f.ok;
|
|
@@ -1177,10 +1824,18 @@
|
|
|
1177
1824
|
}
|
|
1178
1825
|
async function reload() {
|
|
1179
1826
|
// Forms edit the user-authored config, not defaults-expanded runtime values.
|
|
1827
|
+
const schema = await api("/api/settings/schema");
|
|
1828
|
+
if (schema.ok && schema.body) state.schema = schema.body;
|
|
1180
1829
|
const f = await api("/api/settings/fleet/raw");
|
|
1181
1830
|
if (!f.ok) { $("banner").style.display = "block"; $("banner").style.background = "#fef2f2"; $("banner").style.color = "var(--red)"; $("banner").textContent = t("configLoadFailed"); return; }
|
|
1182
1831
|
state.fleet = f.body || {};
|
|
1183
1832
|
const c = await api("/api/settings/classic"); state.classic = c.body || {};
|
|
1833
|
+
const connections = await api("/api/settings/connections");
|
|
1834
|
+
state.connections = connections.ok && Array.isArray(connections.body) ? connections.body : [];
|
|
1835
|
+
const providerSecrets = await api("/api/settings/provider-secrets");
|
|
1836
|
+
const providerHost = $("providerSecrets");
|
|
1837
|
+
if (providerHost) providerHost.style.display = providerSecrets.ok ? "" : "none";
|
|
1838
|
+
state.providerSecrets = providerSecrets.ok && Array.isArray(providerSecrets.body) ? providerSecrets.body : [];
|
|
1184
1839
|
await reloadLive();
|
|
1185
1840
|
renderAll();
|
|
1186
1841
|
}
|
|
@@ -1191,21 +1846,18 @@
|
|
|
1191
1846
|
$("helpBackdrop").onclick = () => setHelp(false);
|
|
1192
1847
|
$("langBtn").onclick = () => { lang = lang === "en" ? "zh-TW" : "en"; localStorage.setItem("agend_lang", lang); renderAll(); };
|
|
1193
1848
|
$("verBadge").onclick = () => window.open("https://github.com/songsid/AgEnD/releases/latest", "_blank", "noopener");
|
|
1194
|
-
$("agentSearch").oninput = () => { renderAgents(); renderClassic(); };
|
|
1849
|
+
$("agentSearch").oninput = () => { renderAgents(); renderClassic(); renderBots(); };
|
|
1195
1850
|
$("newAgentBtn").onclick = () => { state.newAgentOpen = !state.newAgentOpen; $("newAgentBtn").textContent = state.newAgentOpen ? t("cancel") : t("new"); renderNewAgent(state.newAgentOpen); };
|
|
1196
1851
|
$("newBotBtn").onclick = () => { state.newBotOpen = !state.newBotOpen; $("newBotBtn").textContent = state.newBotOpen ? t("cancel") : t("add"); renderNewBot(state.newBotOpen); };
|
|
1197
1852
|
$("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
|
-
};
|
|
1853
|
+
// Closing stages; cancelling discards. Neither writes anything.
|
|
1854
|
+
$("modalDone").onclick = () => { if (modalState?.stage() !== false) closeModal(); };
|
|
1855
|
+
$("modalCancel").onclick = () => closeModal();
|
|
1856
|
+
$("modalBackdrop").onclick = () => closeModal();
|
|
1857
|
+
document.addEventListener("keydown", e => { if (e.key === "Escape" && modalState) closeModal(); });
|
|
1858
|
+
$("wizardBtn").onclick = () => openWizard();
|
|
1207
1859
|
$("applyChanges").onclick = applyPendingChanges;
|
|
1208
|
-
$("discardChanges").onclick = async () => { state.pending.clear(); updatePendingBar(); await reload(); };
|
|
1860
|
+
$("discardChanges").onclick = async () => { for (const change of state.pending.values()) change.cleanup?.(); state.pending.clear(); updatePendingBar(); await reload(); };
|
|
1209
1861
|
|
|
1210
1862
|
reload();
|
|
1211
1863
|
})();
|