@songsid/agend 2.1.4-beta.18 → 2.1.4-beta.19

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 (49) hide show
  1. package/dist/backend/antigravity.d.ts +1 -0
  2. package/dist/backend/antigravity.js +4 -0
  3. package/dist/backend/antigravity.js.map +1 -1
  4. package/dist/backend/codex.d.ts +17 -0
  5. package/dist/backend/codex.js +51 -0
  6. package/dist/backend/codex.js.map +1 -1
  7. package/dist/backend/types.d.ts +10 -0
  8. package/dist/backend/types.js.map +1 -1
  9. package/dist/channel/adapters/discord.js +48 -21
  10. package/dist/channel/adapters/discord.js.map +1 -1
  11. package/dist/channel/adapters/telegram.js +5 -3
  12. package/dist/channel/adapters/telegram.js.map +1 -1
  13. package/dist/channel/types.d.ts +1 -1
  14. package/dist/cli.js +31 -21
  15. package/dist/cli.js.map +1 -1
  16. package/dist/config-validator.js +3 -0
  17. package/dist/config-validator.js.map +1 -1
  18. package/dist/config.js +5 -1
  19. package/dist/config.js.map +1 -1
  20. package/dist/daemon.d.ts +33 -0
  21. package/dist/daemon.js +134 -6
  22. package/dist/daemon.js.map +1 -1
  23. package/dist/doctor.js +34 -27
  24. package/dist/doctor.js.map +1 -1
  25. package/dist/fleet-context.d.ts +2 -0
  26. package/dist/fleet-manager.d.ts +14 -0
  27. package/dist/fleet-manager.js +305 -75
  28. package/dist/fleet-manager.js.map +1 -1
  29. package/dist/locale.js +656 -330
  30. package/dist/locale.js.map +1 -1
  31. package/dist/outbound-handlers.js +5 -1
  32. package/dist/outbound-handlers.js.map +1 -1
  33. package/dist/restart-progress.d.ts +2 -0
  34. package/dist/restart-progress.js +3 -0
  35. package/dist/restart-progress.js.map +1 -1
  36. package/dist/scheduler/db.d.ts +5 -0
  37. package/dist/scheduler/db.js +35 -0
  38. package/dist/scheduler/db.js.map +1 -1
  39. package/dist/tips.d.ts +34 -0
  40. package/dist/tips.js +554 -0
  41. package/dist/tips.js.map +1 -0
  42. package/dist/topic-commands.d.ts +5 -2
  43. package/dist/topic-commands.js +101 -61
  44. package/dist/topic-commands.js.map +1 -1
  45. package/dist/types.d.ts +2 -0
  46. package/dist/ui/dashboard.html +55 -32
  47. package/dist/ui/settings.html +46 -37
  48. package/dist/ui/view.html +23 -15
  49. 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>
@@ -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 建立。",
@@ -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,14 +351,14 @@
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);
@@ -424,7 +433,7 @@
424
433
  const filter = ($("agentSearch")?.value || "").trim().toLowerCase();
425
434
  let names = Object.keys(insts);
426
435
  if (filter) names = names.filter(n => n.toLowerCase().includes(filter) || shortName(n, insts[n]).toLowerCase().includes(filter));
427
- 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; }
428
437
  host.className = "card";
429
438
  // Group by first tag; "Other" sorts last. Within a group: Running → Crashed
430
439
  // → Stopped, then name.
@@ -474,9 +483,9 @@
474
483
  const saveBtn = el("button", { class: "primary sm" }, t("save"));
475
484
  const validate = () => {
476
485
  const autoValue = Number(auto.input.value), hangValue = Number(hang.input.value);
477
- const validAuto = setValidation(auto.input, autoFeedback, !auto.toggle.checked && (!Number.isFinite(autoValue) || autoValue < 0) ? "Must be 0 or greater" : "");
478
- const validHang = setValidation(hang.input, hangFeedback, !hang.toggle.checked && (!Number.isFinite(hangValue) || hangValue <= 0) ? "Must be greater than 0" : "");
479
- 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") : "");
480
489
  saveBtn.disabled = !(validAuto && validHang);
481
490
  return !saveBtn.disabled;
482
491
  };
