@songsid/agend 2.1.4-beta.3 → 2.1.4-beta.30

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 (139) hide show
  1. package/README.md +1 -1
  2. package/README.zh-TW.md +1 -1
  3. package/dist/agent-endpoint.js +1 -1
  4. package/dist/agent-endpoint.js.map +1 -1
  5. package/dist/backend/antigravity.d.ts +9 -5
  6. package/dist/backend/antigravity.js +155 -30
  7. package/dist/backend/antigravity.js.map +1 -1
  8. package/dist/backend/claude-code.d.ts +23 -21
  9. package/dist/backend/claude-code.js +47 -24
  10. package/dist/backend/claude-code.js.map +1 -1
  11. package/dist/backend/codex.d.ts +17 -0
  12. package/dist/backend/codex.js +122 -7
  13. package/dist/backend/codex.js.map +1 -1
  14. package/dist/backend/kiro.d.ts +1 -0
  15. package/dist/backend/kiro.js +14 -0
  16. package/dist/backend/kiro.js.map +1 -1
  17. package/dist/backend/opencode.d.ts +27 -11
  18. package/dist/backend/opencode.js +48 -37
  19. package/dist/backend/opencode.js.map +1 -1
  20. package/dist/backend/types.d.ts +14 -0
  21. package/dist/backend/types.js +7 -0
  22. package/dist/backend/types.js.map +1 -1
  23. package/dist/channel/adapters/discord.js +106 -19
  24. package/dist/channel/adapters/discord.js.map +1 -1
  25. package/dist/channel/adapters/telegram.js +5 -3
  26. package/dist/channel/adapters/telegram.js.map +1 -1
  27. package/dist/channel/agy-mcp-launcher.d.ts +2 -0
  28. package/dist/channel/agy-mcp-launcher.js +28 -0
  29. package/dist/channel/agy-mcp-launcher.js.map +1 -0
  30. package/dist/channel/mcp-server.js +6 -20
  31. package/dist/channel/mcp-server.js.map +1 -1
  32. package/dist/channel/mcp-tools.js +4 -2
  33. package/dist/channel/mcp-tools.js.map +1 -1
  34. package/dist/channel/types.d.ts +1 -1
  35. package/dist/classic-channel-manager.d.ts +40 -3
  36. package/dist/classic-channel-manager.js +218 -31
  37. package/dist/classic-channel-manager.js.map +1 -1
  38. package/dist/cli.js +148 -33
  39. package/dist/cli.js.map +1 -1
  40. package/dist/completion-install.d.ts +70 -0
  41. package/dist/completion-install.js +152 -0
  42. package/dist/completion-install.js.map +1 -0
  43. package/dist/completion.d.ts +7 -1
  44. package/dist/completion.js +14 -4
  45. package/dist/completion.js.map +1 -1
  46. package/dist/config-validator.js +38 -7
  47. package/dist/config-validator.js.map +1 -1
  48. package/dist/config.d.ts +2 -0
  49. package/dist/config.js +10 -1
  50. package/dist/config.js.map +1 -1
  51. package/dist/daemon.d.ts +173 -5
  52. package/dist/daemon.js +721 -83
  53. package/dist/daemon.js.map +1 -1
  54. package/dist/doctor.d.ts +26 -0
  55. package/dist/doctor.js +216 -0
  56. package/dist/doctor.js.map +1 -0
  57. package/dist/fleet-context.d.ts +47 -0
  58. package/dist/fleet-manager.d.ts +247 -10
  59. package/dist/fleet-manager.js +1978 -384
  60. package/dist/fleet-manager.js.map +1 -1
  61. package/dist/fleet-yaml-slim.d.ts +10 -0
  62. package/dist/fleet-yaml-slim.js +59 -0
  63. package/dist/fleet-yaml-slim.js.map +1 -0
  64. package/dist/general-knowledge/skills/backend-providers/SKILL.md +114 -0
  65. package/dist/general-knowledge/skills/cross-instance-messaging/SKILL.md +3 -1
  66. package/dist/general-knowledge/skills/delegation-playbook/SKILL.md +52 -0
  67. package/dist/general-knowledge/skills/development-workflow/SKILL.md +28 -0
  68. package/dist/general-knowledge/skills/fleet-config/SKILL.md +1 -0
  69. package/dist/general-knowledge/skills/fleet-health/SKILL.md +1 -0
  70. package/dist/general-knowledge/skills/fleet-restart/SKILL.md +1 -0
  71. package/dist/general-knowledge/skills/instance-lifecycle/SKILL.md +1 -0
  72. package/dist/general-knowledge/skills/model-discovery/SKILL.md +64 -16
  73. package/dist/general-knowledge/skills/multi-channel/SKILL.md +1 -0
  74. package/dist/general-knowledge/skills/scheduling/SKILL.md +1 -0
  75. package/dist/general-knowledge/skills/session-management/SKILL.md +1 -0
  76. package/dist/general-knowledge/skills/tui-effort/SKILL.md +1 -0
  77. package/dist/general-knowledge/skills/worker-collaboration/SKILL.md +30 -0
  78. package/dist/instance-lifecycle.d.ts +28 -0
  79. package/dist/instance-lifecycle.js +79 -5
  80. package/dist/instance-lifecycle.js.map +1 -1
  81. package/dist/instructions.d.ts +12 -0
  82. package/dist/instructions.js +35 -1
  83. package/dist/instructions.js.map +1 -1
  84. package/dist/locale.js +746 -288
  85. package/dist/locale.js.map +1 -1
  86. package/dist/login-flows.d.ts +85 -0
  87. package/dist/login-flows.js +159 -0
  88. package/dist/login-flows.js.map +1 -0
  89. package/dist/login-manager.d.ts +63 -0
  90. package/dist/login-manager.js +134 -0
  91. package/dist/login-manager.js.map +1 -0
  92. package/dist/outbound-handlers.d.ts +8 -0
  93. package/dist/outbound-handlers.js +44 -5
  94. package/dist/outbound-handlers.js.map +1 -1
  95. package/dist/outbound-schemas.d.ts +5 -0
  96. package/dist/outbound-schemas.js +7 -0
  97. package/dist/outbound-schemas.js.map +1 -1
  98. package/dist/quickstart.js +25 -0
  99. package/dist/quickstart.js.map +1 -1
  100. package/dist/restart-progress.d.ts +2 -0
  101. package/dist/restart-progress.js +3 -0
  102. package/dist/restart-progress.js.map +1 -1
  103. package/dist/scheduler/db.d.ts +12 -0
  104. package/dist/scheduler/db.js +59 -0
  105. package/dist/scheduler/db.js.map +1 -1
  106. package/dist/service-installer.d.ts +11 -0
  107. package/dist/service-installer.js +84 -18
  108. package/dist/service-installer.js.map +1 -1
  109. package/dist/settings-api.js +1 -1
  110. package/dist/settings-api.js.map +1 -1
  111. package/dist/setup-wizard.js +2 -2
  112. package/dist/setup-wizard.js.map +1 -1
  113. package/dist/tips.d.ts +44 -0
  114. package/dist/tips.js +355 -0
  115. package/dist/tips.js.map +1 -0
  116. package/dist/tmux-manager.d.ts +6 -0
  117. package/dist/tmux-manager.js +12 -0
  118. package/dist/tmux-manager.js.map +1 -1
  119. package/dist/tool-progress.d.ts +40 -0
  120. package/dist/tool-progress.js +289 -0
  121. package/dist/tool-progress.js.map +1 -0
  122. package/dist/topic-commands.d.ts +36 -4
  123. package/dist/topic-commands.js +322 -64
  124. package/dist/topic-commands.js.map +1 -1
  125. package/dist/transcript-monitor.d.ts +15 -2
  126. package/dist/transcript-monitor.js +63 -17
  127. package/dist/transcript-monitor.js.map +1 -1
  128. package/dist/transcript-sources.d.ts +131 -0
  129. package/dist/transcript-sources.js +580 -0
  130. package/dist/transcript-sources.js.map +1 -0
  131. package/dist/types.d.ts +11 -0
  132. package/dist/ui/dashboard.html +55 -32
  133. package/dist/ui/settings.html +120 -51
  134. package/dist/ui/view.html +23 -15
  135. package/dist/usage/usage-api.d.ts +8 -2
  136. package/dist/usage/usage-api.js +24 -3
  137. package/dist/usage/usage-api.js.map +1 -1
  138. package/dist/workflow-templates/default.md +2 -1
  139. package/package.json +1 -1
