@songsid/agend 2.1.0-beta.27 → 2.1.0-beta.29

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.
@@ -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.paused { background:#f59e0b; } .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; }
@@ -306,7 +306,7 @@
306
306
  }
307
307
 
308
308
  // ── State ──
309
- const state = { fleet: {}, classic: {}, classicStatus: {}, live: {}, fleetUp: false, advFmt: "yaml", advEdit: false, collapsedGroups: new Set(), version: "", newAgentOpen: false, newBotOpen: false, pending: new Map() };
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() };
310
310
  const channels = () => state.fleet.channels || (state.fleet.channel ? [state.fleet.channel] : []);
311
311
  const channelIds = () => channels().map(c => c.id || c.type).filter(Boolean);
312
312
  const chLabel = (i) => i === 0 ? "primary" : "persona";
@@ -383,6 +383,7 @@
383
383
  // Display name: profile display_name → else strip the -t<topicId> suffix.
384
384
  const shortName = (name, inst) => (inst && inst.display_name) || name.replace(/-t\d+$/, "");
385
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" };
386
387
  const STATUS_ORDER = { running: 0, paused: 1, crashed: 2, stopped: 3 };
387
388
  const groupOf = (inst) => (inst.tags && inst.tags[0]) || "Other";
388
389
  function compactRow(name, inst) {
@@ -628,6 +629,78 @@
628
629
  }
629
630
 
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
+ }
631
704
  function renderClassic() {
632
705
  const host = $("classicList"); if (!host) return; host.innerHTML = "";
633
706
  const chans = Object.entries(state.classic?.channels || {}).map(([key, channel]) => ({ key, ...channel }));
@@ -638,29 +711,29 @@
638
711
  if (!list.length) { host.className = "card empty"; host.textContent = "—"; return; }
639
712
  host.className = "card";
640
713
  list.sort((a, b) => {
641
- 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;
642
716
  return sa !== sb ? sa - sb : String(a.name || "").localeCompare(String(b.name || ""));
643
717
  });
644
718
  for (const c of list) {
645
- const st = state.classicStatus[c.instanceName] || "stopped";
719
+ const runtime = classicRuntime(c.instanceName);
720
+ const st = runtime.status;
646
721
  const adapter = c.adapterId || "discord";
647
- const backend = select(c.backend || state.classic?.defaults?.backend || "claude-code", BACKENDS.filter(value => value !== "gemini-cli"));
648
- backend.style.width = "150px";
649
- backend.onchange = () => {
650
- const value = backend.value;
651
- state.classic.channels[c.key].backend = value;
652
- stageChange(`classic:${c.key}`, {
653
- label: `Change ${c.name || c.instanceName} backend`, impact: "instance",
654
- apply: () => api(`/api/settings/classic/channels/${encodeURIComponent(c.key)}`, { method: "PATCH", body: JSON.stringify({ backend: value }) }),
655
- });
656
- };
657
- host.append(el("div", { class: "item" }, el("div", { class: "item-row" },
658
- 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 }),
659
725
  el("span", { class: "name", title: c.instanceName || "" }, c.name || c.instanceName || "?"),
660
- backend,
726
+ el("span", { class: "sub", style: "margin:0" }, c.backend || state.classic?.defaults?.backend || state.fleet?.defaults?.backend || "claude-code"),
661
727
  el("span", { class: "tag" + (adapter !== "discord" ? " persona" : "") }, adapter),
662
728
  c.collab ? el("span", { class: "tag", style: "background:#ecfdf5;color:#065f46" }, "collab") : null,
663
- )));
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);
664
737
  }
665
738
  }
666
739
 
@@ -872,9 +945,13 @@
872
945
  async function reloadLive() {
873
946
  const f = await api("/api/fleet");
874
947
  state.fleetUp = f.ok;
875
- state.live = {}; if (f.ok && f.body?.instances) for (const i of f.body.instances) state.live[i.name] = { status: i.status };
876
- // /api/fleet is fleet-config only; classic instance statuses come from the
877
- // 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.
878
955
  const pr = await api("/api/profiles");
879
956
  state.classicStatus = {}; if (pr.ok && Array.isArray(pr.body)) for (const r of pr.body) state.classicStatus[r.instance_name] = r.status;
880
957
  if (f.ok && f.body?.version) { state.version = f.body.version; $("verBadge").textContent = "v" + state.version; }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@songsid/agend",
3
- "version": "2.1.0-beta.27",
3
+ "version": "2.1.0-beta.29",
4
4
  "description": "Multi-agent fleet daemon — run any coding CLI (Claude, Gemini, Codex, OpenCode) from Telegram",
5
5
  "type": "module",
6
6
  "bin": {