@@ -571,11 +580,11 @@
571
580
  }
572
581
  async function pauseWakeAgent(name, action) {
573
582
  const res = await api(`/api/settings/instances/${encodeURIComponent(name)}/${action}`, { method: "POST" });
574
- if (!res.ok) alert(res.body?.error || `${action} failed`);
583
+ if (!res.ok) alert(res.body?.error || tf("actionFailed", action));
575
584
  await reloadLive();
576
585
  }
577
586
  async function delAgent(name) {
578
- if (!confirm(`Delete agent "${name}"? This will be applied only after you confirm the pending changes.`)) return;
587
+ if (!confirm(tf("deleteAgent", name))) return;
579
588
  delete state.fleet.instances[name];
580
589
  stageChange(`agent:${name}`, {
581
590
  label: `Delete ${name}`, impact: "fleet",
@@ -596,7 +605,7 @@
596
605
  el("div", { class: "grid2" }, el("div", {}, el("label", {}, t("nameField")), fName), el("div", {}, el("label", {}, t("backend")), fBackend)),
597
606
  el("div", { class: "grid2" }, el("div", {}, el("label", {}, t("workingDir")), fWd), el("div", {}, el("label", {}, t("channelBinding")), fChan)),
598
607
  el("div", { class: "actions" }, el("button", { class: "primary sm", onclick: async () => {
599
- 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; }
600
609
  const body = { working_directory: fWd.value, backend: fBackend.value, channel_id: fChan.value || undefined };
601
610
  const res = await api(`/api/settings/fleet/instances/${encodeURIComponent(name)}`, { method: "POST", body: JSON.stringify(body) });
602
611
  resultMsg(msg, res); if (res.ok) { state.newAgentOpen = false; $("newAgentBtn").textContent = t("new"); renderNewAgent(false); await reload(); }
@@ -682,8 +691,8 @@
682
691
  const validate = () => {
683
692
  const autoValue = Number(auto.input.value);
684
693
  const contextValue = Number(fContext.value);
685
- const validAuto = setValidation(auto.input, autoFeedback, !auto.toggle.checked && (!Number.isFinite(autoValue) || autoValue < 0) ? "Must be 0 or greater" : "");
686
- 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") : "");
687
696
  saveBtn.disabled = !(validAuto && validContext);
688
697
  return !saveBtn.disabled;
689
698
  };
@@ -771,10 +780,10 @@
771
780
  }
772
781
  async function delBot(i) {
773
782
  const chs = channels(); const ch = chs[i];
774
- 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;
775
784
  chs.splice(i, 1);
776
785
  state.fleet.channels = chs; delete state.fleet.channel;
777
- 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"));
778
787
  }
779
788
  function renderNewBot(open) {
780
789
  const host = $("newBot"); host.innerHTML = ""; if (!open) return;
@@ -789,7 +798,7 @@
789
798
  el("div", { class: "grid2" }, el("div", {}, el("label", {}, t("tokenEnv")), fEnv), el("div", {}, el("label", {}, t("groupId")), fGroup)),
790
799
  el("div", { class: "hint" }, t("envHint")),
791
800
  el("div", { class: "actions" }, el("button", { class: "primary sm", onclick: async () => {
792
- 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; }
793
802
  const chs = channels();
794
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: [] } });
795
804
  state.fleet.channels = chs; delete state.fleet.channel;
@@ -840,17 +849,17 @@
840
849
  const validate = () => {
841
850
  const auto = Number(fAutoPause.value), timeout = Number(fHangTimeout.value);
842
851
  const concurrency = Number(fConcurrency.value), stagger = Number(fStagger.value);
843
- const validAuto = setValidation(fAutoPause, autoFeedback, !Number.isFinite(auto) || auto < 0 ? "Must be 0 or greater" : "");
844
- const validHang = setValidation(fHangTimeout, hangFeedback, !Number.isFinite(timeout) || timeout <= 0 ? "Must be greater than 0" : "");
845
- 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" : "");
846
- 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") : "");
847
856
  saveBtn.disabled = !(validAuto && validHang && validConcurrency && validStagger);
848
857
  return !saveBtn.disabled;
849
858
  };
850
859
  for (const input of [fAutoPause, fHangTimeout, fConcurrency, fStagger]) {
851
860
  input.addEventListener("input", validate); input.addEventListener("change", validate);
852
861
  }
853
- saveBtn.textContent = "Review changes";
862
+ saveBtn.textContent = t("reviewChanges");
854
863
  saveBtn.onclick = () => {
855
864
  if (!validate()) return;
856
865
  const nextDefaults = {
@@ -896,7 +905,7 @@
896
905
  if (Object.keys(classicPatch).length > 0) {
897
906
  stageChange("defaults:classic", { label: "Update ClassicBot defaults and access", impact: "fleet", apply: () => api("/api/settings/classic/defaults", { method: "PUT", body: JSON.stringify(classicPatch) }) });
898
907
  }
899
- msg.innerHTML = `<span class="s">Ready to apply</span>`;
908
+ msg.innerHTML = `<span class="s">${esc(t("readyApply"))}</span>`;
900
909
  };
901
910
 
902
911
  const section = (title, open, ...children) => {
@@ -966,7 +975,7 @@
966
975
  el("button", { class: "sm" + (fmt === "yaml" ? " on" : ""), onclick: () => { state.advFmt = "yaml"; renderAdvanced(); } }, "YAML"),
967
976
  el("button", { class: "sm" + (fmt === "json" ? " on" : ""), onclick: () => { state.advFmt = "json"; renderAdvanced(); } }, "JSON")),
968
977
  el("button", { class: "sm", onclick: () => { state.advEdit = !state.advEdit; renderAdvanced(); } }, state.advEdit ? "View" : "Edit"),
969
- 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"),
970
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"),
971
980
  );
972
981
  host.append(toolbar);
@@ -982,7 +991,7 @@
982
991
  }
983
992
  // Apply the edited full model: PUT defaults + channels, and diff instances.
984
993
  async function applyFullModel(m, msg) {
985
- 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; }
986
995
  const results = [];
987
996
  if (m.defaults) results.push(await api("/api/settings/fleet/defaults", { method: "PUT", body: JSON.stringify(m.defaults) }));
988
997
  const chs = m.channels || (m.channel ? [m.channel] : null);
@@ -1020,7 +1029,7 @@
1020
1029
  async function reload() {
1021
1030
  // Forms edit the user-authored config, not defaults-expanded runtime values.
1022
1031
  const f = await api("/api/settings/fleet/raw");
1023
- 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; }
1024
1033
  state.fleet = f.body || {};
1025
1034
  const c = await api("/api/settings/classic"); state.classic = c.body || {};
1026
1035
  await reloadLive();
@@ -1039,12 +1048,12 @@
1039
1048
  $("advToggle").onclick = () => { const b = $("advBody"); const open = !b.classList.contains("open"); b.classList.toggle("open", open); $("advToggle").textContent = open ? t("collapse") : t("expand"); };
1040
1049
  const advancedEnabled = localStorage.getItem("agend_settings_advanced") === "true";
1041
1050
  document.body.classList.toggle("show-advanced", advancedEnabled);
1042
- $("advancedToggle").textContent = advancedEnabled ? "Hide advanced" : "Show advanced";
1051
+ $("advancedToggle").textContent = advancedEnabled ? t("hideAdvanced") : t("showAdvanced");
1043
1052
  $("advancedToggle").onclick = () => {
1044
1053
  const enabled = !document.body.classList.contains("show-advanced");
1045
1054
  document.body.classList.toggle("show-advanced", enabled);
1046
1055
  localStorage.setItem("agend_settings_advanced", String(enabled));
1047
- $("advancedToggle").textContent = enabled ? "Hide advanced" : "Show advanced";
1056
+ $("advancedToggle").textContent = enabled ? t("hideAdvanced") : t("showAdvanced");
1048
1057
  };
1049
1058
  $("applyChanges").onclick = applyPendingChanges;
1050
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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@songsid/agend",
3
- "version": "2.1.4-beta.18",
3
+ "version": "2.1.4-beta.19",
4
4
  "description": "Multi-agent fleet daemon — run any coding CLI (Claude, Gemini, Codex, OpenCode) from Telegram",
5
5
  "type": "module",
6
6
  "bin": {