@songsid/agend 2.1.0-beta.15 → 2.1.0-beta.17

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.
@@ -8,7 +8,7 @@
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
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; }
@@ -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 { background: var(--green); } .dot.paused { background:#f59e0b; } .dot.crashed { 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,6 +63,19 @@
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; }
68
81
  .toolbar { display: flex; gap: 6px; align-items: center; margin-bottom: 8px; flex-wrap: wrap; }
@@ -84,6 +97,16 @@
84
97
  .help-panel li { font-size: 13px; margin: 3px 0; }
85
98
  .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
99
  .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; }
100
+ @media (max-width: 640px) {
101
+ header { padding:10px 12px; flex-wrap:wrap; } header h1 { font-size:16px; }
102
+ main { padding:12px 10px; } section { margin-bottom:18px; }
103
+ .grid2 { grid-template-columns:1fr; gap:0; }
104
+ .item { padding:9px 10px; } .item-row { flex-wrap:wrap; }
105
+ .item-row .spacer { flex-basis:100%; height:0; }
106
+ .sub { margin-left:0; } .field { padding:10px 12px; }
107
+ #agentSearch { width:130px !important; margin-right:0 !important; }
108
+ button { min-height:36px; } button.sm { min-height:32px; }
109
+ }
87
110
  </style>
88
111
  </head>
89
112
  <body>
@@ -153,6 +176,7 @@
153
176
  backend: "Backend", model: "Model", workingDir: "Working directory", channelBinding: "Channel binding",
154
177
  description: "Description", systemPrompt: "System prompt", tags: "Tags", generalTopicField: "General topic (fleet dispatcher)",
155
178
  runtimeResources: "Runtime & Resources", autoPause: "Auto-pause after", minutes: "minutes", inheritDefault: "Inherit from defaults",
179
+ 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",
156
180
  language: "Language (bot replies)", defaultBackend: "Default backend", defaultModel: "Default model", allowedUsers: "Allowed users (primary bot)",
157
181
  classicAdmins: "ClassicBot admins", allowedGuilds: "Allowed guilds", type: "Type", adapterId: "Adapter id",
158
182
  tokenEnv: "Bot token env var", groupId: "Group / guild id", nameField: "Name",
@@ -170,6 +194,7 @@
170
194
  backend: "後端", model: "模型", workingDir: "工作目錄", channelBinding: "頻道綁定",
171
195
  description: "描述", systemPrompt: "System prompt", tags: "標籤", generalTopicField: "General topic(fleet 分派器)",
172
196
  runtimeResources: "執行環境與資源", autoPause: "閒置後自動暫停", minutes: "分鐘", inheritDefault: "沿用全域預設值",
197
+ detection: "偵測", startup: "啟動", backendSection: "後端", advancedSection: "進階與存取", hangEnabled: "啟用卡住偵測", hangTimeout: "卡住逾時", agentMode: "Agent 模式", toolSet: "工具集", logLevel: "Log 等級", concurrency: "啟動併發數", staggerDelay: "錯開延遲", lightweight: "輕量模式", modelFailover: "備援模型", displayName: "顯示名稱", pause: "暫停", wake: "喚醒", accessMode: "存取模式", affected: "受影響 instances",
173
198
  language: "語言(bot 回覆)", defaultBackend: "預設後端", defaultModel: "預設模型", allowedUsers: "允許使用者(主 bot)",
174
199
  classicAdmins: "ClassicBot 管理員", allowedGuilds: "允許的伺服器", type: "類型", adapterId: "Adapter id",
175
200
  tokenEnv: "Bot token 環境變數", groupId: "群組 / guild id", nameField: "名稱",
@@ -253,6 +278,14 @@
253
278
  else if (!html) html = `<span class="e">✗ ${esc((b && b.error) || ("HTTP " + res.status))}</span>`;
254
279
  elm.innerHTML = html;
255
280
  }
281
+ function showBanner(text, error = false) {
282
+ const b = $("banner");
283
+ b.style.display = "block";
284
+ b.style.background = error ? "#fef2f2" : "#ecfdf5";
285
+ b.style.color = error ? "var(--red)" : "var(--green)";
286
+ b.textContent = text;
287
+ setTimeout(() => { b.style.display = "none"; }, 6000);
288
+ }
256
289
 