@@ -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,10 @@
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
+ 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
209
  },
207
210
  "zh-TW": {
208
211
  title: "⚙️ AgEnD 設定", reloadFleet: "↻ 重新載入", myAgents: "🤖 我的 Agent", bots: "🔌 機器人", classicChannels: "🎮 ClassicBot 頻道", noClassic: "沒有 ClassicBot 頻道 — 在聊天中用 /start 建立。",
@@ -212,7 +215,7 @@
212
215
  backend: "後端", model: "模型", workingDir: "工作目錄", channelBinding: "頻道綁定",
213
216
  description: "描述", systemPrompt: "System prompt", tags: "標籤", generalTopicField: "General topic(fleet 分派器)",
214
217
  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",
218
+ detection: "偵測", startup: "啟動", backendSection: "後端", advancedSection: "進階與存取", hangEnabled: "啟用卡住偵測", hangTimeout: "卡住逾時", agentMode: "Agent 模式", toolSet: "工具集", toolProgress: "工具進度", logLevel: "Log 等級", concurrency: "啟動併發數", staggerDelay: "錯開延遲", lightweight: "輕量模式", modelFailover: "備援模型", displayName: "顯示名稱", pause: "暫停", wake: "喚醒", accessMode: "存取模式", affected: "受影響 instances",
216
219
  language: "語言(bot 回覆)", defaultBackend: "預設後端", defaultModel: "預設模型", allowedUsers: "允許使用者(主 bot)",
217
220
  classicAdmins: "ClassicBot 管理員", allowedGuilds: "允許的伺服器", type: "類型", adapterId: "Adapter id",
218
221
  tokenEnv: "Bot token 環境變數", groupId: "群組 / guild id", nameField: "名稱",
@@ -220,7 +223,10 @@
220
223
  noAgents: "還沒有 agent — 點 + 新增。", noBots: "尚未設定機器人。", online: "✅ 上線",
221
224
  currentVersion: "目前版本",
222
225
  helpTitle: "📖 使用說明", tipReload: "💡 修改後點 [↻ 重新載入] 讓設定生效。",
223
- wnIntro: "近期更新重點:", releaseNotes: "📄 查看完整 Release Notes →",
226
+ wnIntro: "近期更新重點:", releaseNotes: "📄 查看完整 Release Notes →", languageButton: "語言", helpButton: "使用說明", showAdditional: "顯示更多控制項", hideAdvanced: "隱藏進階設定", showAdvanced: "顯示進階設定",
227
+ saved: "已儲存", failed: "失敗", actionFailed: "{0}失敗", noAgentMatch: "沒有符合「{0}」的 Agent。", deleteAgent: "確定刪除 Agent「{0}」嗎?只有在確認套用待處理變更後才會生效。", nameRequired: "必須填寫名稱", tokenEnvRequired: "必須填寫 token 環境變數", removeBot: "確定移除機器人「{0}」嗎?(.env 中的 token 會保留)",
228
+ changesPending: "有 {0} 項變更尚未套用", impactNow: "⚡ 立即生效", impactAgent: "🔄 重新啟動此 Agent", impactFleet: "🔄🔄 重新啟動 AgEnD", queued: "已加入待套用:{0}", applyFailed: "套用失敗", changesApplied: "✓ 變更已套用,AgEnD 正在協調受影響的 Agent。", reviewChanges: "檢查變更", readyApply: "可套用變更", copied: "已複製", expectedMapping: "預期為 mapping", configLoadFailed: "載入設定失敗 — 請檢查網址中的 token。",
229
+ mustNonNegative: "必須為 0 或更大的數值", mustPositive: "必須大於 0", nonnegativeInteger: "必須是非負整數", failoverEmpty: "空白清單會停用模型備援", concurrencyRange: "必須是 1 到 20 的整數", concurrencyWarning: "在較小的主機上可能超出 CPU 負荷", staggerRange: "必須介於 0 到 30000", staggerWarning: "數值太低可能造成啟動尖峰",
224
230
  },
225
231
  };
