@songsid/agend 2.1.4-beta.9 → 2.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (166) hide show
  1. package/README.md +1 -1
  2. package/README.zh-TW.md +1 -1
  3. package/dist/agent-cli-instructions.md +2 -2
  4. package/dist/agent-endpoint.js +1 -1
  5. package/dist/agent-endpoint.js.map +1 -1
  6. package/dist/backend/antigravity.d.ts +12 -8
  7. package/dist/backend/antigravity.js +189 -42
  8. package/dist/backend/antigravity.js.map +1 -1
  9. package/dist/backend/claude-code.d.ts +87 -23
  10. package/dist/backend/claude-code.js +361 -46
  11. package/dist/backend/claude-code.js.map +1 -1
  12. package/dist/backend/codex.d.ts +18 -0
  13. package/dist/backend/codex.js +139 -5
  14. package/dist/backend/codex.js.map +1 -1
  15. package/dist/backend/gemini-cli.js +2 -2
  16. package/dist/backend/gemini-cli.js.map +1 -1
  17. package/dist/backend/grok.js +2 -2
  18. package/dist/backend/grok.js.map +1 -1
  19. package/dist/backend/kiro.d.ts +52 -2
  20. package/dist/backend/kiro.js +267 -12
  21. package/dist/backend/kiro.js.map +1 -1
  22. package/dist/backend/opencode.js +17 -4
  23. package/dist/backend/opencode.js.map +1 -1
  24. package/dist/backend/types.d.ts +122 -3
  25. package/dist/backend/types.js +5 -1
  26. package/dist/backend/types.js.map +1 -1
  27. package/dist/backend-outage.d.ts +61 -0
  28. package/dist/backend-outage.js +71 -0
  29. package/dist/backend-outage.js.map +1 -0
  30. package/dist/channel/adapters/discord.d.ts +44 -2
  31. package/dist/channel/adapters/discord.js +523 -90
  32. package/dist/channel/adapters/discord.js.map +1 -1
  33. package/dist/channel/adapters/telegram.d.ts +30 -0
  34. package/dist/channel/adapters/telegram.js +221 -19
  35. package/dist/channel/adapters/telegram.js.map +1 -1
  36. package/dist/channel/agy-mcp-launcher.d.ts +2 -0
  37. package/dist/channel/agy-mcp-launcher.js +28 -0
  38. package/dist/channel/agy-mcp-launcher.js.map +1 -0
  39. package/dist/channel/ipc-bridge.d.ts +2 -1
  40. package/dist/channel/ipc-bridge.js +33 -4
  41. package/dist/channel/ipc-bridge.js.map +1 -1
  42. package/dist/channel/markdown-chunk.d.ts +42 -0
  43. package/dist/channel/markdown-chunk.js +205 -0
  44. package/dist/channel/markdown-chunk.js.map +1 -0
  45. package/dist/channel/mcp-server.js +34 -5
  46. package/dist/channel/mcp-server.js.map +1 -1
  47. package/dist/channel/mcp-tools.js +5 -3
  48. package/dist/channel/mcp-tools.js.map +1 -1
  49. package/dist/channel/message-queue.js +64 -8
  50. package/dist/channel/message-queue.js.map +1 -1
  51. package/dist/channel/types.d.ts +31 -1
  52. package/dist/classic-channel-manager.d.ts +65 -0
  53. package/dist/classic-channel-manager.js +172 -3
  54. package/dist/classic-channel-manager.js.map +1 -1
  55. package/dist/cli.js +124 -83
  56. package/dist/cli.js.map +1 -1
  57. package/dist/config-validator.js +13 -0
  58. package/dist/config-validator.js.map +1 -1
  59. package/dist/config.d.ts +4 -0
  60. package/dist/config.js +12 -1
  61. package/dist/config.js.map +1 -1
  62. package/dist/cross-instance-envelope.d.ts +6 -0
  63. package/dist/cross-instance-envelope.js +30 -0
  64. package/dist/cross-instance-envelope.js.map +1 -0
  65. package/dist/daemon.d.ts +491 -14
  66. package/dist/daemon.js +2121 -276
  67. package/dist/daemon.js.map +1 -1
  68. package/dist/doctor.d.ts +41 -0
  69. package/dist/doctor.js +267 -0
  70. package/dist/doctor.js.map +1 -0
  71. package/dist/fleet-context.d.ts +63 -0
  72. package/dist/fleet-context.js.map +1 -1
  73. package/dist/fleet-manager.d.ts +402 -11
  74. package/dist/fleet-manager.js +2594 -422
  75. package/dist/fleet-manager.js.map +1 -1
  76. package/dist/fleet-yaml-slim.d.ts +10 -0
  77. package/dist/fleet-yaml-slim.js +59 -0
  78. package/dist/fleet-yaml-slim.js.map +1 -0
  79. package/dist/general-knowledge/skills/cross-instance-messaging/SKILL.md +1 -0
  80. package/dist/general-knowledge/skills/model-discovery/SKILL.md +63 -16
  81. package/dist/general-knowledge/skills/session-management/SKILL.md +171 -8
  82. package/dist/instance-lifecycle.d.ts +114 -1
  83. package/dist/instance-lifecycle.js +404 -14
  84. package/dist/instance-lifecycle.js.map +1 -1
  85. package/dist/instructions.js +42 -4
  86. package/dist/instructions.js.map +1 -1
  87. package/dist/locale.js +1024 -320
  88. package/dist/locale.js.map +1 -1
  89. package/dist/login-flows.d.ts +91 -0
  90. package/dist/login-flows.js +170 -0
  91. package/dist/login-flows.js.map +1 -0
  92. package/dist/login-manager.d.ts +63 -0
  93. package/dist/login-manager.js +134 -0
  94. package/dist/login-manager.js.map +1 -0
  95. package/dist/network-family.d.ts +18 -0
  96. package/dist/network-family.js +20 -0
  97. package/dist/network-family.js.map +1 -0
  98. package/dist/outbound-handlers.d.ts +16 -1
  99. package/dist/outbound-handlers.js +174 -40
  100. package/dist/outbound-handlers.js.map +1 -1
  101. package/dist/outbound-schemas.d.ts +10 -3
  102. package/dist/outbound-schemas.js +10 -0
  103. package/dist/outbound-schemas.js.map +1 -1
  104. package/dist/pane-input-residue.d.ts +52 -0
  105. package/dist/pane-input-residue.js +107 -0
  106. package/dist/pane-input-residue.js.map +1 -0
  107. package/dist/process-memory.d.ts +6 -0
  108. package/dist/process-memory.js +45 -0
  109. package/dist/process-memory.js.map +1 -1
  110. package/dist/reply-dedup.d.ts +7 -8
  111. package/dist/reply-dedup.js +0 -0
  112. package/dist/reply-dedup.js.map +1 -1
  113. package/dist/restart-progress.d.ts +2 -0
  114. package/dist/restart-progress.js +3 -0
  115. package/dist/restart-progress.js.map +1 -1
  116. package/dist/scheduler/db.d.ts +12 -0
  117. package/dist/scheduler/db.js +59 -0
  118. package/dist/scheduler/db.js.map +1 -1
  119. package/dist/service-installer.d.ts +11 -0
  120. package/dist/service-installer.js +84 -18
  121. package/dist/service-installer.js.map +1 -1
  122. package/dist/settings-api.js +1 -1
  123. package/dist/settings-api.js.map +1 -1
  124. package/dist/setup-wizard.js +2 -2
  125. package/dist/setup-wizard.js.map +1 -1
  126. package/dist/spawn-gate.d.ts +30 -0
  127. package/dist/spawn-gate.js +79 -0
  128. package/dist/spawn-gate.js.map +1 -0
  129. package/dist/steer-capability.d.ts +1 -0
  130. package/dist/steer-capability.js +6 -0
  131. package/dist/steer-capability.js.map +1 -0
  132. package/dist/storm-window.d.ts +83 -0
  133. package/dist/storm-window.js +251 -0
  134. package/dist/storm-window.js.map +1 -0
  135. package/dist/tips.d.ts +44 -0
  136. package/dist/tips.js +355 -0
  137. package/dist/tips.js.map +1 -0
  138. package/dist/tmux-manager.d.ts +16 -0
  139. package/dist/tmux-manager.js +110 -25
  140. package/dist/tmux-manager.js.map +1 -1
  141. package/dist/topic-commands.d.ts +31 -7
  142. package/dist/topic-commands.js +342 -70
  143. package/dist/topic-commands.js.map +1 -1
  144. package/dist/transcript-sources.d.ts +35 -10
  145. package/dist/transcript-sources.js +190 -38
  146. package/dist/transcript-sources.js.map +1 -1
  147. package/dist/types.d.ts +6 -1
  148. package/dist/ui/dashboard.html +55 -32
  149. package/dist/ui/settings.html +200 -60
  150. package/dist/ui/view.html +147 -30
  151. package/dist/usage/format-rich.d.ts +1 -1
  152. package/dist/usage/format-rich.js +28 -24
  153. package/dist/usage/format-rich.js.map +1 -1
  154. package/dist/usage/i18n-keys.d.ts +7 -0
  155. package/dist/usage/i18n-keys.js +34 -0
  156. package/dist/usage/i18n-keys.js.map +1 -0
  157. package/dist/usage/i18n.d.ts +5 -0
  158. package/dist/usage/i18n.js +27 -0
  159. package/dist/usage/i18n.js.map +1 -0
  160. package/dist/usage/providers.d.ts +21 -0
  161. package/dist/usage/providers.js +153 -75
  162. package/dist/usage/providers.js.map +1 -1
  163. package/dist/usage/usage-api.d.ts +11 -3
  164. package/dist/usage/usage-api.js +61 -24
  165. package/dist/usage/usage-api.js.map +1 -1
  166. package/package.json +2 -2
