@songsid/agend 2.1.4-beta.33 → 2.1.4-beta.34

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.
@@ -205,6 +205,7 @@
205
205
  wnIntro: "Recent highlights:", releaseNotes: "📄 View full Release Notes →", languageButton: "Language", helpButton: "Help", showAdditional: "Show additional controls", hideAdvanced: "Hide advanced", showAdvanced: "Show advanced",
206
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
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",
208
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",
209
210
  },
210
211
  "zh-TW": {
@@ -226,6 +227,7 @@
226
227
  wnIntro: "近期更新重點:", releaseNotes: "📄 查看完整 Release Notes →", languageButton: "語言", helpButton: "使用說明", showAdditional: "顯示更多控制項", hideAdvanced: "隱藏進階設定", showAdvanced: "顯示進階設定",
227
228
  saved: "已儲存", failed: "失敗", actionFailed: "{0}失敗", noAgentMatch: "沒有符合「{0}」的 Agent。", deleteAgent: "確定刪除 Agent「{0}」嗎?只有在確認套用待處理變更後才會生效。", nameRequired: "必須填寫名稱", tokenEnvRequired: "必須填寫 token 環境變數", removeBot: "確定移除機器人「{0}」嗎?(.env 中的 token 會保留)",
228
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 變更",
229
231
  mustNonNegative: "必須為 0 或更大的數值", mustPositive: "必須大於 0", nonnegativeInteger: "必須是非負整數", failoverEmpty: "空白清單會停用模型備援", concurrencyRange: "必須是 1 到 20 的整數", concurrencyWarning: "在較小的主機上可能超出 CPU 負荷", staggerRange: "必須介於 0 到 30000", staggerWarning: "數值太低可能造成啟動尖峰",
230
232
  },
231
233
  };
@@ -346,6 +348,15 @@
346
348
  }
347
349
  async function applyPendingChanges() {
348
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
+ }
349
360
  for (const [key, change] of [...state.pending]) {
350
361
  let result;
351
362
  try { result = await change.apply(); }
@@ -384,6 +395,15 @@
384
395
  feedback.textContent = error ? `✗ ${error}` : warning ? `⚠ ${warning}` : "";
385
396
  return !error;
386
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
+ }
387
407
 
388
408
  // ── My Agents ──
389
409
  let editing = null;
@@ -399,12 +419,23 @@
399
419
  const STATUS_TEXT = { running: "🟢 Running", paused: "⏸ Paused", crashed: "🔴 Crashed", stopped: "⚪ Stopped" };
400
420
  const EXECUTION_TEXT = { idle: "🟢 Idle", working: "🔵 Working", stuck: "🔴 Stuck", paused: "⏸ Paused", crashed: "🔴 Crashed", stopped: "⚪ Stopped", running: "🟢 Running" };
401
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"]);
402
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
+ }
403
433
  function compactRow(name, inst) {
404
434
  const st = state.live[name]?.status || "stopped";
405
435
  const running = st === "running";
406
436
  const paused = st === "paused";
407
437
  const short = shortName(name, inst);
438
+ const summary = effectiveSummary(name, inst);
408
439
  const chTag = agentChannelLabel(inst);
409
440
  const item = el("div", { class: "item" });
410
441
  // Single fixed-height line: dot + short name + backend + channel tag + actions.
@@ -412,7 +443,8 @@
412
443
  item.append(el("div", { class: "item-row" },
413
444
  el("span", { class: "dot " + st, title: STATUS_TEXT[st] || st }),
414
445
  el("span", { class: "name", title: name }, short),
415
- 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}`),
416
448
  chTag ? el("span", { class: "tag" + (chTag.includes("persona") ? " persona" : chTag.includes("telegram") ? " tg" : "") }, chTag) : null,
417
449
  el("span", { class: "spacer" }),
418
450
  el("button", { class: "sm ghost", onclick: () => { editing = editing === name ? null : name; renderAgents(); } }, editing === name ? t("close") : t("edit")),
@@ -640,16 +672,21 @@
640
672
  function botEditForm(ch, i) {
641
673
  ch.access = ch.access || { mode: "locked", allowed_users: [] };
642
674
  ch.access.allowed_users = ch.access.allowed_users || [];
675
+ const previousAccess = structuredClone(ch.access);
643
676
  const box = el("div", { class: "edit" });
644
677
  const fMode = select(ch.access.mode || "locked", ["open", "locked", "pairing"]);
645
678
  const users = [...ch.access.allowed_users];
646
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);
647
683
  box.append(
648
684
  el("div", { class: "grid2" },
649
- el("div", {}, el("label", {}, t("accessMode"), impact("fleet")), fMode),
650
- 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"))),
651
687
  el("div", { class: "actions" },
652
688
  el("button", { class: "primary sm", onclick: async () => {
689
+ if (!confirmAccessChange(previousAccess, fMode.value, users)) return;
653
690
  const chs = channels();
654
691
  chs[i] = { ...chs[i], access: { ...chs[i].access, mode: fMode.value, allowed_users: users } };
655
692
  const res = await saveChannels(msg);
@@ -661,6 +698,7 @@
661
698
  } }, t("save")),
662
699
  el("button", { class: "sm ghost", onclick: () => { editingBot = null; renderBots(); } }, t("cancel")), msg),
663
700
  );
701
+ validateAccess();
664
702
  return box;
665
703
  }
666
704
 
@@ -755,11 +793,14 @@
755
793
  const runtime = classicRuntime(c.instanceName);
756
794
  const st = runtime.status;
757
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 });
758
798
  const item = el("div", { class: "item" });
759
799
  item.append(el("div", { class: "item-row" },
760
800
  el("span", { class: "dot " + runtime.execution, title: EXECUTION_TEXT[runtime.execution] || runtime.execution }),
761
- el("span", { class: "name", title: c.instanceName || "" }, c.name || c.instanceName || "?"),
762
- 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}`),
763
804
  el("span", { class: "tag" + (adapter !== "discord" ? " persona" : "") }, adapter),
764
805
  c.collab ? el("span", { class: "tag", style: "background:#ecfdf5;color:#065f46" }, "collab") : null,
765
806
  el("span", { class: "spacer" }),
@@ -822,8 +863,8 @@
822
863
  const hang = d.hang_detector || {};
823
864
  const startup = d.startup || {};
824
865
  const primary = channels()[0];
825
- if (primary) primary.access = primary.access || { mode: "locked", allowed_users: [] };
826
- 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 || [])];
827
868
  c.admin_users = c.admin_users || []; c.allowed_guilds = c.allowed_guilds || [];
