@songsid/agend 2.1.3-beta.2 → 2.1.3-beta.3

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 (2) hide show
  1. package/dist/ui/view.html +85 -48
  2. package/package.json +1 -1
package/dist/ui/view.html CHANGED
@@ -28,6 +28,14 @@
28
28
  .dot.running { background: #3fb950; } .dot.stopped { background: #6e7681; } .dot.crashed { background: #f85149; }
29
29
  .inst .nm { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
30
30
  .inst .pct { color: var(--dim); font-size: 11px; }
31
+ .cli-icon { width: 10px; height: 10px; flex: 0 0 10px; border-radius: 50%; border: 1px solid rgba(255,255,255,.24); box-shadow: 0 0 0 1px rgba(0,0,0,.18); }
32
+ .cli-claude-code { background: #c678dd; }
33
+ .cli-kiro-cli { background: #58a6ff; }
34
+ .cli-codex { background: #3fb950; }
35
+ .cli-grok { background: #0d1117; border-color: #f0f6fc; }
36
+ .cli-antigravity { background: #f85149; }
37
+ .cli-opencode { background: #f0f6fc; }
38
+ .cli-other { background: #6e7681; }
31
39
 
32
40
  /* Main */
33
41
  #main { flex: 1; display: flex; flex-direction: column; min-width: 0; }
@@ -86,9 +94,13 @@
86
94
  /* AI usage panel (shares .help-panel chrome) */
87
95
  .u-provider { margin: 14px 0 4px; padding-top: 10px; border-top: 1px solid var(--border); }
88
96
  .u-provider:first-of-type { border-top: none; padding-top: 0; }
89
- .u-head { display: flex; align-items: baseline; gap: 8px; }
97
+ .u-head { display: flex; align-items: center; gap: 8px; }
90
98
  .u-head .u-name { font-size: 14px; font-weight: 600; }
91
99
  .u-head .u-plan { color: var(--dim); font-size: 11px; }
100
+ .u-order { display: flex; gap: 3px; margin-left: auto; }
101
+ .u-move { width: 24px; height: 22px; border: 1px solid var(--border); border-radius: 5px; background: none; color: var(--dim); cursor: pointer; line-height: 1; }
102
+ .u-move:hover:not(:disabled) { color: var(--fg); border-color: var(--accent); }
103
+ .u-move:disabled { opacity: .25; cursor: default; }
92
104
  .u-metric { margin: 8px 0; }
93
105
  .u-row { display: flex; align-items: baseline; gap: 6px; font-size: 12px; }
94
106
  .u-row .u-label { color: var(--dim); flex: 1; min-width: 0; }
@@ -160,11 +172,9 @@
160
172
 
161
173
  <script>
162
174
  (() => {
163
- // GET routes are open (no token). Writes (profile/avatar/sort-order POST)
164
- // require the web.token, which the user enters in the edit panel; we keep it
165
- // in localStorage. A ?token= URL param is still honored as a fallback.
175
+ // GET routes are open (no token). Profile/avatar writes require web.token;
176
+ // visual ordering is browser-local and never leaves localStorage.
166
177
  const urlToken = new URLSearchParams(location.search).get("token") || "";
167
- const webToken = () => localStorage.getItem("agend_web_token") || urlToken;
168
178
  const q = (id) => document.getElementById(id);
169
179
 
170
180
  // ── i18n ──
@@ -174,22 +184,24 @@
174
184
  saveHint: "Viewing is open (read-only). Saving requires the web.token (~/.agend/web.token).", helpTitle: "📖 Help",
175
185
  help1: "Pick an instance on the left → its live terminal shows on the right",
176
186
  help2: "Click a group header ▼/▶ to collapse/expand", help3: "✏️ Edit changes display name, role, description",
177
- help4: "Drag to reorder (needs web.token permission)",
187
+ help4: "Drag groups or instances to reorder them in this browser",
178
188
  help5: "Click ⤢ to cycle font density: Fit → Comfortable → Compact",
179
189
  densityFit: "Fit", densityComfortable: "Comfortable", densityCompact: "Compact", densityLabel: "Font density",
180
190
  usageTitle: "📊 AI Usage", usageEmpty: "No AI CLI logins found on this machine.",
181
191
  usageUpdated: "updated", usageRefresh: "Refresh", usageHigh: "high", usageNear: "near limit",
192
+ usageMoveUp: "Move up", usageMoveDown: "Move down",
182
193
  usageResets: "resets in {t}", usageSoon: "resets soon", usageFail: "Could not load usage data." },
183
194
  "zh-TW": { viewTitle: "AgEnD · 檢視", editProfile: "✏️ 編輯", save: "儲存", cancel: "取消",
184
195
  displayName: "顯示名稱", role: "角色", description: "描述", tokenPh: "web.token(儲存時需要)",
185
196
  saveHint: "檢視為公開(唯讀)。儲存需要 web.token(~/.agend/web.token)。", helpTitle: "📖 使用說明",
186
197
  help1: "左側選 instance → 右側顯示即時 terminal 畫面",
187
198
  help2: "點 group header ▼/▶ 收合/展開", help3: "✏️ Edit 可修改顯示名稱、角色、描述",
188
- help4: "拖曳可調整順序(需 web.token 權限)",
199
+ help4: "拖曳 group 或 instance 可調整此瀏覽器的顯示順序",
189
200
  help5: "點 ⤢ 循環切換字級密度:剛好填滿 → 舒適 → 精簡",
190
201
  densityFit: "剛好填滿", densityComfortable: "舒適", densityCompact: "精簡", densityLabel: "字級密度",
191
202
  usageTitle: "📊 AI 訂閱用量", usageEmpty: "本機沒有偵測到已登入的 AI CLI。",
192
203
  usageUpdated: "更新於", usageRefresh: "重新整理", usageHigh: "偏高", usageNear: "接近上限",
204
+ usageMoveUp: "上移", usageMoveDown: "下移",
193
205
  usageResets: "{t} 後重置", usageSoon: "即將重置", usageFail: "無法載入用量資料。" },
194
206
  };
195
207
  let lang = localStorage.getItem("agend_lang") || (navigator.language && navigator.language.toLowerCase().startsWith("zh") ? "zh-TW" : "en");
@@ -228,9 +240,9 @@
228
240
  let sortInsts = new Map(); // instanceName -> sort_index (within its group)
229
241
  let groupNames = []; // group display order (computed)
230
242
  let instByGroup = new Map(); // group -> [instance_name, …] display order (computed)
231
- let writable = false; // whether the current web.token can POST
232
- let probed = false;
233
243
  let dragItem = null; // { type:'group'|'instance', name, group }
244
+ const SIDEBAR_ORDER_KEY = "agend_view_sidebar_order";
245
+ const USAGE_ORDER_KEY = "agend_view_usage_order";
234
246
 
235
247
  const api = (path) => urlToken ? `${path}${path.includes("?") ? "&" : "?"}token=${encodeURIComponent(urlToken)}` : path;
236
248
 
@@ -310,6 +322,13 @@
310
322
  return n || it.instance_name;
311
323
  }
312
324
 
325
+ function backendIconHtml(backend) {
326
+ const known = new Set(["claude-code", "kiro-cli", "codex", "grok", "antigravity", "opencode"]);
327
+ const id = known.has(backend) ? backend : "other";
328
+ const label = backend || "unknown backend";
329
+ return `<span class="cli-icon cli-${id}" title="${esc(label)}" aria-label="${esc(label)}"></span>`;
330
+ }
331
+
313
332
  // Compute display order: persisted sort-order first (by index), then any
314
333
  // new/unsorted groups & instances by the tag+alpha fallback. Stored in
315
334
  // groupNames / instByGroup so drag handlers can mutate + persist them.
@@ -349,10 +368,10 @@
349
368
  const isCollapsed = collapsed.has(g);
350
369
  const header = document.createElement("div");
351
370
  header.className = "group-header";
352
- header.draggable = writable;
371
+ header.draggable = true;
353
372
  header.innerHTML = `<span class="caret">${isCollapsed ? "▶" : "▼"}</span><span>${esc(g)}</span><span class="gcount">${names.length}</span>`;
354
373
  header.onclick = () => { if (collapsed.has(g)) collapsed.delete(g); else collapsed.add(g); renderList(); };
355
- if (writable) wireDrag(header, { type: "group", name: g });
374
+ wireDrag(header, { type: "group", name: g });
356
375
  list.appendChild(header);
357
376
  if (isCollapsed) continue;
358
377
 
@@ -361,10 +380,10 @@
361
380
  if (!it) continue;
362
381
  const el = document.createElement("div");
363
382
  el.className = "inst" + (name === current ? " active" : "");
364
- el.draggable = writable;
365
- el.innerHTML = `<span class="dot ${it.status}"></span><span class="nm">${esc(shortName(it))}</span><span class="pct">${it.context_pct ? Math.round(it.context_pct)+"%" : ""}</span>`;
383
+ el.draggable = true;
384
+ el.innerHTML = `<span class="dot ${it.status}"></span><span class="nm">${esc(shortName(it))}</span><span class="pct">${it.context_pct ? Math.round(it.context_pct)+"%" : ""}</span>${backendIconHtml(it.backend)}`;
366
385
  el.onclick = () => select(name);
367
- if (writable) wireDrag(el, { type: "instance", name, group: g });
386
+ wireDrag(el, { type: "instance", name, group: g });
368
387
  list.appendChild(el);
369
388
  }
370
389
  }
@@ -426,35 +445,25 @@
426
445
  return rows;
427
446
  }
428
447
 
429
- async function persistOrder() {
448
+ function persistOrder() {
430
449
  // Mirror into the local maps so a mid-flight refresh doesn't revert.
431
450
  sortGroups = new Map(groupNames.map((g, i) => [g, i]));
432
451
  sortInsts = new Map();
433
452
  for (const g of groupNames) (instByGroup.get(g) || []).forEach((n, j) => sortInsts.set(n, j));
434
- try {
435
- const r = await fetch("/api/sort-order", {
436
- method: "POST",
437
- headers: { "Content-Type": "application/json", "X-Agend-Token": webToken() },
438
- body: JSON.stringify(orderRows()),
439
- });
440
- if (r.status === 401) { writable = false; renderList(); }
441
- } catch { /* transient */ }
453
+ try { localStorage.setItem(SIDEBAR_ORDER_KEY, JSON.stringify(orderRows())); }
454
+ catch { /* ordering still applies for this page even if storage is unavailable */ }
442
455
  }
443
456
 
444
- // Probe write permission by POSTing the current order (idempotent for a
445
- // web-token user; 401 without a valid web.token → drag stays disabled).
446
- // Re-run after the user enters a web.token so drag-sort lights up.
447
- async function probeWrite() {
448
- if (!webToken()) { if (writable) { writable = false; renderList(); } return; }
457
+ function restoreSidebarOrder() {
458
+ sortGroups = new Map(); sortInsts = new Map();
449
459
  try {
450
- const r = await fetch("/api/sort-order", {
451
- method: "POST",
452
- headers: { "Content-Type": "application/json", "X-Agend-Token": webToken() },
453
- body: JSON.stringify(orderRows()),
454
- });
455
- const nowWritable = r.ok;
456
- if (nowWritable !== writable) { writable = nowWritable; renderList(); }
457
- } catch { /* leave disabled */ }
460
+ const rows = JSON.parse(localStorage.getItem(SIDEBAR_ORDER_KEY) || "[]");
461
+ if (!Array.isArray(rows)) return;
462
+ for (const row of rows) {
463
+ if (row.item_type === "group" && typeof row.item_name === "string") sortGroups.set(row.item_name, Number(row.sort_index) || 0);
464
+ else if (row.item_type === "instance" && typeof row.item_name === "string") sortInsts.set(row.item_name, Number(row.sort_index) || 0);
465
+ }
466
+ } catch { /* ignore invalid browser-local state */ }
458
467
  }
459
468
 
460
469
  function renderCard() {
@@ -496,20 +505,13 @@
496
505
 
497
506
  async function loadRoster() {
498
507
  try {
499
- const [pr, so] = await Promise.all([fetch(api("/api/profiles")), fetch(api("/api/sort-order"))]);
508
+ const pr = await fetch(api("/api/profiles"));
500
509
  if (!pr.ok) throw new Error("HTTP " + pr.status);
501
510
  roster = await pr.json();
502
511
  rosterByName = new Map(roster.map(r => [r.instance_name, r]));
503
- const soRows = so.ok ? await so.json() : [];
504
- sortGroups = new Map(); sortInsts = new Map();
505
- for (const row of soRows) {
506
- if (row.item_type === "group") sortGroups.set(row.item_name, row.sort_index);
507
- else if (row.item_type === "instance") sortInsts.set(row.item_name, row.sort_index);
508
- }
509
512
  if (!current && roster.length) current = roster[0].instance_name;
510
513
  computeOrder();
511
514
  renderList(); renderCard();
512
- if (!probed) { probed = true; probeWrite(); }
513
515
  } catch (e) { q("pre").textContent = "Failed to load roster: " + e.message + "\n(Is the fleet running?)"; }
514
516
  }
515
517
 
@@ -684,7 +686,6 @@
684
686
  }
685
687
  q("msg").className = "ok"; q("msg").textContent = "Saved";
686
688
  await loadRoster();
687
- probeWrite(); // web.token now stored — enable drag-sort
688
689
  setTimeout(exitEdit, 500);
689
690
  } catch (e) { q("msg").className = "err"; q("msg").textContent = e.message; }
690
691
  }
@@ -705,6 +706,33 @@
705
706
  // ── AI usage panel ────────────────────────────────────
706
707
  let usageData = null;
707
708
  let usageTimer = null;
709
+ let usageOrder = [];
710
+ try {
711
+ const saved = JSON.parse(localStorage.getItem(USAGE_ORDER_KEY) || "[]");
712
+ if (Array.isArray(saved)) usageOrder = saved.filter(v => typeof v === "string");
713
+ } catch { /* ignore invalid browser-local state */ }
714
+
715
+ function providerKey(provider) { return String(provider.id || provider.name || ""); }
716
+ function orderedUsageProviders(providers) {
717
+ const rank = new Map(usageOrder.map((id, i) => [id, i]));
718
+ return providers.map((provider, index) => ({ provider, index })).sort((a, b) => {
719
+ const ar = rank.has(providerKey(a.provider)) ? rank.get(providerKey(a.provider)) : Infinity;
720
+ const br = rank.has(providerKey(b.provider)) ? rank.get(providerKey(b.provider)) : Infinity;
721
+ return ar === br ? a.index - b.index : ar - br;
722
+ }).map(entry => entry.provider);
723
+ }
724
+
725
+ function moveUsageProvider(id, delta) {
726
+ if (!usageData || !Array.isArray(usageData.providers)) return;
727
+ const ids = orderedUsageProviders(usageData.providers).map(providerKey);
728
+ const from = ids.indexOf(id), to = from + delta;
729
+ if (from < 0 || to < 0 || to >= ids.length) return;
730
+ [ids[from], ids[to]] = [ids[to], ids[from]];
731
+ usageOrder = ids;
732
+ try { localStorage.setItem(USAGE_ORDER_KEY, JSON.stringify(usageOrder)); }
733
+ catch { /* ordering still applies for this page even if storage is unavailable */ }
734
+ renderUsage();
735
+ }
708
736
 
709
737
  function usageResetText(iso) {
710
738
  if (!iso) return "";
@@ -741,10 +769,15 @@
741
769
  const box = q("usageContent");
742
770
  if (!usageData || !Array.isArray(usageData.providers)) { box.innerHTML = `<h3>${T("usageTitle")}</h3><div class="u-err">${T("usageFail")}</div>`; return; }
743
771
  let html = `<h3>${T("usageTitle")}</h3>`;
744
- if (!usageData.providers.length) html += `<div class="u-err">${T("usageEmpty")}</div>`;
745
- for (const p of usageData.providers) {
772
+ const providers = orderedUsageProviders(usageData.providers);
773
+ if (!providers.length) html += `<div class="u-err">${T("usageEmpty")}</div>`;
774
+ for (let index = 0; index < providers.length; index++) {
775
+ const p = providers[index];
776
+ const key = providerKey(p);
746
777
  html += `<div class="u-provider"><div class="u-head"><span class="u-name">${esc(p.name)}</span>` +
747
- (p.plan ? `<span class="u-plan">${esc(p.plan)}</span>` : "") + `</div>`;
778
+ (p.plan ? `<span class="u-plan">${esc(p.plan)}</span>` : "") +
779
+ `<span class="u-order"><button class="u-move" data-provider="${esc(key)}" data-delta="-1" title="${T("usageMoveUp")}" aria-label="${T("usageMoveUp")}"${index === 0 ? " disabled" : ""}>↑</button>` +
780
+ `<button class="u-move" data-provider="${esc(key)}" data-delta="1" title="${T("usageMoveDown")}" aria-label="${T("usageMoveDown")}"${index === providers.length - 1 ? " disabled" : ""}>↓</button></span></div>`;
748
781
  if (p.status === "error") html += `<div class="u-err">${esc(p.error || "error")}</div>`;
749
782
  else {
750
783
  html += (p.metrics || []).map(usageMetricHtml).join("");
@@ -758,6 +791,9 @@
758
791
  const at = new Date(usageData.fetchedAt).toLocaleTimeString();
759
792
  html += `<div class="u-foot"><span>${T("usageUpdated")} ${esc(at)}</span><button id="usageRefresh">${T("usageRefresh")}</button></div>`;
760
793
  box.innerHTML = html;
794
+ box.querySelectorAll(".u-move").forEach(button => {
795
+ button.onclick = () => moveUsageProvider(button.dataset.provider, Number(button.dataset.delta));
796
+ });
761
797
  const rb = q("usageRefresh");
762
798
  if (rb) rb.onclick = () => loadUsage(true);
763
799
  }
@@ -794,6 +830,7 @@
794
830
  if (window.ResizeObserver) new ResizeObserver(scheduleFit).observe(q("term"));
795
831
  applyI18n();
796
832
  applyCardExpanded(cardExpanded);
833
+ restoreSidebarOrder();
797
834
 
798
835
  // ── Boot ──────────────────────────────────────────────
799
836
  loadRoster().then(() => { refreshPane(); });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@songsid/agend",
3
- "version": "2.1.3-beta.2",
3
+ "version": "2.1.3-beta.3",
4
4
  "description": "Multi-agent fleet daemon — run any coding CLI (Claude, Gemini, Codex, OpenCode) from Telegram",
5
5
  "type": "module",
6
6
  "bin": {