@@ -127,15 +127,15 @@
127
127
  <body>
128
128
  <header>
129
129
  <h1 data-i18n="title">⚙️ AgEnD Settings</h1>
130
- <span id="verBadge" title="What's New" style="font-size:12px;color:var(--muted);cursor:pointer;padding:2px 8px;border:1px solid var(--border);border-radius:12px">v…</span>
130
+ <span id="verBadge" data-i18n-title="whatsNew" title="What's New" style="font-size:12px;color:var(--muted);cursor:pointer;padding:2px 8px;border:1px solid var(--border);border-radius:12px">v…</span>
131
131
  <div class="grow"></div>
132
- <button class="help-btn" id="langBtn" title="Language">🌐</button>
133
- <button class="help-btn" id="helpBtn" title="Help">ℹ️</button>
134
- <button id="advancedToggle" title="Show additional controls">Show advanced</button>
132
+ <button class="help-btn" id="langBtn" data-i18n-title="languageButton" title="Language">🌐</button>
133
+ <button class="help-btn" id="helpBtn" data-i18n-title="helpButton" title="Help">ℹ️</button>
134
+ <button id="advancedToggle" data-i18n-title="showAdditional" title="Show additional controls">Show advanced</button>
135
135
  </header>
136
136
  <div class="help-backdrop" id="helpBackdrop"></div>
137
137
  <aside class="help-panel" id="helpPanel">
138
- <button class="close" id="helpClose" title="Close">×</button>
138
+ <button class="close" id="helpClose" data-i18n-title="close" title="Close">×</button>
139
139
  <div id="helpContent"></div>
140
140
  </aside>
141
141
  <div class="banner" id="banner"></div>
@@ -194,7 +194,7 @@
194
194
  backend: "Backend", model: "Model", workingDir: "Working directory", channelBinding: "Channel binding",
195
195
  description: "Description", systemPrompt: "System prompt", tags: "Tags", generalTopicField: "General topic (fleet dispatcher)",
196
196
  runtimeResources: "Runtime & Resources", autoPause: "Auto-pause after", minutes: "minutes", inheritDefault: "Inherit from defaults",
197
- detection: "Detection", startup: "Startup", backendSection: "Backend", advancedSection: "Advanced & Access", hangEnabled: "Hang detector enabled", hangTimeout: "Stuck timeout", agentMode: "Agent mode", toolSet: "Tool set", logLevel: "Log level", concurrency: "Startup concurrency", staggerDelay: "Stagger delay", lightweight: "Lightweight mode", modelFailover: "Fallback model", displayName: "Display name", pause: "Pause", wake: "Wake", accessMode: "Access mode", affected: "Affected instances",
197
+ detection: "Detection", startup: "Startup", backendSection: "Backend", advancedSection: "Advanced & Access", hangEnabled: "Hang detector enabled", hangTimeout: "Stuck timeout", agentMode: "Agent mode", toolSet: "Tool set", toolProgress: "Tool progress", logLevel: "Log level", concurrency: "Startup concurrency", staggerDelay: "Stagger delay", lightweight: "Lightweight mode", modelFailover: "Fallback model", displayName: "Display name", pause: "Pause", wake: "Wake", accessMode: "Access mode", affected: "Affected instances",
198
198
  language: "Language (bot replies)", defaultBackend: "Default backend", defaultModel: "Default model", allowedUsers: "Allowed users (primary bot)",
199
199
  classicAdmins: "ClassicBot admins", allowedGuilds: "Allowed guilds", type: "Type", adapterId: "Adapter id",
200
200
  tokenEnv: "Bot token env var", groupId: "Group / guild id", nameField: "Name",
@@ -202,7 +202,11 @@
202
202
  noAgents: "No agents yet — click + New.", noBots: "No bots configured.", online: "✅ Online",
203
203
  currentVersion: "Current version",
204
204
  helpTitle: "📖 Help", tipReload: "💡 Click [↻ Reload Fleet] after changes to apply them.",
205
- wnIntro: "Recent highlights:", releaseNotes: "📄 View full Release Notes →",
205
+ wnIntro: "Recent highlights:", releaseNotes: "📄 View full Release Notes →", languageButton: "Language", helpButton: "Help", showAdditional: "Show additional controls", hideAdvanced: "Hide advanced", showAdvanced: "Show advanced",
206
+ 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
+ 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 — check the token in the URL.",
208
+ 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
+ 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",
206
210
  },
