@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.
- package/dist/ui/view.html +85 -48
- 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:
|
|
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).
|
|
164
|
-
//
|
|
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
|
|
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: "
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
445
|
-
|
|
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
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
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
|
|
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
|
-
|
|
745
|
-
|
|
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>` : "") +
|
|
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(); });
|