@songsid/agend 2.0.11-beta.29 → 2.0.11-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.
@@ -22,9 +22,15 @@
22
22
  .sec-head .grow { flex: 1; }
23
23
  .card { background: var(--card); border: 1px solid var(--border); border-radius: var(--radius); overflow: hidden; }
24
24
  .card.empty { padding: 16px; color: var(--muted); }
25
- .item { padding: 14px 16px; border-bottom: 1px solid var(--border); }
25
+ .item { padding: 9px 16px; border-bottom: 1px solid var(--border); }
26
26
  .item:last-child { border-bottom: none; }
27
- .item-row { display: flex; align-items: center; gap: 10px; }
27
+ .item-row { display: flex; align-items: center; gap: 10px; min-height: 24px; }
28
+ .group-head { display: flex; align-items: center; gap: 8px; padding: 7px 16px; cursor: pointer; font-size: 12px; font-weight: 600; color: var(--muted); text-transform: uppercase; letter-spacing: .3px; background: #f3f4f6; border-bottom: 1px solid var(--border); user-select: none; }
29
+ .group-head:hover { color: var(--fg); }
30
+ .group-head .caret { width: 12px; flex: 0 0 12px; font-size: 10px; }
31
+ .group-head .gcount { color: var(--muted); font-weight: 400; }
32
+ .item .del-btn { visibility: hidden; }
33
+ .item:hover .del-btn { visibility: visible; }
28
34
  .dot { width: 9px; height: 9px; border-radius: 50%; flex: 0 0 9px; background: #9ca3af; }
29
35
  .dot.running { background: var(--green); } .dot.crashed { background: var(--red); } .dot.stopped { background: #9ca3af; }
30
36
  .name { font-weight: 600; }
@@ -144,7 +150,7 @@
144
150
  }
145
151
 
146
152
  // ── State ──
147
- const state = { fleet: {}, classic: {}, live: {}, fleetUp: false, advFmt: "yaml", advEdit: false };
153
+ const state = { fleet: {}, classic: {}, live: {}, fleetUp: false, advFmt: "yaml", advEdit: false, collapsedGroups: new Set() };
148
154
  const channels = () => state.fleet.channels || (state.fleet.channel ? [state.fleet.channel] : []);
149
155
  const channelIds = () => channels().map(c => c.id || c.type).filter(Boolean);
150
156
  const chLabel = (i) => i === 0 ? "primary" : "persona";
@@ -174,42 +180,52 @@
174
180
  const shortName = (name, inst) => (inst && inst.display_name) || name.replace(/-t\d+$/, "");
175
181
  const STATUS_TEXT = { running: "🟢 Running", crashed: "🔴 Crashed", stopped: "⚪ Stopped" };
176
182
  const STATUS_ORDER = { running: 0, crashed: 1, stopped: 2 };
183
+ const groupOf = (inst) => (inst.tags && inst.tags[0]) || "Other";
184
+ function compactRow(name, inst) {
185
+ const st = state.live[name]?.status || "stopped";
186
+ const running = st === "running";
187
+ const short = shortName(name, inst);
188
+ const chTag = agentChannelLabel(inst);
189
+ const item = el("div", { class: "item" });
190
+ // Single fixed-height line: dot + short name + backend + channel tag + actions.
191
+ // Full ID lives in the tooltip and the Edit form; working dir only in Edit.
192
+ item.append(el("div", { class: "item-row" },
193
+ el("span", { class: "dot " + st, title: STATUS_TEXT[st] || st }),
194
+ el("span", { class: "name", title: name }, short),
195
+ el("span", { class: "sub", style: "margin:0" }, inst.backend || state.fleet.defaults?.backend || "claude-code"),
196
+ chTag ? el("span", { class: "tag" + (chTag.includes("persona") ? " persona" : chTag.includes("telegram") ? " tg" : "") }, chTag) : null,
197
+ el("span", { class: "spacer" }),
198
+ el("button", { class: "sm ghost", onclick: () => { editing = editing === name ? null : name; renderAgents(); } }, editing === name ? "Close" : "Edit"),
199
+ el("button", { class: "sm", onclick: () => toggleAgent(name, running) }, running ? "Stop" : "Start"),
200
+ el("button", { class: "sm danger del-btn", title: "Delete", onclick: () => delAgent(name) }, "🗑️"),
201
+ ));
202
+ if (editing === name) item.append(agentEditForm(name, inst));
203
+ return item;
204
+ }
177
205
  function renderAgents() {
178
206
  const host = $("agents"); host.innerHTML = "";
179
207
  const insts = state.fleet.instances || {};
208
+ if (!Object.keys(insts).length) { host.className = "card empty"; host.textContent = "No agents yet — click + New."; return; }
180
209
  const filter = ($("agentSearch")?.value || "").trim().toLowerCase();
181
210
  let names = Object.keys(insts);
182
211
  if (filter) names = names.filter(n => n.toLowerCase().includes(filter) || shortName(n, insts[n]).toLowerCase().includes(filter));
183
- // Group by status: Running first, then Crashed, then Stopped; name within.
184
- names.sort((a, b) => {
185
- const sa = STATUS_ORDER[state.live[a]?.status || "stopped"], sb = STATUS_ORDER[state.live[b]?.status || "stopped"];
186
- return sa !== sb ? sa - sb : a.localeCompare(b);
187
- });
188
- if (!Object.keys(insts).length) { host.className = "card empty"; host.textContent = "No agents yet — click + New."; return; }
189
212
  if (!names.length) { host.className = "card empty"; host.textContent = "No agents match \"" + filter + "\"."; return; }
190
213
  host.className = "card";
191
- for (const name of names) {
192
- const inst = insts[name];
193
- const st = state.live[name]?.status || "stopped";
194
- const running = st === "running";
195
- const short = shortName(name, inst);
196
- const item = el("div", { class: "item" });
197
- const chTag = agentChannelLabel(inst);
198
- item.append(el("div", { class: "item-row" },
199
- el("span", { class: "dot " + st }),
200
- el("span", { class: "name", title: name }, short),
201
- short !== name ? el("span", { class: "sub", style: "margin:0;font-size:11px" }, name) : null,
202
- el("span", { style: "font-size:12px;color:var(--muted)" }, STATUS_TEXT[st] || st),
203
- chTag ? el("span", { class: "tag" + (chTag.includes("persona") ? " persona" : chTag.includes("telegram") ? " tg" : "") }, chTag) : null,
204
- el("span", { class: "sub", style: "margin:0" }, inst.backend || state.fleet.defaults?.backend || "claude-code"),
205
- el("span", { class: "spacer" }),
206
- el("button", { class: "sm ghost", onclick: () => { editing = editing === name ? null : name; renderAgents(); } }, editing === name ? "Close" : "Edit"),
207
- el("button", { class: "sm", onclick: () => toggleAgent(name, running) }, running ? "Stop" : "Start"),
208
- el("button", { class: "sm danger", onclick: () => delAgent(name) }, "🗑️"),
209
- ));
210
- if (inst.working_directory) item.append(el("div", { class: "sub" }, inst.working_directory));
211
- if (editing === name) item.append(agentEditForm(name, inst));
212
- host.append(item);
214
+ // Group by first tag; "Other" sorts last. Within a group: Running → Crashed
215
+ // Stopped, then name.
216
+ const groups = {};
217
+ for (const n of names) (groups[groupOf(insts[n])] ??= []).push(n);
218
+ const gnames = Object.keys(groups).sort((a, b) => a === "Other" ? 1 : b === "Other" ? -1 : a.localeCompare(b));
219
+ for (const g of gnames) {
220
+ const members = groups[g].sort((a, b) => {
221
+ const sa = STATUS_ORDER[state.live[a]?.status || "stopped"], sb = STATUS_ORDER[state.live[b]?.status || "stopped"];
222
+ return sa !== sb ? sa - sb : a.localeCompare(b);
223
+ });
224
+ const collapsed = state.collapsedGroups.has(g) && !filter; // searching expands all
225
+ host.append(el("div", { class: "group-head", onclick: () => { state.collapsedGroups.has(g) ? state.collapsedGroups.delete(g) : state.collapsedGroups.add(g); renderAgents(); } },
226
+ el("span", { class: "caret" }, collapsed ? "" : ""), el("span", {}, g), el("span", { class: "gcount" }, `(${members.length})`)));
227
+ if (collapsed) continue;
228
+ for (const name of members) host.append(compactRow(name, insts[name]));
213
229
  }
214
230
  }
215
231
  function agentEditForm(name, inst) {
@@ -224,6 +240,7 @@
224
240
  const fGeneral = el("input", { type: "checkbox" }); fGeneral.checked = !!inst.general_topic;
225
241
  const msg = el("span", { class: "msg" });
226
242
  box.append(
243
+ el("div", { class: "sub", style: "margin:0 0 8px" }, "ID: " + name),
227
244
  el("div", { class: "grid2" }, el("div", {}, el("label", {}, "Backend"), fBackend), el("div", {}, el("label", {}, "Model"), fModel)),
228
245
  el("label", {}, "Working directory"), fWd,
229
246
  el("div", { class: "grid2" }, el("div", {}, el("label", {}, "Channel binding"), fChan), el("div", {}, el("label", {}, "Description"), fDesc)),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@songsid/agend",
3
- "version": "2.0.11-beta.29",
3
+ "version": "2.0.11-beta.30",
4
4
  "description": "Multi-agent fleet daemon — run any coding CLI (Claude, Gemini, Codex, OpenCode) from Telegram",
5
5
  "type": "module",
6
6
  "bin": {