@songsid/agend 2.1.0-beta.2 → 2.1.0-beta.20

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 (78) 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-endpoint.d.ts +8 -0
  6. package/dist/agent-endpoint.js +36 -8
  7. package/dist/agent-endpoint.js.map +1 -1
  8. package/dist/backend/antigravity.js +3 -2
  9. package/dist/backend/antigravity.js.map +1 -1
  10. package/dist/backend/factory.js +5 -1
  11. package/dist/backend/factory.js.map +1 -1
  12. package/dist/backend/grok.d.ts +41 -0
  13. package/dist/backend/grok.js +246 -0
  14. package/dist/backend/grok.js.map +1 -0
  15. package/dist/backend/kiro.js +3 -2
  16. package/dist/backend/kiro.js.map +1 -1
  17. package/dist/backend/types.d.ts +12 -2
  18. package/dist/backend/types.js +1 -0
  19. package/dist/backend/types.js.map +1 -1
  20. package/dist/classic-channel-manager.js +1 -1
  21. package/dist/cli.js +56 -6
  22. package/dist/cli.js.map +1 -1
  23. package/dist/config-validator.js +50 -1
  24. package/dist/config-validator.js.map +1 -1
  25. package/dist/config.d.ts +3 -1
  26. package/dist/config.js +13 -0
  27. package/dist/config.js.map +1 -1
  28. package/dist/context-guardian.d.ts +2 -0
  29. package/dist/context-guardian.js +10 -2
  30. package/dist/context-guardian.js.map +1 -1
  31. package/dist/daemon-entry.js +2 -1
  32. package/dist/daemon-entry.js.map +1 -1
  33. package/dist/daemon.d.ts +51 -3
  34. package/dist/daemon.js +380 -36
  35. package/dist/daemon.js.map +1 -1
  36. package/dist/fleet-context.d.ts +2 -1
  37. package/dist/fleet-manager.d.ts +23 -5
  38. package/dist/fleet-manager.js +229 -123
  39. package/dist/fleet-manager.js.map +1 -1
  40. package/dist/general-knowledge/skills/fleet-config/SKILL.md +1 -1
  41. package/dist/general-knowledge/skills/model-discovery/SKILL.md +1 -0
  42. package/dist/instance-lifecycle.d.ts +6 -1
  43. package/dist/instance-lifecycle.js +46 -3
  44. package/dist/instance-lifecycle.js.map +1 -1
  45. package/dist/logger.d.ts +6 -0
  46. package/dist/logger.js +29 -10
  47. package/dist/logger.js.map +1 -1
  48. package/dist/outbound-handlers.d.ts +5 -0
  49. package/dist/outbound-handlers.js +68 -16
  50. package/dist/outbound-handlers.js.map +1 -1
  51. package/dist/outbound-schemas.d.ts +1 -0
  52. package/dist/outbound-schemas.js +1 -1
  53. package/dist/outbound-schemas.js.map +1 -1
  54. package/dist/settings-api.d.ts +17 -2
  55. package/dist/settings-api.js +105 -8
  56. package/dist/settings-api.js.map +1 -1
  57. package/dist/setup-wizard.js +8 -0
  58. package/dist/setup-wizard.js.map +1 -1
  59. package/dist/statusline-watcher.d.ts +1 -1
  60. package/dist/statusline-watcher.js +3 -2
  61. package/dist/statusline-watcher.js.map +1 -1
  62. package/dist/tmux-manager.d.ts +3 -1
  63. package/dist/tmux-manager.js +9 -0
  64. package/dist/tmux-manager.js.map +1 -1
  65. package/dist/topic-archiver.d.ts +1 -1
  66. package/dist/topic-commands.d.ts +14 -1
  67. package/dist/topic-commands.js +96 -25
  68. package/dist/topic-commands.js.map +1 -1
  69. package/dist/transcript-monitor.js +2 -0
  70. package/dist/transcript-monitor.js.map +1 -1
  71. package/dist/types.d.ts +18 -1
  72. package/dist/ui/dashboard.html +2 -2
  73. package/dist/ui/settings.html +251 -40
  74. package/dist/view-api.d.ts +1 -1
  75. package/dist/web-api.d.ts +2 -1
  76. package/dist/web-api.js +24 -15
  77. package/dist/web-api.js.map +1 -1
  78. package/package.json +2 -1
@@ -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>
@@ -141,7 +164,7 @@
141
164
  const $ = (id) => document.getElementById(id);
142
165
  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