226
232
  const HELP = {
@@ -242,9 +248,11 @@
242
248
  let lang = localStorage.getItem("agend_lang") || (navigator.language && navigator.language.toLowerCase().startsWith("zh") ? "zh-TW" : "en");
243
249
  if (!I18N[lang]) lang = "en";
244
250
  const t = (k) => (I18N[lang] && I18N[lang][k]) || I18N.en[k] || k;
251
+ const tf = (k, ...values) => values.reduce((text, value, index) => text.replaceAll(`{${index}}`, String(value)), t(k));
245
252
  function applyStaticI18n() {
246
253
  document.querySelectorAll("[data-i18n]").forEach(e => { e.textContent = t(e.getAttribute("data-i18n")); });
247
254
  document.querySelectorAll("[data-i18n-ph]").forEach(e => { e.setAttribute("placeholder", t(e.getAttribute("data-i18n-ph"))); });
255
+ document.querySelectorAll("[data-i18n-title]").forEach(e => { e.setAttribute("title", t(e.getAttribute("data-i18n-title"))); });
248
256
  // toggle labels that carry expand/collapse state
249
257
  for (const [id, body] of [["advToggle", "advBody"]]) {
250
258
  const b = $(body); if (b) $(id).textContent = b.classList.contains("open") ? t("collapse") : t("expand");
@@ -292,7 +300,7 @@
292
300
  const b = res.body || {}; let html = "";
293
301
  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
302
  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 : "");
303
+ if (res.ok) html = `<span class="s">✓ ${esc(t("saved"))}</span>` + (html ? " " + html : "");
296
304
  else if (!html) html = `<span class="e">✗ ${esc((b && b.error) || ("HTTP " + res.status))}</span>`;
297
305
  elm.innerHTML = html;
298
306
  }
@@ -322,18 +330,19 @@
322
330
  redraw(); return box;
323
331
  }
324
332
  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");
333
+ const impactText = (kind) => t(kind === "now" ? "impactNow" : kind === "fleet" ? "impactFleet" : "impactAgent");
334
+ const impact = (kind) => el("span", { class: "impact" }, impactText(kind));
326
335
  function updatePendingBar() {
327
336
  const n = state.pending.size;
328
337
  $("pendingBar").classList.toggle("open", n > 0);
329
- $("pendingCount").textContent = `${n} change${n === 1 ? "" : "s"} not applied`;
338
+ $("pendingCount").textContent = tf("changesPending", n);
330
339
  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(" · ");
340
+ $("pendingImpact").textContent = [...kinds].map(impactText).join(" · ");
332
341
  }
333
342
  function stageChange(key, change) {
334
343
  state.pending.set(key, change);
335
344
  updatePendingBar();
336
- showBanner(`Queued: ${change.label}`);
345
+ showBanner(tf("queued", change.label));
337
346
  }
338
347
  async function applyPendingChanges() {
339
348
  const button = $("applyChanges"); button.disabled = true;
@@ -342,17 +351,22 @@
342
351
  try { result = await change.apply(); }
343
352
  catch (error) { result = { ok: false, body: { error: error.message } }; }
344
353
  if (!result?.ok) {
345
- showBanner(`${change.label}: ${result?.body?.error || "Apply failed"}`, true);
354
+ showBanner(`${change.label}: ${result?.body?.error || t("applyFailed")}`, true);
346
355
  button.disabled = false; return;
347
356
  }
348
357
  state.pending.delete(key); updatePendingBar();
349
358
  }
350
359
  await api("/api/settings/reload", { method: "POST" });
351
360
  await reload();
352
- showBanner("✓ Changes applied. AgEnD is reconciling affected Agents.");
361
+ showBanner(t("changesApplied"));
353
362
  button.disabled = false;
354
363
  }
355
364
  const hasOwn = (obj, key) => Object.prototype.hasOwnProperty.call(obj || {}, key);
365
+ const HOT_FIELDS = new Set(["tool_progress", "mcp_proxy_reply", "auto_pause_after", "warm_cap", "display_name", "description", "tags", "log_level"]);
366
+ const sameValue = (a, b) => JSON.stringify(a) === JSON.stringify(b);
367
+ function changedFields(next, baseline) {
368
+ return Object.fromEntries(Object.entries(next).filter(([key, value]) => !sameValue(value, baseline[key])));
369
+ }
356
370
  function overrideControl(inst, key, input, inheritedValue, impactKind = "instance") {
357
371
  const inherited = !hasOwn(inst, key);
358
372
  const toggle = el("input", { type: "checkbox" }); toggle.checked = inherited;
@@ -419,7 +433,7 @@
419
433
  const filter = ($("agentSearch")?.value || "").trim().toLowerCase();
420
434
  let names = Object.keys(insts);
421
435
  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; }
436
+ if (!names.length) { host.className = "card empty"; host.textContent = tf("noAgentMatch", filter); return; }
423
437
  host.className = "card";
424
438
  // Group by first tag; "Other" sorts last. Within a group: Running → Crashed
425
439
  // → Stopped, then name.
@@ -455,6 +469,7 @@
455
469
  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
470
  const agentMode = overrideControl(inst, "agent_mode", select(inst.agent_mode || defaults.agent_mode || "mcp", ["mcp", "cli"]), defaults.agent_mode || "mcp");
457
471
  const toolSet = overrideControl(inst, "tool_set", select(inst.tool_set || defaults.tool_set || "full", ["full", "standard", "minimal"]), defaults.tool_set || "full");
472
+ const toolProgress = overrideControl(inst, "tool_progress", select(inst.tool_progress || defaults.tool_progress || "off", ["off", "standard", "verbose"]), defaults.tool_progress || "off", "now");
458
473
  const logLevel = overrideControl(inst, "log_level", select(inst.log_level || defaults.log_level || "info", ["trace", "debug", "info", "warn", "error"]), defaults.log_level || "info");
459
474
  const fLightweight = el("input", { type: "checkbox" }); fLightweight.checked = inst.lightweight ?? defaults.lightweight ?? false;
460
475
  const lightweight = overrideControl(inst, "lightweight", fLightweight, defaults.lightweight ?? false);
@@ -468,18 +483,18 @@
468
483
  const saveBtn = el("button", { class: "primary sm" }, t("save"));