257
290
  // ── State ──
258
291
  const state = { fleet: {}, classic: {}, classicStatus: {}, live: {}, fleetUp: false, advFmt: "yaml", advEdit: false, collapsedGroups: new Set(), version: "", newAgentOpen: false, newBotOpen: false };
@@ -271,6 +304,25 @@
271
304
  redraw(); return box;
272
305
  }
273
306
  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; }
307
+ const impact = (kind) => el("span", { class: "impact" }, kind === "now" ? "⚡ immediate" : kind === "fleet" ? "🔄🔄 restart fleet" : "🔄 restart instance");
308
+ const hasOwn = (obj, key) => Object.prototype.hasOwnProperty.call(obj || {}, key);
309
+ function overrideControl(inst, key, input, inheritedValue, impactKind = "instance") {
310
+ const inherited = !hasOwn(inst, key);
311
+ const toggle = el("input", { type: "checkbox" }); toggle.checked = inherited;
312
+ input.disabled = inherited;
313
+ if (inherited && inheritedValue !== undefined) {
314
+ if (input.type === "checkbox") input.checked = !!inheritedValue;
315
+ else input.value = inheritedValue;
316
+ }
317
+ toggle.onchange = () => { input.disabled = toggle.checked; };
318
+ return { input, toggle, row: el("div", {}, input, el("label", { class: "inherit-row" }, toggle, el("span", {}, t("inheritDefault")), impact(impactKind))) };
319
+ }
320
+ function setValidation(input, feedback, error, warning) {
321
+ input.classList.toggle("invalid", !!error);
322
+ feedback.className = "feedback" + (error ? " error" : warning ? " warning" : "");
323
+ feedback.textContent = error ? `✗ ${error}` : warning ? `⚠ ${warning}` : "";
324
+ return !error;
325
+ }
274
326
 
275
327
  // ── My Agents ──
276
328
  let editing = null;
@@ -283,12 +335,13 @@
283
335
  }
284
336
  // Display name: profile display_name → else strip the -t<topicId> suffix.
285
337
  const shortName = (name, inst) => (inst && inst.display_name) || name.replace(/-t\d+$/, "");
286
- const STATUS_TEXT = { running: "🟢 Running", crashed: "🔴 Crashed", stopped: "⚪ Stopped" };
287
- const STATUS_ORDER = { running: 0, crashed: 1, stopped: 2 };
338
+ const STATUS_TEXT = { running: "🟢 Running", paused: "⏸ Paused", crashed: "🔴 Crashed", stopped: "⚪ Stopped" };
339
+ const STATUS_ORDER = { running: 0, paused: 1, crashed: 2, stopped: 3 };
288
340
  const groupOf = (inst) => (inst.tags && inst.tags[0]) || "Other";
289
341
  function compactRow(name, inst) {
290
342
  const st = state.live[name]?.status || "stopped";
291
343
  const running = st === "running";
344
+ const paused = st === "paused";
292
345
  const short = shortName(name, inst);
293
346
  const chTag = agentChannelLabel(inst);
294
347
  const item = el("div", { class: "item" });
@@ -301,7 +354,9 @@
301
354
  chTag ? el("span", { class: "tag" + (chTag.includes("persona") ? " persona" : chTag.includes("telegram") ? " tg" : "") }, chTag) : null,
302
355
  el("span", { class: "spacer" }),
303
356
  el("button", { class: "sm ghost", onclick: () => { editing = editing === name ? null : name; renderAgents(); } }, editing === name ? t("close") : t("edit")),
304
- el("button", { class: "sm", onclick: () => toggleAgent(name, running) }, running ? t("stop") : t("start")),
357
+ running ? el("button", { class: "sm", onclick: () => pauseWakeAgent(name, "pause") }, t("pause")) : null,
358
+ paused ? el("button", { class: "sm", onclick: () => pauseWakeAgent(name, "wake") }, t("wake")) : null,
359
+ !paused ? el("button", { class: "sm", onclick: () => toggleAgent(name, running) }, running ? t("stop") : t("start")) : null,
305
360
  el("button", { class: "sm danger del-btn", title: t("close"), onclick: () => delAgent(name) }, "🗑️"),
306
361
  ));