207
211
  "zh-TW": {
208
212
  title: "⚙️ AgEnD 設定", reloadFleet: "↻ 重新載入", myAgents: "🤖 我的 Agent", bots: "🔌 機器人", classicChannels: "🎮 ClassicBot 頻道", noClassic: "沒有 ClassicBot 頻道 — 在聊天中用 /start 建立。",
@@ -212,7 +216,7 @@
212
216
  backend: "後端", model: "模型", workingDir: "工作目錄", channelBinding: "頻道綁定",
213
217
  description: "描述", systemPrompt: "System prompt", tags: "標籤", generalTopicField: "General topic(fleet 分派器)",
214
218
  runtimeResources: "執行環境與資源", autoPause: "閒置後自動暫停", minutes: "分鐘", inheritDefault: "沿用全域預設值",
215
- detection: "偵測", startup: "啟動", backendSection: "後端", advancedSection: "進階與存取", hangEnabled: "啟用卡住偵測", hangTimeout: "卡住逾時", agentMode: "Agent 模式", toolSet: "工具集", logLevel: "Log 等級", concurrency: "啟動併發數", staggerDelay: "錯開延遲", lightweight: "輕量模式", modelFailover: "備援模型", displayName: "顯示名稱", pause: "暫停", wake: "喚醒", accessMode: "存取模式", affected: "受影響 instances",
219
+ detection: "偵測", startup: "啟動", backendSection: "後端", advancedSection: "進階與存取", hangEnabled: "啟用卡住偵測", hangTimeout: "卡住逾時", agentMode: "Agent 模式", toolSet: "工具集", toolProgress: "工具進度", logLevel: "Log 等級", concurrency: "啟動併發數", staggerDelay: "錯開延遲", lightweight: "輕量模式", modelFailover: "備援模型", displayName: "顯示名稱", pause: "暫停", wake: "喚醒", accessMode: "存取模式", affected: "受影響 instances",
216
220
  language: "語言(bot 回覆)", defaultBackend: "預設後端", defaultModel: "預設模型", allowedUsers: "允許使用者(主 bot)",
217
221
  classicAdmins: "ClassicBot 管理員", allowedGuilds: "允許的伺服器", type: "類型", adapterId: "Adapter id",
218
222
  tokenEnv: "Bot token 環境變數", groupId: "群組 / guild id", nameField: "名稱",
@@ -220,7 +224,11 @@
220
224
  noAgents: "還沒有 agent — 點 + 新增。", noBots: "尚未設定機器人。", online: "✅ 上線",
221
225
  currentVersion: "目前版本",
222
226
  helpTitle: "📖 使用說明", tipReload: "💡 修改後點 [↻ 重新載入] 讓設定生效。",
223
- wnIntro: "近期更新重點:", releaseNotes: "📄 查看完整 Release Notes →",
227
+ wnIntro: "近期更新重點:", releaseNotes: "📄 查看完整 Release Notes →", languageButton: "語言", helpButton: "使用說明", showAdditional: "顯示更多控制項", hideAdvanced: "隱藏進階設定", showAdvanced: "顯示進階設定",
228
+ saved: "已儲存", failed: "失敗", actionFailed: "{0}失敗", noAgentMatch: "沒有符合「{0}」的 Agent。", deleteAgent: "確定刪除 Agent「{0}」嗎?只有在確認套用待處理變更後才會生效。", nameRequired: "必須填寫名稱", tokenEnvRequired: "必須填寫 token 環境變數", removeBot: "確定移除機器人「{0}」嗎?(.env 中的 token 會保留)",
229
+ changesPending: "有 {0} 項變更尚未套用", impactNow: "⚡ 立即生效", impactAgent: "🔄 重新啟動此 Agent", impactFleet: "🔄🔄 重新啟動 AgEnD", queued: "已加入待套用:{0}", applyFailed: "套用失敗", changesApplied: "✓ 變更已套用,AgEnD 正在協調受影響的 Agent。", reviewChanges: "檢查變更", readyApply: "可套用變更", copied: "已複製", expectedMapping: "預期為 mapping", configLoadFailed: "載入設定失敗 — 請檢查網址中的 token。",
230
+ confirmOpenAccess: "Open 模式會允許此頻道中的所有人操作 bot,確定要繼續嗎?", confirmLockedEmpty: "Locked 模式目前沒有允許的使用者,可能導致沒有人能管理 bot,確定要繼續嗎?", accessLockedEmpty: "Locked 模式沒有允許的使用者;建議至少加入一位管理員,避免無法操作。", accessChangeCancelled: "已取消 Access Mode 變更",
231
+ mustNonNegative: "必須為 0 或更大的數值", mustPositive: "必須大於 0", nonnegativeInteger: "必須是非負整數", failoverEmpty: "空白清單會停用模型備援", concurrencyRange: "必須是 1 到 20 的整數", concurrencyWarning: "在較小的主機上可能超出 CPU 負荷", staggerRange: "必須介於 0 到 30000", staggerWarning: "數值太低可能造成啟動尖峰",
224
232
  },
225
233
  };
226
234
  const HELP = {
@@ -242,9 +250,11 @@
242
250
  let lang = localStorage.getItem("agend_lang") || (navigator.language && navigator.language.toLowerCase().startsWith("zh") ? "zh-TW" : "en");
243
251
  if (!I18N[lang]) lang = "en";
244
252
  const t = (k) => (I18N[lang] && I18N[lang][k]) || I18N.en[k] || k;
253
+ const tf = (k, ...values) => values.reduce((text, value, index) => text.replaceAll(`{${index}}`, String(value)), t(k));
245
254
  function applyStaticI18n() {
246
255
  document.querySelectorAll("[data-i18n]").forEach(e => { e.textContent = t(e.getAttribute("data-i18n")); });
247
256
  document.querySelectorAll("[data-i18n-ph]").forEach(e => { e.setAttribute("placeholder", t(e.getAttribute("data-i18n-ph"))); });
257
+ document.querySelectorAll("[data-i18n-title]").forEach(e => { e.setAttribute("title", t(e.getAttribute("data-i18n-title"))); });
248
258
  // toggle labels that carry expand/collapse state
249
259
  for (const [id, body] of [["advToggle", "advBody"]]) {
250
260
  const b = $(body); if (b) $(id).textContent = b.classList.contains("open") ? t("collapse") : t("expand");
@@ -292,7 +302,7 @@
292
302
  const b = res.body || {}; let html = "";
293
303
  if (b.errors && b.errors.length) html += b.errors.map(e => `<span class="e">✗ ${esc(e.path ? e.path + ": " : "")}${esc(e.message)}</span>`).join(" ");
294
304
  if (b.warnings && b.warnings.length) html += " " + b.warnings.map(w => `<span class="w">⚠ ${esc(w.message)}</span>`).join(" ");
295
- if (res.ok) html = `<span class="s">✓ Saved</span>` + (html ? " " + html : "");
305
+ if (res.ok) html = `<span class="s">✓ ${esc(t("saved"))}</span>` + (html ? " " + html : "");
296
306
  else if (!html) html = `<span class="e">✗ ${esc((b && b.error) || ("HTTP " + res.status))}</span>`;
297
307
  elm.innerHTML = html;
298
308
  }
@@ -322,37 +332,52 @@
322
332
  redraw(); return box;
323
333
  }
324
334
  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; }
325
- const impact = (kind) => el("span", { class: "impact" }, kind === "now" ? "⚡ Immediately" : kind === "fleet" ? "🔄🔄 Restart AgEnD" : "🔄 Restart this Agent");
335
+ const impactText = (kind) => t(kind === "now" ? "impactNow" : kind === "fleet" ? "impactFleet" : "impactAgent");
336
+ const impact = (kind) => el("span", { class: "impact" }, impactText(kind));
326
337
  function updatePendingBar() {
327
338
  const n = state.pending.size;
328
339
  $("pendingBar").classList.toggle("open", n > 0);
329
- $("pendingCount").textContent = `${n} change${n === 1 ? "" : "s"} not applied`;
340
+ $("pendingCount").textContent = tf("changesPending", n);
330
341
  const kinds = new Set([...state.pending.values()].map(change => change.impact));
331
- $("pendingImpact").textContent = [...kinds].map(kind => kind === "now" ? "⚡ Immediately" : kind === "fleet" ? "🔄🔄 Restart AgEnD" : "🔄 Restart this Agent").join(" · ");
342
+ $("pendingImpact").textContent = [...kinds].map(impactText).join(" · ");
332
343
  }
333
344
  function stageChange(key, change) {
334
345
  state.pending.set(key, change);
335
346
  updatePendingBar();
336
- showBanner(`Queued: ${change.label}`);
347
+ showBanner(tf("queued", change.label));
337
348
  }
338
349
  async function applyPendingChanges() {
339
350
  const button = $("applyChanges"); button.disabled = true;
351
+ // Complete every safety confirmation before the first write. Otherwise a
352
+ // cancelled access-mode change could leave earlier staged settings applied
353
+ // and later ones pending (a surprising partial apply).
354
+ for (const change of state.pending.values()) {
355
+ if (change.confirm && !change.confirm()) {
356
+ showBanner(change.cancelMessage || t("accessChangeCancelled"), true);
357
+ button.disabled = false; return;
358
+ }
359
+ }
340
360
  for (const [key, change] of [...state.pending]) {
341
361
  let result;
342
362
  try { result = await change.apply(); }
343
363
  catch (error) { result = { ok: false, body: { error: error.message } }; }
344
364
  if (!result?.ok) {
345
- showBanner(`${change.label}: ${result?.body?.error || "Apply failed"}`, true);
365
+ showBanner(`${change.label}: ${result?.body?.error || t("applyFailed")}`, true);
346
366
  button.disabled = false; return;
347
367
  }
348
368
  state.pending.delete(key); updatePendingBar();
349
369
  }
350
370
  await api("/api/settings/reload", { method: "POST" });
351
371
  await reload();
352
- showBanner("✓ Changes applied. AgEnD is reconciling affected Agents.");
372
+ showBanner(t("changesApplied"));
353
373
  button.disabled = false;
354
374
  }