469
484
  const validate = () => {
470
485
  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" : "");
486
+ const validAuto = setValidation(auto.input, autoFeedback, !auto.toggle.checked && (!Number.isFinite(autoValue) || autoValue < 0) ? t("mustNonNegative") : "");
487
+ const validHang = setValidation(hang.input, hangFeedback, !hang.toggle.checked && (!Number.isFinite(hangValue) || hangValue <= 0) ? t("mustPositive") : "");
488
+ setValidation(failover.input, failoverFeedback, "", !failover.toggle.checked && !failover.input.value.trim() ? t("failoverEmpty") : "");
474
489
  saveBtn.disabled = !(validAuto && validHang);
475
490
  return !saveBtn.disabled;
476
491
  };
477
- for (const ctl of [auto, hang, agentMode, toolSet, logLevel, lightweight, failover, displayName]) {
492
+ for (const ctl of [auto, hang, agentMode, toolSet, toolProgress, logLevel, lightweight, failover, displayName]) {
478
493
  ctl.input.addEventListener("input", validate); ctl.input.addEventListener("change", validate); ctl.toggle.addEventListener("change", validate);
479
494
  }
480
495
  saveBtn.onclick = async () => {
481
496
  if (!validate()) return;
482
- const patch = {
497
+ const nextPatch = {
483
498
  backend: fBackend.value, model: fModel.value.trim() || null,
484
499
  working_directory: fWd.value, channel_id: fChan.value || undefined,
485
500
  description: fDesc.value || undefined, systemPrompt: fSys.value || undefined,
@@ -488,11 +503,31 @@
488
503
  hang_detector: { timeout_minutes: hang.toggle.checked ? null : Number(hang.input.value) },
489
504
  agent_mode: agentMode.toggle.checked ? null : agentMode.input.value,
490
505
  tool_set: toolSet.toggle.checked ? null : toolSet.input.value,
506
+ tool_progress: toolProgress.toggle.checked ? null : toolProgress.input.value,
491
507
  log_level: logLevel.toggle.checked ? null : logLevel.input.value,
492
508
  lightweight: lightweight.toggle.checked ? null : lightweight.input.checked,
493
509
  model_failover: failover.toggle.checked ? null : (failover.input.value.trim() ? [failover.input.value.trim()] : []),
494
510
  display_name: displayName.toggle.checked ? null : displayName.input.value.trim(),
495
511
  };
512
+ const patch = changedFields(nextPatch, {
513
+ backend: inst.backend || defaults.backend || "claude-code",
514
+ model: inst.model?.trim() || null,
515
+ working_directory: inst.working_directory || "",
516
+ channel_id: inst.channel_id || undefined,
517
+ description: inst.description || undefined,
518
+ systemPrompt: inst.systemPrompt || undefined,
519
+ tags: inst.tags?.length ? inst.tags : undefined,
520
+ general_topic: !!inst.general_topic,
521
+ auto_pause_after: hasOwn(inst, "auto_pause_after") ? inst.auto_pause_after : null,
522
+ hang_detector: { timeout_minutes: hasOwn(hangRaw, "timeout_minutes") ? hangRaw.timeout_minutes : null },
523
+ agent_mode: hasOwn(inst, "agent_mode") ? inst.agent_mode : null,
524
+ tool_set: hasOwn(inst, "tool_set") ? inst.tool_set : null,
525
+ tool_progress: hasOwn(inst, "tool_progress") ? inst.tool_progress : null,
526
+ log_level: hasOwn(inst, "log_level") ? inst.log_level : null,
527
+ lightweight: hasOwn(inst, "lightweight") ? inst.lightweight : null,
528
+ model_failover: hasOwn(inst, "model_failover") ? inst.model_failover : null,
529
+ display_name: hasOwn(inst, "display_name") ? inst.display_name : null,
530
+ });
496
531
  state.fleet.instances[name] = { ...inst, ...patch };
497
532
  for (const [key, value] of Object.entries(patch)) if (value === null) delete state.fleet.instances[name][key];
498
533
  if (patch.hang_detector?.timeout_minutes === null) {
@@ -501,7 +536,7 @@
501
536
  }
502
537
  stageChange(`agent:${name}`, {
503
538
  label: `Update ${shortName(name, state.fleet.instances[name])}`,
504
- impact: "instance",
539
+ impact: Object.keys(patch).every(key => HOT_FIELDS.has(key)) ? "now" : "instance",
505
540
  apply: () => api(`/api/settings/fleet/instances/${encodeURIComponent(name)}`, { method: "PATCH", body: JSON.stringify(patch) }),
506
541
  });
507
542
  editing = null; renderAgents();
@@ -515,7 +550,7 @@
515
550
  el("div", {}, el("label", {}, t("model"), impact("instance")), fModel)),
516
551
  el("label", {}, t("workingDir"), impact("instance")), fWd,
517
552
  el("div", { class: "grid2" },
518
- el("div", {}, el("label", {}, `${t("autoPause")} (${t("minutes")})`, impact("instance")), auto.row, autoFeedback)),
553
+ el("div", {}, el("label", {}, `${t("autoPause")} (${t("minutes")})`, impact("now")), auto.row, autoFeedback)),
519
554
  el("div", { class: "advanced-only" },
520
555
  el("div", { class: "sub", style: "margin:14px 0 6px;font-weight:600" }, "Advanced Agent settings"),
521
556
  el("div", { class: "grid2" },
@@ -523,7 +558,8 @@
523
558
  el("div", {}, el("label", {}, `${t("hangTimeout")} (${t("minutes")})`), hang.row, hangFeedback),
524
559
  el("div", {}, el("label", {}, t("agentMode")), agentMode.row),
525
560
  el("div", {}, el("label", {}, t("toolSet")), toolSet.row),
526
- el("div", {}, el("label", {}, t("logLevel")), logLevel.row),
561
+ el("div", {}, el("label", {}, t("toolProgress"), impact("now")), toolProgress.row),
562
+ el("div", {}, el("label", {}, t("logLevel"), impact("now")), logLevel.row),
527
563
  el("div", {}, el("label", {}, t("lightweight")), lightweight.row),
528
564
  el("div", {}, el("label", {}, t("modelFailover")), failover.row, failoverFeedback)),
529
565
  el("label", {}, t("systemPrompt"), impact("instance")), fSys,
@@ -544,11 +580,11 @@
544
580
  }
545
581
  async function pauseWakeAgent(name, action) {
546
582
  const res = await api(`/api/settings/instances/${encodeURIComponent(name)}/${action}`, { method: "POST" });
547
- if (!res.ok) alert(res.body?.error || `${action} failed`);
583
+ if (!res.ok) alert(res.body?.error || tf("actionFailed", action));
548
584
  await reloadLive();
549
585
  }
550
586
  async function delAgent(name) {
551
- if (!confirm(`Delete agent "${name}"? This will be applied only after you confirm the pending changes.`)) return;
587
+ if (!confirm(tf("deleteAgent", name))) return;
552
588
  delete state.fleet.instances[name];
553
589
  stageChange(`agent:${name}`, {
554
590
  label: `Delete ${name}`, impact: "fleet",
@@ -569,7 +605,7 @@
569
605
  el("div", { class: "grid2" }, el("div", {}, el("label", {}, t("nameField")), fName), el("div", {}, el("label", {}, t("backend")), fBackend)),
570
606
  el("div", { class: "grid2" }, el("div", {}, el("label", {}, t("workingDir")), fWd), el("div", {}, el("label", {}, t("channelBinding")), fChan)),
571
607
  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; }
608
+ const name = fName.value.trim(); if (!name) { msg.innerHTML = `<span class="e">✗ ${esc(t("nameRequired"))}</span>`; return; }
573
609
  const body = { working_directory: fWd.value, backend: fBackend.value, channel_id: fChan.value || undefined };
574
610
  const res = await api(`/api/settings/fleet/instances/${encodeURIComponent(name)}`, { method: "POST", body: JSON.stringify(body) });
575
611
  resultMsg(msg, res); if (res.ok) { state.newAgentOpen = false; $("newAgentBtn").textContent = t("new"); renderNewAgent(false); await reload(); }
@@ -655,8 +691,8 @@
655
691
  const validate = () => {
656
692
  const autoValue = Number(auto.input.value);
657
693
  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" : "");
694
+ const validAuto = setValidation(auto.input, autoFeedback, !auto.toggle.checked && (!Number.isFinite(autoValue) || autoValue < 0) ? t("mustNonNegative") : "");
695
+ const validContext = setValidation(fContext, contextFeedback, !Number.isInteger(contextValue) || contextValue < 0 ? t("nonnegativeInteger") : "");
660
696
  saveBtn.disabled = !(validAuto && validContext);
661
697
  return !saveBtn.disabled;
662
698
  };
@@ -744,10 +780,10 @@
744
780
  }
745
781
  async function delBot(i) {
746
782
  const chs = channels(); const ch = chs[i];
747
- if (!confirm(`Remove bot "${ch.type} (${chLabel(i)})"? (the token in .env is left untouched)`)) return;
783
+ if (!confirm(tf("removeBot", `${ch.type} (${chLabel(i)})`))) return;
748
784
  chs.splice(i, 1);
749
785
  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");
786
+ const res = await saveChannels(); if (res.ok) await reload(); else alert((res.body && res.body.error) || t("failed"));
751
787
  }
752
788
  function renderNewBot(open) {
753
789
  const host = $("newBot"); host.innerHTML = ""; if (!open) return;
@@ -762,7 +798,7 @@
762
798
  el("div", { class: "grid2" }, el("div", {}, el("label", {}, t("tokenEnv")), fEnv), el("div", {}, el("label", {}, t("groupId")), fGroup)),
763
799
  el("div", { class: "hint" }, t("envHint")),
764
800
  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; }
801
+ if (!fEnv.value.trim()) { msg.innerHTML = `<span class="e">✗ ${esc(t("tokenEnvRequired"))}</span>`; return; }
766
802
  const chs = channels();
767
803
  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
804
  state.fleet.channels = chs; delete state.fleet.channel;
@@ -778,6 +814,11 @@
778
814
  state.fleet.defaults = state.fleet.defaults || {};
779
815
  state.classic.defaults = state.classic.defaults || {};
780
816
  const d = state.fleet.defaults, c = state.classic.defaults;
817
+ // The controls below mutate their arrays in place. Keep raw snapshots so
818
+ // changing one hot default does not also stage untouched access settings as
819
+ // a fleet restart.
820
+ const originalChannels = structuredClone(channels());
821
+ const originalClassicDefaults = structuredClone(c);
781
822
  const hang = d.hang_detector || {};
782
823
  const startup = d.startup || {};
783
824
  const primary = channels()[0];
@@ -790,6 +831,7 @@
790
831
  const fModel = el("input", { type: "text", value: d.model || "", placeholder: "e.g. claude-opus-4-8" });
791
832
  const fAgentMode = select(d.agent_mode || "mcp", ["mcp", "cli"]);
792
833
  const fToolSet = select(d.tool_set || "full", ["full", "standard", "minimal"]);
834
+ const fToolProgress = select(d.tool_progress || "off", ["off", "standard", "verbose"]);
793
835
  const fLogLevel = select(d.log_level || "info", ["trace", "debug", "info", "warn", "error"]);
794
836
  const fLocale = el("select");
795
837
  const fAutoPause = el("input", { type: "number", min: "0", step: "1", value: d.auto_pause_after ?? 0, placeholder: "0 = disabled" });
@@ -807,37 +849,63 @@
807
849
  const validate = () => {
808
850
  const auto = Number(fAutoPause.value), timeout = Number(fHangTimeout.value);
809
851
  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" : "");
852
+ const validAuto = setValidation(fAutoPause, autoFeedback, !Number.isFinite(auto) || auto < 0 ? t("mustNonNegative") : "");
853
+ const validHang = setValidation(fHangTimeout, hangFeedback, !Number.isFinite(timeout) || timeout <= 0 ? t("mustPositive") : "");
854
+ const validConcurrency = setValidation(fConcurrency, concurrencyFeedback, !Number.isInteger(concurrency) || concurrency < 1 || concurrency > 20 ? t("concurrencyRange") : "", concurrency > 4 ? t("concurrencyWarning") : "");
855
+ const validStagger = setValidation(fStagger, staggerFeedback, !Number.isFinite(stagger) || stagger < 0 || stagger > 30000 ? t("staggerRange") : "", stagger < 100 ? t("staggerWarning") : "");
814
856
  saveBtn.disabled = !(validAuto && validHang && validConcurrency && validStagger);
815
857
  return !saveBtn.disabled;
816
858
  };
817
859
  for (const input of [fAutoPause, fHangTimeout, fConcurrency, fStagger]) {
818
860
  input.addEventListener("input", validate); input.addEventListener("change", validate);
819
861
  }
820
- saveBtn.textContent = "Review changes";
862
+ saveBtn.textContent = t("reviewChanges");
821
863
  saveBtn.onclick = () => {
822
864
  if (!validate()) return;
823
- const defaultsPatch = {
865
+ const nextDefaults = {
824
866
  backend: fBackend.value,
825
867
  model: fModel.value || undefined,
826
868
  locale: fLocale.value,
827
869
  auto_pause_after: Number(fAutoPause.value),
828
870
  agent_mode: fAgentMode.value,
829
871
  tool_set: fToolSet.value,
872
+ tool_progress: fToolProgress.value,
830
873
  log_level: fLogLevel.value,
831
874
  hang_detector: { enabled: fHangEnabled.checked, timeout_minutes: Number(fHangTimeout.value) },
832
875
  startup: { concurrency: Number(fConcurrency.value), stagger_delay_ms: Number(fStagger.value) },
833
876
  };
834
- const classicPatch = { backend: fClassicBackend.value, admin_users: c.admin_users, allowed_guilds: c.allowed_guilds };
877
+ const defaultsPatch = changedFields(nextDefaults, {
878
+ backend: d.backend || "claude-code",
879
+ model: d.model || undefined,
880
+ locale: d.locale || "",
881
+ auto_pause_after: d.auto_pause_after ?? 0,
882
+ agent_mode: d.agent_mode || "mcp",
883
+ tool_set: d.tool_set || "full",
884
+ tool_progress: d.tool_progress || "off",
885
+ log_level: d.log_level || "info",
886
+ hang_detector: { enabled: hang.enabled !== false, timeout_minutes: hang.timeout_minutes ?? 15 },
887
+ startup: { concurrency: startup.concurrency ?? 10, stagger_delay_ms: startup.stagger_delay_ms ?? 500 },
888
+ });
889
+ const classicPatch = changedFields(
890
+ { backend: fClassicBackend.value, admin_users: c.admin_users, allowed_guilds: c.allowed_guilds },
891
+ {
892
+ backend: originalClassicDefaults.backend || d.backend || "claude-code",
893
+ admin_users: originalClassicDefaults.admin_users || [],
894
+ allowed_guilds: originalClassicDefaults.allowed_guilds || [],
895
+ },
896
+ );
835
897
  state.fleet.defaults = { ...d, ...defaultsPatch };
836
898
  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>`;
899
+ if (Object.keys(defaultsPatch).length > 0) {
900
+ 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) }) });
901
+ }
902
+ if (primary && !sameValue(channels(), originalChannels)) {
903
+ stageChange("connections:primary", { label: "Update primary connection access", impact: "fleet", apply: () => api("/api/settings/fleet/channels", { method: "PUT", body: JSON.stringify(channels()) }) });
904
+ }
905
+ if (Object.keys(classicPatch).length > 0) {
906
+ stageChange("defaults:classic", { label: "Update ClassicBot defaults and access", impact: "fleet", apply: () => api("/api/settings/classic/defaults", { method: "PUT", body: JSON.stringify(classicPatch) }) });
907
+ }
908
+ msg.innerHTML = `<span class="s">${esc(t("readyApply"))}</span>`;
841
909
  };
842
910
 
843
911
  const section = (title, open, ...children) => {
@@ -851,7 +919,7 @@
851
919
  el("div", { class: "field" }, el("label", {}, t("language"), impact("now")), fLocale),
852
920
  el("div", { class: "field" }, el("label", {}, t("defaultBackend"), impact("instance")), fBackend),
853
921
  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))),
922
+ el("div", { class: "field" }, el("label", {}, `${t("autoPause")} (${t("minutes")})`, impact("now")), fAutoPause, autoFeedback))),
855
923
  section("ClassicBot", true,
856
924
  el("div", { class: "grid2" },
857
925
  el("div", { class: "field" }, el("label", {}, "Classic default backend", impact("instance")), fClassicBackend)),
@@ -863,7 +931,8 @@
863
931
  section(t("runtimeResources"), false,
864
932
  el("div", { class: "grid2" },
865
933
  el("div", { class: "field" }, el("label", {}, t("toolSet"), impact("instance")), fToolSet),
866
- el("div", { class: "field" }, el("label", {}, t("logLevel"), impact("instance")), fLogLevel),
934
+ el("div", { class: "field" }, el("label", {}, t("toolProgress"), impact("now")), fToolProgress),
935
+ el("div", { class: "field" }, el("label", {}, t("logLevel"), impact("now")), fLogLevel),
867
936
  el("div", { class: "field" }, el("label", {}, t("agentMode"), impact("instance")), fAgentMode))),
868
937
  section(t("detection"), false,
869
938
  el("div", { class: "grid2" },
@@ -906,7 +975,7 @@
906
975
  el("button", { class: "sm" + (fmt === "yaml" ? " on" : ""), onclick: () => { state.advFmt = "yaml"; renderAdvanced(); } }, "YAML"),
907
976
  el("button", { class: "sm" + (fmt === "json" ? " on" : ""), onclick: () => { state.advFmt = "json"; renderAdvanced(); } }, "JSON")),
908
977
  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"),
978
+ el("button", { class: "sm", onclick: () => navigator.clipboard.writeText(ser).then(() => { msg.innerHTML = `<span class="s">${esc(t("copied"))}</span>`; }) }, "Copy"),
910
979
  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
980
  );
912
981
  host.append(toolbar);
@@ -922,7 +991,7 @@
922
991
  }
923
992
  // Apply the edited full model: PUT defaults + channels, and diff instances.
924
993
  async function applyFullModel(m, msg) {
925
- if (!m || typeof m !== "object") { msg.innerHTML = `<span class="e">✗ expected a mapping</span>`; return; }
994
+ if (!m || typeof m !== "object") { msg.innerHTML = `<span class="e">✗ ${esc(t("expectedMapping"))}</span>`; return; }
926
995
  const results = [];
927
996
  if (m.defaults) results.push(await api("/api/settings/fleet/defaults", { method: "PUT", body: JSON.stringify(m.defaults) }));
928
997
  const chs = m.channels || (m.channel ? [m.channel] : null);
@@ -960,7 +1029,7 @@
960
1029
  async function reload() {
961
1030
  // Forms edit the user-authored config, not defaults-expanded runtime values.
962
1031
  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; }
1032
+ if (!f.ok) { $("banner").style.display = "block"; $("banner").style.background = "#fef2f2"; $("banner").style.color = "var(--red)"; $("banner").textContent = t("configLoadFailed"); return; }
964
1033
  state.fleet = f.body || {};
965
1034
  const c = await api("/api/settings/classic"); state.classic = c.body || {};
966
1035
  await reloadLive();
@@ -979,12 +1048,12 @@
979
1048
  $("advToggle").onclick = () => { const b = $("advBody"); const open = !b.classList.contains("open"); b.classList.toggle("open", open); $("advToggle").textContent = open ? t("collapse") : t("expand"); };
980
1049
  const advancedEnabled = localStorage.getItem("agend_settings_advanced") === "true";
981
1050
  document.body.classList.toggle("show-advanced", advancedEnabled);
982
- $("advancedToggle").textContent = advancedEnabled ? "Hide advanced" : "Show advanced";
1051
+ $("advancedToggle").textContent = advancedEnabled ? t("hideAdvanced") : t("showAdvanced");
983
1052
  $("advancedToggle").onclick = () => {
984
1053
  const enabled = !document.body.classList.contains("show-advanced");
985
1054
  document.body.classList.toggle("show-advanced", enabled);
986
1055
  localStorage.setItem("agend_settings_advanced", String(enabled));
987
- $("advancedToggle").textContent = enabled ? "Hide advanced" : "Show advanced";
1056
+ $("advancedToggle").textContent = enabled ? t("hideAdvanced") : t("showAdvanced");
988
1057
  };
989
1058
  $("applyChanges").onclick = applyPendingChanges;
990
1059
  $("discardChanges").onclick = async () => { state.pending.clear(); updatePendingBar(); await reload(); };
package/dist/ui/view.html CHANGED
@@ -129,17 +129,17 @@
129
129
  </style>
130
130
  </head>
131
131
  <body>
132
- <button class="help-btn" id="usageBtn" title="AI usage" style="right:140px; display:none">📊</button>
133
- <button class="help-btn" id="densityBtn" title="Font density" style="right:98px">⤢</button>
134
- <button class="help-btn" id="langBtn" title="Language" style="right:56px">🌐</button>
135
- <button class="help-btn" id="helpBtn" title="Help">ℹ️</button>
132
+ <button class="help-btn" id="usageBtn" data-i18n-title="usageButton" title="AI usage" style="right:140px; display:none">📊</button>
133
+ <button class="help-btn" id="densityBtn" data-i18n-title="densityLabel" title="Font density" style="right:98px">⤢</button>
134
+ <button class="help-btn" id="langBtn" data-i18n-title="languageButton" title="Language" style="right:56px">🌐</button>
135
+ <button class="help-btn" id="helpBtn" data-i18n-title="helpButton" title="Help">ℹ️</button>
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
  <aside class="help-panel" id="usagePanel">
142
- <button class="close" id="usageClose" title="Close">×</button>
142
+ <button class="close" id="usageClose" data-i18n-title="close" title="Close">×</button>
143
143
  <div id="usageContent"></div>
144
144
  </aside>
145
145
  <div id="sidebar"><h1 data-i18n="viewTitle">AgEnD · View</h1><div id="list"></div></div>
@@ -152,7 +152,7 @@
152
152
  <div class="meta" id="meta"></div>
153
153
  <div class="desc" id="desc"></div>
154
154
  </div>
155
- <button id="cardToggle" title="Expand profile" aria-expanded="false">▲</button>
155
+ <button id="cardToggle" data-i18n-title="expandProfile" title="Expand profile" aria-expanded="false">▲</button>
156
156
  <button id="editBtn" data-i18n="editProfile">✏️ Edit</button>
157
157
  <div id="edit">
158
158
  <input id="fDisplay" data-i18n-ph="displayName" placeholder="Display name">
@@ -193,7 +193,10 @@
193
193
  tooltipBackend: "Backend: {value}", tooltipStatus: "Status: {value}", tooltipContext: "Context: {value}",
194
194
  tooltipUnavailable: "Unavailable", statusRunning: "Running", statusPaused: "Paused",
195
195
  statusStopped: "Stopped", statusCrashed: "Crashed", statusUnknown: "Unknown",
196
- usageResets: "resets in {t}", usageSoon: "resets soon", usageFail: "Could not load usage data." },
196
+ usageResets: "resets in {t}", usageSoon: "resets soon", usageFail: "Could not load usage data.",
197
+ usageButton: "AI usage", languageButton: "Language", helpButton: "Help", close: "Close", expandProfile: "Expand profile",
198
+ rosterFailed: "Failed to load roster: {error}\n(Is the fleet running?)", paneFailed: "⚠ Could not load pane for {name} (HTTP {status}).",
199
+ tokenRequired: "web.token required", avatarFailed: "Avatar upload failed: {error}", saved: "Saved", saveFailed: "Save failed: {error}" },
197
200
  "zh-TW": { viewTitle: "AgEnD · 檢視", editProfile: "✏️ 編輯", save: "儲存", cancel: "取消",
198
201
  displayName: "顯示名稱", role: "角色", description: "描述", tokenPh: "web.token(儲存時需要)",
199
202
  saveHint: "檢視為公開(唯讀)。儲存需要 web.token(~/.agend/web.token)。", helpTitle: "📖 使用說明",
@@ -208,14 +211,19 @@
208
211
  tooltipBackend: "後端:{value}", tooltipStatus: "狀態:{value}", tooltipContext: "上下文:{value}",
209
212
  tooltipUnavailable: "無資料", statusRunning: "運行中", statusPaused: "已暫停",
210
213
  statusStopped: "已停止", statusCrashed: "已崩潰", statusUnknown: "未知",
211
- usageResets: "{t} 後重置", usageSoon: "即將重置", usageFail: "無法載入用量資料。" },
214
+ usageResets: "{t} 後重置", usageSoon: "即將重置", usageFail: "無法載入用量資料。",
215
+ usageButton: "AI 訂閱用量", languageButton: "語言", helpButton: "使用說明", close: "關閉", expandProfile: "展開個人資料",
216
+ rosterFailed: "載入 instance 清單失敗:{error}\n(Fleet 是否正在執行?)", paneFailed: "⚠ 無法載入 {name} 的 pane(HTTP {status})。",
217
+ tokenRequired: "需要 web.token", avatarFailed: "上傳頭像失敗:{error}", saved: "已儲存", saveFailed: "儲存失敗:{error}" },
212
218
  };
213
219
  let lang = localStorage.getItem("agend_lang") || (navigator.language && navigator.language.toLowerCase().startsWith("zh") ? "zh-TW" : "en");
214
220
  if (!I18N[lang]) lang = "en";
215
221
  const T = (k) => (I18N[lang] && I18N[lang][k]) || I18N.en[k] || k;
222
+ const TF = (k, values = {}) => Object.entries(values).reduce((s, [name, value]) => s.replaceAll(`{${name}}`, String(value)), T(k));
216
223
  function applyI18n() {
217
224
  document.querySelectorAll("[data-i18n]").forEach(e => { e.textContent = T(e.getAttribute("data-i18n")); });
218
225
  document.querySelectorAll("[data-i18n-ph]").forEach(e => { e.setAttribute("placeholder", T(e.getAttribute("data-i18n-ph"))); });
226
+ document.querySelectorAll("[data-i18n-title]").forEach(e => { e.setAttribute("title", T(e.getAttribute("data-i18n-title"))); });
219
227
  const hc = q("helpContent");
220
228
  if (hc) { hc.innerHTML = ""; const h3 = document.createElement("h3"); h3.textContent = T("helpTitle"); hc.append(h3);
221
229
  const ul = document.createElement("ul"); for (const k of ["help1","help2","help3","help4","help5"]) { const li = document.createElement("li"); li.textContent = T(k); ul.append(li); } hc.append(ul); }
@@ -557,7 +565,7 @@
557
565
  if (!current && roster.length) current = roster[0].instance_name;
558
566
  computeOrder();
559
567
  renderList(); renderCard();
560
- } catch (e) { q("pre").textContent = "Failed to load roster: " + e.message + "\n(Is the fleet running?)"; }
568
+ } catch (e) { q("pre").textContent = TF("rosterFailed", { error: e.message }); }
561
569
  }
562
570
 
563
571
  // ── Terminal ──────────────────────────────────────────
@@ -575,7 +583,7 @@
575
583
  if (!r.ok) {
576
584
  // Surface the failure instead of leaving the previous instance's output
577
585
  // on screen (which reads as "switching did nothing").
578
- q("pre").textContent = `⚠ Could not load pane for ${target} (HTTP ${r.status}).`;
586
+ q("pre").textContent = TF("paneFailed", { name: target, status: r.status });
579
587
  return;
580
588
  }
581
589
  const text = await r.text();
@@ -714,7 +722,7 @@
714
722
  const it = roster.find(r => r.instance_name === current);
715
723
  if (!it) return;
716
724
  const wt = q("fToken").value.trim();
717
- if (!wt) { q("msg").className = "err"; q("msg").textContent = "web.token required"; return; }
725
+ if (!wt) { q("msg").className = "err"; q("msg").textContent = T("tokenRequired"); return; }
718
726
  localStorage.setItem("agend_web_token", wt);
719
727
  const name = encodeURIComponent(current);
720
728
  try {
@@ -727,12 +735,12 @@
727
735
  const file = q("fAvatar").files[0];
728
736
  if (file) {
729
737
  const ar = await fetch(`/api/avatar/${name}`, { method: "POST", headers: { "Content-Type": file.type, "X-Agend-Token": wt }, body: file });
730
- if (!ar.ok) { const e = await ar.json().catch(() => ({})); throw new Error("avatar: " + (e.error || ar.status)); }
738
+ if (!ar.ok) { const e = await ar.json().catch(() => ({})); throw new Error(TF("avatarFailed", { error: e.error || ar.status })); }
731
739
  }
732
- q("msg").className = "ok"; q("msg").textContent = "Saved";
740
+ q("msg").className = "ok"; q("msg").textContent = T("saved");
733
741
  await loadRoster();
734
742
  setTimeout(exitEdit, 500);
735
- } catch (e) { q("msg").className = "err"; q("msg").textContent = e.message; }
743
+ } catch (e) { q("msg").className = "err"; q("msg").textContent = e.message.startsWith(T("avatarFailed").split("{")[0]) ? e.message : TF("saveFailed", { error: e.message }); }
736
744
  }
737
745
 
738
746
  q("editBtn").onclick = enterEdit;
@@ -2,7 +2,7 @@
2
2
  * AI usage HTTP API.
3
3
  *
4
4
  * GET /api/ai-usage → { fetchedAt, providers: [...] } — live subscription
5
- * usage for the CLI backends logged in on this machine (see providers.ts).
5
+ * usage for providers used by running or paused instances (see providers.ts).
6
6
  *
7
7
  * Auth: read-only GET, open like the other /view data routes (the caller's
8
8
  * global token gate exempts isUsagePath, mirroring isViewPath). Server binds
@@ -18,11 +18,17 @@ import { type ProviderUsage } from "./providers.js";
18
18
  export interface UsageApiContext {
19
19
  readonly fleetConfig: FleetConfig | null;
20
20
  readonly logger: Logger;
21
+ /** Providers used by running or paused fleet/Classic instances. */
22
+ getActiveUsageProviderIds?(): ReadonlySet<string>;
21
23
  }
22
24
  export type UsagePayload = {
23
25
  fetchedAt: string;
24
26
  providers: ProviderUsage[];
25
27
  };
28
+ /** Map runtime backend names to the subscription provider row they consume. */
29
+ export declare function usageProviderIdForBackend(backend: string | undefined): string | null;
30
+ /** Filter a cached all-provider snapshot without mutating the shared cache. */
31
+ export declare function filterUsageProviders(payload: UsagePayload, providerIds?: Iterable<string>): UsagePayload;
26
32
  export declare function setUsageFetcherForTests(fn: (() => Promise<UsagePayload>) | null): void;
27
33
  /**
28
34
  * The same cached snapshot the HTTP route serves, for in-process callers: the
@@ -30,7 +36,7 @@ export declare function setUsageFetcherForTests(fn: (() => Promise<UsagePayload>
30
36
  * surfaces, because the 5-minute TTL exists to protect vendor rate limits and a
31
37
  * second entry point that bypassed it would defeat that.
32
38
  */
33
- export declare function getUsageSnapshot(force?: boolean): Promise<UsagePayload>;
39
+ export declare function getUsageSnapshot(force?: boolean, providerIds?: Iterable<string>): Promise<UsagePayload>;
34
40
  /**
35
41
  * Render a usage payload as one compact chat message.
36
42
  *