307
362
  if (editing === name) item.append(agentEditForm(name, inst));
@@ -335,6 +390,7 @@
335
390
  }
336
391
  function agentEditForm(name, inst) {
337
392
  const box = el("div", { class: "edit" });
393
+ const defaults = state.fleet.defaults || {};
338
394
  const fBackend = select(inst.backend || state.fleet.defaults?.backend || "claude-code", BACKENDS);
339
395
  const fModel = el("input", { type: "text", value: inst.model || "", placeholder: "(inherit default)" });
340
396
  const fWd = el("input", { type: "text", value: inst.working_directory || "" });
@@ -343,43 +399,77 @@
343
399
  const fSys = el("input", { type: "text", value: inst.systemPrompt || "", placeholder: "e.g. file:prompts/dev.md" });
344
400
  const tags = [...(inst.tags || [])];
345
401
  const fGeneral = el("input", { type: "checkbox" }); fGeneral.checked = !!inst.general_topic;
346
- const hasAutoPauseOverride = Object.prototype.hasOwnProperty.call(inst, "auto_pause_after");
347
- const fAutoPauseInherit = el("input", { type: "checkbox" }); fAutoPauseInherit.checked = !hasAutoPauseOverride;
348
- const fAutoPause = el("input", { type: "number", min: "0", step: "1", value: hasAutoPauseOverride ? inst.auto_pause_after : (state.fleet.defaults?.auto_pause_after ?? 0), placeholder: "0 = disabled" });
349
- fAutoPause.disabled = fAutoPauseInherit.checked;
350
- fAutoPauseInherit.onchange = () => { fAutoPause.disabled = fAutoPauseInherit.checked; };
402
+ 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);
403
+ const hangRaw = inst.hang_detector || {};
404
+ const hangHolder = hasOwn(hangRaw, "timeout_minutes") ? { value: hangRaw.timeout_minutes } : {};
405
+ 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);
406
+ const agentMode = overrideControl(inst, "agent_mode", select(inst.agent_mode || defaults.agent_mode || "mcp", ["mcp", "cli"]), defaults.agent_mode || "mcp");
407
+ const toolSet = overrideControl(inst, "tool_set", select(inst.tool_set || defaults.tool_set || "full", ["full", "standard", "minimal"]), defaults.tool_set || "full");
408
+ const logLevel = overrideControl(inst, "log_level", select(inst.log_level || defaults.log_level || "info", ["trace", "debug", "info", "warn", "error"]), defaults.log_level || "info");
409
+ const fLightweight = el("input", { type: "checkbox" }); fLightweight.checked = inst.lightweight ?? defaults.lightweight ?? false;
410
+ const lightweight = overrideControl(inst, "lightweight", fLightweight, defaults.lightweight ?? false);
411
+ const fallbackValue = Array.isArray(inst.model_failover) ? (inst.model_failover[0] || "") : (defaults.model_failover?.[0] || "");
412
+ const failover = overrideControl(inst, "model_failover", el("input", { type: "text", value: fallbackValue, placeholder: "e.g. sonnet" }), defaults.model_failover?.[0] || "");
413
+ const displayName = overrideControl(inst, "display_name", el("input", { type: "text", value: inst.display_name ?? defaults.display_name ?? "", placeholder: name }), defaults.display_name || "");
414
+ const autoFeedback = el("div", { class: "feedback" });
415
+ const hangFeedback = el("div", { class: "feedback" });
416
+ const failoverFeedback = el("div", { class: "feedback" });
351
417
  const msg = el("span", { class: "msg" });
