@songsid/agend 2.1.0-beta.3 → 2.1.0-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 (97) hide show
  1. package/README.md +1 -0
  2. package/README.zh-TW.md +2 -1
  3. package/dist/access-path.js +3 -3
  4. package/dist/access-path.js.map +1 -1
  5. package/dist/agent-cli.js +1 -1
  6. package/dist/agent-cli.js.map +1 -1
  7. package/dist/agent-endpoint.d.ts +8 -0
  8. package/dist/agent-endpoint.js +36 -8
  9. package/dist/agent-endpoint.js.map +1 -1
  10. package/dist/backend/antigravity.js +3 -2
  11. package/dist/backend/antigravity.js.map +1 -1
  12. package/dist/backend/claude-code.js +4 -4
  13. package/dist/backend/claude-code.js.map +1 -1
  14. package/dist/backend/factory.js +4 -1
  15. package/dist/backend/factory.js.map +1 -1
  16. package/dist/backend/grok.d.ts +51 -0
  17. package/dist/backend/grok.js +282 -0
  18. package/dist/backend/grok.js.map +1 -0
  19. package/dist/backend/kiro.js +3 -2
  20. package/dist/backend/kiro.js.map +1 -1
  21. package/dist/backend/types.d.ts +12 -2
  22. package/dist/backend/types.js +1 -0
  23. package/dist/backend/types.js.map +1 -1
  24. package/dist/channel/adapters/discord.d.ts +20 -0
  25. package/dist/channel/adapters/discord.js +85 -11
  26. package/dist/channel/adapters/discord.js.map +1 -1
  27. package/dist/channel/adapters/telegram.js +4 -1
  28. package/dist/channel/adapters/telegram.js.map +1 -1
  29. package/dist/channel/mcp-tools.js +17 -1
  30. package/dist/channel/mcp-tools.js.map +1 -1
  31. package/dist/classic-channel-manager.d.ts +14 -1
  32. package/dist/classic-channel-manager.js +65 -4
  33. package/dist/classic-channel-manager.js.map +1 -1
  34. package/dist/cli.js +108 -16
  35. package/dist/cli.js.map +1 -1
  36. package/dist/config-validator.js +68 -1
  37. package/dist/config-validator.js.map +1 -1
  38. package/dist/config.d.ts +3 -1
  39. package/dist/config.js +13 -1
  40. package/dist/config.js.map +1 -1
  41. package/dist/context-guardian.d.ts +2 -0
  42. package/dist/context-guardian.js +10 -2
  43. package/dist/context-guardian.js.map +1 -1
  44. package/dist/daemon-entry.js +2 -1
  45. package/dist/daemon-entry.js.map +1 -1
  46. package/dist/daemon.d.ts +76 -6
  47. package/dist/daemon.js +451 -113
  48. package/dist/daemon.js.map +1 -1
  49. package/dist/fleet-context.d.ts +4 -0
  50. package/dist/fleet-manager.d.ts +88 -7
  51. package/dist/fleet-manager.js +707 -129
  52. package/dist/fleet-manager.js.map +1 -1
  53. package/dist/general-knowledge/skills/fleet-config/SKILL.md +1 -1
  54. package/dist/general-knowledge/skills/model-discovery/SKILL.md +1 -0
  55. package/dist/instance-lifecycle.d.ts +11 -1
  56. package/dist/instance-lifecycle.js +64 -22
  57. package/dist/instance-lifecycle.js.map +1 -1
  58. package/dist/locale.js +24 -2
  59. package/dist/locale.js.map +1 -1
  60. package/dist/logger.d.ts +6 -0
  61. package/dist/logger.js +29 -10
  62. package/dist/logger.js.map +1 -1
  63. package/dist/outbound-handlers.d.ts +3 -0
  64. package/dist/outbound-handlers.js +203 -2
  65. package/dist/outbound-handlers.js.map +1 -1
  66. package/dist/outbound-schemas.d.ts +40 -1
  67. package/dist/outbound-schemas.js +36 -1
  68. package/dist/outbound-schemas.js.map +1 -1
  69. package/dist/pause-marker.d.ts +5 -0
  70. package/dist/pause-marker.js +36 -0
  71. package/dist/pause-marker.js.map +1 -0
  72. package/dist/settings-api.d.ts +20 -2
  73. package/dist/settings-api.js +213 -9
  74. package/dist/settings-api.js.map +1 -1
  75. package/dist/setup-wizard.js +8 -0
  76. package/dist/setup-wizard.js.map +1 -1
  77. package/dist/statusline-watcher.d.ts +1 -1
  78. package/dist/statusline-watcher.js +3 -2
  79. package/dist/statusline-watcher.js.map +1 -1
  80. package/dist/tmux-control.d.ts +7 -0
  81. package/dist/tmux-control.js +22 -1
  82. package/dist/tmux-control.js.map +1 -1
  83. package/dist/tmux-manager.d.ts +1 -1
  84. package/dist/tmux-manager.js.map +1 -1
  85. package/dist/topic-commands.d.ts +19 -1
  86. package/dist/topic-commands.js +132 -23
  87. package/dist/topic-commands.js.map +1 -1
  88. package/dist/transcript-monitor.js +2 -0
  89. package/dist/transcript-monitor.js.map +1 -1
  90. package/dist/types.d.ts +23 -1
  91. package/dist/ui/dashboard.html +2 -2
  92. package/dist/ui/settings.html +453 -74
  93. package/dist/web-api.d.ts +1 -0
  94. package/dist/web-api.js +24 -15
  95. package/dist/web-api.js.map +1 -1
  96. package/dist/workflow-templates/default.md +0 -4
  97. package/package.json +2 -1
@@ -8,10 +8,10 @@
8
8
  * { margin: 0; padding: 0; box-sizing: border-box; }
9
9
  :root {
10
10
  --bg:#ffffff; --card:#f9fafb; --border:#e5e7eb; --fg:#111827; --muted:#6b7280;
11
- --accent:#2563eb; --accent-hover:#1d4ed8; --green:#059669; --red:#dc2626;
11
+ --accent:#2563eb; --accent-hover:#1d4ed8; --green:#059669; --red:#dc2626; --yellow:#b45309;
12
12
  --radius:8px; --radius-sm:6px;
13
13
  }
14
- body { background: var(--bg); color: var(--fg); font-family: Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; font-size: 14px; line-height: 1.5; padding-bottom: 60px; }
14
+ body { background: var(--bg); color: var(--fg); font-family: Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; font-size: 14px; line-height: 1.5; padding-bottom: 88px; }
15
15
  header { display: flex; align-items: center; gap: 12px; padding: 16px 24px; border-bottom: 1px solid var(--border); position: sticky; top: 0; background: var(--bg); z-index: 20; }
16
16
  header h1 { font-size: 18px; font-weight: 700; }
17
17
  .grow { flex: 1; }
@@ -32,7 +32,7 @@
32
32
  .item .del-btn { visibility: hidden; }
33
33
  .item:hover .del-btn { visibility: visible; }