355
375
  const hasOwn = (obj, key) => Object.prototype.hasOwnProperty.call(obj || {}, key);
376
+ const HOT_FIELDS = new Set(["tool_progress", "mcp_proxy_reply", "auto_pause_after", "warm_cap", "display_name", "description", "tags", "log_level"]);
377
+ const sameValue = (a, b) => JSON.stringify(a) === JSON.stringify(b);
378
+ function changedFields(next, baseline) {
379
+ return Object.fromEntries(Object.entries(next).filter(([key, value]) => !sameValue(value, baseline[key])));
380
+ }
356
381
  function overrideControl(inst, key, input, inheritedValue, impactKind = "instance") {
357
382
  const inherited = !hasOwn(inst, key);
358
383
  const toggle = el("input", { type: "checkbox" }); toggle.checked = inherited;
@@ -370,6 +395,15 @@
370
395
  feedback.textContent = error ? `✗ ${error}` : warning ? `⚠ ${warning}` : "";
371
396
  return !error;
372
397
  }
398
+ function confirmAccessChange(previousAccess, nextMode, nextUsers) {
399
+ const previousMode = previousAccess?.mode || "locked";
400
+ const previousUsers = previousAccess?.allowed_users || [];
401
+ if (previousMode !== "open" && nextMode === "open" && !confirm(t("confirmOpenAccess"))) return false;
402
+ const newlyEmptyLocked = nextMode === "locked" && nextUsers.length === 0
403
+ && (previousMode !== "locked" || previousUsers.length > 0);
404
+ if (newlyEmptyLocked && !confirm(t("confirmLockedEmpty"))) return false;
405
+ return true;
406
+ }
373
407
 
374
408
  // ── My Agents ──
375
409
  let editing = null;
@@ -385,12 +419,23 @@
385
419
  const STATUS_TEXT = { running: "🟢 Running", paused: "⏸ Paused", crashed: "🔴 Crashed", stopped: "⚪ Stopped" };
386
420
  const EXECUTION_TEXT = { idle: "🟢 Idle", working: "🔵 Working", stuck: "🔴 Stuck", paused: "⏸ Paused", crashed: "🔴 Crashed", stopped: "⚪ Stopped", running: "🟢 Running" };
387
421
  const STATUS_ORDER = { running: 0, paused: 1, crashed: 2, stopped: 3 };
422
+ const EFFORT_BACKENDS = new Set(["claude-code", "codex", "kiro-cli", "grok", "antigravity"]);
388
423
  const groupOf = (inst) => (inst.tags && inst.tags[0]) || "Other";