418
+ const saveBtn = el("button", { class: "primary sm" }, t("save"));
419
+ const validate = () => {
420
+ const autoValue = Number(auto.input.value), hangValue = Number(hang.input.value);
421
+ const validAuto = setValidation(auto.input, autoFeedback, !auto.toggle.checked && (!Number.isFinite(autoValue) || autoValue < 0) ? "Must be 0 or greater" : "");
422
+ const validHang = setValidation(hang.input, hangFeedback, !hang.toggle.checked && (!Number.isFinite(hangValue) || hangValue <= 0) ? "Must be greater than 0" : "");
423
+ setValidation(failover.input, failoverFeedback, "", !failover.toggle.checked && !failover.input.value.trim() ? "Empty list disables failover" : "");
424
+ saveBtn.disabled = !(validAuto && validHang);
425
+ return !saveBtn.disabled;
426
+ };
427
+ for (const ctl of [auto, hang, agentMode, toolSet, logLevel, lightweight, failover, displayName]) {
428
+ ctl.input.addEventListener("input", validate); ctl.input.addEventListener("change", validate); ctl.toggle.addEventListener("change", validate);
429
+ }
430
+ saveBtn.onclick = async () => {
431
+ if (!validate()) return;
432
+ const patch = {
433
+ backend: fBackend.value, model: fModel.value || undefined,
434
+ working_directory: fWd.value, channel_id: fChan.value || undefined,
435
+ description: fDesc.value || undefined, systemPrompt: fSys.value || undefined,
436
+ tags: tags.length ? tags : undefined, general_topic: fGeneral.checked,
437
+ auto_pause_after: auto.toggle.checked ? null : Number(auto.input.value),
438
+ hang_detector: { timeout_minutes: hang.toggle.checked ? null : Number(hang.input.value) },
439
+ agent_mode: agentMode.toggle.checked ? null : agentMode.input.value,
440
+ tool_set: toolSet.toggle.checked ? null : toolSet.input.value,
441
+ log_level: logLevel.toggle.checked ? null : logLevel.input.value,
442
+ lightweight: lightweight.toggle.checked ? null : lightweight.input.checked,
443
+ model_failover: failover.toggle.checked ? null : (failover.input.value.trim() ? [failover.input.value.trim()] : []),
444
+ display_name: displayName.toggle.checked ? null : displayName.input.value.trim(),
445
+ };
446
+ const res = await api(`/api/settings/fleet/instances/${encodeURIComponent(name)}`, { method: "PATCH", body: JSON.stringify(patch) });
447
+ resultMsg(msg, res); if (res.ok) { await reload(); showBanner(`✓ ${t("affected")}: ${name} — 🔄 restart instance`); }
448
+ };
352
449
  box.append(
353
450
  el("div", { class: "sub", style: "margin:0 0 8px" }, "ID: " + name),
354
- el("div", { class: "grid2" }, el("div", {}, el("label", {}, t("backend")), fBackend), el("div", {}, el("label", {}, t("model")), fModel)),
355
- el("label", {}, t("workingDir")), fWd,
356
- el("div", { class: "grid2" }, el("div", {}, el("label", {}, t("channelBinding")), fChan), el("div", {}, el("label", {}, t("description")), fDesc)),
357
- el("label", {}, t("systemPrompt")), fSys,
358
- el("label", {}, t("tags")), chipList(tags, null, "tag"),
359
- el("label", { style: "display:flex;align-items:center;gap:8px;cursor:pointer" }, fGeneral, el("span", {}, t("generalTopicField"))),
451
+ el("div", { class: "grid2" }, el("div", {}, el("label", {}, t("backend"), impact("instance")), fBackend), el("div", {}, el("label", {}, t("model"), impact("instance")), fModel)),
452
+ el("label", {}, t("workingDir"), impact("instance")), fWd,
453
+ el("div", { class: "grid2" }, el("div", {}, el("label", {}, t("channelBinding"), impact("fleet")), fChan), el("div", {}, el("label", {}, t("description"), impact("now")), fDesc)),
454
+ el("label", {}, t("systemPrompt"), impact("instance")), fSys,
455
+ el("label", {}, t("tags"), impact("now")), chipList(tags, null, "tag"),
456
+ el("label", { style: "display:flex;align-items:center;gap:8px;cursor:pointer" }, fGeneral, el("span", {}, t("generalTopicField")), impact("fleet")),
360
457
  el("div", { class: "sub", style: "margin:14px 0 6px;font-weight:600" }, t("runtimeResources")),
361
458
  el("div", { class: "grid2" },
362
- el("div", {}, el("label", {}, `${t("autoPause")} (${t("minutes")})`), fAutoPause),
363
- el("label", { style: "display:flex;align-items:center;gap:8px;cursor:pointer;margin-top:25px" }, fAutoPauseInherit, el("span", {}, t("inheritDefault")))),
459
+ el("div", {}, el("label", {}, `${t("autoPause")} (${t("minutes")})`), auto.row, autoFeedback),
460
+ el("div", {}, el("label", {}, `${t("hangTimeout")} (${t("minutes")})`), hang.row, hangFeedback),
461
+ el("div", {}, el("label", {}, t("agentMode")), agentMode.row),
462
+ el("div", {}, el("label", {}, t("toolSet")), toolSet.row),
463
+ el("div", {}, el("label", {}, t("logLevel")), logLevel.row),
464
+ el("div", {}, el("label", {}, t("lightweight")), lightweight.row),
465
+ el("div", {}, el("label", {}, t("modelFailover")), failover.row, failoverFeedback),
466
+ el("div", {}, el("label", {}, t("displayName")), displayName.row)),
364
467
  el("div", { class: "actions" },
365
- el("button", { class: "primary sm", onclick: async () => {
366
- const autoPause = Number(fAutoPause.value);
367
- if (!fAutoPauseInherit.checked && (!Number.isFinite(autoPause) || autoPause < 0)) {
368
- msg.innerHTML = `<span class="e">✗ ${esc(t("autoPause"))}: ≥ 0</span>`; return;
369
- }
370
- const patch = {
371
- backend: fBackend.value, model: fModel.value || undefined,
372
- working_directory: fWd.value, channel_id: fChan.value || undefined,
373
- description: fDesc.value || undefined, systemPrompt: fSys.value || undefined,
374
- tags: tags.length ? tags : undefined, general_topic: fGeneral.checked,
375
- auto_pause_after: fAutoPauseInherit.checked ? null : autoPause,
376
- };
377
- const res = await api(`/api/settings/fleet/instances/${encodeURIComponent(name)}`, { method: "PATCH", body: JSON.stringify(patch) });
378
- resultMsg(msg, res); if (res.ok) await reload();
379
- } }, t("save")),
468
+ saveBtn,
380
469
  el("button", { class: "sm ghost", onclick: () => { editing = null; renderAgents(); } }, t("cancel")),
381
470
  msg),