166
  const esc = (s) => String(s).replace(/[&<>]/g, c => ({ "&":"&amp;","<":"&lt;",">":"&gt;" }[c]));
144
- const BACKENDS = ["claude-code", "kiro-cli", "codex", "opencode", "gemini-cli", "antigravity"];
167
+ const BACKENDS = ["claude-code", "kiro-cli", "codex", "opencode", "gemini-cli", "antigravity", { value: "grok", label: "grok (experimental)" }];
145
168
 
146
169
  // ── i18n ──
147
170
  const I18N = {
@@ -152,6 +175,8 @@
152
175
  edit: "Edit", close: "Close", save: "Save", cancel: "Cancel", create: "Create", start: "Start", stop: "Stop",
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)",
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",
155
180
  language: "Language (bot replies)", defaultBackend: "Default backend", defaultModel: "Default model", allowedUsers: "Allowed users (primary bot)",
156
181
  classicAdmins: "ClassicBot admins", allowedGuilds: "Allowed guilds", type: "Type", adapterId: "Adapter id",
157
182
  tokenEnv: "Bot token env var", groupId: "Group / guild id", nameField: "Name",
@@ -168,6 +193,8 @@
168
193
  edit: "編輯", close: "關閉", save: "儲存", cancel: "取消", create: "建立", start: "啟動", stop: "停止",
169
194
  backend: "後端", model: "模型", workingDir: "工作目錄", channelBinding: "頻道綁定",
170
195
  description: "描述", systemPrompt: "System prompt", tags: "標籤", generalTopicField: "General topic(fleet 分派器)",
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",
171
198
  language: "語言(bot 回覆)", defaultBackend: "預設後端", defaultModel: "預設模型", allowedUsers: "允許使用者(主 bot)",
172
199
  classicAdmins: "ClassicBot 管理員", allowedGuilds: "允許的伺服器", type: "類型", adapterId: "Adapter id",
173
200
  tokenEnv: "Bot token 環境變數", groupId: "群組 / guild id", nameField: "名稱",
@@ -251,6 +278,14 @@
251
278
  else if (!html) html = `<span class="e">✗ ${esc((b && b.error) || ("HTTP " + res.status))}</span>`;
252
279
  elm.innerHTML = html;
253
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
+ }
254
289
 
255
290
  // ── State ──
256
291
  const state = { fleet: {}, classic: {}, classicStatus: {}, live: {}, fleetUp: false, advFmt: "yaml", advEdit: false, collapsedGroups: new Set(), version: "", newAgentOpen: false, newBotOpen: false };
@@ -268,7 +303,26 @@
268
303
  box.append(chips, el("div", { class: "chip-add" }, inp, el("button", { class: "sm", onclick: add }, "+ Add")));
269
304
  redraw(); return box;
270
305
  }
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; }
306
+ 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; }
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
+ }
272
326
 
273
327
  // ── My Agents ──
274
328
  let editing = null;
@@ -281,12 +335,13 @@
281
335
  }
282
336
  // Display name: profile display_name → else strip the -t<topicId> suffix.
283
337
  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 };
338
+ const STATUS_TEXT = { running: "🟢 Running", paused: "⏸ Paused", crashed: "🔴 Crashed", stopped: "⚪ Stopped" };
339
+ const STATUS_ORDER = { running: 0, paused: 1, crashed: 2, stopped: 3 };
286
340
  const groupOf = (inst) => (inst.tags && inst.tags[0]) || "Other";
287
341
  function compactRow(name, inst) {
288
342
  const st = state.live[name]?.status || "stopped";
289
343
  const running = st === "running";
344
+ const paused = st === "paused";
290
345
  const short = shortName(name, inst);
291
346
  const chTag = agentChannelLabel(inst);
292
347
  const item = el("div", { class: "item" });
@@ -299,7 +354,9 @@
299
354
  chTag ? el("span", { class: "tag" + (chTag.includes("persona") ? " persona" : chTag.includes("telegram") ? " tg" : "") }, chTag) : null,
300
355
  el("span", { class: "spacer" }),
301
356
  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")),
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,
303
360
  el("button", { class: "sm danger del-btn", title: t("close"), onclick: () => delAgent(name) }, "🗑️"),
304
361
  ));
305
362
  if (editing === name) item.append(agentEditForm(name, inst));
@@ -333,6 +390,7 @@
333
390
  }
