@songsid/agend 2.0.11-beta.4 → 2.0.11-beta.40
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/channel/adapters/discord.d.ts +2 -0
- package/dist/channel/adapters/discord.js +45 -36
- package/dist/channel/adapters/discord.js.map +1 -1
- package/dist/channel/adapters/telegram.js +1 -0
- package/dist/channel/adapters/telegram.js.map +1 -1
- package/dist/channel/factory.d.ts +4 -0
- package/dist/channel/factory.js +1 -0
- package/dist/channel/factory.js.map +1 -1
- package/dist/channel/mcp-tools.js +3 -0
- package/dist/channel/mcp-tools.js.map +1 -1
- package/dist/channel/tool-router.js +29 -2
- package/dist/channel/tool-router.js.map +1 -1
- package/dist/channel/types.d.ts +2 -0
- package/dist/classic-channel-manager.d.ts +59 -12
- package/dist/classic-channel-manager.js +121 -30
- package/dist/classic-channel-manager.js.map +1 -1
- package/dist/cli.js +94 -7
- package/dist/cli.js.map +1 -1
- package/dist/config-validator.d.ts +20 -0
- package/dist/config-validator.js +154 -0
- package/dist/config-validator.js.map +1 -0
- package/dist/fleet-manager.d.ts +29 -6
- package/dist/fleet-manager.js +455 -258
- package/dist/fleet-manager.js.map +1 -1
- package/dist/instance-lifecycle.d.ts +1 -1
- package/dist/instance-lifecycle.js +15 -13
- package/dist/instance-lifecycle.js.map +1 -1
- package/dist/locale.d.ts +19 -0
- package/dist/locale.js +179 -0
- package/dist/locale.js.map +1 -0
- package/dist/outbound-schemas.d.ts +1 -0
- package/dist/outbound-schemas.js +1 -0
- package/dist/outbound-schemas.js.map +1 -1
- package/dist/quickstart.js +252 -16
- package/dist/quickstart.js.map +1 -1
- package/dist/scheduler/scheduler.js +8 -2
- package/dist/scheduler/scheduler.js.map +1 -1
- package/dist/service-installer.d.ts +1 -0
- package/dist/service-installer.js +2 -1
- package/dist/service-installer.js.map +1 -1
- package/dist/settings-api.d.ts +33 -0
- package/dist/settings-api.js +283 -0
- package/dist/settings-api.js.map +1 -0
- package/dist/topic-commands.d.ts +14 -0
- package/dist/topic-commands.js +78 -16
- package/dist/topic-commands.js.map +1 -1
- package/dist/types.d.ts +2 -0
- package/dist/ui/settings.html +617 -0
- package/dist/ui/view.html +295 -39
- package/dist/view-api.d.ts +9 -0
- package/dist/view-api.js +101 -40
- package/dist/view-api.js.map +1 -1
- package/package.json +1 -1
- package/templates/systemd.service.ejs +2 -1
package/dist/ui/view.html
CHANGED
|
@@ -13,7 +13,15 @@
|
|
|
13
13
|
/* Sidebar */
|
|
14
14
|
#sidebar { width: 240px; flex: 0 0 240px; background: var(--panel); border-right: 1px solid var(--border); display: flex; flex-direction: column; overflow-y: auto; }
|
|
15
15
|
#sidebar h1 { font-size: 14px; color: var(--accent); padding: 14px 16px; border-bottom: 1px solid var(--border); font-weight: 600; letter-spacing: .5px; }
|
|
16
|
-
.
|
|
16
|
+
.group-header { display: flex; align-items: center; gap: 6px; padding: 8px 12px; cursor: pointer; font-size: 12px; font-weight: 600; color: var(--dim); text-transform: uppercase; letter-spacing: .4px; background: rgba(255,255,255,.02); border-bottom: 1px solid var(--border); user-select: none; }
|
|
17
|
+
.group-header:hover { color: var(--fg); }
|
|
18
|
+
.group-header .caret { width: 10px; flex: 0 0 10px; }
|
|
19
|
+
.group-header .gcount { margin-left: auto; opacity: .7; font-weight: 400; }
|
|
20
|
+
.inst { display: flex; align-items: center; gap: 8px; padding: 10px 16px 10px 24px; cursor: pointer; border-bottom: 1px solid rgba(255,255,255,.03); font-size: 13px; }
|
|
21
|
+
.dragging { opacity: .4; }
|
|
22
|
+
.drop-before { border-top: 2px solid var(--accent) !important; }
|
|
23
|
+
.drop-after { border-bottom: 2px solid var(--accent) !important; }
|
|
24
|
+
[draggable="true"] { cursor: grab; }
|
|
17
25
|
.inst:hover { background: rgba(255,255,255,.04); }
|
|
18
26
|
.inst.active { background: rgba(88,166,255,.14); border-left: 3px solid var(--accent); padding-left: 13px; }
|
|
19
27
|
.dot { width: 8px; height: 8px; border-radius: 50%; flex: 0 0 8px; }
|
|
@@ -28,7 +36,9 @@
|
|
|
28
36
|
|
|
29
37
|
/* Card */
|
|
30
38
|
#card { background: var(--panel); border-top: 1px solid var(--border); padding: 14px 18px; display: flex; gap: 16px; align-items: flex-start; }
|
|
31
|
-
#
|
|
39
|
+
#avatarBox { width: 56px; height: 56px; flex: 0 0 56px; }
|
|
40
|
+
.avatar-img { width: 56px; height: 56px; border-radius: 50%; object-fit: cover; display: block; }
|
|
41
|
+
.avatar-ph { width: 56px; height: 56px; border-radius: 50%; background: #30363d; color: var(--dim); display: flex; align-items: center; justify-content: center; font-size: 24px; font-weight: 600; text-transform: uppercase; user-select: none; }
|
|
32
42
|
#cardBody { flex: 1; min-width: 0; }
|
|
33
43
|
#cardBody .row1 { display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap; }
|
|
34
44
|
#cardBody .dname { font-size: 16px; font-weight: 600; }
|
|
@@ -48,46 +58,104 @@
|
|
|
48
58
|
#edit .hint { color: var(--dim); font-size: 11px; }
|
|
49
59
|
#msg { font-size: 12px; }
|
|
50
60
|
.err { color: #f85149; } .ok { color: #3fb950; }
|
|
61
|
+
.help-btn { position: fixed; top: 10px; right: 14px; width: 34px; height: 34px; border-radius: 50%; border: 1px solid var(--border); background: var(--panel); color: var(--fg); cursor: pointer; font-size: 16px; z-index: 60; display: inline-flex; align-items: center; justify-content: center; padding: 0; }
|
|
62
|
+
.help-btn:hover { background: var(--accent); border-color: var(--accent); }
|
|
63
|
+
.help-backdrop { position: fixed; inset: 0; background: rgba(0,0,0,.5); display: none; z-index: 70; }
|
|
64
|
+
.help-backdrop.open { display: block; }
|
|
65
|
+
.help-panel { position: fixed; top: 0; right: 0; height: 100%; width: 360px; max-width: 90vw; background: var(--panel); border-left: 1px solid var(--border); box-shadow: -8px 0 24px rgba(0,0,0,.4); transform: translateX(100%); transition: transform .2s ease; z-index: 80; overflow-y: auto; padding: 20px 22px; }
|
|
66
|
+
.help-panel.open { transform: translateX(0); }
|
|
67
|
+
.help-panel h3 { font-size: 16px; margin-bottom: 8px; }
|
|
68
|
+
.help-panel ul { margin: 8px 0 0 18px; }
|
|
69
|
+
.help-panel li { font-size: 13px; margin: 6px 0; color: var(--fg); }
|
|
70
|
+
.help-panel .close { position: absolute; top: 14px; right: 16px; cursor: pointer; color: var(--dim); font-size: 22px; line-height: 1; background: none; border: none; }
|
|
51
71
|
</style>
|
|
52
72
|
</head>
|
|
53
73
|
<body>
|
|
54
|
-
<
|
|
74
|
+
<button class="help-btn" id="langBtn" title="Language" style="right:56px">🌐</button>
|
|
75
|
+
<button class="help-btn" id="helpBtn" title="Help">ℹ️</button>
|
|
76
|
+
<div class="help-backdrop" id="helpBackdrop"></div>
|
|
77
|
+
<aside class="help-panel" id="helpPanel">
|
|
78
|
+
<button class="close" id="helpClose" title="Close">×</button>
|
|
79
|
+
<div id="helpContent"></div>
|
|
80
|
+
</aside>
|
|
81
|
+
<div id="sidebar"><h1 data-i18n="viewTitle">AgEnD · View</h1><div id="list"></div></div>
|
|
55
82
|
<div id="main">
|
|
56
83
|
<div id="term"><pre id="pre"></pre></div>
|
|
57
84
|
<div id="card">
|
|
58
|
-
<
|
|
85
|
+
<div id="avatarBox"></div>
|
|
59
86
|
<div id="cardBody">
|
|
60
87
|
<div class="row1"><span class="dname" id="dname">—</span><span class="role" id="role"></span></div>
|
|
61
88
|
<div class="meta" id="meta"></div>
|
|
62
89
|
<div class="desc" id="desc"></div>
|
|
63
90
|
</div>
|
|
64
|
-
<button id="editBtn">✏️ Edit</button>
|
|
91
|
+
<button id="editBtn" data-i18n="editProfile">✏️ Edit</button>
|
|
65
92
|
<div id="edit">
|
|
66
|
-
<input id="fDisplay" placeholder="Display name">
|
|
67
|
-
<input id="fRole" placeholder="Role">
|
|
68
|
-
<textarea id="fDesc" placeholder="Description"></textarea>
|
|
93
|
+
<input id="fDisplay" data-i18n-ph="displayName" placeholder="Display name">
|
|
94
|
+
<input id="fRole" data-i18n-ph="role" placeholder="Role">
|
|
95
|
+
<textarea id="fDesc" data-i18n-ph="description" placeholder="Description"></textarea>
|
|
69
96
|
<input id="fAvatar" type="file" accept="image/png,image/jpeg,image/gif,image/webp">
|
|
70
|
-
<input id="fToken" type="password" placeholder="web.token (required to save)">
|
|
97
|
+
<input id="fToken" type="password" data-i18n-ph="tokenPh" placeholder="web.token (required to save)">
|
|
71
98
|
<div class="actions">
|
|
72
|
-
<button id="saveBtn">Save</button>
|
|
73
|
-
<button id="cancelBtn">Cancel</button>
|
|
99
|
+
<button id="saveBtn" data-i18n="save">Save</button>
|
|
100
|
+
<button id="cancelBtn" data-i18n="cancel">Cancel</button>
|
|
74
101
|
<span id="msg"></span>
|
|
75
102
|
</div>
|
|
76
|
-
<div class="hint">
|
|
103
|
+
<div class="hint" data-i18n="saveHint">Viewing is open (read-only). Saving requires the web.token (~/.agend/web.token).</div>
|
|
77
104
|
</div>
|
|
78
105
|
</div>
|
|
79
106
|
</div>
|
|
80
107
|
|
|
81
108
|
<script>
|
|
82
109
|
(() => {
|
|
83
|
-
|
|
110
|
+
// GET routes are open (no token). Writes (profile/avatar/sort-order POST)
|
|
111
|
+
// require the web.token, which the user enters in the edit panel; we keep it
|
|
112
|
+
// in localStorage. A ?token= URL param is still honored as a fallback.
|
|
113
|
+
const urlToken = new URLSearchParams(location.search).get("token") || "";
|
|
114
|
+
const webToken = () => localStorage.getItem("agend_web_token") || urlToken;
|
|
84
115
|
const q = (id) => document.getElementById(id);
|
|
116
|
+
|
|
117
|
+
// ── i18n ──
|
|
118
|
+
const I18N = {
|
|
119
|
+
en: { viewTitle: "AgEnD · View", editProfile: "✏️ Edit", save: "Save", cancel: "Cancel",
|
|
120
|
+
displayName: "Display name", role: "Role", description: "Description", tokenPh: "web.token (required to save)",
|
|
121
|
+
saveHint: "Viewing is open (read-only). Saving requires the web.token (~/.agend/web.token).", helpTitle: "📖 Help",
|
|
122
|
+
help1: "Pick an instance on the left → its live terminal shows on the right",
|
|
123
|
+
help2: "Click a group header ▼/▶ to collapse/expand", help3: "✏️ Edit changes display name, role, description",
|
|
124
|
+
help4: "Drag to reorder (needs web.token permission)" },
|
|
125
|
+
"zh-TW": { viewTitle: "AgEnD · 檢視", editProfile: "✏️ 編輯", save: "儲存", cancel: "取消",
|
|
126
|
+
displayName: "顯示名稱", role: "角色", description: "描述", tokenPh: "web.token(儲存時需要)",
|
|
127
|
+
saveHint: "檢視為公開(唯讀)。儲存需要 web.token(~/.agend/web.token)。", helpTitle: "📖 使用說明",
|
|
128
|
+
help1: "左側選 instance → 右側顯示即時 terminal 畫面",
|
|
129
|
+
help2: "點 group header ▼/▶ 收合/展開", help3: "✏️ Edit 可修改顯示名稱、角色、描述",
|
|
130
|
+
help4: "拖曳可調整順序(需 web.token 權限)" },
|
|
131
|
+
};
|
|
132
|
+
let lang = localStorage.getItem("agend_lang") || (navigator.language && navigator.language.toLowerCase().startsWith("zh") ? "zh-TW" : "en");
|
|
133
|
+
if (!I18N[lang]) lang = "en";
|
|
134
|
+
const T = (k) => (I18N[lang] && I18N[lang][k]) || I18N.en[k] || k;
|
|
135
|
+
function applyI18n() {
|
|
136
|
+
document.querySelectorAll("[data-i18n]").forEach(e => { e.textContent = T(e.getAttribute("data-i18n")); });
|
|
137
|
+
document.querySelectorAll("[data-i18n-ph]").forEach(e => { e.setAttribute("placeholder", T(e.getAttribute("data-i18n-ph"))); });
|
|
138
|
+
const hc = q("helpContent");
|
|
139
|
+
if (hc) { hc.innerHTML = ""; const h3 = document.createElement("h3"); h3.textContent = T("helpTitle"); hc.append(h3);
|
|
140
|
+
const ul = document.createElement("ul"); for (const k of ["help1","help2","help3","help4"]) { const li = document.createElement("li"); li.textContent = T(k); ul.append(li); } hc.append(ul); }
|
|
141
|
+
}
|
|
142
|
+
|
|
85
143
|
let roster = [];
|
|
144
|
+
let rosterByName = new Map();
|
|
86
145
|
let current = null;
|
|
87
146
|
let poll = null;
|
|
88
147
|
let fitted = false;
|
|
148
|
+
const collapsed = new Set(); // group names the user has collapsed
|
|
149
|
+
// Drag-sort state
|
|
150
|
+
let sortGroups = new Map(); // groupName -> sort_index (persisted override)
|
|
151
|
+
let sortInsts = new Map(); // instanceName -> sort_index (within its group)
|
|
152
|
+
let groupNames = []; // group display order (computed)
|
|
153
|
+
let instByGroup = new Map(); // group -> [instance_name, …] display order (computed)
|
|
154
|
+
let writable = false; // whether the current web.token can POST
|
|
155
|
+
let probed = false;
|
|
156
|
+
let dragItem = null; // { type:'group'|'instance', name, group }
|
|
89
157
|
|
|
90
|
-
const api = (path) => `${path}${path.includes("?") ? "&" : "?"}token=${encodeURIComponent(
|
|
158
|
+
const api = (path) => urlToken ? `${path}${path.includes("?") ? "&" : "?"}token=${encodeURIComponent(urlToken)}` : path;
|
|
91
159
|
|
|
92
160
|
// ── ANSI (SGR) → HTML ─────────────────────────────────
|
|
93
161
|
const BASE = ["#000000","#cd3131","#0dbc79","#e5e510","#2472c8","#bc3fbc","#11a8cd","#e5e5e5"];
|
|
@@ -147,18 +215,169 @@
|
|
|
147
215
|
const l = (n - 232) * 10 + 8; return `rgb(${l},${l},${l})`;
|
|
148
216
|
}
|
|
149
217
|
|
|
150
|
-
// ── Sidebar
|
|
218
|
+
// ── Sidebar (grouped by tag) ──────────────────────────
|
|
219
|
+
// Group name: "Classic" if tagged classic, else the first tag, else "Other".
|
|
220
|
+
function groupOf(it) {
|
|
221
|
+
const tags = it.tags || [];
|
|
222
|
+
if (tags.includes("classic")) return "Classic";
|
|
223
|
+
if (tags.length) return tags[0];
|
|
224
|
+
return "Other";
|
|
225
|
+
}
|
|
226
|
+
// Short label: display name if set, else instance name minus its tag prefix
|
|
227
|
+
// and the -t<id> suffix (doupo-server-t1503… → server).
|
|
228
|
+
function shortName(it) {
|
|
229
|
+
if (it.display_name) return it.display_name;
|
|
230
|
+
let n = it.instance_name.replace(/-t[0-9a-z]+$/i, "");
|
|
231
|
+
const tag = (it.tags || [])[0];
|
|
232
|
+
if (tag && n.toLowerCase().startsWith(tag.toLowerCase() + "-")) n = n.slice(tag.length + 1);
|
|
233
|
+
return n || it.instance_name;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Compute display order: persisted sort-order first (by index), then any
|
|
237
|
+
// new/unsorted groups & instances by the tag+alpha fallback. Stored in
|
|
238
|
+
// groupNames / instByGroup so drag handlers can mutate + persist them.
|
|
239
|
+
function computeOrder() {
|
|
240
|
+
const groups = new Map();
|
|
241
|
+
for (const it of roster) {
|
|
242
|
+
const g = groupOf(it);
|
|
243
|
+
if (!groups.has(g)) groups.set(g, []);
|
|
244
|
+
groups.get(g).push(it);
|
|
245
|
+
}
|
|
246
|
+
const gidx = (g) => (sortGroups.has(g) ? sortGroups.get(g) : Infinity);
|
|
247
|
+
groupNames = [...groups.keys()].sort((a, b) => {
|
|
248
|
+
const ia = gidx(a), ib = gidx(b);
|
|
249
|
+
if (ia !== ib) return ia - ib;
|
|
250
|
+
if (a === "Other") return 1;
|
|
251
|
+
if (b === "Other") return -1;
|
|
252
|
+
return a.localeCompare(b);
|
|
253
|
+
});
|
|
254
|
+
instByGroup = new Map();
|
|
255
|
+
for (const [g, items] of groups) {
|
|
256
|
+
// stable sort keeps roster order for equal (unsorted) indices
|
|
257
|
+
const names = items.map(it => it.instance_name).sort((a, b) => {
|
|
258
|
+
const ia = sortInsts.has(a) ? sortInsts.get(a) : Infinity;
|
|
259
|
+
const ib = sortInsts.has(b) ? sortInsts.get(b) : Infinity;
|
|
260
|
+
return ia - ib;
|
|
261
|
+
});
|
|
262
|
+
instByGroup.set(g, names);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
151
266
|
function renderList() {
|
|
152
267
|
const list = q("list");
|
|
153
268
|
list.innerHTML = "";
|
|
154
|
-
for (const
|
|
155
|
-
const
|
|
156
|
-
|
|
157
|
-
const
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
269
|
+
for (const g of groupNames) {
|
|
270
|
+
const names = instByGroup.get(g) || [];
|
|
271
|
+
if (!names.length) continue;
|
|
272
|
+
const isCollapsed = collapsed.has(g);
|
|
273
|
+
const header = document.createElement("div");
|
|
274
|
+
header.className = "group-header";
|
|
275
|
+
header.draggable = writable;
|
|
276
|
+
header.innerHTML = `<span class="caret">${isCollapsed ? "▶" : "▼"}</span><span>${esc(g)}</span><span class="gcount">${names.length}</span>`;
|
|
277
|
+
header.onclick = () => { if (collapsed.has(g)) collapsed.delete(g); else collapsed.add(g); renderList(); };
|
|
278
|
+
if (writable) wireDrag(header, { type: "group", name: g });
|
|
279
|
+
list.appendChild(header);
|
|
280
|
+
if (isCollapsed) continue;
|
|
281
|
+
|
|
282
|
+
for (const name of names) {
|
|
283
|
+
const it = rosterByName.get(name);
|
|
284
|
+
if (!it) continue;
|
|
285
|
+
const el = document.createElement("div");
|
|
286
|
+
el.className = "inst" + (name === current ? " active" : "");
|
|
287
|
+
el.draggable = writable;
|
|
288
|
+
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>`;
|
|
289
|
+
el.onclick = () => select(name);
|
|
290
|
+
if (writable) wireDrag(el, { type: "instance", name, group: g });
|
|
291
|
+
list.appendChild(el);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// ── Drag & drop ───────────────────────────────────────
|
|
297
|
+
function wireDrag(el, item) {
|
|
298
|
+
el.addEventListener("dragstart", (e) => {
|
|
299
|
+
dragItem = item;
|
|
300
|
+
el.classList.add("dragging");
|
|
301
|
+
e.dataTransfer.effectAllowed = "move";
|
|
302
|
+
try { e.dataTransfer.setData("text/plain", item.name); } catch { /* Firefox needs data set */ }
|
|
303
|
+
});
|
|
304
|
+
el.addEventListener("dragend", () => {
|
|
305
|
+
el.classList.remove("dragging");
|
|
306
|
+
document.querySelectorAll(".drop-before,.drop-after").forEach(n => n.classList.remove("drop-before", "drop-after"));
|
|
307
|
+
dragItem = null;
|
|
308
|
+
});
|
|
309
|
+
el.addEventListener("dragover", (e) => {
|
|
310
|
+
if (!dragItem || dragItem.type !== item.type) return; // only same kind
|
|
311
|
+
if (item.type === "instance" && dragItem.group !== item.group) return; // no cross-group
|
|
312
|
+
if (dragItem.name === item.name) return;
|
|
313
|
+
e.preventDefault();
|
|
314
|
+
const before = e.clientY < el.getBoundingClientRect().top + el.offsetHeight / 2;
|
|
315
|
+
el.classList.toggle("drop-before", before);
|
|
316
|
+
el.classList.toggle("drop-after", !before);
|
|
317
|
+
});
|
|
318
|
+
el.addEventListener("dragleave", () => el.classList.remove("drop-before", "drop-after"));
|
|
319
|
+
el.addEventListener("drop", (e) => {
|
|
320
|
+
e.preventDefault();
|
|
321
|
+
const before = el.classList.contains("drop-before");
|
|
322
|
+
el.classList.remove("drop-before", "drop-after");
|
|
323
|
+
if (!dragItem || dragItem.type !== item.type) return;
|
|
324
|
+
if (item.type === "group") reorder(groupNames, dragItem.name, item.name, before);
|
|
325
|
+
else if (dragItem.group === item.group) reorder(instByGroup.get(item.group), dragItem.name, item.name, before);
|
|
326
|
+
else return;
|
|
327
|
+
renderList();
|
|
328
|
+
persistOrder();
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function reorder(arr, moving, target, before) {
|
|
333
|
+
if (!arr) return;
|
|
334
|
+
const from = arr.indexOf(moving);
|
|
335
|
+
if (from === -1) return;
|
|
336
|
+
arr.splice(from, 1);
|
|
337
|
+
let to = arr.indexOf(target);
|
|
338
|
+
if (to === -1) { arr.push(moving); return; }
|
|
339
|
+
if (!before) to += 1;
|
|
340
|
+
arr.splice(to, 0, moving);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function orderRows() {
|
|
344
|
+
const rows = [];
|
|
345
|
+
groupNames.forEach((g, i) => rows.push({ item_type: "group", item_name: g, sort_index: i, group_name: null }));
|
|
346
|
+
for (const g of groupNames) {
|
|
347
|
+
(instByGroup.get(g) || []).forEach((n, j) => rows.push({ item_type: "instance", item_name: n, sort_index: j, group_name: g }));
|
|
161
348
|
}
|
|
349
|
+
return rows;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
async function persistOrder() {
|
|
353
|
+
// Mirror into the local maps so a mid-flight refresh doesn't revert.
|
|
354
|
+
sortGroups = new Map(groupNames.map((g, i) => [g, i]));
|
|
355
|
+
sortInsts = new Map();
|
|
356
|
+
for (const g of groupNames) (instByGroup.get(g) || []).forEach((n, j) => sortInsts.set(n, j));
|
|
357
|
+
try {
|
|
358
|
+
const r = await fetch("/api/sort-order", {
|
|
359
|
+
method: "POST",
|
|
360
|
+
headers: { "Content-Type": "application/json", "X-Agend-Token": webToken() },
|
|
361
|
+
body: JSON.stringify(orderRows()),
|
|
362
|
+
});
|
|
363
|
+
if (r.status === 401) { writable = false; renderList(); }
|
|
364
|
+
} catch { /* transient */ }
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// Probe write permission by POSTing the current order (idempotent for a
|
|
368
|
+
// web-token user; 401 without a valid web.token → drag stays disabled).
|
|
369
|
+
// Re-run after the user enters a web.token so drag-sort lights up.
|
|
370
|
+
async function probeWrite() {
|
|
371
|
+
if (!webToken()) { if (writable) { writable = false; renderList(); } return; }
|
|
372
|
+
try {
|
|
373
|
+
const r = await fetch("/api/sort-order", {
|
|
374
|
+
method: "POST",
|
|
375
|
+
headers: { "Content-Type": "application/json", "X-Agend-Token": webToken() },
|
|
376
|
+
body: JSON.stringify(orderRows()),
|
|
377
|
+
});
|
|
378
|
+
const nowWritable = r.ok;
|
|
379
|
+
if (nowWritable !== writable) { writable = nowWritable; renderList(); }
|
|
380
|
+
} catch { /* leave disabled */ }
|
|
162
381
|
}
|
|
163
382
|
|
|
164
383
|
function renderCard() {
|
|
@@ -166,33 +385,61 @@
|
|
|
166
385
|
if (!it) return;
|
|
167
386
|
q("dname").textContent = it.display_name || it.instance_name;
|
|
168
387
|
q("role").textContent = it.role || "";
|
|
169
|
-
|
|
388
|
+
// Only show ctx% when we actually have a reading (>0); 0/null means the
|
|
389
|
+
// backend didn't report one, so hiding it avoids a misleading "0%".
|
|
390
|
+
const ctxPart = it.context_pct ? ` · ctx ${Math.round(it.context_pct)}%` : "";
|
|
391
|
+
q("meta").textContent = `${it.instance_name} · ${it.backend}${it.model ? " · " + it.model : ""}${ctxPart} · ${it.status}`;
|
|
170
392
|
q("desc").textContent = it.description || "";
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
393
|
+
// Only issue an <img> request when an avatar actually exists — otherwise a
|
|
394
|
+
// 404 shows a broken-image icon. No avatar → CSS placeholder (initial).
|
|
395
|
+
const box = q("avatarBox");
|
|
396
|
+
if (it.has_avatar) {
|
|
397
|
+
const base = api(`/api/avatar/${encodeURIComponent(it.instance_name)}`);
|
|
398
|
+
const src = base + (base.includes("?") ? "&" : "?") + "t=" + Date.now(); // cache-bust, correct query separator
|
|
399
|
+
box.innerHTML = `<img class="avatar-img" alt="" src="${src}">`;
|
|
400
|
+
} else {
|
|
401
|
+
const initial = (it.display_name || it.instance_name || "?").trim().charAt(0) || "?";
|
|
402
|
+
box.innerHTML = `<div class="avatar-ph">${esc(initial)}</div>`;
|
|
403
|
+
}
|
|
174
404
|
}
|
|
175
405
|
|
|
176
406
|
async function loadRoster() {
|
|
177
407
|
try {
|
|
178
|
-
const
|
|
179
|
-
if (!
|
|
180
|
-
roster = await
|
|
408
|
+
const [pr, so] = await Promise.all([fetch(api("/api/profiles")), fetch(api("/api/sort-order"))]);
|
|
409
|
+
if (!pr.ok) throw new Error("HTTP " + pr.status);
|
|
410
|
+
roster = await pr.json();
|
|
411
|
+
rosterByName = new Map(roster.map(r => [r.instance_name, r]));
|
|
412
|
+
const soRows = so.ok ? await so.json() : [];
|
|
413
|
+
sortGroups = new Map(); sortInsts = new Map();
|
|
414
|
+
for (const row of soRows) {
|
|
415
|
+
if (row.item_type === "group") sortGroups.set(row.item_name, row.sort_index);
|
|
416
|
+
else if (row.item_type === "instance") sortInsts.set(row.item_name, row.sort_index);
|
|
417
|
+
}
|
|
181
418
|
if (!current && roster.length) current = roster[0].instance_name;
|
|
419
|
+
computeOrder();
|
|
182
420
|
renderList(); renderCard();
|
|
183
|
-
|
|
421
|
+
if (!probed) { probed = true; probeWrite(); }
|
|
422
|
+
} catch (e) { q("pre").textContent = "Failed to load roster: " + e.message + "\n(Is the fleet running?)"; }
|
|
184
423
|
}
|
|
185
424
|
|
|
186
425
|
// ── Terminal ──────────────────────────────────────────
|
|
187
426
|
async function refreshPane() {
|
|
188
427
|
if (!current) return;
|
|
428
|
+
const target = current; // guard against a switch mid-fetch
|
|
189
429
|
try {
|
|
190
|
-
const r = await fetch(api(`/api/pane/${encodeURIComponent(
|
|
430
|
+
const r = await fetch(api(`/api/pane/${encodeURIComponent(target)}`));
|
|
431
|
+
if (target !== current) return; // user switched while awaiting — drop stale
|
|
432
|
+
if (!r.ok) {
|
|
433
|
+
// Surface the failure instead of leaving the previous instance's output
|
|
434
|
+
// on screen (which reads as "switching did nothing").
|
|
435
|
+
q("pre").textContent = `⚠ Could not load pane for ${target} (HTTP ${r.status}).`;
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
191
438
|
const text = await r.text();
|
|
192
|
-
|
|
193
|
-
pre.innerHTML = ansiToHtml(text);
|
|
439
|
+
if (target !== current) return;
|
|
440
|
+
q("pre").innerHTML = ansiToHtml(text);
|
|
194
441
|
if (!fitted) { fitFont(text); fitted = true; }
|
|
195
|
-
} catch { /* transient */ }
|
|
442
|
+
} catch { /* transient network error — keep last frame */ }
|
|
196
443
|
}
|
|
197
444
|
|
|
198
445
|
// Fit font size so the widest line fits the terminal width. Recomputed only
|
|
@@ -211,6 +458,7 @@
|
|
|
211
458
|
if (name === current) return;
|
|
212
459
|
current = name; fitted = false;
|
|
213
460
|
exitEdit();
|
|
461
|
+
q("pre").textContent = ""; // blank the old instance's output immediately
|
|
214
462
|
renderList(); renderCard(); refreshPane();
|
|
215
463
|
}
|
|
216
464
|
|
|
@@ -236,24 +484,25 @@
|
|
|
236
484
|
async function save() {
|
|
237
485
|
const it = roster.find(r => r.instance_name === current);
|
|
238
486
|
if (!it) return;
|
|
239
|
-
const
|
|
240
|
-
if (!
|
|
241
|
-
localStorage.setItem("agend_web_token",
|
|
487
|
+
const wt = q("fToken").value.trim();
|
|
488
|
+
if (!wt) { q("msg").className = "err"; q("msg").textContent = "web.token required"; return; }
|
|
489
|
+
localStorage.setItem("agend_web_token", wt);
|
|
242
490
|
const name = encodeURIComponent(current);
|
|
243
491
|
try {
|
|
244
492
|
const r = await fetch(`/api/profile/${name}`, {
|
|
245
493
|
method: "POST",
|
|
246
|
-
headers: { "Content-Type": "application/json", "X-Agend-Token":
|
|
494
|
+
headers: { "Content-Type": "application/json", "X-Agend-Token": wt },
|
|
247
495
|
body: JSON.stringify({ display_name: q("fDisplay").value, role: q("fRole").value, description: q("fDesc").value }),
|
|
248
496
|
});
|
|
249
497
|
if (!r.ok) { const e = await r.json().catch(() => ({})); throw new Error(e.error || ("HTTP " + r.status)); }
|
|
250
498
|
const file = q("fAvatar").files[0];
|
|
251
499
|
if (file) {
|
|
252
|
-
const ar = await fetch(`/api/avatar/${name}`, { method: "POST", headers: { "Content-Type": file.type, "X-Agend-Token":
|
|
500
|
+
const ar = await fetch(`/api/avatar/${name}`, { method: "POST", headers: { "Content-Type": file.type, "X-Agend-Token": wt }, body: file });
|
|
253
501
|
if (!ar.ok) { const e = await ar.json().catch(() => ({})); throw new Error("avatar: " + (e.error || ar.status)); }
|
|
254
502
|
}
|
|
255
503
|
q("msg").className = "ok"; q("msg").textContent = "Saved";
|
|
256
504
|
await loadRoster();
|
|
505
|
+
probeWrite(); // web.token now stored — enable drag-sort
|
|
257
506
|
setTimeout(exitEdit, 500);
|
|
258
507
|
} catch (e) { q("msg").className = "err"; q("msg").textContent = e.message; }
|
|
259
508
|
}
|
|
@@ -262,6 +511,13 @@
|
|
|
262
511
|
q("cancelBtn").onclick = exitEdit;
|
|
263
512
|
q("saveBtn").onclick = save;
|
|
264
513
|
|
|
514
|
+
const setHelp = (open) => { q("helpPanel").classList.toggle("open", open); q("helpBackdrop").classList.toggle("open", open); };
|
|
515
|
+
q("helpBtn").onclick = () => setHelp(!q("helpPanel").classList.contains("open"));
|
|
516
|
+
q("helpClose").onclick = () => setHelp(false);
|
|
517
|
+
q("helpBackdrop").onclick = () => setHelp(false);
|
|
518
|
+
q("langBtn").onclick = () => { lang = lang === "en" ? "zh-TW" : "en"; localStorage.setItem("agend_lang", lang); applyI18n(); };
|
|
519
|
+
applyI18n();
|
|
520
|
+
|
|
265
521
|
// ── Boot ──────────────────────────────────────────────
|
|
266
522
|
loadRoster().then(() => { refreshPane(); });
|
|
267
523
|
poll = setInterval(refreshPane, 800);
|
package/dist/view-api.d.ts
CHANGED
|
@@ -7,6 +7,15 @@ export interface ViewApiContext {
|
|
|
7
7
|
readonly dataDir: string;
|
|
8
8
|
readonly fleetConfig: FleetConfig | null;
|
|
9
9
|
readonly logger: Logger;
|
|
10
|
+
readonly classicChannels: {
|
|
11
|
+
getAll(): {
|
|
12
|
+
instanceName: string;
|
|
13
|
+
name: string;
|
|
14
|
+
backend?: string;
|
|
15
|
+
channelId: string;
|
|
16
|
+
}[];
|
|
17
|
+
getBackendByInstance(name: string, fleetDefault?: string): string;
|
|
18
|
+
} | null;
|
|
10
19
|
getInstanceStatus(name: string): "running" | "stopped" | "crashed";
|
|
11
20
|
getUiStatus(): unknown;
|
|
12
21
|
}
|