382
471
  );
472
+ validate();
383
473
  return box;
384
474
  }
385
475
  async function toggleAgent(name, running) {
@@ -387,6 +477,11 @@
387
477
  else await api(`/api/instance/${encodeURIComponent(name)}/start`, { method: "POST" });
388
478
  setTimeout(reloadLive, 800);
389
479
  }
480
+ async function pauseWakeAgent(name, action) {
481
+ const res = await api(`/api/settings/instances/${encodeURIComponent(name)}/${action}`, { method: "POST" });
482
+ if (!res.ok) alert(res.body?.error || `${action} failed`);
483
+ await reloadLive();
484
+ }
390
485
  async function delAgent(name) {
391
486
  if (!confirm(`Delete agent "${name}"? (config removed; running daemon stops on next Reload)`)) return;
392
487
  const res = await api(`/api/settings/fleet/instances/${encodeURIComponent(name)}`, { method: "DELETE" });
@@ -415,6 +510,7 @@
415
510
  }
416
511
 
417
512
  // ── Bots ──
513
+ let editingBot = null;
418
514
  function renderBots() {
419
515
  const host = $("bots"); host.innerHTML = ""; const chs = channels();
420
516
  if (!chs.length) { host.className = "card empty"; host.textContent = t("noBots"); return; }
@@ -428,12 +524,41 @@
428
524
  el("span", { class: "sub", style: "margin:0" }, ch.bot_token_env || "(no token env)"),
429
525
  el("span", { class: "spacer" }),
430
526
  el("span", { style: "color:var(--green);font-size:13px" }, state.fleetUp ? t("online") : "—"),
527
+ el("button", { class: "sm ghost", onclick: () => { editingBot = editingBot === i ? null : i; renderBots(); } }, editingBot === i ? t("close") : t("edit")),
431
528
  el("button", { class: "sm danger", onclick: () => delBot(i) }, "🗑️"),
432
529
  ));
