@songsid/agend 2.0.11-beta.8 โ 2.0.11
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.js +21 -16
- 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/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 +70 -21
- 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 +21 -5
- package/dist/fleet-manager.js +412 -258
- package/dist/fleet-manager.js.map +1 -1
- package/dist/instance-lifecycle.d.ts +1 -1
- package/dist/instance-lifecycle.js +12 -11
- 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 +222 -9
- package/dist/quickstart.js.map +1 -1
- package/dist/scheduler/scheduler.js +8 -2
- package/dist/scheduler/scheduler.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 -15
- 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 +271 -43
- package/dist/view-api.d.ts +9 -0
- package/dist/view-api.js +101 -41
- package/dist/view-api.js.map +1 -1
- package/package.json +1 -1
package/dist/ui/view.html
CHANGED
|
@@ -18,6 +18,10 @@
|
|
|
18
18
|
.group-header .caret { width: 10px; flex: 0 0 10px; }
|
|
19
19
|
.group-header .gcount { margin-left: auto; opacity: .7; font-weight: 400; }
|
|
20
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; }
|
|
21
25
|
.inst:hover { background: rgba(255,255,255,.04); }
|
|
22
26
|
.inst.active { background: rgba(88,166,255,.14); border-left: 3px solid var(--accent); padding-left: 13px; }
|
|
23
27
|
.dot { width: 8px; height: 8px; border-radius: 50%; flex: 0 0 8px; }
|
|
@@ -32,7 +36,9 @@
|
|
|
32
36
|
|
|
33
37
|
/* Card */
|
|
34
38
|
#card { background: var(--panel); border-top: 1px solid var(--border); padding: 14px 18px; display: flex; gap: 16px; align-items: flex-start; }
|
|
35
|
-
#
|
|
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; }
|
|
36
42
|
#cardBody { flex: 1; min-width: 0; }
|
|
37
43
|
#cardBody .row1 { display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap; }
|
|
38
44
|
#cardBody .dname { font-size: 16px; font-weight: 600; }
|
|
@@ -52,47 +58,104 @@
|
|
|
52
58
|
#edit .hint { color: var(--dim); font-size: 11px; }
|
|
53
59
|
#msg { font-size: 12px; }
|
|
54
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; }
|
|
55
71
|
</style>
|
|
56
72
|
</head>
|
|
57
73
|
<body>
|
|
58
|
-
<
|
|
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>
|
|
59
82
|
<div id="main">
|
|
60
83
|
<div id="term"><pre id="pre"></pre></div>
|
|
61
84
|
<div id="card">
|
|
62
|
-
<
|
|
85
|
+
<div id="avatarBox"></div>
|
|
63
86
|
<div id="cardBody">
|
|
64
87
|
<div class="row1"><span class="dname" id="dname">โ</span><span class="role" id="role"></span></div>
|
|
65
88
|
<div class="meta" id="meta"></div>
|
|
66
89
|
<div class="desc" id="desc"></div>
|
|
67
90
|
</div>
|
|
68
|
-
<button id="editBtn">โ๏ธ Edit</button>
|
|
91
|
+
<button id="editBtn" data-i18n="editProfile">โ๏ธ Edit</button>
|
|
69
92
|
<div id="edit">
|
|
70
|
-
<input id="fDisplay" placeholder="Display name">
|
|
71
|
-
<input id="fRole" placeholder="Role">
|
|
72
|
-
<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>
|
|
73
96
|
<input id="fAvatar" type="file" accept="image/png,image/jpeg,image/gif,image/webp">
|
|
74
|
-
<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)">
|
|
75
98
|
<div class="actions">
|
|
76
|
-
<button id="saveBtn">Save</button>
|
|
77
|
-
<button id="cancelBtn">Cancel</button>
|
|
99
|
+
<button id="saveBtn" data-i18n="save">Save</button>
|
|
100
|
+
<button id="cancelBtn" data-i18n="cancel">Cancel</button>
|
|
78
101
|
<span id="msg"></span>
|
|
79
102
|
</div>
|
|
80
|
-
<div class="hint">
|
|
103
|
+
<div class="hint" data-i18n="saveHint">Viewing is open (read-only). Saving requires the web.token (~/.agend/web.token).</div>
|
|
81
104
|
</div>
|
|
82
105
|
</div>
|
|
83
106
|
</div>
|
|
84
107
|
|
|
85
108
|
<script>
|
|
86
109
|
(() => {
|
|
87
|
-
|
|
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;
|
|
88
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
|
+
|
|
89
143
|
let roster = [];
|
|
144
|
+
let rosterByName = new Map();
|
|
90
145
|
let current = null;
|
|
91
146
|
let poll = null;
|
|
92
147
|
let fitted = false;
|
|
93
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 }
|
|
94
157
|
|
|
95
|
-
const api = (path) => `${path}${path.includes("?") ? "&" : "?"}token=${encodeURIComponent(
|
|
158
|
+
const api = (path) => urlToken ? `${path}${path.includes("?") ? "&" : "?"}token=${encodeURIComponent(urlToken)}` : path;
|
|
96
159
|
|
|
97
160
|
// โโ ANSI (SGR) โ HTML โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
98
161
|
const BASE = ["#000000","#cd3131","#0dbc79","#e5e510","#2472c8","#bc3fbc","#11a8cd","#e5e5e5"];
|
|
@@ -170,76 +233,232 @@
|
|
|
170
233
|
return n || it.instance_name;
|
|
171
234
|
}
|
|
172
235
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
// Bucket instances by group.
|
|
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() {
|
|
178
240
|
const groups = new Map();
|
|
179
241
|
for (const it of roster) {
|
|
180
242
|
const g = groupOf(it);
|
|
181
243
|
if (!groups.has(g)) groups.set(g, []);
|
|
182
244
|
groups.get(g).push(it);
|
|
183
245
|
}
|
|
184
|
-
|
|
185
|
-
|
|
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;
|
|
186
250
|
if (a === "Other") return 1;
|
|
187
251
|
if (b === "Other") return -1;
|
|
188
252
|
return a.localeCompare(b);
|
|
189
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
|
+
}
|
|
190
265
|
|
|
191
|
-
|
|
192
|
-
|
|
266
|
+
function renderList() {
|
|
267
|
+
const list = q("list");
|
|
268
|
+
list.innerHTML = "";
|
|
269
|
+
for (const g of groupNames) {
|
|
270
|
+
const names = instByGroup.get(g) || [];
|
|
271
|
+
if (!names.length) continue;
|
|
193
272
|
const isCollapsed = collapsed.has(g);
|
|
194
273
|
const header = document.createElement("div");
|
|
195
274
|
header.className = "group-header";
|
|
196
|
-
header.
|
|
275
|
+
header.draggable = writable;
|
|
276
|
+
header.innerHTML = `<span class="caret">${isCollapsed ? "โถ" : "โผ"}</span><span>${esc(g)}</span><span class="gcount">${names.length}</span>`;
|
|
197
277
|
header.onclick = () => { if (collapsed.has(g)) collapsed.delete(g); else collapsed.add(g); renderList(); };
|
|
278
|
+
if (writable) wireDrag(header, { type: "group", name: g });
|
|
198
279
|
list.appendChild(header);
|
|
199
280
|
if (isCollapsed) continue;
|
|
200
281
|
|
|
201
|
-
for (const
|
|
282
|
+
for (const name of names) {
|
|
283
|
+
const it = rosterByName.get(name);
|
|
284
|
+
if (!it) continue;
|
|
202
285
|
const el = document.createElement("div");
|
|
203
|
-
el.className = "inst" + (
|
|
286
|
+
el.className = "inst" + (name === current ? " active" : "");
|
|
287
|
+
el.draggable = writable;
|
|
204
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>`;
|
|
205
|
-
el.onclick = () => select(
|
|
289
|
+
el.onclick = () => select(name);
|
|
290
|
+
if (writable) wireDrag(el, { type: "instance", name, group: g });
|
|
206
291
|
list.appendChild(el);
|
|
207
292
|
}
|
|
208
293
|
}
|
|
209
294
|
}
|
|
210
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 }));
|
|
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 */ }
|
|
381
|
+
}
|
|
382
|
+
|
|
211
383
|
function renderCard() {
|
|
212
384
|
const it = roster.find(r => r.instance_name === current);
|
|
213
385
|
if (!it) return;
|
|
214
386
|
q("dname").textContent = it.display_name || it.instance_name;
|
|
215
387
|
q("role").textContent = it.role || "";
|
|
216
|
-
|
|
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}`;
|
|
217
392
|
q("desc").textContent = it.description || "";
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
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
|
+
// Only rebuild (and cache-bust) when the avatar's instance changed.
|
|
399
|
+
// Rebuilding every renderCard re-issued the <img> request each 5s
|
|
400
|
+
// loadRoster tick, which made the avatar flicker. Compare the raw src
|
|
401
|
+
// attribute (relative, as set) rather than img.src (resolved absolute).
|
|
402
|
+
const existingImg = box.querySelector("img");
|
|
403
|
+
const cur = existingImg ? (existingImg.getAttribute("src") || "") : "";
|
|
404
|
+
// The delimiter (base+"?" / base+"&") prevents a prefix collision between
|
|
405
|
+
// e.g. ".../foo" and ".../foobar".
|
|
406
|
+
const sameAvatar = existingImg && (cur.startsWith(base + "?") || cur.startsWith(base + "&"));
|
|
407
|
+
if (!sameAvatar) {
|
|
408
|
+
const src = base + (base.includes("?") ? "&" : "?") + "t=" + Date.now(); // cache-bust, correct query separator
|
|
409
|
+
box.innerHTML = `<img class="avatar-img" alt="" src="${src}">`;
|
|
410
|
+
}
|
|
411
|
+
} else {
|
|
412
|
+
const initial = (it.display_name || it.instance_name || "?").trim().charAt(0) || "?";
|
|
413
|
+
const ph = box.querySelector(".avatar-ph");
|
|
414
|
+
if (!ph || ph.textContent !== initial) {
|
|
415
|
+
box.innerHTML = `<div class="avatar-ph">${esc(initial)}</div>`;
|
|
416
|
+
}
|
|
417
|
+
}
|
|
221
418
|
}
|
|
222
419
|
|
|
223
420
|
async function loadRoster() {
|
|
224
421
|
try {
|
|
225
|
-
const
|
|
226
|
-
if (!
|
|
227
|
-
roster = await
|
|
422
|
+
const [pr, so] = await Promise.all([fetch(api("/api/profiles")), fetch(api("/api/sort-order"))]);
|
|
423
|
+
if (!pr.ok) throw new Error("HTTP " + pr.status);
|
|
424
|
+
roster = await pr.json();
|
|
425
|
+
rosterByName = new Map(roster.map(r => [r.instance_name, r]));
|
|
426
|
+
const soRows = so.ok ? await so.json() : [];
|
|
427
|
+
sortGroups = new Map(); sortInsts = new Map();
|
|
428
|
+
for (const row of soRows) {
|
|
429
|
+
if (row.item_type === "group") sortGroups.set(row.item_name, row.sort_index);
|
|
430
|
+
else if (row.item_type === "instance") sortInsts.set(row.item_name, row.sort_index);
|
|
431
|
+
}
|
|
228
432
|
if (!current && roster.length) current = roster[0].instance_name;
|
|
433
|
+
computeOrder();
|
|
229
434
|
renderList(); renderCard();
|
|
230
|
-
|
|
435
|
+
if (!probed) { probed = true; probeWrite(); }
|
|
436
|
+
} catch (e) { q("pre").textContent = "Failed to load roster: " + e.message + "\n(Is the fleet running?)"; }
|
|
231
437
|
}
|
|
232
438
|
|
|
233
439
|
// โโ Terminal โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
234
440
|
async function refreshPane() {
|
|
235
441
|
if (!current) return;
|
|
442
|
+
// If the user is selecting text inside the pane, skip this refresh so the
|
|
443
|
+
// 800ms poll doesn't wipe the selection mid-copy. Resumes on the next tick
|
|
444
|
+
// once the selection is released/collapsed.
|
|
445
|
+
const sel = window.getSelection();
|
|
446
|
+
if (sel && sel.toString().length > 0 && sel.anchorNode && q("pre").contains(sel.anchorNode)) return;
|
|
447
|
+
const target = current; // guard against a switch mid-fetch
|
|
236
448
|
try {
|
|
237
|
-
const r = await fetch(api(`/api/pane/${encodeURIComponent(
|
|
449
|
+
const r = await fetch(api(`/api/pane/${encodeURIComponent(target)}`));
|
|
450
|
+
if (target !== current) return; // user switched while awaiting โ drop stale
|
|
451
|
+
if (!r.ok) {
|
|
452
|
+
// Surface the failure instead of leaving the previous instance's output
|
|
453
|
+
// on screen (which reads as "switching did nothing").
|
|
454
|
+
q("pre").textContent = `โ Could not load pane for ${target} (HTTP ${r.status}).`;
|
|
455
|
+
return;
|
|
456
|
+
}
|
|
238
457
|
const text = await r.text();
|
|
239
|
-
|
|
240
|
-
pre.innerHTML = ansiToHtml(text);
|
|
458
|
+
if (target !== current) return;
|
|
459
|
+
q("pre").innerHTML = ansiToHtml(text);
|
|
241
460
|
if (!fitted) { fitFont(text); fitted = true; }
|
|
242
|
-
} catch { /* transient */ }
|
|
461
|
+
} catch { /* transient network error โ keep last frame */ }
|
|
243
462
|
}
|
|
244
463
|
|
|
245
464
|
// Fit font size so the widest line fits the terminal width. Recomputed only
|
|
@@ -258,6 +477,7 @@
|
|
|
258
477
|
if (name === current) return;
|
|
259
478
|
current = name; fitted = false;
|
|
260
479
|
exitEdit();
|
|
480
|
+
q("pre").textContent = ""; // blank the old instance's output immediately
|
|
261
481
|
renderList(); renderCard(); refreshPane();
|
|
262
482
|
}
|
|
263
483
|
|
|
@@ -283,24 +503,25 @@
|
|
|
283
503
|
async function save() {
|
|
284
504
|
const it = roster.find(r => r.instance_name === current);
|
|
285
505
|
if (!it) return;
|
|
286
|
-
const
|
|
287
|
-
if (!
|
|
288
|
-
localStorage.setItem("agend_web_token",
|
|
506
|
+
const wt = q("fToken").value.trim();
|
|
507
|
+
if (!wt) { q("msg").className = "err"; q("msg").textContent = "web.token required"; return; }
|
|
508
|
+
localStorage.setItem("agend_web_token", wt);
|
|
289
509
|
const name = encodeURIComponent(current);
|
|
290
510
|
try {
|
|
291
511
|
const r = await fetch(`/api/profile/${name}`, {
|
|
292
512
|
method: "POST",
|
|
293
|
-
headers: { "Content-Type": "application/json", "X-Agend-Token":
|
|
513
|
+
headers: { "Content-Type": "application/json", "X-Agend-Token": wt },
|
|
294
514
|
body: JSON.stringify({ display_name: q("fDisplay").value, role: q("fRole").value, description: q("fDesc").value }),
|
|
295
515
|
});
|
|
296
516
|
if (!r.ok) { const e = await r.json().catch(() => ({})); throw new Error(e.error || ("HTTP " + r.status)); }
|
|
297
517
|
const file = q("fAvatar").files[0];
|
|
298
518
|
if (file) {
|
|
299
|
-
const ar = await fetch(`/api/avatar/${name}`, { method: "POST", headers: { "Content-Type": file.type, "X-Agend-Token":
|
|
519
|
+
const ar = await fetch(`/api/avatar/${name}`, { method: "POST", headers: { "Content-Type": file.type, "X-Agend-Token": wt }, body: file });
|
|
300
520
|
if (!ar.ok) { const e = await ar.json().catch(() => ({})); throw new Error("avatar: " + (e.error || ar.status)); }
|
|
301
521
|
}
|
|
302
522
|
q("msg").className = "ok"; q("msg").textContent = "Saved";
|
|
303
523
|
await loadRoster();
|
|
524
|
+
probeWrite(); // web.token now stored โ enable drag-sort
|
|
304
525
|
setTimeout(exitEdit, 500);
|
|
305
526
|
} catch (e) { q("msg").className = "err"; q("msg").textContent = e.message; }
|
|
306
527
|
}
|
|
@@ -309,6 +530,13 @@
|
|
|
309
530
|
q("cancelBtn").onclick = exitEdit;
|
|
310
531
|
q("saveBtn").onclick = save;
|
|
311
532
|
|
|
533
|
+
const setHelp = (open) => { q("helpPanel").classList.toggle("open", open); q("helpBackdrop").classList.toggle("open", open); };
|
|
534
|
+
q("helpBtn").onclick = () => setHelp(!q("helpPanel").classList.contains("open"));
|
|
535
|
+
q("helpClose").onclick = () => setHelp(false);
|
|
536
|
+
q("helpBackdrop").onclick = () => setHelp(false);
|
|
537
|
+
q("langBtn").onclick = () => { lang = lang === "en" ? "zh-TW" : "en"; localStorage.setItem("agend_lang", lang); applyI18n(); };
|
|
538
|
+
applyI18n();
|
|
539
|
+
|
|
312
540
|
// โโ Boot โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
313
541
|
loadRoster().then(() => { refreshPane(); });
|
|
314
542
|
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
|
}
|