334
391
  function agentEditForm(name, inst) {
335
392
  const box = el("div", { class: "edit" });
393
+ const defaults = state.fleet.defaults || {};
336
394
  const fBackend = select(inst.backend || state.fleet.defaults?.backend || "claude-code", BACKENDS);
337
395
  const fModel = el("input", { type: "text", value: inst.model || "", placeholder: "(inherit default)" });
338
396
  const fWd = el("input", { type: "text", value: inst.working_directory || "" });
@@ -341,29 +399,77 @@
341
399
  const fSys = el("input", { type: "text", value: inst.systemPrompt || "", placeholder: "e.g. file:prompts/dev.md" });
342
400
  const tags = [...(inst.tags || [])];
343
401
  const fGeneral = el("input", { type: "checkbox" }); fGeneral.checked = !!inst.general_topic;
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" });
344
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
+ };
345
449
  box.append(
346
450
  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"))),
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")),
457
+ el("div", { class: "sub", style: "margin:14px 0 6px;font-weight:600" }, t("runtimeResources")),
458
+ el("div", { class: "grid2" },
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)),
353
467
  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")),
468
+ saveBtn,
364
469
  el("button", { class: "sm ghost", onclick: () => { editing = null; renderAgents(); } }, t("cancel")),
365
470
  msg),
366
471
  );
472
+ validate();
367
473
  return box;
368
474
  }
369
475
  async function toggleAgent(name, running) {
@@ -371,6 +477,11 @@
371
477
  else await api(`/api/instance/${encodeURIComponent(name)}/start`, { method: "POST" });
372
478
  setTimeout(reloadLive, 800);
373
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
+ }
374
485
  async function delAgent(name) {
375
486
  if (!confirm(`Delete agent "${name}"? (config removed; running daemon stops on next Reload)`)) return;
376
487
  const res = await api(`/api/settings/fleet/instances/${encodeURIComponent(name)}`, { method: "DELETE" });
@@ -399,6 +510,7 @@
399
510
  }
400
511
 
401
512
  // ── Bots ──
513
+ let editingBot = null;
402
514
  function renderBots() {
403
515
  const host = $("bots"); host.innerHTML = ""; const chs = channels();
404
516
  if (!chs.length) { host.className = "card empty"; host.textContent = t("noBots"); return; }
@@ -412,12 +524,41 @@
412
524
  el("span", { class: "sub", style: "margin:0" }, ch.bot_token_env || "(no token env)"),
413
525
  el("span", { class: "spacer" }),
414
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")),
415
528
  el("button", { class: "sm danger", onclick: () => delBot(i) }, "🗑️"),
416
529
  ));
530
+ if (editingBot === i) item.append(botEditForm(ch, i));
417
531
  host.append(item);
418
532
  });
419
533
  }
420
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
+
421
562
  // ── ClassicBot Channels (read-only; created dynamically via /start /stop) ──
422
563
  function renderClassic() {
423
564
  const host = $("classicList"); if (!host) return; host.innerHTML = "";
@@ -486,6 +627,8 @@
486
627
  state.fleet.defaults = state.fleet.defaults || {};
487
628
  state.classic.defaults = state.classic.defaults || {};
488
629
  const d = state.fleet.defaults, c = state.classic.defaults;
630
+ const hang = d.hang_detector || {};
631
+ const startup = d.startup || {};
489
632
  const primary = channels()[0];
490
633
  if (primary) primary.access = primary.access || { mode: "locked", allowed_users: [] };
491
634
  const allowedUsers = primary ? (primary.access.allowed_users = primary.access.allowed_users || []) : [];
@@ -493,26 +636,93 @@
493
636
 
494
637
  const fBackend = select(d.backend || "claude-code", BACKENDS);
495
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"]);
496
642
  const fLocale = el("select");
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 });
497
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); });
498
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
+ };
499
699
  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)),
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)),
515
724
  );
725
+ validate();
516
726
  }
517
727
 
518
728
  // ── What's New ──
@@ -589,7 +799,8 @@
589
799
  renderAgents(); renderBots(); renderClassic();
590
800
  }
591
801
  async function reload() {
592
- const f = await api("/api/settings/fleet");
802
+ // Forms edit the user-authored config, not defaults-expanded runtime values.
803
+ const f = await api("/api/settings/fleet/raw");
593
804
  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
805
  state.fleet = f.body || {};
595
806
  const c = await api("/api/settings/classic"); state.classic = c.body || {};
@@ -16,7 +16,7 @@ export interface ViewApiContext {
16
16
  }[];
17
17
  getBackendByInstance(name: string, fleetDefault?: string): string;
18
18
  } | null;