34
34
  .dot { width: 9px; height: 9px; border-radius: 50%; flex: 0 0 9px; background: #9ca3af; }
35
- .dot.running { background: var(--green); } .dot.crashed { background: var(--red); } .dot.stopped { background: #9ca3af; }
35
+ .dot.running, .dot.idle { background: var(--green); } .dot.working { background:var(--accent); } .dot.paused { background:#f59e0b; } .dot.crashed, .dot.stuck { background: var(--red); } .dot.stopped { background: #9ca3af; }
36
36
  .name { font-weight: 600; }
37
37
  .tag { font-size: 12px; color: var(--muted); background: #eef2ff; color: #3730a3; padding: 1px 7px; border-radius: 10px; }
38
38
  .tag.persona { background: #fdf2f8; color: #9d174d; }
@@ -49,7 +49,7 @@
49
49
  button.danger:hover { background: #fef2f2; }
50
50
  button.sm { padding: 4px 9px; font-size: 12px; }
51
51
  label { display: block; font-size: 12px; font-weight: 500; color: var(--muted); margin: 10px 0 4px; }
52
- input[type=text], select { width: 100%; font-family: inherit; font-size: 13px; padding: 8px 10px; border: 1px solid var(--border); border-radius: var(--radius-sm); background: #fff; color: var(--fg); }
52
+ input[type=text], input[type=number], select { width: 100%; font-family: inherit; font-size: 13px; padding: 8px 10px; border: 1px solid var(--border); border-radius: var(--radius-sm); background: #fff; color: var(--fg); }
53
53
  input:focus, select:focus { outline: 2px solid #bfdbfe; border-color: var(--accent); }
54
54
  .edit { padding: 14px 16px; background: #fff; border-top: 1px solid var(--border); }
55
55
  .grid2 { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
@@ -63,8 +63,33 @@
63
63
  .msg { font-size: 12px; margin-left: 4px; }
64
64
  .msg .e { color: var(--red); } .msg .w { color: #b45309; } .msg .s { color: var(--green); }
65
65
  .hint { font-size: 12px; color: var(--muted); margin-top: 6px; }
66
+ .impact { display:inline-block; font-size:11px; color:var(--muted); margin-left:6px; font-weight:400; }
67
+ .feedback { min-height:17px; font-size:12px; margin-top:3px; }
68
+ .feedback.error { color:var(--red); } .feedback.warning { color:var(--yellow); }
69
+ input.invalid, select.invalid { border-color:var(--red); background:#fef2f2; }
70
+ button:disabled { opacity:.5; cursor:not-allowed; }
71
+ .settings-section { border-bottom:1px solid var(--border); background:#fff; }
72
+ .settings-section:last-child { border-bottom:none; }
73
+ .settings-section > summary { cursor:pointer; list-style:none; padding:12px 16px; font-weight:650; background:#f9fafb; }
74
+ .settings-section > summary::-webkit-details-marker { display:none; }
75
+ .settings-section > summary::before { content:"▶"; display:inline-block; width:18px; color:var(--muted); font-size:10px; }
76
+ .settings-section[open] > summary::before { content:"▼"; }
77
+ .section-body { padding:0 0 2px; }
78
+ .inherit-row { display:flex; gap:8px; align-items:center; margin-top:6px; color:var(--muted); font-size:12px; }
66
79
  .adv-body { display: none; padding: 14px 16px; }
67
80
  .adv-body.open { display: block; }
81
+ .advanced-only { display:none !important; }
82
+ body.show-advanced .advanced-only { display:block !important; }
83
+ .more-menu { position:relative; }
84
+ .more-menu summary { list-style:none; cursor:pointer; padding:4px 9px; color:var(--muted); }
85
+ .more-menu summary::-webkit-details-marker { display:none; }
86
+ .more-menu .menu { position:absolute; right:0; top:30px; z-index:10; background:#fff; border:1px solid var(--border); border-radius:var(--radius-sm); box-shadow:0 8px 24px rgba(0,0,0,.12); padding:5px; min-width:130px; }
87
+ .more-menu .menu button { width:100%; text-align:left; border:0; }
88
+ .pending-bar { position:fixed; left:0; right:0; bottom:0; display:none; align-items:center; gap:12px; padding:12px max(16px, calc((100vw - 860px) / 2 + 24px)); background:#111827; color:#fff; z-index:35; box-shadow:0 -4px 18px rgba(0,0,0,.18); }
89
+ .pending-bar.open { display:flex; }
90
+ .pending-bar .grow { flex:1; }
91
+ .pending-bar button.primary { background:#fff; color:#111827; border-color:#fff; }
92
+ .level-label { font-size:11px; color:var(--muted); text-transform:uppercase; letter-spacing:.4px; margin-bottom:8px; }
68
93
  .toolbar { display: flex; gap: 6px; align-items: center; margin-bottom: 8px; flex-wrap: wrap; }
69
94
  .seg { display: inline-flex; border: 1px solid var(--border); border-radius: var(--radius-sm); overflow: hidden; }
70
95
  .seg button { border: none; border-radius: 0; }
@@ -84,6 +109,19 @@
84
109
  .help-panel li { font-size: 13px; margin: 3px 0; }
85
110
  .help-panel .close { position: absolute; top: 14px; right: 16px; cursor: pointer; color: var(--muted); font-size: 22px; line-height: 1; background: none; border: none; }
86
111
  .help-panel .tip { margin-top: 16px; padding: 10px 12px; background: var(--card); border: 1px solid var(--border); border-radius: var(--radius-sm); font-size: 13px; }
112
+ @media (max-width: 640px) {
113
+ header { padding:10px 12px; flex-wrap:wrap; } header h1 { font-size:16px; }
114
+ main { padding:12px 10px; } section { margin-bottom:18px; }
115
+ .grid2 { grid-template-columns:1fr; gap:0; }
116
+ .item { padding:9px 10px; } .item-row { flex-wrap:wrap; }
117
+ .item-row .spacer { flex-basis:100%; height:0; }
118
+ .sub { margin-left:0; } .field { padding:10px 12px; }
119
+ #agentSearch { width:130px !important; margin-right:0 !important; }
120
+ button { min-height:36px; } button.sm { min-height:32px; }
121
+ .pending-bar { padding:10px 12px; flex-wrap:wrap; }
122
+ .pending-bar .grow { display:none; }
123
+ .pending-bar button { flex:1; }
124
+ }
87
125
  </style>
88
126
  </head>
89
127
  <body>
@@ -93,7 +131,7 @@
93
131
  <div class="grow"></div>
94
132
  <button class="help-btn" id="langBtn" title="Language">🌐</button>
95
133
  <button class="help-btn" id="helpBtn" title="Help">ℹ️</button>
96
- <button id="reloadBtn" class="primary" data-i18n="reloadFleet" title="Apply saved config to running instances (SIGHUP)">↻ Reload Fleet</button>
134
+ <button id="advancedToggle" title="Show additional controls">Show advanced</button>
97
135
  </header>
98
136
  <div class="help-backdrop" id="helpBackdrop"></div>
99
137
  <aside class="help-panel" id="helpPanel">
@@ -114,7 +152,7 @@
114
152
  </section>
115
153
 
116
154
  <section>
117
- <div class="sec-head"><h2 data-i18n="bots">🔌 Bots</h2><div class="grow"></div><button class="sm" id="newBotBtn" data-i18n="add">+ Add</button></div>
155
+ <div class="sec-head"><h2>🔌 Connections &amp; Bots</h2><div class="grow"></div><button class="sm advanced-only" id="newBotBtn" data-i18n="add">+ Add</button></div>
118
156
  <div id="newBot"></div>
119
157
  <div class="card" id="bots"></div>
120
158
  </section>
@@ -125,15 +163,18 @@
125
163
  </section>
126
164
 
127
165
  <section>
128
- <div class="sec-head"><h2 data-i18n="whatsNew">🆕 What's New</h2><div class="grow"></div><button class="sm" id="wnToggle" data-i18n="expand">▶ Expand</button></div>
129
- <div class="card"><div class="adv-body" id="wnBody"></div></div>
130
- </section>
131
-
132
- <section>
133
- <div class="sec-head"><h2 data-i18n="advanced">📋 Advanced</h2><div class="grow"></div><button class="sm" id="advToggle" data-i18n="expand">▶ Expand</button></div>
166
+ <div class="level-label">Developer · Level 3</div>
167
+ <div class="sec-head"><h2>📋 Developer YAML</h2><div class="grow"></div><button class="sm" id="advToggle" data-i18n="expand">▶ Expand</button></div>
134
168
  <div class="card"><div class="adv-body" id="advBody"></div></div>
135
169
  </section>
136
170
  </main>
171
+ <div class="pending-bar" id="pendingBar">
172
+ <strong id="pendingCount">0 changes not applied</strong>
173
+ <span id="pendingImpact">⚡ Immediately · 🔄 Restart this Agent · 🔄🔄 Restart AgEnD</span>
174
+ <span class="grow"></span>
175
+ <button id="discardChanges">Discard</button>
176
+ <button class="primary" id="applyChanges">Apply changes</button>
177
+ </div>
137
178
  <script>
138
179
  (() => {
139
180
  "use strict";
@@ -141,7 +182,7 @@
141
182
  const $ = (id) => document.getElementById(id);
142
183
  const el = (tag, attrs = {}, ...kids) => { const e = document.createElement(tag); for (const k in attrs) { if (k === "class") e.className = attrs[k]; else if (k === "html") e.innerHTML = attrs[k]; else if (k.startsWith("on")) e.addEventListener(k.slice(2), attrs[k]); else e.setAttribute(k, attrs[k]); } for (const c of kids) if (c != null) e.append(c); return e; };
143
184
  const esc = (s) => String(s).replace(/[&<>]/g, c => ({ "&":"&amp;","<":"&lt;",">":"&gt;" }[c]));
144
- const BACKENDS = ["claude-code", "kiro-cli", "codex", "opencode", "gemini-cli", "antigravity"];
185
+ const BACKENDS = ["claude-code", "kiro-cli", "codex", "opencode", "gemini-cli", "antigravity", "grok"];
145
186
 
146
187
  // ── i18n ──
147
188
  const I18N = {
@@ -152,6 +193,8 @@
152
193
  edit: "Edit", close: "Close", save: "Save", cancel: "Cancel", create: "Create", start: "Start", stop: "Stop",
153
194
  backend: "Backend", model: "Model", workingDir: "Working directory", channelBinding: "Channel binding",
154
195
  description: "Description", systemPrompt: "System prompt", tags: "Tags", generalTopicField: "General topic (fleet dispatcher)",
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",
155
198
  language: "Language (bot replies)", defaultBackend: "Default backend", defaultModel: "Default model", allowedUsers: "Allowed users (primary bot)",
156
199
  classicAdmins: "ClassicBot admins", allowedGuilds: "Allowed guilds", type: "Type", adapterId: "Adapter id",
157
200
  tokenEnv: "Bot token env var", groupId: "Group / guild id", nameField: "Name",
@@ -168,6 +211,8 @@
168
211
  edit: "編輯", close: "關閉", save: "儲存", cancel: "取消", create: "建立", start: "啟動", stop: "停止",
169
212
  backend: "後端", model: "模型", workingDir: "工作目錄", channelBinding: "頻道綁定",
170
213
  description: "描述", systemPrompt: "System prompt", tags: "標籤", generalTopicField: "General topic(fleet 分派器)",
214
+ 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",
171
216
  language: "語言(bot 回覆)", defaultBackend: "預設後端", defaultModel: "預設模型", allowedUsers: "允許使用者(主 bot)",
172
217
  classicAdmins: "ClassicBot 管理員", allowedGuilds: "允許的伺服器", type: "類型", adapterId: "Adapter id",
173
218
  tokenEnv: "Bot token 環境變數", groupId: "群組 / guild id", nameField: "名稱",
@@ -201,7 +246,7 @@
201
246
  document.querySelectorAll("[data-i18n]").forEach(e => { e.textContent = t(e.getAttribute("data-i18n")); });
202
247
  document.querySelectorAll("[data-i18n-ph]").forEach(e => { e.setAttribute("placeholder", t(e.getAttribute("data-i18n-ph"))); });
203
248
  // toggle labels that carry expand/collapse state
204
- for (const [id, body] of [["advToggle", "advBody"], ["wnToggle", "wnBody"]]) {
249
+ for (const [id, body] of [["advToggle", "advBody"]]) {
205
250
  const b = $(body); if (b) $(id).textContent = b.classList.contains("open") ? t("collapse") : t("expand");
206
251
  }
207
252
  renderHelp();
@@ -251,9 +296,17 @@
251
296
  else if (!html) html = `<span class="e">✗ ${esc((b && b.error) || ("HTTP " + res.status))}</span>`;
252
297
  elm.innerHTML = html;
253
298
  }
299
+ function showBanner(text, error = false) {
300
+ const b = $("banner");
301
+ b.style.display = "block";
302
+ b.style.background = error ? "#fef2f2" : "#ecfdf5";
303
+ b.style.color = error ? "var(--red)" : "var(--green)";
304
+ b.textContent = text;
305
+ setTimeout(() => { b.style.display = "none"; }, 6000);
306
+ }
254
307
 
255
308
  // ── State ──
256
- const state = { fleet: {}, classic: {}, classicStatus: {}, live: {}, fleetUp: false, advFmt: "yaml", advEdit: false, collapsedGroups: new Set(), version: "", newAgentOpen: false, newBotOpen: false };
309
+ const state = { fleet: {}, classic: {}, classicStatus: {}, classicLive: {}, live: {}, fleetUp: false, advFmt: "yaml", advEdit: false, collapsedGroups: new Set(), version: "", newAgentOpen: false, newBotOpen: false, pending: new Map() };
257
310
  const channels = () => state.fleet.channels || (state.fleet.channel ? [state.fleet.channel] : []);
258
311
  const channelIds = () => channels().map(c => c.id || c.type).filter(Boolean);
259
312
  const chLabel = (i) => i === 0 ? "primary" : "persona";
@@ -268,7 +321,55 @@
268
321
  box.append(chips, el("div", { class: "chip-add" }, inp, el("button", { class: "sm", onclick: add }, "+ Add")));
269
322
  redraw(); return box;
270
323
  }
271
- function select(value, options) { const s = el("select"); for (const o of options) { const opt = el("option", {}, o); opt.value = o; if (o === value) opt.selected = true; s.append(opt); } return s; }
324
+ 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");
326
+ function updatePendingBar() {
327
+ const n = state.pending.size;
328
+ $("pendingBar").classList.toggle("open", n > 0);
329
+ $("pendingCount").textContent = `${n} change${n === 1 ? "" : "s"} not applied`;
330
+ 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(" · ");
332
+ }
333
+ function stageChange(key, change) {
334
+ state.pending.set(key, change);
335
+ updatePendingBar();
336
+ showBanner(`Queued: ${change.label}`);
337
+ }
338
+ async function applyPendingChanges() {
339
+ const button = $("applyChanges"); button.disabled = true;
340
+ for (const [key, change] of [...state.pending]) {
341
+ let result;
342
+ try { result = await change.apply(); }
343
+ catch (error) { result = { ok: false, body: { error: error.message } }; }
344
+ if (!result?.ok) {
345
+ showBanner(`${change.label}: ${result?.body?.error || "Apply failed"}`, true);
346
+ button.disabled = false; return;
347
+ }
348
+ state.pending.delete(key); updatePendingBar();
349
+ }
350
+ await api("/api/settings/reload", { method: "POST" });
351
+ await reload();
352
+ showBanner("✓ Changes applied. AgEnD is reconciling affected Agents.");
353
+ button.disabled = false;
354
+ }
355
+ const hasOwn = (obj, key) => Object.prototype.hasOwnProperty.call(obj || {}, key);
356
+ function overrideControl(inst, key, input, inheritedValue, impactKind = "instance") {
357
+ const inherited = !hasOwn(inst, key);
358
+ const toggle = el("input", { type: "checkbox" }); toggle.checked = inherited;
359
+ input.disabled = inherited;
360
+ if (inherited && inheritedValue !== undefined) {
361
+ if (input.type === "checkbox") input.checked = !!inheritedValue;
362
+ else input.value = inheritedValue;
363
+ }
364
+ toggle.onchange = () => { input.disabled = toggle.checked; };
365
+ return { input, toggle, row: el("div", {}, input, el("label", { class: "inherit-row" }, toggle, el("span", {}, t("inheritDefault")), impact(impactKind))) };
366
+ }
367
+ function setValidation(input, feedback, error, warning) {
368
+ input.classList.toggle("invalid", !!error);
369
+ feedback.className = "feedback" + (error ? " error" : warning ? " warning" : "");
370
+ feedback.textContent = error ? `✗ ${error}` : warning ? `⚠ ${warning}` : "";
371
+ return !error;
372
+ }
272
373
 
273
374
  // ── My Agents ──
274
375
  let editing = null;
@@ -281,12 +382,14 @@
281
382
  }
282
383
  // Display name: profile display_name → else strip the -t<topicId> suffix.
283
384
  const shortName = (name, inst) => (inst && inst.display_name) || name.replace(/-t\d+$/, "");
284
- const STATUS_TEXT = { running: "🟢 Running", crashed: "🔴 Crashed", stopped: "⚪ Stopped" };
285
- const STATUS_ORDER = { running: 0, crashed: 1, stopped: 2 };
385
+ const STATUS_TEXT = { running: "🟢 Running", paused: "⏸ Paused", crashed: "🔴 Crashed", stopped: "⚪ Stopped" };
386
+ const EXECUTION_TEXT = { idle: "🟢 Idle", working: "🔵 Working", stuck: "🔴 Stuck", paused: "⏸ Paused", crashed: "🔴 Crashed", stopped: "⚪ Stopped", running: "🟢 Running" };
387
+ const STATUS_ORDER = { running: 0, paused: 1, crashed: 2, stopped: 3 };
286
388
  const groupOf = (inst) => (inst.tags && inst.tags[0]) || "Other";
287
389
  function compactRow(name, inst) {
288
390
  const st = state.live[name]?.status || "stopped";
289
391
  const running = st === "running";
392
+ const paused = st === "paused";
290
393
  const short = shortName(name, inst);
291
394
  const chTag = agentChannelLabel(inst);
292
395
  const item = el("div", { class: "item" });
@@ -299,8 +402,12 @@
299
402
  chTag ? el("span", { class: "tag" + (chTag.includes("persona") ? " persona" : chTag.includes("telegram") ? " tg" : "") }, chTag) : null,
300
403
  el("span", { class: "spacer" }),
301
404
  el("button", { class: "sm ghost", onclick: () => { editing = editing === name ? null : name; renderAgents(); } }, editing === name ? t("close") : t("edit")),
302
- el("button", { class: "sm", onclick: () => toggleAgent(name, running) }, running ? t("stop") : t("start")),
303
- el("button", { class: "sm danger del-btn", title: t("close"), onclick: () => delAgent(name) }, "🗑️"),
405
+ running ? el("button", { class: "sm", onclick: () => pauseWakeAgent(name, "pause") }, t("pause")) : null,
406
+ paused ? el("button", { class: "sm", onclick: () => pauseWakeAgent(name, "wake") }, t("wake")) : null,
407
+ !paused ? el("button", { class: "sm", onclick: () => toggleAgent(name, running) }, running ? t("stop") : t("start")) : null,
408
+ el("details", { class: "more-menu" },
409
+ el("summary", { title: "More actions" }, "⋯"),
410
+ el("div", { class: "menu" }, el("button", { class: "sm danger", onclick: () => delAgent(name) }, "Delete Agent"))),
304
411
  ));
305
412
  if (editing === name) item.append(agentEditForm(name, inst));
306
413
  return item;
@@ -333,6 +440,7 @@
333
440
  }
334
441
  function agentEditForm(name, inst) {
335
442
  const box = el("div", { class: "edit" });
443
+ const defaults = state.fleet.defaults || {};
336
444
  const fBackend = select(inst.backend || state.fleet.defaults?.backend || "claude-code", BACKENDS);
337
445
  const fModel = el("input", { type: "text", value: inst.model || "", placeholder: "(inherit default)" });
338
446
  const fWd = el("input", { type: "text", value: inst.working_directory || "" });
@@ -341,29 +449,92 @@
341
449
  const fSys = el("input", { type: "text", value: inst.systemPrompt || "", placeholder: "e.g. file:prompts/dev.md" });
342
450
  const tags = [...(inst.tags || [])];
343
451
  const fGeneral = el("input", { type: "checkbox" }); fGeneral.checked = !!inst.general_topic;
452
+ const auto = overrideControl(inst, "auto_pause_after", el("input", { type: "number", min: "0", step: "1", value: inst.auto_pause_after ?? defaults.auto_pause_after ?? 0, placeholder: "0 = disabled" }), defaults.auto_pause_after ?? 0);
453
+ const hangRaw = inst.hang_detector || {};
454
+ const hangHolder = hasOwn(hangRaw, "timeout_minutes") ? { value: hangRaw.timeout_minutes } : {};
455
+ 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
+ const agentMode = overrideControl(inst, "agent_mode", select(inst.agent_mode || defaults.agent_mode || "mcp", ["mcp", "cli"]), defaults.agent_mode || "mcp");
457
+ const toolSet = overrideControl(inst, "tool_set", select(inst.tool_set || defaults.tool_set || "full", ["full", "standard", "minimal"]), defaults.tool_set || "full");
458
+ const logLevel = overrideControl(inst, "log_level", select(inst.log_level || defaults.log_level || "info", ["trace", "debug", "info", "warn", "error"]), defaults.log_level || "info");
459
+ const fLightweight = el("input", { type: "checkbox" }); fLightweight.checked = inst.lightweight ?? defaults.lightweight ?? false;
460
+ const lightweight = overrideControl(inst, "lightweight", fLightweight, defaults.lightweight ?? false);
461
+ const fallbackValue = Array.isArray(inst.model_failover) ? (inst.model_failover[0] || "") : (defaults.model_failover?.[0] || "");
462
+ const failover = overrideControl(inst, "model_failover", el("input", { type: "text", value: fallbackValue, placeholder: "e.g. sonnet" }), defaults.model_failover?.[0] || "");
463
+ const displayName = overrideControl(inst, "display_name", el("input", { type: "text", value: inst.display_name ?? defaults.display_name ?? "", placeholder: name }), defaults.display_name || "");
464
+ const autoFeedback = el("div", { class: "feedback" });
465
+ const hangFeedback = el("div", { class: "feedback" });
466
+ const failoverFeedback = el("div", { class: "feedback" });
344
467
  const msg = el("span", { class: "msg" });
468
+ const saveBtn = el("button", { class: "primary sm" }, t("save"));
469
+ const validate = () => {
470
+ 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" : "");
474
+ saveBtn.disabled = !(validAuto && validHang);
475
+ return !saveBtn.disabled;
476
+ };
477
+ for (const ctl of [auto, hang, agentMode, toolSet, logLevel, lightweight, failover, displayName]) {
478
+ ctl.input.addEventListener("input", validate); ctl.input.addEventListener("change", validate); ctl.toggle.addEventListener("change", validate);
479
+ }
480
+ saveBtn.onclick = async () => {
481
+ if (!validate()) return;
482
+ const patch = {
483
+ backend: fBackend.value, model: fModel.value || undefined,
484
+ working_directory: fWd.value, channel_id: fChan.value || undefined,
485
+ description: fDesc.value || undefined, systemPrompt: fSys.value || undefined,
486
+ tags: tags.length ? tags : undefined, general_topic: fGeneral.checked,
487
+ auto_pause_after: auto.toggle.checked ? null : Number(auto.input.value),
488
+ hang_detector: { timeout_minutes: hang.toggle.checked ? null : Number(hang.input.value) },
489
+ agent_mode: agentMode.toggle.checked ? null : agentMode.input.value,
490
+ tool_set: toolSet.toggle.checked ? null : toolSet.input.value,
491
+ log_level: logLevel.toggle.checked ? null : logLevel.input.value,
492
+ lightweight: lightweight.toggle.checked ? null : lightweight.input.checked,
493
+ model_failover: failover.toggle.checked ? null : (failover.input.value.trim() ? [failover.input.value.trim()] : []),
494
+ display_name: displayName.toggle.checked ? null : displayName.input.value.trim(),
495
+ };
496
+ state.fleet.instances[name] = { ...inst, ...patch };
497
+ for (const [key, value] of Object.entries(patch)) if (value === null) delete state.fleet.instances[name][key];
498
+ if (patch.hang_detector?.timeout_minutes === null) {
499
+ state.fleet.instances[name].hang_detector = { ...(inst.hang_detector || {}) };
500
+ delete state.fleet.instances[name].hang_detector.timeout_minutes;
501
+ }
502
+ stageChange(`agent:${name}`, {
503
+ label: `Update ${shortName(name, state.fleet.instances[name])}`,
504
+ impact: "instance",
505
+ apply: () => api(`/api/settings/fleet/instances/${encodeURIComponent(name)}`, { method: "PATCH", body: JSON.stringify(patch) }),
506
+ });
507
+ editing = null; renderAgents();
508
+ };
345
509
  box.append(
346
510
  el("div", { class: "sub", style: "margin:0 0 8px" }, "ID: " + name),
347
- el("div", { class: "grid2" }, el("div", {}, el("label", {}, t("backend")), fBackend), el("div", {}, el("label", {}, t("model")), fModel)),
348
- el("label", {}, t("workingDir")), fWd,
349
- el("div", { class: "grid2" }, el("div", {}, el("label", {}, t("channelBinding")), fChan), el("div", {}, el("label", {}, t("description")), fDesc)),
350
- el("label", {}, t("systemPrompt")), fSys,
351
- el("label", {}, t("tags")), chipList(tags, null, "tag"),
352
- el("label", { style: "display:flex;align-items:center;gap:8px;cursor:pointer" }, fGeneral, el("span", {}, t("generalTopicField"))),
511
+ el("div", { class: "grid2" },
512
+ el("div", {}, el("label", {}, t("displayName"), impact("now")), displayName.row),
513
+ el("div", {}, el("label", {}, t("description"), impact("now")), fDesc),
514
+ el("div", {}, el("label", {}, t("backend"), impact("instance")), fBackend),
515
+ el("div", {}, el("label", {}, t("model"), impact("instance")), fModel)),
516
+ el("label", {}, t("workingDir"), impact("instance")), fWd,
517
+ el("div", { class: "grid2" },
518
+ el("div", {}, el("label", {}, `${t("autoPause")} (${t("minutes")})`, impact("instance")), auto.row, autoFeedback)),
519
+ el("div", { class: "advanced-only" },
520
+ el("div", { class: "sub", style: "margin:14px 0 6px;font-weight:600" }, "Advanced Agent settings"),
521
+ el("div", { class: "grid2" },
522
+ el("div", {}, el("label", {}, t("channelBinding"), impact("fleet")), fChan),
523
+ el("div", {}, el("label", {}, `${t("hangTimeout")} (${t("minutes")})`), hang.row, hangFeedback),
524
+ el("div", {}, el("label", {}, t("agentMode")), agentMode.row),
525
+ el("div", {}, el("label", {}, t("toolSet")), toolSet.row),
526
+ el("div", {}, el("label", {}, t("logLevel")), logLevel.row),
527
+ el("div", {}, el("label", {}, t("lightweight")), lightweight.row),
528
+ el("div", {}, el("label", {}, t("modelFailover")), failover.row, failoverFeedback)),
529
+ el("label", {}, t("systemPrompt"), impact("instance")), fSys,
530
+ el("label", {}, t("tags"), impact("now")), chipList(tags, null, "tag"),
531
+ el("label", { style: "display:flex;align-items:center;gap:8px;cursor:pointer" }, fGeneral, el("span", {}, t("generalTopicField")), impact("fleet"))),
353
532
  el("div", { class: "actions" },
354
- el("button", { class: "primary sm", onclick: async () => {
355
- const patch = {
356
- backend: fBackend.value, model: fModel.value || undefined,
357
- working_directory: fWd.value, channel_id: fChan.value || undefined,
358
- description: fDesc.value || undefined, systemPrompt: fSys.value || undefined,
359
- tags: tags.length ? tags : undefined, general_topic: fGeneral.checked,
360
- };
361
- const res = await api(`/api/settings/fleet/instances/${encodeURIComponent(name)}`, { method: "PATCH", body: JSON.stringify(patch) });
362
- resultMsg(msg, res); if (res.ok) await reload();
363
- } }, t("save")),
533
+ saveBtn,
364
534
  el("button", { class: "sm ghost", onclick: () => { editing = null; renderAgents(); } }, t("cancel")),
365
535
  msg),
366
536
  );
537
+ validate();
367
538
  return box;
368
539
  }
369
540
  async function toggleAgent(name, running) {
@@ -371,10 +542,19 @@
371
542
  else await api(`/api/instance/${encodeURIComponent(name)}/start`, { method: "POST" });
372
543
  setTimeout(reloadLive, 800);
373
544
  }
545
+ async function pauseWakeAgent(name, action) {
546
+ const res = await api(`/api/settings/instances/${encodeURIComponent(name)}/${action}`, { method: "POST" });
547
+ if (!res.ok) alert(res.body?.error || `${action} failed`);
548
+ await reloadLive();
549
+ }
374
550
  async function delAgent(name) {
375
- if (!confirm(`Delete agent "${name}"? (config removed; running daemon stops on next Reload)`)) return;
376
- const res = await api(`/api/settings/fleet/instances/${encodeURIComponent(name)}`, { method: "DELETE" });
377
- if (res.ok) await reload(); else alert((res.body && res.body.error) || "Delete failed");
551
+ if (!confirm(`Delete agent "${name}"? This will be applied only after you confirm the pending changes.`)) return;
552
+ delete state.fleet.instances[name];
553
+ stageChange(`agent:${name}`, {
554
+ label: `Delete ${name}`, impact: "fleet",
555
+ apply: () => api(`/api/settings/fleet/instances/${encodeURIComponent(name)}`, { method: "DELETE" }),
556
+ });
557
+ renderAgents();
378
558
  }
379
559
  function renderNewAgent(open) {
380
560
  const host = $("newAgent"); host.innerHTML = "";
@@ -399,6 +579,7 @@
399
579
  }
400
580
 
401
581
  // ── Bots ──
582
+ let editingBot = null;
402
583
  function renderBots() {
403
584
  const host = $("bots"); host.innerHTML = ""; const chs = channels();
404
585
  if (!chs.length) { host.className = "card empty"; host.textContent = t("noBots"); return; }
@@ -408,20 +589,121 @@
408
589
  const type = ch.type || "?";
409
590
  item.append(el("div", { class: "item-row" },
410
591
  el("span", { class: "name" }, type === "telegram" ? "Telegram" : "Discord"),
411
- el("span", { class: "tag" + (i === 0 ? "" : " persona") + (type === "telegram" ? " tg" : "") }, chLabel(i)),
412
- el("span", { class: "sub", style: "margin:0" }, ch.bot_token_env || "(no token env)"),
592
+ el("span", { class: "tag" + (i === 0 ? "" : " persona") + (type === "telegram" ? " tg" : "") }, ch.id || chLabel(i)),
593
+ el("span", { class: "sub advanced-only", style: "margin:0" }, ch.bot_token_env || "(no token env)"),
413
594
  el("span", { class: "spacer" }),
414
- el("span", { style: "color:var(--green);font-size:13px" }, state.fleetUp ? t("online") : ""),
415
- el("button", { class: "sm danger", onclick: () => delBot(i) }, "🗑️"),
595
+ el("span", { style: `color:${state.fleetUp ? "var(--green)" : "var(--red)"};font-size:13px` }, state.fleetUp ? "Connected" : "Problem"),
596
+ el("button", { class: "sm ghost advanced-only", onclick: () => { editingBot = editingBot === i ? null : i; renderBots(); } }, editingBot === i ? t("close") : t("edit")),
597
+ el("button", { class: "sm danger advanced-only", onclick: () => delBot(i) }, "Delete"),
416
598
  ));
599
+ if (editingBot === i) item.append(botEditForm(ch, i));
417
600
  host.append(item);
418
601
  });
419
602
  }
420
603
 
421
- // ── ClassicBot Channels (read-only; created dynamically via /start /stop) ──
604
+ function botEditForm(ch, i) {
605
+ ch.access = ch.access || { mode: "locked", allowed_users: [] };
606
+ ch.access.allowed_users = ch.access.allowed_users || [];
607
+ const box = el("div", { class: "edit" });
608
+ const fMode = select(ch.access.mode || "locked", ["open", "locked", "pairing"]);
609
+ const users = [...ch.access.allowed_users];
610
+ const msg = el("span", { class: "msg" });
611
+ box.append(
612
+ el("div", { class: "grid2" },
613
+ el("div", {}, el("label", {}, t("accessMode"), impact("fleet")), fMode),
614
+ el("div", {}, el("label", {}, t("allowedUsers"), impact("fleet")), chipList(users, null, "user id"))),
615
+ el("div", { class: "actions" },
616
+ el("button", { class: "primary sm", onclick: async () => {
617
+ const chs = channels();
618
+ chs[i] = { ...chs[i], access: { ...chs[i].access, mode: fMode.value, allowed_users: users } };
619
+ const res = await saveChannels(msg);
620
+ if (res.ok) {
621
+ const affected = Object.keys(state.fleet.instances || {});
622
+ editingBot = null; await reload();
623
+ showBanner(`✓ ${t("affected")}: ${affected.length ? affected.join(", ") : chLabel(i) + " bot"} — 🔄🔄 restart fleet`);
624
+ }
625
+ } }, t("save")),
626
+ el("button", { class: "sm ghost", onclick: () => { editingBot = null; renderBots(); } }, t("cancel")), msg),
627
+ );
628
+ return box;
629
+ }
630
+
631
+ // ── ClassicBot Channels ──
632
+ let editingClassic = null;
633
+ function classicRuntime(instanceName) {
634
+ const live = state.classicLive[instanceName] || {};
635
+ const status = live.status || state.classicStatus[instanceName] || "stopped";
636
+ const execution = status === "paused" ? "paused" : (live.state || status);
637
+ return { status, execution };
638
+ }
639
+ function classicEditForm(c) {
640
+ const box = el("div", { class: "edit" });
641
+ const classicDefaults = state.classic?.defaults || {};
642
+ const fleetDefaults = state.fleet?.defaults || {};
643
+ const inheritedBackend = classicDefaults.backend || fleetDefaults.backend || "claude-code";
644
+ const inheritedModel = classicDefaults.model || fleetDefaults.model || "";
645
+ const inheritedAutoPause = classicDefaults.auto_pause_after ?? fleetDefaults.auto_pause_after ?? 0;
646
+ const runtime = classicRuntime(c.instanceName);
647
+ const fBackend = select(c.backend || inheritedBackend, BACKENDS.filter(value => value !== "gemini-cli"));
648
+ const fModel = el("input", { type: "text", value: c.model || "", placeholder: inheritedModel || "(inherit default)" });
649
+ const auto = overrideControl(c, "auto_pause_after", el("input", { type: "number", min: "0", step: "1", value: c.auto_pause_after ?? inheritedAutoPause, placeholder: "0 = disabled" }), inheritedAutoPause);
650
+ const fCollab = el("input", { type: "checkbox" }); fCollab.checked = !!c.collab;
651
+ const fContext = el("input", { type: "number", min: "0", step: "1", value: c.context_lines ?? classicDefaults.context_lines ?? 5 });
652
+ const autoFeedback = el("div", { class: "feedback" });
653
+ const contextFeedback = el("div", { class: "feedback" });
654
+ const saveBtn = el("button", { class: "primary sm" }, t("save"));
655
+ const validate = () => {
656
+ const autoValue = Number(auto.input.value);
657
+ 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" : "");
660
+ saveBtn.disabled = !(validAuto && validContext);
661
+ return !saveBtn.disabled;
662
+ };
663
+ for (const input of [fBackend, fModel, auto.input, auto.toggle, fCollab, fContext]) {
664
+ input.addEventListener("input", validate); input.addEventListener("change", validate);
665
+ }
666
+ saveBtn.onclick = () => {
667
+ if (!validate()) return;
668
+ const patch = {
669
+ backend: fBackend.value,
670
+ model: fModel.value.trim() || null,
671
+ auto_pause_after: auto.toggle.checked ? null : Number(auto.input.value),
672
+ collab: fCollab.checked,
673
+ context_lines: Number(fContext.value),
674
+ };
675
+ state.classic.channels[c.key] = { ...state.classic.channels[c.key], ...patch };
676
+ if (patch.model === null) delete state.classic.channels[c.key].model;
677
+ if (patch.auto_pause_after === null) delete state.classic.channels[c.key].auto_pause_after;
678
+ stageChange(`classic:${c.key}`, {
679
+ label: `Update ${c.name || c.instanceName}`,
680
+ impact: "instance",
681
+ apply: () => api(`/api/settings/classic/channels/${encodeURIComponent(c.key)}`, { method: "PATCH", body: JSON.stringify(patch) }),
682
+ });
683
+ editingClassic = null;
684
+ renderClassic();
685
+ };
686
+ box.append(
687
+ el("div", { class: "sub", style: "margin:0 0 8px" }, `ID: ${c.instanceName} · ${EXECUTION_TEXT[runtime.execution] || runtime.execution}`),
688
+ el("div", { class: "grid2" },
689
+ el("div", {}, el("label", {}, t("backend"), impact("instance")), fBackend),
690
+ el("div", {}, el("label", {}, t("model"), impact("instance")), fModel),
691
+ el("div", {}, el("label", {}, `${t("autoPause")} (${t("minutes")})`, impact("instance")), auto.row, autoFeedback),
692
+ el("div", {}, el("label", { style: "display:flex;align-items:center;gap:8px;cursor:pointer;margin-top:28px" }, fCollab, el("span", {}, "Collaboration mode"), impact("instance")))),
693
+ el("div", { class: "advanced-only" },
694
+ el("label", {}, "Context lines", impact("instance")), fContext, contextFeedback),
695
+ el("div", { class: "actions" },
696
+ saveBtn,
697
+ runtime.status === "running" ? el("button", { class: "sm", onclick: () => pauseWakeAgent(c.instanceName, "pause") }, t("pause")) : null,
698
+ runtime.status === "paused" ? el("button", { class: "sm", onclick: () => pauseWakeAgent(c.instanceName, "wake") }, t("wake")) : null,
699
+ el("button", { class: "sm ghost", onclick: () => { editingClassic = null; renderClassic(); } }, t("cancel"))),
700
+ );
701
+ validate();
702
+ return box;
703
+ }
422
704
  function renderClassic() {
423
705
  const host = $("classicList"); if (!host) return; host.innerHTML = "";
424
- const chans = Object.values(state.classic?.channels || {});
706
+ const chans = Object.entries(state.classic?.channels || {}).map(([key, channel]) => ({ key, ...channel }));
425
707
  if (!chans.length) { host.className = "card empty"; host.textContent = t("noClassic"); return; }
426
708
  const filter = ($("agentSearch")?.value || "").trim().toLowerCase();
427
709
  let list = chans;
@@ -429,19 +711,29 @@
429
711
  if (!list.length) { host.className = "card empty"; host.textContent = "—"; return; }
430
712
  host.className = "card";
431
713
  list.sort((a, b) => {
432
- const sa = STATUS_ORDER[state.classicStatus[a.instanceName] || "stopped"], sb = STATUS_ORDER[state.classicStatus[b.instanceName] || "stopped"];
714
+ const sa = STATUS_ORDER[classicRuntime(a.instanceName).status] ?? 9;
715
+ const sb = STATUS_ORDER[classicRuntime(b.instanceName).status] ?? 9;
433
716
  return sa !== sb ? sa - sb : String(a.name || "").localeCompare(String(b.name || ""));
434
717
  });
435
718
  for (const c of list) {
436
- const st = state.classicStatus[c.instanceName] || "stopped";
719
+ const runtime = classicRuntime(c.instanceName);
720
+ const st = runtime.status;
437
721
  const adapter = c.adapterId || "discord";
438
- host.append(el("div", { class: "item" }, el("div", { class: "item-row" },
439
- el("span", { class: "dot " + st, title: STATUS_TEXT[st] || st }),
722
+ const item = el("div", { class: "item" });
723
+ item.append(el("div", { class: "item-row" },
724
+ el("span", { class: "dot " + runtime.execution, title: EXECUTION_TEXT[runtime.execution] || runtime.execution }),
440
725
  el("span", { class: "name", title: c.instanceName || "" }, c.name || c.instanceName || "?"),
441
- el("span", { class: "sub", style: "margin:0" }, c.backend || state.classic?.defaults?.backend || "claude-code"),
726
+ el("span", { class: "sub", style: "margin:0" }, c.backend || state.classic?.defaults?.backend || state.fleet?.defaults?.backend || "claude-code"),
442
727
  el("span", { class: "tag" + (adapter !== "discord" ? " persona" : "") }, adapter),
443
728
  c.collab ? el("span", { class: "tag", style: "background:#ecfdf5;color:#065f46" }, "collab") : null,
444
- )));
729
+ el("span", { class: "spacer" }),
730
+ el("span", { class: "tag", title: STATUS_TEXT[st] || st }, EXECUTION_TEXT[runtime.execution] || runtime.execution),
731
+ el("button", { class: "sm ghost", onclick: () => { editingClassic = editingClassic === c.key ? null : c.key; renderClassic(); } }, editingClassic === c.key ? t("close") : t("edit")),
732
+ st === "running" ? el("button", { class: "sm", onclick: () => pauseWakeAgent(c.instanceName, "pause") }, t("pause")) : null,
733
+ st === "paused" ? el("button", { class: "sm", onclick: () => pauseWakeAgent(c.instanceName, "wake") }, t("wake")) : null,
734
+ ));
735
+ if (editingClassic === c.key) item.append(classicEditForm(c));
736
+ host.append(item);
445
737
  }
446
738
  }
447
739
 
@@ -486,33 +778,106 @@
486
778
  state.fleet.defaults = state.fleet.defaults || {};
487
779
  state.classic.defaults = state.classic.defaults || {};
488
780
  const d = state.fleet.defaults, c = state.classic.defaults;
781
+ const hang = d.hang_detector || {};
782
+ const startup = d.startup || {};
489
783
  const primary = channels()[0];
490
784
  if (primary) primary.access = primary.access || { mode: "locked", allowed_users: [] };
491
785
  const allowedUsers = primary ? (primary.access.allowed_users = primary.access.allowed_users || []) : [];
492
786
  c.admin_users = c.admin_users || []; c.allowed_guilds = c.allowed_guilds || [];
493
787
 
494
788
  const fBackend = select(d.backend || "claude-code", BACKENDS);
789
+ const fClassicBackend = select(c.backend || d.backend || "claude-code", BACKENDS.filter(value => value !== "gemini-cli"));
495
790
  const fModel = el("input", { type: "text", value: d.model || "", placeholder: "e.g. claude-opus-4-8" });
791
+ const fAgentMode = select(d.agent_mode || "mcp", ["mcp", "cli"]);
792
+ const fToolSet = select(d.tool_set || "full", ["full", "standard", "minimal"]);
793
+ const fLogLevel = select(d.log_level || "info", ["trace", "debug", "info", "warn", "error"]);
496
794
  const fLocale = el("select");
795
+ const fAutoPause = el("input", { type: "number", min: "0", step: "1", value: d.auto_pause_after ?? 0, placeholder: "0 = disabled" });
796
+ const fHangEnabled = el("input", { type: "checkbox" }); fHangEnabled.checked = hang.enabled !== false;
797
+ const fHangTimeout = el("input", { type: "number", min: "0.1", step: "0.5", value: hang.timeout_minutes ?? 15 });
798
+ const fConcurrency = el("input", { type: "number", min: "1", max: "20", step: "1", value: startup.concurrency ?? 10 });
799
+ const fStagger = el("input", { type: "number", min: "0", max: "30000", step: "100", value: startup.stagger_delay_ms ?? 500 });
497
800
  [["en", "English"], ["zh-TW", "繁體中文 (zh-TW)"]].forEach(([v, lbl]) => { const o = el("option", {}, lbl); o.value = v; if ((d.locale || "") === v) o.selected = true; fLocale.append(o); });
498
801
  const msg = el("span", { class: "msg" });
802
+ const autoFeedback = el("div", { class: "feedback" });
803
+ const hangFeedback = el("div", { class: "feedback" });
804
+ const concurrencyFeedback = el("div", { class: "feedback" });
805
+ const staggerFeedback = el("div", { class: "feedback" });
806
+ const saveBtn = el("button", { class: "primary" }, t("save"));
807
+ const validate = () => {
808
+ const auto = Number(fAutoPause.value), timeout = Number(fHangTimeout.value);
809
+ 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" : "");
814
+ saveBtn.disabled = !(validAuto && validHang && validConcurrency && validStagger);
815
+ return !saveBtn.disabled;
816
+ };
817
+ for (const input of [fAutoPause, fHangTimeout, fConcurrency, fStagger]) {
818
+ input.addEventListener("input", validate); input.addEventListener("change", validate);
819
+ }
820
+ saveBtn.textContent = "Review changes";
821
+ saveBtn.onclick = () => {
822
+ if (!validate()) return;
823
+ const defaultsPatch = {
824
+ backend: fBackend.value,
825
+ model: fModel.value || undefined,
826
+ locale: fLocale.value,
827
+ auto_pause_after: Number(fAutoPause.value),
828
+ agent_mode: fAgentMode.value,
829
+ tool_set: fToolSet.value,
830
+ log_level: fLogLevel.value,
831
+ hang_detector: { enabled: fHangEnabled.checked, timeout_minutes: Number(fHangTimeout.value) },
832
+ startup: { concurrency: Number(fConcurrency.value), stagger_delay_ms: Number(fStagger.value) },
833
+ };
834
+ const classicPatch = { backend: fClassicBackend.value, admin_users: c.admin_users, allowed_guilds: c.allowed_guilds };
835
+ state.fleet.defaults = { ...d, ...defaultsPatch };
836
+ 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>`;
841
+ };
842
+
843
+ const section = (title, open, ...children) => {
844
+ const details = el("details", { class: "settings-section" }); details.open = open;
845
+ details.append(el("summary", {}, title), el("div", { class: "section-body" }, ...children));
846
+ return details;
847
+ };
499
848
  host.append(
500
- el("div", { class: "field" }, el("label", {}, t("language")), fLocale),
501
- el("div", { class: "field" }, el("label", {}, t("defaultBackend")), fBackend),
502
- el("div", { class: "field" }, el("label", {}, t("defaultModel")), fModel),
503
- el("div", { class: "field" }, el("label", {}, t("allowedUsers")), chipList(allowedUsers, null, "user id")),
504
- el("div", { class: "field" }, el("label", {}, t("classicAdmins")), chipList(c.admin_users, null, "user id")),
505
- el("div", { class: "field" }, el("label", {}, t("allowedGuilds")), chipList(c.allowed_guilds, null, "guild id")),
506
- el("div", { class: "field" }, el("div", { class: "actions" }, el("button", { class: "primary", onclick: async () => {
507
- d.backend = fBackend.value; d.model = fModel.value || undefined; d.locale = fLocale.value;
508
- const r1 = await api("/api/settings/fleet/defaults", { method: "PUT", body: JSON.stringify({ backend: d.backend, model: d.model, locale: d.locale }) });
509
- let r2 = { ok: true, body: {} };
510
- if (primary) r2 = await saveChannels();
511
- const r3 = await api("/api/settings/classic/defaults", { method: "PUT", body: JSON.stringify({ backend: c.backend, admin_users: c.admin_users, allowed_guilds: c.allowed_guilds }) });
512
- const bad = [r1, r2, r3].find(r => !r.ok);
513
- resultMsg(msg, bad || { ok: true, body: { warnings: [...(r1.body?.warnings || []), ...(r3.body?.warnings || [])] } });
514
- } }, t("save")), msg)),
849
+ section("Defaults", true,
850
+ el("div", { class: "grid2" },
851
+ el("div", { class: "field" }, el("label", {}, t("language"), impact("now")), fLocale),
852
+ el("div", { class: "field" }, el("label", {}, t("defaultBackend"), impact("instance")), fBackend),
853
+ 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))),
855
+ section("ClassicBot", true,
856
+ el("div", { class: "grid2" },
857
+ el("div", { class: "field" }, el("label", {}, "Classic default backend", impact("instance")), fClassicBackend)),
858
+ el("div", { class: "field" }, el("strong", {}, "Who can use ClassicBot"),
859
+ el("div", { class: "grid2" },
860
+ el("div", {}, el("label", {}, t("classicAdmins"), impact("fleet")), chipList(c.admin_users, null, "user id")),
861
+ el("div", {}, el("label", {}, t("allowedGuilds"), impact("fleet")), chipList(c.allowed_guilds, null, "guild id"))))),
862
+ el("div", { class: "advanced-only" },
863
+ section(t("runtimeResources"), false,
864
+ el("div", { class: "grid2" },
865
+ el("div", { class: "field" }, el("label", {}, t("toolSet"), impact("instance")), fToolSet),
866
+ el("div", { class: "field" }, el("label", {}, t("logLevel"), impact("instance")), fLogLevel),
867
+ el("div", { class: "field" }, el("label", {}, t("agentMode"), impact("instance")), fAgentMode))),
868
+ section(t("detection"), false,
869
+ el("div", { class: "grid2" },
870
+ el("div", { class: "field" }, el("label", { style: "display:flex;align-items:center;gap:8px" }, fHangEnabled, el("span", {}, t("hangEnabled")), impact("instance"))),
871
+ el("div", { class: "field" }, el("label", {}, `${t("hangTimeout")} (${t("minutes")})`, impact("instance")), fHangTimeout, hangFeedback))),
872
+ section(t("startup"), false,
873
+ el("div", { class: "grid2" },
874
+ el("div", { class: "field" }, el("label", {}, t("concurrency"), impact("fleet")), fConcurrency, concurrencyFeedback),
875
+ el("div", { class: "field" }, el("label", {}, `${t("staggerDelay")} (ms)`, impact("fleet")), fStagger, staggerFeedback))),
876
+ section("Advanced & Access", false,
877
+ el("div", { class: "field" }, el("label", {}, t("allowedUsers"), impact("fleet")), chipList(allowedUsers, null, "user id")))),
878
+ el("div", { class: "field" }, el("div", { class: "actions" }, saveBtn, msg)),
515
879
  );
880
+ validate();
516
881
  }
517
882
 
518
883
  // ── What's New ──
@@ -576,20 +941,25 @@
576
941
  }
577
942
 
578
943
  // ── Load / render ──
579
- function renderAll() { renderAgents(); renderClassic(); renderBots(); renderGeneral(); renderWhatsNew(); renderAdvanced(); applyStaticI18n(); }
944
+ function renderAll() { renderAgents(); renderClassic(); renderBots(); renderGeneral(); renderAdvanced(); applyStaticI18n(); }
580
945
  async function reloadLive() {
581
946
  const f = await api("/api/fleet");
582
947
  state.fleetUp = f.ok;
583
- state.live = {}; if (f.ok && f.body?.instances) for (const i of f.body.instances) state.live[i.name] = { status: i.status };
584
- // /api/fleet is fleet-config only; classic instance statuses come from the
585
- // merged roster (/api/profiles, open GET).
948
+ state.live = {}; state.classicLive = {};
949
+ if (f.ok && f.body?.instances) for (const i of f.body.instances) {
950
+ state.live[i.name] = { status: i.status, state: i.state };
951
+ if (i.classic) state.classicLive[i.name] = { status: i.status, state: i.state };
952
+ }
953
+ // /api/fleet carries live tri-state for Classic instances. Profiles remain
954
+ // a fallback roster for a daemon that has not yet appeared in live status.
586
955
  const pr = await api("/api/profiles");
587
956
  state.classicStatus = {}; if (pr.ok && Array.isArray(pr.body)) for (const r of pr.body) state.classicStatus[r.instance_name] = r.status;
588
957
  if (f.ok && f.body?.version) { state.version = f.body.version; $("verBadge").textContent = "v" + state.version; }
589
958
  renderAgents(); renderBots(); renderClassic();
590
959
  }
591
960
  async function reload() {
592
- const f = await api("/api/settings/fleet");
961
+ // Forms edit the user-authored config, not defaults-expanded runtime values.
962
+ const f = await api("/api/settings/fleet/raw");
593
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; }
594
964
  state.fleet = f.body || {};
595
965
  const c = await api("/api/settings/classic"); state.classic = c.body || {};
@@ -602,13 +972,22 @@
602
972
  $("helpClose").onclick = () => setHelp(false);
603
973
  $("helpBackdrop").onclick = () => setHelp(false);
604
974
  $("langBtn").onclick = () => { lang = lang === "en" ? "zh-TW" : "en"; localStorage.setItem("agend_lang", lang); renderAll(); };
605
- $("verBadge").onclick = () => { const b = $("wnBody"); b.classList.add("open"); $("wnToggle").textContent = t("collapse"); b.scrollIntoView({ behavior: "smooth" }); };
975
+ $("verBadge").onclick = () => window.open("https://github.com/songsid/AgEnD/releases/latest", "_blank", "noopener");
606
976
  $("agentSearch").oninput = () => { renderAgents(); renderClassic(); };
607
977
  $("newAgentBtn").onclick = () => { state.newAgentOpen = !state.newAgentOpen; $("newAgentBtn").textContent = state.newAgentOpen ? t("cancel") : t("new"); renderNewAgent(state.newAgentOpen); };
608
978
  $("newBotBtn").onclick = () => { state.newBotOpen = !state.newBotOpen; $("newBotBtn").textContent = state.newBotOpen ? t("cancel") : t("add"); renderNewBot(state.newBotOpen); };
609
979
  $("advToggle").onclick = () => { const b = $("advBody"); const open = !b.classList.contains("open"); b.classList.toggle("open", open); $("advToggle").textContent = open ? t("collapse") : t("expand"); };
610
- $("wnToggle").onclick = () => { const b = $("wnBody"); const open = !b.classList.contains("open"); b.classList.toggle("open", open); $("wnToggle").textContent = open ? t("collapse") : t("expand"); };
611
- $("reloadBtn").onclick = async () => { const r = await api("/api/settings/reload", { method: "POST" }); const b = $("banner"); b.style.display = "block"; b.style.background = "#ecfdf5"; b.style.color = "var(--green)"; b.textContent = r.ok ? "✓ Reload signal sent — instances reconciling." : "Reload failed."; setTimeout(() => b.style.display = "none", 4000); setTimeout(reloadLive, 1500); };
980
+ const advancedEnabled = localStorage.getItem("agend_settings_advanced") === "true";
981
+ document.body.classList.toggle("show-advanced", advancedEnabled);
982
+ $("advancedToggle").textContent = advancedEnabled ? "Hide advanced" : "Show advanced";
983
+ $("advancedToggle").onclick = () => {
984
+ const enabled = !document.body.classList.contains("show-advanced");
985
+ document.body.classList.toggle("show-advanced", enabled);
986
+ localStorage.setItem("agend_settings_advanced", String(enabled));
987
+ $("advancedToggle").textContent = enabled ? "Hide advanced" : "Show advanced";
988
+ };
989
+ $("applyChanges").onclick = applyPendingChanges;
990
+ $("discardChanges").onclick = async () => { state.pending.clear(); updatePendingBar(); await reload(); };
612
991
 
613
992
  reload();
614
993
  })();