424
+ function effectiveSummary(name, config, classic = false) {
425
+ const live = state.live[name] || {};
426
+ const defaults = classic ? (state.classic?.defaults || {}) : (state.fleet?.defaults || {});
427
+ const backend = live.backend || config.backend || defaults.backend || state.fleet?.defaults?.backend || "claude-code";
428
+ const model = live.model_display || config.model || defaults.model || state.fleet?.defaults?.model || "default";
429
+ const effortSupported = live.effort_supported ?? EFFORT_BACKENDS.has(backend);
430
+ const effort = effortSupported ? (live.effort || config.effort || state.fleet?.defaults?.effort || "auto") : "—";
431
+ return { backend, model, effort };
432
+ }
389
433
  function compactRow(name, inst) {
390
434
  const st = state.live[name]?.status || "stopped";
391
435
  const running = st === "running";
392
436
  const paused = st === "paused";
393
437
  const short = shortName(name, inst);
438
+ const summary = effectiveSummary(name, inst);
394
439
  const chTag = agentChannelLabel(inst);
395
440
  const item = el("div", { class: "item" });
396
441
  // Single fixed-height line: dot + short name + backend + channel tag + actions.
@@ -398,7 +443,8 @@
398
443
  item.append(el("div", { class: "item-row" },
399
444
  el("span", { class: "dot " + st, title: STATUS_TEXT[st] || st }),
400
445
  el("span", { class: "name", title: name }, short),
401
- el("span", { class: "sub", style: "margin:0" }, inst.backend || state.fleet.defaults?.backend || "claude-code"),
446
+ el("span", { class: "sub", style: "margin:0", title: summary.backend }, summary.model),
447
+ el("span", { class: "tag" }, `effort: ${summary.effort}`),
402
448
  chTag ? el("span", { class: "tag" + (chTag.includes("persona") ? " persona" : chTag.includes("telegram") ? " tg" : "") }, chTag) : null,
403
449
  el("span", { class: "spacer" }),
404
450
  el("button", { class: "sm ghost", onclick: () => { editing = editing === name ? null : name; renderAgents(); } }, editing === name ? t("close") : t("edit")),
@@ -419,7 +465,7 @@
419
465
  const filter = ($("agentSearch")?.value || "").trim().toLowerCase();
420
466
  let names = Object.keys(insts);
421
467
  if (filter) names = names.filter(n => n.toLowerCase().includes(filter) || shortName(n, insts[n]).toLowerCase().includes(filter));
422
- if (!names.length) { host.className = "card empty"; host.textContent = "No agents match \"" + filter + "\"."; return; }
468
+ if (!names.length) { host.className = "card empty"; host.textContent = tf("noAgentMatch", filter); return; }
423
469
  host.className = "card";
424
470
  // Group by first tag; "Other" sorts last. Within a group: Running → Crashed
425
471
  // → Stopped, then name.
@@ -455,6 +501,7 @@
455
501
  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);
456
502
  const agentMode = overrideControl(inst, "agent_mode", select(inst.agent_mode || defaults.agent_mode || "mcp", ["mcp", "cli"]), defaults.agent_mode || "mcp");
457
503
  const toolSet = overrideControl(inst, "tool_set", select(inst.tool_set || defaults.tool_set || "full", ["full", "standard", "minimal"]), defaults.tool_set || "full");
504
+ const toolProgress = overrideControl(inst, "tool_progress", select(inst.tool_progress || defaults.tool_progress || "off", ["off", "standard", "verbose"]), defaults.tool_progress || "off", "now");
458
505
  const logLevel = overrideControl(inst, "log_level", select(inst.log_level || defaults.log_level || "info", ["trace", "debug", "info", "warn", "error"]), defaults.log_level || "info");
459
506
  const fLightweight = el("input", { type: "checkbox" }); fLightweight.checked = inst.lightweight ?? defaults.lightweight ?? false;
460
507
  const lightweight = overrideControl(inst, "lightweight", fLightweight, defaults.lightweight ?? false);
@@ -468,18 +515,18 @@
468
515
  const saveBtn = el("button", { class: "primary sm" }, t("save"));
469
516
  const validate = () => {
470
517
  const autoValue = Number(auto.input.value), hangValue = Number(hang.input.value);
471
- const validAuto = setValidation(auto.input, autoFeedback, !auto.toggle.checked && (!Number.isFinite(autoValue) || autoValue < 0) ? "Must be 0 or greater" : "");
472
- const validHang = setValidation(hang.input, hangFeedback, !hang.toggle.checked && (!Number.isFinite(hangValue) || hangValue <= 0) ? "Must be greater than 0" : "");
473
- setValidation(failover.input, failoverFeedback, "", !failover.toggle.checked && !failover.input.value.trim() ? "Empty list disables failover" : "");
518
+ const validAuto = setValidation(auto.input, autoFeedback, !auto.toggle.checked && (!Number.isFinite(autoValue) || autoValue < 0) ? t("mustNonNegative") : "");
519
+ const validHang = setValidation(hang.input, hangFeedback, !hang.toggle.checked && (!Number.isFinite(hangValue) || hangValue <= 0) ? t("mustPositive") : "");
520
+ setValidation(failover.input, failoverFeedback, "", !failover.toggle.checked && !failover.input.value.trim() ? t("failoverEmpty") : "");
474
521
  saveBtn.disabled = !(validAuto && validHang);
475
522
  return !saveBtn.disabled;
476
523
  };
477
- for (const ctl of [auto, hang, agentMode, toolSet, logLevel, lightweight, failover, displayName]) {
524
+ for (const ctl of [auto, hang, agentMode, toolSet, toolProgress, logLevel, lightweight, failover, displayName]) {
478
525
  ctl.input.addEventListener("input", validate); ctl.input.addEventListener("change", validate); ctl.toggle.addEventListener("change", validate);
479
526
  }
480
527
  saveBtn.onclick = async () => {
481
528
  if (!validate()) return;
482
- const patch = {
529
+ const nextPatch = {
483
530
  backend: fBackend.value, model: fModel.value.trim() || null,
484
531
  working_directory: fWd.value, channel_id: fChan.value || undefined,
485
532
  description: fDesc.value || undefined, systemPrompt: fSys.value || undefined,
@@ -488,11 +535,31 @@
488
535
  hang_detector: { timeout_minutes: hang.toggle.checked ? null : Number(hang.input.value) },
489
536
  agent_mode: agentMode.toggle.checked ? null : agentMode.input.value,
490
537
  tool_set: toolSet.toggle.checked ? null : toolSet.input.value,
538
+ tool_progress: toolProgress.toggle.checked ? null : toolProgress.input.value,
491
539
  log_level: logLevel.toggle.checked ? null : logLevel.input.value,
492
540
  lightweight: lightweight.toggle.checked ? null : lightweight.input.checked,
493
541
  model_failover: failover.toggle.checked ? null : (failover.input.value.trim() ? [failover.input.value.trim()] : []),
494
542
  display_name: displayName.toggle.checked ? null : displayName.input.value.trim(),
495
543
  };
544
+ const patch = changedFields(nextPatch, {
545
+ backend: inst.backend || defaults.backend || "claude-code",
546
+ model: inst.model?.trim() || null,
547
+ working_directory: inst.working_directory || "",
548
+ channel_id: inst.channel_id || undefined,
549
+ description: inst.description || undefined,
550
+ systemPrompt: inst.systemPrompt || undefined,
551
+ tags: inst.tags?.length ? inst.tags : undefined,
552
+ general_topic: !!inst.general_topic,
553
+ auto_pause_after: hasOwn(inst, "auto_pause_after") ? inst.auto_pause_after : null,
554
+ hang_detector: { timeout_minutes: hasOwn(hangRaw, "timeout_minutes") ? hangRaw.timeout_minutes : null },
555
+ agent_mode: hasOwn(inst, "agent_mode") ? inst.agent_mode : null,
556
+ tool_set: hasOwn(inst, "tool_set") ? inst.tool_set : null,
557
+ tool_progress: hasOwn(inst, "tool_progress") ? inst.tool_progress : null,
558
+ log_level: hasOwn(inst, "log_level") ? inst.log_level : null,
559
+ lightweight: hasOwn(inst, "lightweight") ? inst.lightweight : null,
560
+ model_failover: hasOwn(inst, "model_failover") ? inst.model_failover : null,
561
+ display_name: hasOwn(inst, "display_name") ? inst.display_name : null,
562
+ });
496
563
  state.fleet.instances[name] = { ...inst, ...patch };
497
564
  for (const [key, value] of Object.entries(patch)) if (value === null) delete state.fleet.instances[name][key];
498
565
  if (patch.hang_detector?.timeout_minutes === null) {
@@ -501,7 +568,7 @@
501
568
  }
502
569
  stageChange(`agent:${name}`, {
503
570
  label: `Update ${shortName(name, state.fleet.instances[name])}`,
504
- impact: "instance",
571
+ impact: Object.keys(patch).every(key => HOT_FIELDS.has(key)) ? "now" : "instance",
505
572
  apply: () => api(`/api/settings/fleet/instances/${encodeURIComponent(name)}`, { method: "PATCH", body: JSON.stringify(patch) }),
506
573
  });
507
574
  editing = null; renderAgents();
@@ -515,7 +582,7 @@
515
582
  el("div", {}, el("label", {}, t("model"), impact("instance")), fModel)),
516
583
  el("label", {}, t("workingDir"), impact("instance")), fWd,
517
584
  el("div", { class: "grid2" },
518
- el("div", {}, el("label", {}, `${t("autoPause")} (${t("minutes")})`, impact("instance")), auto.row, autoFeedback)),
585
+ el("div", {}, el("label", {}, `${t("autoPause")} (${t("minutes")})`, impact("now")), auto.row, autoFeedback)),
519
586
  el("div", { class: "advanced-only" },
520
587
  el("div", { class: "sub", style: "margin:14px 0 6px;font-weight:600" }, "Advanced Agent settings"),
521
588
  el("div", { class: "grid2" },
@@ -523,7 +590,8 @@
523
590
  el("div", {}, el("label", {}, `${t("hangTimeout")} (${t("minutes")})`), hang.row, hangFeedback),
524
591
  el("div", {}, el("label", {}, t("agentMode")), agentMode.row),
525
592
  el("div", {}, el("label", {}, t("toolSet")), toolSet.row),
526
- el("div", {}, el("label", {}, t("logLevel")), logLevel.row),
593
+ el("div", {}, el("label", {}, t("toolProgress"), impact("now")), toolProgress.row),
594
+ el("div", {}, el("label", {}, t("logLevel"), impact("now")), logLevel.row),
527
595
  el("div", {}, el("label", {}, t("lightweight")), lightweight.row),
528
596
  el("div", {}, el("label", {}, t("modelFailover")), failover.row, failoverFeedback)),
529
597
  el("label", {}, t("systemPrompt"), impact("instance")), fSys,
@@ -544,11 +612,11 @@
544
612
  }
545
613
  async function pauseWakeAgent(name, action) {
546
614
  const res = await api(`/api/settings/instances/${encodeURIComponent(name)}/${action}`, { method: "POST" });
547
- if (!res.ok) alert(res.body?.error || `${action} failed`);
615
+ if (!res.ok) alert(res.body?.error || tf("actionFailed", action));
548
616
  await reloadLive();
549
617
  }
550
618
  async function delAgent(name) {
551
- if (!confirm(`Delete agent "${name}"? This will be applied only after you confirm the pending changes.`)) return;
619
+ if (!confirm(tf("deleteAgent", name))) return;
552
620
  delete state.fleet.instances[name];
553
621
  stageChange(`agent:${name}`, {
554
622
  label: `Delete ${name}`, impact: "fleet",
@@ -569,7 +637,7 @@
569
637
  el("div", { class: "grid2" }, el("div", {}, el("label", {}, t("nameField")), fName), el("div", {}, el("label", {}, t("backend")), fBackend)),
570
638
  el("div", { class: "grid2" }, el("div", {}, el("label", {}, t("workingDir")), fWd), el("div", {}, el("label", {}, t("channelBinding")), fChan)),
571
639
  el("div", { class: "actions" }, el("button", { class: "primary sm", onclick: async () => {
572
- const name = fName.value.trim(); if (!name) { msg.innerHTML = `<span class="e">✗ name required</span>`; return; }
640
+ const name = fName.value.trim(); if (!name) { msg.innerHTML = `<span class="e">✗ ${esc(t("nameRequired"))}</span>`; return; }
573
641
  const body = { working_directory: fWd.value, backend: fBackend.value, channel_id: fChan.value || undefined };
574
642
  const res = await api(`/api/settings/fleet/instances/${encodeURIComponent(name)}`, { method: "POST", body: JSON.stringify(body) });
575
643
  resultMsg(msg, res); if (res.ok) { state.newAgentOpen = false; $("newAgentBtn").textContent = t("new"); renderNewAgent(false); await reload(); }
@@ -604,16 +672,21 @@
604
672
  function botEditForm(ch, i) {
605
673
  ch.access = ch.access || { mode: "locked", allowed_users: [] };
606
674
  ch.access.allowed_users = ch.access.allowed_users || [];
675
+ const previousAccess = structuredClone(ch.access);
607
676
  const box = el("div", { class: "edit" });
608
677
  const fMode = select(ch.access.mode || "locked", ["open", "locked", "pairing"]);
609
678
  const users = [...ch.access.allowed_users];
610
679
  const msg = el("span", { class: "msg" });
680
+ const accessFeedback = el("div", { class: "feedback" });
681
+ const validateAccess = () => setValidation(fMode, accessFeedback, "", fMode.value === "locked" && users.length === 0 ? t("accessLockedEmpty") : "");
682
+ fMode.addEventListener("change", validateAccess);
611
683
  box.append(
612
684
  el("div", { class: "grid2" },
613
- el("div", {}, el("label", {}, t("accessMode"), impact("fleet")), fMode),
614
- el("div", {}, el("label", {}, t("allowedUsers"), impact("fleet")), chipList(users, null, "user id"))),
685
+ el("div", {}, el("label", {}, t("accessMode"), impact("fleet")), fMode, accessFeedback),
686
+ el("div", {}, el("label", {}, t("allowedUsers"), impact("fleet")), chipList(users, validateAccess, "user id"))),
615
687
  el("div", { class: "actions" },
616
688
  el("button", { class: "primary sm", onclick: async () => {
689
+ if (!confirmAccessChange(previousAccess, fMode.value, users)) return;
617
690
  const chs = channels();
618
691
  chs[i] = { ...chs[i], access: { ...chs[i].access, mode: fMode.value, allowed_users: users } };
619
692
  const res = await saveChannels(msg);
@@ -625,6 +698,7 @@
625
698
  } }, t("save")),
626
699
  el("button", { class: "sm ghost", onclick: () => { editingBot = null; renderBots(); } }, t("cancel")), msg),
627
700
  );
701
+ validateAccess();
628
702
  return box;
629
703
  }
630
704
 
@@ -655,8 +729,8 @@
655
729
  const validate = () => {
656
730
  const autoValue = Number(auto.input.value);
657
731
  const contextValue = Number(fContext.value);
658
- const validAuto = setValidation(auto.input, autoFeedback, !auto.toggle.checked && (!Number.isFinite(autoValue) || autoValue < 0) ? "Must be 0 or greater" : "");
659
- const validContext = setValidation(fContext, contextFeedback, !Number.isInteger(contextValue) || contextValue < 0 ? "Must be a non-negative integer" : "");
732
+ const validAuto = setValidation(auto.input, autoFeedback, !auto.toggle.checked && (!Number.isFinite(autoValue) || autoValue < 0) ? t("mustNonNegative") : "");
733
+ const validContext = setValidation(fContext, contextFeedback, !Number.isInteger(contextValue) || contextValue < 0 ? t("nonnegativeInteger") : "");
660
734
  saveBtn.disabled = !(validAuto && validContext);
661
735
  return !saveBtn.disabled;
662
736
  };
@@ -719,11 +793,14 @@
719
793
  const runtime = classicRuntime(c.instanceName);
720
794
  const st = runtime.status;
721
795
  const adapter = c.adapterId || "discord";
796
+ const summary = effectiveSummary(c.instanceName, c, true);
797
+ const displayName = shortName(c.name || c.instanceName || "?", { display_name: c.display_name });
722
798
  const item = el("div", { class: "item" });
723
799
  item.append(el("div", { class: "item-row" },
724
800
  el("span", { class: "dot " + runtime.execution, title: EXECUTION_TEXT[runtime.execution] || runtime.execution }),
725
- el("span", { class: "name", title: c.instanceName || "" }, c.name || c.instanceName || "?"),
726
- el("span", { class: "sub", style: "margin:0" }, c.backend || state.classic?.defaults?.backend || state.fleet?.defaults?.backend || "claude-code"),
801
+ el("span", { class: "name", title: c.instanceName || "" }, displayName),
802
+ el("span", { class: "sub", style: "margin:0", title: summary.backend }, summary.model),
803
+ el("span", { class: "tag" }, `effort: ${summary.effort}`),
727
804
  el("span", { class: "tag" + (adapter !== "discord" ? " persona" : "") }, adapter),
728
805
  c.collab ? el("span", { class: "tag", style: "background:#ecfdf5;color:#065f46" }, "collab") : null,
729
806
  el("span", { class: "spacer" }),
@@ -744,10 +821,10 @@
744
821
  }
745
822
  async function delBot(i) {
746
823
  const chs = channels(); const ch = chs[i];
747
- if (!confirm(`Remove bot "${ch.type} (${chLabel(i)})"? (the token in .env is left untouched)`)) return;
824
+ if (!confirm(tf("removeBot", `${ch.type} (${chLabel(i)})`))) return;
748
825
  chs.splice(i, 1);
749
826
  state.fleet.channels = chs; delete state.fleet.channel;
750
- const res = await saveChannels(); if (res.ok) await reload(); else alert((res.body && res.body.error) || "Failed");
827
+ const res = await saveChannels(); if (res.ok) await reload(); else alert((res.body && res.body.error) || t("failed"));
751
828
  }
752
829
  function renderNewBot(open) {
753
830
  const host = $("newBot"); host.innerHTML = ""; if (!open) return;
@@ -762,7 +839,7 @@
762
839
  el("div", { class: "grid2" }, el("div", {}, el("label", {}, t("tokenEnv")), fEnv), el("div", {}, el("label", {}, t("groupId")), fGroup)),
763
840
  el("div", { class: "hint" }, t("envHint")),
764
841
  el("div", { class: "actions" }, el("button", { class: "primary sm", onclick: async () => {
765
- if (!fEnv.value.trim()) { msg.innerHTML = `<span class="e">✗ token env required</span>`; return; }
842
+ if (!fEnv.value.trim()) { msg.innerHTML = `<span class="e">✗ ${esc(t("tokenEnvRequired"))}</span>`; return; }
766
843
  const chs = channels();
767
844
  chs.push({ id: fId.value.trim() || undefined, type: fType.value, bot_token_env: fEnv.value.trim(), group_id: fGroup.value.trim() || undefined, access: { mode: "locked", allowed_users: [] } });
768
845
  state.fleet.channels = chs; delete state.fleet.channel;
@@ -778,11 +855,16 @@
778
855
  state.fleet.defaults = state.fleet.defaults || {};
779
856
  state.classic.defaults = state.classic.defaults || {};
780
857
  const d = state.fleet.defaults, c = state.classic.defaults;
858
+ // The controls below mutate their arrays in place. Keep raw snapshots so
859
+ // changing one hot default does not also stage untouched access settings as
860
+ // a fleet restart.
861
+ const originalChannels = structuredClone(channels());
862
+ const originalClassicDefaults = structuredClone(c);
781
863
  const hang = d.hang_detector || {};
782
864
  const startup = d.startup || {};
783
865
  const primary = channels()[0];
784
- if (primary) primary.access = primary.access || { mode: "locked", allowed_users: [] };
785
- const allowedUsers = primary ? (primary.access.allowed_users = primary.access.allowed_users || []) : [];
866
+ const originalPrimaryAccess = structuredClone(primary?.access || { mode: "locked", allowed_users: [] });
867
+ const allowedUsers = [...(originalPrimaryAccess.allowed_users || [])];
786
868
  c.admin_users = c.admin_users || []; c.allowed_guilds = c.allowed_guilds || [];
787
869
 
788
870
  const fBackend = select(d.backend || "claude-code", BACKENDS);
@@ -790,6 +872,8 @@
790
872
  const fModel = el("input", { type: "text", value: d.model || "", placeholder: "e.g. claude-opus-4-8" });
791
873
  const fAgentMode = select(d.agent_mode || "mcp", ["mcp", "cli"]);
792
874
  const fToolSet = select(d.tool_set || "full", ["full", "standard", "minimal"]);
875
+ const fToolProgress = select(d.tool_progress || "off", ["off", "standard", "verbose"]);
876
+ const fAccessMode = select(originalPrimaryAccess.mode || "locked", ["locked", "open", "pairing"]);
793
877
  const fLogLevel = select(d.log_level || "info", ["trace", "debug", "info", "warn", "error"]);
794
878
  const fLocale = el("select");
795
879
  const fAutoPause = el("input", { type: "number", min: "0", step: "1", value: d.auto_pause_after ?? 0, placeholder: "0 = disabled" });
@@ -803,41 +887,84 @@
803
887
  const hangFeedback = el("div", { class: "feedback" });
804
888
  const concurrencyFeedback = el("div", { class: "feedback" });
805
889
  const staggerFeedback = el("div", { class: "feedback" });
890
+ const accessFeedback = el("div", { class: "feedback" });
806
891
  const saveBtn = el("button", { class: "primary" }, t("save"));
807
892
  const validate = () => {
808
893
  const auto = Number(fAutoPause.value), timeout = Number(fHangTimeout.value);
809
894
  const concurrency = Number(fConcurrency.value), stagger = Number(fStagger.value);
810
- const validAuto = setValidation(fAutoPause, autoFeedback, !Number.isFinite(auto) || auto < 0 ? "Must be 0 or greater" : "");
811
- const validHang = setValidation(fHangTimeout, hangFeedback, !Number.isFinite(timeout) || timeout <= 0 ? "Must be greater than 0" : "");
812
- const validConcurrency = setValidation(fConcurrency, concurrencyFeedback, !Number.isInteger(concurrency) || concurrency < 1 || concurrency > 20 ? "Must be an integer from 1 to 20" : "", concurrency > 4 ? "May exceed CPU capacity on smaller hosts" : "");
813
- const validStagger = setValidation(fStagger, staggerFeedback, !Number.isFinite(stagger) || stagger < 0 || stagger > 30000 ? "Must be between 0 and 30000" : "", stagger < 100 ? "Very low values can cause startup spikes" : "");
895
+ const validAuto = setValidation(fAutoPause, autoFeedback, !Number.isFinite(auto) || auto < 0 ? t("mustNonNegative") : "");
896
+ const validHang = setValidation(fHangTimeout, hangFeedback, !Number.isFinite(timeout) || timeout <= 0 ? t("mustPositive") : "");
897
+ const validConcurrency = setValidation(fConcurrency, concurrencyFeedback, !Number.isInteger(concurrency) || concurrency < 1 || concurrency > 20 ? t("concurrencyRange") : "", concurrency > 4 ? t("concurrencyWarning") : "");
898
+ const validStagger = setValidation(fStagger, staggerFeedback, !Number.isFinite(stagger) || stagger < 0 || stagger > 30000 ? t("staggerRange") : "", stagger < 100 ? t("staggerWarning") : "");
899
+ if (primary) setValidation(fAccessMode, accessFeedback, "", fAccessMode.value === "locked" && allowedUsers.length === 0 ? t("accessLockedEmpty") : "");
814
900
  saveBtn.disabled = !(validAuto && validHang && validConcurrency && validStagger);
815
901
  return !saveBtn.disabled;
816
902
  };
817
903
  for (const input of [fAutoPause, fHangTimeout, fConcurrency, fStagger]) {
818
904
  input.addEventListener("input", validate); input.addEventListener("change", validate);
819
905
  }
820
- saveBtn.textContent = "Review changes";
906
+ fAccessMode.addEventListener("change", validate);
907
+ saveBtn.textContent = t("reviewChanges");
821
908
  saveBtn.onclick = () => {
822
909
  if (!validate()) return;
823
- const defaultsPatch = {
910
+ const nextDefaults = {
824
911
  backend: fBackend.value,
825
912
  model: fModel.value || undefined,
826
913
  locale: fLocale.value,
827
914
  auto_pause_after: Number(fAutoPause.value),
828
915
  agent_mode: fAgentMode.value,
829
916
  tool_set: fToolSet.value,
917
+ tool_progress: fToolProgress.value,
830
918
  log_level: fLogLevel.value,
831
919
  hang_detector: { enabled: fHangEnabled.checked, timeout_minutes: Number(fHangTimeout.value) },
832
920
  startup: { concurrency: Number(fConcurrency.value), stagger_delay_ms: Number(fStagger.value) },
833
921
  };
834
- const classicPatch = { backend: fClassicBackend.value, admin_users: c.admin_users, allowed_guilds: c.allowed_guilds };
922
+ const defaultsPatch = changedFields(nextDefaults, {
923
+ backend: d.backend || "claude-code",
924
+ model: d.model || undefined,
925
+ locale: d.locale || "",
926
+ auto_pause_after: d.auto_pause_after ?? 0,
927
+ agent_mode: d.agent_mode || "mcp",
928
+ tool_set: d.tool_set || "full",
929
+ tool_progress: d.tool_progress || "off",
930
+ log_level: d.log_level || "info",
931
+ hang_detector: { enabled: hang.enabled !== false, timeout_minutes: hang.timeout_minutes ?? 15 },
932
+ startup: { concurrency: startup.concurrency ?? 10, stagger_delay_ms: startup.stagger_delay_ms ?? 500 },
933
+ });
934
+ const classicPatch = changedFields(
935
+ { backend: fClassicBackend.value, admin_users: c.admin_users, allowed_guilds: c.allowed_guilds },
936
+ {
937
+ backend: originalClassicDefaults.backend || d.backend || "claude-code",
938
+ admin_users: originalClassicDefaults.admin_users || [],
939
+ allowed_guilds: originalClassicDefaults.allowed_guilds || [],
940
+ },
941
+ );
835
942
  state.fleet.defaults = { ...d, ...defaultsPatch };
836
943
  state.classic.defaults = { ...c, ...classicPatch };
837
- stageChange("defaults:fleet", { label: "Update Agent defaults", impact: "instance", apply: () => api("/api/settings/fleet/defaults", { method: "PUT", body: JSON.stringify(defaultsPatch) }) });
838
- if (primary) stageChange("connections:primary", { label: "Update primary connection access", impact: "fleet", apply: () => api("/api/settings/fleet/channels", { method: "PUT", body: JSON.stringify(channels()) }) });
839
- stageChange("defaults:classic", { label: "Update ClassicBot defaults and access", impact: "fleet", apply: () => api("/api/settings/classic/defaults", { method: "PUT", body: JSON.stringify(classicPatch) }) });
840
- msg.innerHTML = `<span class="s">Ready to apply</span>`;
944
+ if (Object.keys(defaultsPatch).length > 0) {
945
+ stageChange("defaults:fleet", { label: "Update Agent defaults", impact: Object.keys(defaultsPatch).every(key => HOT_FIELDS.has(key)) ? "now" : "instance", apply: () => api("/api/settings/fleet/defaults", { method: "PUT", body: JSON.stringify(defaultsPatch) }) });
946
+ }
947
+ if (primary) {
948
+ const stagedAccessMode = fAccessMode.value;
949
+ const stagedAllowedUsers = [...allowedUsers];
950
+ const nextChannels = structuredClone(originalChannels);
951
+ nextChannels[0] = { ...nextChannels[0], access: { ...nextChannels[0].access, mode: stagedAccessMode, allowed_users: stagedAllowedUsers } };
952
+ if (!sameValue(nextChannels, originalChannels)) {
953
+ stageChange("connections:primary", {
954
+ label: "Update primary connection access",
955
+ impact: "fleet",
956
+ // Confirm the immutable staged snapshot, not live form controls
957
+ // that the user may edit again before pressing global Apply.
958
+ confirm: () => confirmAccessChange(originalPrimaryAccess, stagedAccessMode, stagedAllowedUsers),
959
+ cancelMessage: t("accessChangeCancelled"),
960
+ apply: () => api("/api/settings/fleet/channels", { method: "PUT", body: JSON.stringify(nextChannels) }),
961
+ });
962
+ }
963
+ }
964
+ if (Object.keys(classicPatch).length > 0) {
965
+ stageChange("defaults:classic", { label: "Update ClassicBot defaults and access", impact: "fleet", apply: () => api("/api/settings/classic/defaults", { method: "PUT", body: JSON.stringify(classicPatch) }) });
966
+ }
967
+ msg.innerHTML = `<span class="s">${esc(t("readyApply"))}</span>`;
841
968
  };
842
969
 
843
970
  const section = (title, open, ...children) => {
@@ -851,7 +978,7 @@
851
978
  el("div", { class: "field" }, el("label", {}, t("language"), impact("now")), fLocale),
852
979
  el("div", { class: "field" }, el("label", {}, t("defaultBackend"), impact("instance")), fBackend),
853
980
  el("div", { class: "field" }, el("label", {}, t("defaultModel"), impact("instance")), fModel),
854
- el("div", { class: "field" }, el("label", {}, `${t("autoPause")} (${t("minutes")})`, impact("instance")), fAutoPause, autoFeedback))),
981
+ el("div", { class: "field" }, el("label", {}, `${t("autoPause")} (${t("minutes")})`, impact("now")), fAutoPause, autoFeedback))),
855
982
  section("ClassicBot", true,
856
983
  el("div", { class: "grid2" },
857
984
  el("div", { class: "field" }, el("label", {}, "Classic default backend", impact("instance")), fClassicBackend)),
@@ -863,7 +990,8 @@
863
990
  section(t("runtimeResources"), false,
864
991
  el("div", { class: "grid2" },
865
992
  el("div", { class: "field" }, el("label", {}, t("toolSet"), impact("instance")), fToolSet),
866
- el("div", { class: "field" }, el("label", {}, t("logLevel"), impact("instance")), fLogLevel),
993
+ el("div", { class: "field" }, el("label", {}, t("toolProgress"), impact("now")), fToolProgress),
994
+ el("div", { class: "field" }, el("label", {}, t("logLevel"), impact("now")), fLogLevel),
867
995
  el("div", { class: "field" }, el("label", {}, t("agentMode"), impact("instance")), fAgentMode))),
868
996
  section(t("detection"), false,
869
997
  el("div", { class: "grid2" },
@@ -874,7 +1002,11 @@
874
1002
  el("div", { class: "field" }, el("label", {}, t("concurrency"), impact("fleet")), fConcurrency, concurrencyFeedback),
875
1003
  el("div", { class: "field" }, el("label", {}, `${t("staggerDelay")} (ms)`, impact("fleet")), fStagger, staggerFeedback))),
876
1004
  section("Advanced & Access", false,
877
- el("div", { class: "field" }, el("label", {}, t("allowedUsers"), impact("fleet")), chipList(allowedUsers, null, "user id")))),
1005
+ primary
1006
+ ? el("div", { class: "grid2" },
1007
+ el("div", { class: "field" }, el("label", {}, t("accessMode"), impact("fleet")), fAccessMode, accessFeedback),
1008
+ el("div", { class: "field" }, el("label", {}, t("allowedUsers"), impact("fleet")), chipList(allowedUsers, validate, "user id")))
1009
+ : el("div", { class: "field hint" }, t("noBots")))),
878
1010
  el("div", { class: "field" }, el("div", { class: "actions" }, saveBtn, msg)),
879
1011
  );
880
1012
  validate();
@@ -906,7 +1038,7 @@
906
1038
  el("button", { class: "sm" + (fmt === "yaml" ? " on" : ""), onclick: () => { state.advFmt = "yaml"; renderAdvanced(); } }, "YAML"),
907
1039
  el("button", { class: "sm" + (fmt === "json" ? " on" : ""), onclick: () => { state.advFmt = "json"; renderAdvanced(); } }, "JSON")),
908
1040
  el("button", { class: "sm", onclick: () => { state.advEdit = !state.advEdit; renderAdvanced(); } }, state.advEdit ? "View" : "Edit"),
909
- el("button", { class: "sm", onclick: () => navigator.clipboard.writeText(ser).then(() => { msg.innerHTML = `<span class="s">Copied</span>`; }) }, "Copy"),
1041
+ el("button", { class: "sm", onclick: () => navigator.clipboard.writeText(ser).then(() => { msg.innerHTML = `<span class="s">${esc(t("copied"))}</span>`; }) }, "Copy"),
910
1042
  el("button", { class: "sm", onclick: () => { const blob = new Blob([toYaml(fleetModel())], { type: "text/plain" }); const a = el("a", { href: URL.createObjectURL(blob), download: "fleet.yaml" }); a.click(); URL.revokeObjectURL(a.href); } }, "⬇ fleet.yaml"),
911
1043
  );
912
1044
  host.append(toolbar);
@@ -922,7 +1054,7 @@
922
1054
  }
923
1055
  // Apply the edited full model: PUT defaults + channels, and diff instances.
924
1056
  async function applyFullModel(m, msg) {
925
- if (!m || typeof m !== "object") { msg.innerHTML = `<span class="e">✗ expected a mapping</span>`; return; }
1057
+ if (!m || typeof m !== "object") { msg.innerHTML = `<span class="e">✗ ${esc(t("expectedMapping"))}</span>`; return; }
926
1058
  const results = [];
927
1059
  if (m.defaults) results.push(await api("/api/settings/fleet/defaults", { method: "PUT", body: JSON.stringify(m.defaults) }));
928
1060
  const chs = m.channels || (m.channel ? [m.channel] : null);
@@ -947,7 +1079,15 @@
947
1079
  state.fleetUp = f.ok;
948
1080
  state.live = {}; state.classicLive = {};
949
1081
  if (f.ok && f.body?.instances) for (const i of f.body.instances) {
950
- state.live[i.name] = { status: i.status, state: i.state };
1082
+ state.live[i.name] = {
1083
+ status: i.status,
1084
+ state: i.state,
1085
+ backend: i.backend,
1086
+ model: i.model,
1087
+ model_display: i.model_display,
1088
+ effort: i.effort,
1089
+ effort_supported: i.effort_supported,
1090
+ };
951
1091
  if (i.classic) state.classicLive[i.name] = { status: i.status, state: i.state };
952
1092
  }
953
1093
  // /api/fleet carries live tri-state for Classic instances. Profiles remain
@@ -960,7 +1100,7 @@
960
1100
  async function reload() {
961
1101
  // Forms edit the user-authored config, not defaults-expanded runtime values.
962
1102
  const f = await api("/api/settings/fleet/raw");
963
- if (!f.ok) { $("banner").style.display = "block"; $("banner").style.background = "#fef2f2"; $("banner").style.color = "var(--red)"; $("banner").textContent = "Failed to load config — check the token in the URL."; return; }
1103
+ if (!f.ok) { $("banner").style.display = "block"; $("banner").style.background = "#fef2f2"; $("banner").style.color = "var(--red)"; $("banner").textContent = t("configLoadFailed"); return; }
964
1104
  state.fleet = f.body || {};
965
1105
  const c = await api("/api/settings/classic"); state.classic = c.body || {};
966
1106
  await reloadLive();
@@ -979,12 +1119,12 @@
979
1119
  $("advToggle").onclick = () => { const b = $("advBody"); const open = !b.classList.contains("open"); b.classList.toggle("open", open); $("advToggle").textContent = open ? t("collapse") : t("expand"); };
980
1120
  const advancedEnabled = localStorage.getItem("agend_settings_advanced") === "true";
981
1121
  document.body.classList.toggle("show-advanced", advancedEnabled);
982
- $("advancedToggle").textContent = advancedEnabled ? "Hide advanced" : "Show advanced";
1122
+ $("advancedToggle").textContent = advancedEnabled ? t("hideAdvanced") : t("showAdvanced");
983
1123
  $("advancedToggle").onclick = () => {
984
1124
  const enabled = !document.body.classList.contains("show-advanced");
985
1125
  document.body.classList.toggle("show-advanced", enabled);
986
1126
  localStorage.setItem("agend_settings_advanced", String(enabled));
987
- $("advancedToggle").textContent = enabled ? "Hide advanced" : "Show advanced";
1127
+ $("advancedToggle").textContent = enabled ? t("hideAdvanced") : t("showAdvanced");
988
1128
  };
989
1129
  $("applyChanges").onclick = applyPendingChanges;
990
1130
  $("discardChanges").onclick = async () => { state.pending.clear(); updatePendingBar(); await reload(); };