19
- getInstanceStatus(name: string): "running" | "stopped" | "crashed";
19
+ getInstanceStatus(name: string): "running" | "paused" | "stopped" | "crashed";
20
20
  getUiStatus(): unknown;
21
21
  }
22
22
  /** True if the path belongs to the view feature (so the caller can skip the
package/dist/web-api.d.ts CHANGED
@@ -60,7 +60,8 @@ export interface WebApiContext {
60
60
  error(obj: unknown, msg?: string): void;
61
61
  };
62
62
  getInstanceDir(name: string): string;
63
- getInstanceStatus(name: string): "running" | "stopped" | "crashed";
63
+ getInstanceStatus(name: string): "running" | "paused" | "stopped" | "crashed";
64
+ deliverToInstance(instanceName: string, payload: Record<string, unknown>): Promise<void>;
64
65
  getUiStatus(): unknown;
65
66
  emitSseEvent(event: string, data: unknown): void;
66
67
  startInstance(name: string, config: unknown, topicMode: boolean): Promise<void>;
package/dist/web-api.js CHANGED
@@ -41,7 +41,7 @@ const ConfigUpdateSchema = z.object({
41
41
  access: z.record(z.string(), z.unknown()).optional(),
42
42
  }).strict().optional(),
43
43
  defaults: z.object({
44
- backend: z.enum(["claude-code", "gemini-cli", "codex", "opencode", "kiro-cli", "antigravity"]).optional(),
44
+ backend: z.enum(["claude-code", "gemini-cli", "codex", "opencode", "kiro-cli", "antigravity", "grok"]).optional(),
45
45
  model: z.string().max(128).optional(),
46
46
  }).strict().optional(),
47
47
  project_roots: z.array(z.string().min(1).max(1024)).max(64).optional(),
@@ -165,6 +165,8 @@ export function handleWebRequest(req, res, url, ctx) {
165
165
  { name: "gemini-cli", binary: "gemini" },
166
166
  { name: "opencode", binary: "opencode" },
167
167
  { name: "kiro-cli", binary: "kiro-cli" },
168
+ { name: "antigravity", binary: "agy" },
169
+ { name: "grok", binary: "grok" },
168
170
  ];
169
171
  const backends = BACKENDS.map(b => {
170
172
  let installed = false;
@@ -592,7 +594,7 @@ function handleSendMessage(req, res, ctx) {
592
594
  }
593
595
  body += chunk.toString();
594
596
  });
595
- req.on("end", () => {
597
+ req.on("end", async () => {
596
598
  try {
597
599
  const raw = JSON.parse(body);
598
600
  const parsed = SendMessageSchema.safeParse(raw);
@@ -614,19 +616,26 @@ function handleSendMessage(req, res, ctx) {
614
616
  // a separate reply path for that case.
615
617
  const groupId = ctx.fleetConfig?.channel?.group_id;
616
618
  const topicId = ctx.fleetConfig?.instances[instance]?.topic_id;
617
- ipc.send({
618
- type: "fleet_inbound",
619
- content: message,
620
- targetSession: instance,
621
- meta: {
622
- chat_id: groupId ? String(groupId) : "",
623
- message_id: `web-${Date.now()}`,
624
- user: "web-user", user_id: "web-user",
625
- ts,
626
- thread_id: topicId != null ? String(topicId) : "",
627
- source: "web",
628
- },
629
- });
619
+ try {
620
+ await ctx.deliverToInstance(instance, {
621
+ type: "fleet_inbound",
622
+ content: message,
623
+ targetSession: instance,
624
+ meta: {
625
+ chat_id: groupId ? String(groupId) : "",
626
+ message_id: `web-${Date.now()}`,
627
+ user: "web-user", user_id: "web-user",
628
+ ts,
629
+ thread_id: topicId != null ? String(topicId) : "",
630
+ source: "web",
631
+ },
632
+ });
633
+ }
634
+ catch (err) {
635
+ ctx.logger.error({ err, instance }, "Web message delivery failed");
636
+ json(res, 503, { error: "Instance delivery failed" });
637
+ return;
638
+ }
630
639
  ctx.lastInboundUser.set(instance, "web-user");
631
640
  ctx.eventLog?.logActivity("message", "web-user", message.slice(0, 200), instance);
632
641
  ctx.emitSseEvent("message", { instance, sender: "web-user", text: message, ts });