828
869
 
829
870
  const fBackend = select(d.backend || "claude-code", BACKENDS);
@@ -832,6 +873,7 @@
832
873
  const fAgentMode = select(d.agent_mode || "mcp", ["mcp", "cli"]);
833
874
  const fToolSet = select(d.tool_set || "full", ["full", "standard", "minimal"]);
834
875
  const fToolProgress = select(d.tool_progress || "off", ["off", "standard", "verbose"]);
876
+ const fAccessMode = select(originalPrimaryAccess.mode || "locked", ["locked", "open", "pairing"]);
835
877
  const fLogLevel = select(d.log_level || "info", ["trace", "debug", "info", "warn", "error"]);
836
878
  const fLocale = el("select");
837
879
  const fAutoPause = el("input", { type: "number", min: "0", step: "1", value: d.auto_pause_after ?? 0, placeholder: "0 = disabled" });
@@ -845,6 +887,7 @@
845
887
  const hangFeedback = el("div", { class: "feedback" });
846
888
  const concurrencyFeedback = el("div", { class: "feedback" });
847
889
  const staggerFeedback = el("div", { class: "feedback" });
890
+ const accessFeedback = el("div", { class: "feedback" });
848
891
  const saveBtn = el("button", { class: "primary" }, t("save"));
849
892
  const validate = () => {
850
893
  const auto = Number(fAutoPause.value), timeout = Number(fHangTimeout.value);
@@ -853,12 +896,14 @@
853
896
  const validHang = setValidation(fHangTimeout, hangFeedback, !Number.isFinite(timeout) || timeout <= 0 ? t("mustPositive") : "");
854
897
  const validConcurrency = setValidation(fConcurrency, concurrencyFeedback, !Number.isInteger(concurrency) || concurrency < 1 || concurrency > 20 ? t("concurrencyRange") : "", concurrency > 4 ? t("concurrencyWarning") : "");
855
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") : "");
856
900
  saveBtn.disabled = !(validAuto && validHang && validConcurrency && validStagger);
857
901
  return !saveBtn.disabled;
858
902
  };
859
903
  for (const input of [fAutoPause, fHangTimeout, fConcurrency, fStagger]) {
860
904
  input.addEventListener("input", validate); input.addEventListener("change", validate);
861
905
  }
906
+ fAccessMode.addEventListener("change", validate);
862
907
  saveBtn.textContent = t("reviewChanges");
863
908
  saveBtn.onclick = () => {
864
909
  if (!validate()) return;
@@ -899,8 +944,22 @@
899
944
  if (Object.keys(defaultsPatch).length > 0) {
900
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) }) });
901
946
  }
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()) }) });
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
+ }
904
963
  }
905
964
  if (Object.keys(classicPatch).length > 0) {
906
965
  stageChange("defaults:classic", { label: "Update ClassicBot defaults and access", impact: "fleet", apply: () => api("/api/settings/classic/defaults", { method: "PUT", body: JSON.stringify(classicPatch) }) });
@@ -943,7 +1002,11 @@
943
1002
  el("div", { class: "field" }, el("label", {}, t("concurrency"), impact("fleet")), fConcurrency, concurrencyFeedback),
944
1003
  el("div", { class: "field" }, el("label", {}, `${t("staggerDelay")} (ms)`, impact("fleet")), fStagger, staggerFeedback))),
945
1004
  section("Advanced & Access", false,
946
- 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")))),
947
1010
  el("div", { class: "field" }, el("div", { class: "actions" }, saveBtn, msg)),
948
1011
  );
949
1012
  validate();
@@ -1016,7 +1079,15 @@
1016
1079
  state.fleetUp = f.ok;
1017
1080
  state.live = {}; state.classicLive = {};
1018
1081
  if (f.ok && f.body?.instances) for (const i of f.body.instances) {
1019
- 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
+ };
1020
1091
  if (i.classic) state.classicLive[i.name] = { status: i.status, state: i.state };
1021
1092
  }
1022
1093
  // /api/fleet carries live tri-state for Classic instances. Profiles remain
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@songsid/agend",
3
- "version": "2.1.4-beta.33",
3
+ "version": "2.1.4-beta.34",
4
4
  "description": "Multi-agent fleet daemon — run any coding CLI (Claude, Gemini, Codex, OpenCode) from Telegram",
5
5
  "type": "module",
6
6
  "bin": {