530
+ if (editingBot === i) item.append(botEditForm(ch, i));
433
531
  host.append(item);
434
532
  });
435
533
  }
436
534
 
535
+ function botEditForm(ch, i) {
536
+ ch.access = ch.access || { mode: "locked", allowed_users: [] };
537
+ ch.access.allowed_users = ch.access.allowed_users || [];
538
+ const box = el("div", { class: "edit" });
539
+ const fMode = select(ch.access.mode || "locked", ["open", "locked", "pairing"]);
540
+ const users = [...ch.access.allowed_users];
541
+ const msg = el("span", { class: "msg" });
542
+ box.append(
543
+ el("div", { class: "grid2" },
544
+ el("div", {}, el("label", {}, t("accessMode"), impact("fleet")), fMode),
545
+ el("div", {}, el("label", {}, t("allowedUsers"), impact("fleet")), chipList(users, null, "user id"))),
546
+ el("div", { class: "actions" },
547
+ el("button", { class: "primary sm", onclick: async () => {
548
+ const chs = channels();
549
+ chs[i] = { ...chs[i], access: { ...chs[i].access, mode: fMode.value, allowed_users: users } };
550
+ const res = await saveChannels(msg);
551
+ if (res.ok) {
552
+ const affected = Object.keys(state.fleet.instances || {});
553
+ editingBot = null; await reload();
554
+ showBanner(`✓ ${t("affected")}: ${affected.length ? affected.join(", ") : chLabel(i) + " bot"} — 🔄🔄 restart fleet`);
555
+ }
556
+ } }, t("save")),
557
+ el("button", { class: "sm ghost", onclick: () => { editingBot = null; renderBots(); } }, t("cancel")), msg),
558
+ );
559
+ return box;
560
+ }
561
+
437
562
  // ── ClassicBot Channels (read-only; created dynamically via /start /stop) ──
438
563
  function renderClassic() {
439
564
  const host = $("classicList"); if (!host) return; host.innerHTML = "";
@@ -502,6 +627,8 @@
502
627
  state.fleet.defaults = state.fleet.defaults || {};
503
628
  state.classic.defaults = state.classic.defaults || {};
504
629
  const d = state.fleet.defaults, c = state.classic.defaults;
630
+ const hang = d.hang_detector || {};
631
+ const startup = d.startup || {};
505
632
  const primary = channels()[0];
506
633
  if (primary) primary.access = primary.access || { mode: "locked", allowed_users: [] };
507
634
  const allowedUsers = primary ? (primary.access.allowed_users = primary.access.allowed_users || []) : [];
@@ -509,31 +636,93 @@
509
636
 
510
637
  const fBackend = select(d.backend || "claude-code", BACKENDS);
511
638
  const fModel = el("input", { type: "text", value: d.model || "", placeholder: "e.g. claude-opus-4-8" });
639
+ const fAgentMode = select(d.agent_mode || "mcp", ["mcp", "cli"]);
640
+ const fToolSet = select(d.tool_set || "full", ["full", "standard", "minimal"]);
641
+ const fLogLevel = select(d.log_level || "info", ["trace", "debug", "info", "warn", "error"]);
512
642
  const fLocale = el("select");
513
643
  const fAutoPause = el("input", { type: "number", min: "0", step: "1", value: d.auto_pause_after ?? 0, placeholder: "0 = disabled" });
644
+ const fHangEnabled = el("input", { type: "checkbox" }); fHangEnabled.checked = hang.enabled !== false;
645
+ const fHangTimeout = el("input", { type: "number", min: "0.1", step: "0.5", value: hang.timeout_minutes ?? 15 });
646
+ const fConcurrency = el("input", { type: "number", min: "1", max: "20", step: "1", value: startup.concurrency ?? 10 });
647
+ const fStagger = el("input", { type: "number", min: "0", max: "30000", step: "100", value: startup.stagger_delay_ms ?? 500 });
514
648
  [["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); });
515
649
  const msg = el("span", { class: "msg" });
650
+ const autoFeedback = el("div", { class: "feedback" });
651
+ const hangFeedback = el("div", { class: "feedback" });
652
+ const concurrencyFeedback = el("div", { class: "feedback" });
653
+ const staggerFeedback = el("div", { class: "feedback" });
654
+ const saveBtn = el("button", { class: "primary" }, t("save"));
655
+ const validate = () => {
656
+ const auto = Number(fAutoPause.value), timeout = Number(fHangTimeout.value);
657
+ const concurrency = Number(fConcurrency.value), stagger = Number(fStagger.value);
658
+ const validAuto = setValidation(fAutoPause, autoFeedback, !Number.isFinite(auto) || auto < 0 ? "Must be 0 or greater" : "");
659
+ const validHang = setValidation(fHangTimeout, hangFeedback, !Number.isFinite(timeout) || timeout <= 0 ? "Must be greater than 0" : "");
660
+ 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" : "");
661
+ 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" : "");
662
+ saveBtn.disabled = !(validAuto && validHang && validConcurrency && validStagger);
663
+ return !saveBtn.disabled;
664
+ };
665
+ for (const input of [fAutoPause, fHangTimeout, fConcurrency, fStagger]) {
666
+ input.addEventListener("input", validate); input.addEventListener("change", validate);
667
+ }
668
+ saveBtn.onclick = async () => {
669
+ if (!validate()) return;
670
+ const defaultsPatch = {
671
+ backend: fBackend.value,
672
+ model: fModel.value || undefined,
673
+ locale: fLocale.value,
674
+ auto_pause_after: Number(fAutoPause.value),
675
+ agent_mode: fAgentMode.value,
676
+ tool_set: fToolSet.value,
677
+ log_level: fLogLevel.value,
678
+ hang_detector: { enabled: fHangEnabled.checked, timeout_minutes: Number(fHangTimeout.value) },
679
+ startup: { concurrency: Number(fConcurrency.value), stagger_delay_ms: Number(fStagger.value) },
680
+ };
681
+ const r1 = await api("/api/settings/fleet/defaults", { method: "PUT", body: JSON.stringify(defaultsPatch) });
682
+ let r2 = { ok: true, body: {} };
683
+ if (primary) r2 = await saveChannels();
684
+ 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 }) });
685
+ const bad = [r1, r2, r3].find(r => !r.ok);
686
+ resultMsg(msg, bad || { ok: true, body: { warnings: [...(r1.body?.warnings || []), ...(r3.body?.warnings || [])] } });
687
+ if (!bad) {
688
+ const affected = Object.keys(state.fleet.instances || {});
689
+ await reload();
690
+ showBanner(`✓ ${t("affected")}: ${affected.length ? affected.join(", ") : "none"} — 🔄 restart instances; startup changes 🔄🔄 restart fleet`);
691
+ }
692
+ };
693
+
694
+ const section = (title, open, ...children) => {
695
+ const details = el("details", { class: "settings-section" }); details.open = open;
696
+ details.append(el("summary", {}, title), el("div", { class: "section-body" }, ...children));
697
+ return details;
698
+ };
516
699
  host.append(
517
- el("div", { class: "field" }, el("label", {}, t("language")), fLocale),
518
- el("div", { class: "field" }, el("label", {}, t("defaultBackend")), fBackend),
519
- el("div", { class: "field" }, el("label", {}, t("defaultModel")), fModel),
520
- el("div", { class: "sub", style: "padding:14px 14px 0;font-weight:600" }, t("runtimeResources")),
521
- el("div", { class: "field" }, el("label", {}, `${t("autoPause")} (${t("minutes")})`), fAutoPause, el("div", { class: "hint" }, "0 = disabled; Reload Fleet restarts affected daemons")),
522
- el("div", { class: "field" }, el("label", {}, t("allowedUsers")), chipList(allowedUsers, null, "user id")),
523
- el("div", { class: "field" }, el("label", {}, t("classicAdmins")), chipList(c.admin_users, null, "user id")),
524
- el("div", { class: "field" }, el("label", {}, t("allowedGuilds")), chipList(c.allowed_guilds, null, "guild id")),
525
- el("div", { class: "field" }, el("div", { class: "actions" }, el("button", { class: "primary", onclick: async () => {
526
- const autoPause = Number(fAutoPause.value);
527
- if (!Number.isFinite(autoPause) || autoPause < 0) { msg.innerHTML = `<span class="e">✗ ${esc(t("autoPause"))}: ≥ 0</span>`; return; }
528
- d.backend = fBackend.value; d.model = fModel.value || undefined; d.locale = fLocale.value; d.auto_pause_after = autoPause;
529
- const r1 = await api("/api/settings/fleet/defaults", { method: "PUT", body: JSON.stringify({ backend: d.backend, model: d.model, locale: d.locale, auto_pause_after: autoPause }) });
530
- let r2 = { ok: true, body: {} };
531
- if (primary) r2 = await saveChannels();
532
- 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 }) });
533
- const bad = [r1, r2, r3].find(r => !r.ok);
534
- resultMsg(msg, bad || { ok: true, body: { warnings: [...(r1.body?.warnings || []), ...(r3.body?.warnings || [])] } });
535
- } }, t("save")), msg)),
700
+ section(t("runtimeResources"), true,
701
+ el("div", { class: "grid2" },
702
+ el("div", { class: "field" }, el("label", {}, `${t("autoPause")} (${t("minutes")})`, impact("instance")), fAutoPause, autoFeedback),
703
+ el("div", { class: "field" }, el("label", {}, t("toolSet"), impact("instance")), fToolSet),
704
+ el("div", { class: "field" }, el("label", {}, t("logLevel"), impact("instance")), fLogLevel))),
705
+ section(t("backendSection"), true,
706
+ el("div", { class: "grid2" },
707
+ el("div", { class: "field" }, el("label", {}, t("defaultBackend"), impact("instance")), fBackend),
708
+ el("div", { class: "field" }, el("label", {}, t("defaultModel"), impact("instance")), fModel),
709
+ el("div", { class: "field" }, el("label", {}, t("agentMode"), impact("instance")), fAgentMode))),
710
+ section(t("detection"), false,
711
+ el("div", { class: "grid2" },
712
+ el("div", { class: "field" }, el("label", { style: "display:flex;align-items:center;gap:8px" }, fHangEnabled, el("span", {}, t("hangEnabled")), impact("instance"))),
713
+ el("div", { class: "field" }, el("label", {}, `${t("hangTimeout")} (${t("minutes")})`, impact("instance")), fHangTimeout, hangFeedback))),
714
+ section(t("startup"), false,
715
+ el("div", { class: "grid2" },
716
+ el("div", { class: "field" }, el("label", {}, t("concurrency"), impact("fleet")), fConcurrency, concurrencyFeedback),
717
+ el("div", { class: "field" }, el("label", {}, `${t("staggerDelay")} (ms)`, impact("fleet")), fStagger, staggerFeedback))),
718
+ section(t("advancedSection"), false,
719
+ el("div", { class: "field" }, el("label", {}, t("language"), impact("now")), fLocale),
720
+ el("div", { class: "field" }, el("label", {}, t("allowedUsers"), impact("fleet")), chipList(allowedUsers, null, "user id")),
721
+ el("div", { class: "field" }, el("label", {}, t("classicAdmins"), impact("fleet")), chipList(c.admin_users, null, "user id")),
722
+ el("div", { class: "field" }, el("label", {}, t("allowedGuilds"), impact("fleet")), chipList(c.allowed_guilds, null, "guild id"))),
723
+ el("div", { class: "field" }, el("div", { class: "actions" }, saveBtn, msg)),
536
724
  );
725
+ validate();
537
726
  }
538
727
 
539
728
  // ── What's New ──
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@songsid/agend",
3
- "version": "2.1.0-beta.15",
3
+ "version": "2.1.0-beta.17",
4
4
  "description": "Multi-agent fleet daemon — run any coding CLI (Claude, Gemini, Codex, OpenCode) from Telegram",
5
5
  "type": "module",
6
6
  "bin": {