@songsid/agend 2.0.11-beta.17 → 2.0.11-beta.19

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.
@@ -0,0 +1,397 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <title>AgEnD Settings</title>
7
+ <style>
8
+ * { margin: 0; padding: 0; box-sizing: border-box; }
9
+ :root { --bg:#0d1117; --panel:#161b22; --border:#21262d; --fg:#c9d1d9; --dim:#8b949e; --accent:#58a6ff; --err:#f85149; --warn:#d29922; --ok:#3fb950; }
10
+ html, body { height: 100%; }
11
+ body { background: var(--bg); color: var(--fg); font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; padding: 0 0 40px; }
12
+ header { display: flex; align-items: center; gap: 14px; padding: 12px 20px; background: var(--panel); border-bottom: 1px solid var(--border); position: sticky; top: 0; z-index: 10; }
13
+ header h1 { font-size: 15px; color: var(--accent); font-weight: 600; letter-spacing: .5px; }
14
+ .tabs { display: flex; gap: 4px; }
15
+ .tab { padding: 6px 14px; border-radius: 6px; cursor: pointer; font-size: 13px; color: var(--dim); border: 1px solid transparent; }
16
+ .tab:hover { color: var(--fg); } .tab.active { background: var(--bg); color: var(--accent); border-color: var(--border); }
17
+ .spacer { flex: 1; }
18
+ button { background: #21262d; color: var(--fg); border: 1px solid var(--border); border-radius: 6px; padding: 6px 12px; font-size: 13px; cursor: pointer; }
19
+ button:hover { border-color: var(--accent); } button.primary { background: var(--accent); color: #05070a; border-color: var(--accent); font-weight: 600; }
20
+ button.sm { padding: 3px 8px; font-size: 12px; } button.danger { color: var(--err); }
21
+ main { max-width: 1200px; margin: 0 auto; padding: 18px; }
22
+ .panel { display: none; } .panel.active { display: block; }
23
+ .split { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; align-items: start; }
24
+ @media (max-width: 900px) { .split { grid-template-columns: 1fr; } }
25
+ .col { min-width: 0; }
26
+ .card { background: var(--panel); border: 1px solid var(--border); border-radius: 8px; padding: 14px; margin-bottom: 14px; }
27
+ .card h2 { font-size: 13px; color: var(--accent); margin-bottom: 10px; display:flex; align-items:center; gap:8px; }
28
+ .card h2 .grow { flex:1; }
29
+ label { display: block; font-size: 12px; color: var(--dim); margin: 8px 0 3px; }
30
+ input[type=text], input[type=number], select, textarea { width: 100%; background: var(--bg); color: var(--fg); border: 1px solid var(--border); border-radius: 6px; padding: 7px 9px; font-size: 13px; font-family: inherit; }
31
+ textarea { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12px; line-height: 1.5; resize: vertical; min-height: 420px; }
32
+ .chips { display: flex; flex-wrap: wrap; gap: 6px; margin: 4px 0; }
33
+ .chip { display: inline-flex; align-items: center; gap: 6px; background: var(--bg); border: 1px solid var(--border); border-radius: 12px; padding: 3px 6px 3px 10px; font-size: 12px; }
34
+ .chip span.x { cursor: pointer; color: var(--dim); font-weight: 700; } .chip span.x:hover { color: var(--err); }
35
+ .chip-add { display: flex; gap: 6px; margin-top: 4px; } .chip-add input { flex: 1; }
36
+ table { width: 100%; border-collapse: collapse; font-size: 13px; }
37
+ th, td { text-align: left; padding: 7px 8px; border-bottom: 1px solid var(--border); }
38
+ th { color: var(--dim); font-size: 11px; text-transform: uppercase; letter-spacing: .3px; }
39
+ tr.inst-row { cursor: pointer; } tr.inst-row:hover td { background: rgba(255,255,255,.02); }
40
+ .edit-row td { background: rgba(88,166,255,.05); }
41
+ .grid2 { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
42
+ .right-toolbar { display: flex; gap: 6px; align-items: center; margin-bottom: 8px; flex-wrap: wrap; }
43
+ .right-toolbar .seg { display: flex; border: 1px solid var(--border); border-radius: 6px; overflow: hidden; }
44
+ .right-toolbar .seg button { border: none; border-radius: 0; }
45
+ .right-toolbar .seg button.on { background: var(--accent); color: #05070a; }
46
+ pre.preview { background: var(--bg); border: 1px solid var(--border); border-radius: 6px; padding: 10px; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12px; line-height: 1.5; overflow: auto; max-height: 560px; white-space: pre; min-height: 420px; }
47
+ .msg { font-size: 12px; margin-top: 8px; white-space: pre-wrap; } .msg .e { color: var(--err); } .msg .w { color: var(--warn); } .msg .s { color: var(--ok); }
48
+ .row-actions { display: flex; gap: 6px; margin-top: 10px; align-items: center; flex-wrap: wrap; }
49
+ .banner { padding: 9px 20px; font-size: 13px; background: rgba(210,153,34,.12); border-bottom: 1px solid var(--border); color: var(--warn); display: none; }
50
+ code { background: var(--bg); padding: 1px 4px; border-radius: 4px; font-size: 12px; }
51
+ </style>
52
+ </head>
53
+ <body>
54
+ <header>
55
+ <h1>⚙ AgEnD Settings</h1>
56
+ <div class="tabs">
57
+ <div class="tab active" data-tab="fleet">Fleet</div>
58
+ <div class="tab" data-tab="instances">Instances</div>
59
+ <div class="tab" data-tab="classic">ClassicBot</div>
60
+ </div>
61
+ <div class="spacer"></div>
62
+ <button id="reloadBtn" title="Apply saved config to running instances (SIGHUP)">↻ Reload fleet</button>
63
+ </header>
64
+ <div class="banner" id="banner"></div>
65
+ <main>
66
+ <section class="panel active" id="panel-fleet"><div class="split">
67
+ <div class="col" id="fleet-left"></div>
68
+ <div class="col" id="fleet-right"></div>
69
+ </div></section>
70
+ <section class="panel" id="panel-instances"><div class="split">
71
+ <div class="col" id="instances-left"></div>
72
+ <div class="col" id="instances-right"></div>
73
+ </div></section>
74
+ <section class="panel" id="panel-classic"><div class="split">
75
+ <div class="col" id="classic-left"></div>
76
+ <div class="col" id="classic-right"></div>
77
+ </div></section>
78
+ </main>
79
+
80
+ <script>
81
+ (() => {
82
+ "use strict";
83
+ const token = new URLSearchParams(location.search).get("token") || "";
84
+ const $ = (id) => document.getElementById(id);
85
+ const el = (tag, attrs = {}, ...kids) => { const e = document.createElement(tag); for (const k in attrs) { if (k === "class") e.className = attrs[k]; else if (k === "html") e.innerHTML = attrs[k]; else if (k.startsWith("on")) e.addEventListener(k.slice(2), attrs[k]); else e.setAttribute(k, attrs[k]); } for (const c of kids) e.append(c); return e; };
86
+ const esc = (s) => String(s).replace(/[&<>]/g, c => ({ "&":"&amp;","<":"&lt;",">":"&gt;" }[c]));
87
+ const BACKENDS = ["claude-code", "kiro-cli", "codex", "opencode", "gemini-cli", "antigravity"];
88
+ const CH_TYPES = ["telegram", "discord"];
89
+
90
+ // ── YAML dump/parse (config subset; round-trip verified) ──
91
+ function toYaml(value, indent = 0) {
92
+ const pad = " ".repeat(indent);
93
+ const scalar = (v) => {
94
+ if (v === null || v === undefined) return "null";
95
+ if (typeof v === "boolean" || typeof v === "number") return String(v);
96
+ const s = String(v);
97
+ if (s === "") return '""';
98
+ if (/^(true|false|null|~|-?\d+(\.\d+)?)$/i.test(s) || /[:#\[\]{}&*!|>'"%@`]/.test(s) || /^[\s-]/.test(s) || /\s$/.test(s) || s.includes("\n"))
99
+ return '"' + s.replace(/\\/g, "\\\\").replace(/"/g, '\\"') + '"';
100
+ return s;
101
+ };
102
+ if (Array.isArray(value)) {
103
+ if (value.length === 0) return "[]";
104
+ return value.map(item => {
105
+ if (item !== null && typeof item === "object") {
106
+ const dumped = toYaml(item, indent + 1); const lines = dumped.split("\n");
107
+ const first = lines[0].slice((indent + 1) * 2);
108
+ return pad + "- " + first + (lines.length > 1 ? "\n" + lines.slice(1).join("\n") : "");
109
+ }
110
+ return pad + "- " + scalar(item);
111
+ }).join("\n");
112
+ }
113
+ if (value !== null && typeof value === "object") {
114
+ const keys = Object.keys(value); if (keys.length === 0) return "{}";
115
+ return keys.map(k => {
116
+ const v = value[k];
117
+ if (v !== null && typeof v === "object" && (Array.isArray(v) ? v.length : Object.keys(v).length)) return pad + k + ":\n" + toYaml(v, indent + 1);
118
+ if (v !== null && typeof v === "object") return pad + k + ": " + (Array.isArray(v) ? "[]" : "{}");
119
+ return pad + k + ": " + scalar(v);
120
+ }).join("\n");
121
+ }
122
+ return pad + scalar(value);
123
+ }
124
+ function parseScalar(s) {
125
+ s = s.trim();
126
+ if (s === "" || s === "~" || s === "null") return null;
127
+ if (s === "true") return true; if (s === "false") return false;
128
+ if (/^-?\d+$/.test(s)) return parseInt(s, 10);
129
+ if (/^-?\d+\.\d+$/.test(s)) return parseFloat(s);
130
+ if (s.startsWith('"') && s.endsWith('"')) return s.slice(1, -1).replace(/\\"/g, '"').replace(/\\\\/g, "\\");
131
+ if (s.startsWith("'") && s.endsWith("'")) return s.slice(1, -1);
132
+ if (s === "[]") return []; if (s === "{}") return {};
133
+ return s;
134
+ }
135
+ function fromYaml(text) {
136
+ const lines = [];
137
+ for (const raw of text.replace(/\r/g, "").split("\n")) {
138
+ let inS = false, inD = false, out = "";
139
+ for (let i = 0; i < raw.length; i++) { const c = raw[i]; if (c === "'" && !inD) inS = !inS; else if (c === '"' && !inS) inD = !inD; else if (c === "#" && !inS && !inD) break; out += c; }
140
+ if (out.trim() === "") continue;
141
+ lines.push({ indent: out.match(/^ */)[0].length, text: out.trim() });
142
+ }
143
+ let pos = 0;
144
+ function parseBlock() {
145
+ if (pos >= lines.length) return null;
146
+ const first = lines[pos];
147
+ if (first.text.startsWith("- ")) {
148
+ const arr = [];
149
+ while (pos < lines.length && lines[pos].indent === first.indent && lines[pos].text.startsWith("- ")) {
150
+ const at = lines[pos].indent, rest = lines[pos].text.slice(2);
151
+ if (rest.includes(":") && !/^".*"$/.test(rest) && !rest.startsWith("[") && !rest.startsWith("{")) { lines[pos] = { indent: at + 2, text: rest }; arr.push(parseMap(at + 2)); }
152
+ else { arr.push(parseScalar(rest)); pos++; }
153
+ }
154
+ return arr;
155
+ }
156
+ return parseMap(first.indent);
157
+ }
158
+ function parseMap(indent) {
159
+ const obj = {};
160
+ while (pos < lines.length && lines[pos].indent === indent) {
161
+ const line = lines[pos].text, ci = line.indexOf(":");
162
+ if (ci < 0) { pos++; continue; }
163
+ const key = line.slice(0, ci).trim(), val = line.slice(ci + 1).trim();
164
+ if (val === "") { pos++; if (pos < lines.length && lines[pos].indent > indent) obj[key] = parseBlock(); else obj[key] = null; }
165
+ else { obj[key] = parseScalar(val); pos++; }
166
+ }
167
+ return obj;
168
+ }
169
+ return parseBlock();
170
+ }
171
+ const serialize = (obj, fmt) => fmt === "yaml" ? toYaml(obj) : JSON.stringify(obj, null, 2);
172
+ const deserialize = (text, fmt) => fmt === "yaml" ? fromYaml(text) : JSON.parse(text || "{}");
173
+
174
+ // ── API ──
175
+ async function api(path, opts = {}) {
176
+ const headers = Object.assign({ "X-Agend-Token": token }, opts.headers || {});
177
+ if (opts.body) headers["Content-Type"] = "application/json";
178
+ const r = await fetch(path, Object.assign({}, opts, { headers }));
179
+ let body = null; try { body = await r.json(); } catch {}
180
+ return { ok: r.ok, status: r.status, body };
181
+ }
182
+ function renderResult(msgEl, res) {
183
+ const b = res.body || {}; let html = "";
184
+ if (b.errors && b.errors.length) html += b.errors.map(e => `<span class="e">✗ ${e.path ? esc(e.path) + ": " : ""}${esc(e.message)}</span>`).join("\n");
185
+ if (b.warnings && b.warnings.length) html += (html ? "\n" : "") + b.warnings.map(w => `<span class="w">⚠ ${w.path ? esc(w.path) + ": " : ""}${esc(w.message)}</span>`).join("\n");
186
+ if (res.ok) html = `<span class="s">✓ Saved</span>` + (html ? "\n" + html : "");
187
+ else if (!html) html = `<span class="e">✗ ${esc((b && b.error) || ("HTTP " + res.status))}</span>`;
188
+ msgEl.innerHTML = html;
189
+ }
190
+
191
+ // ── State ──
192
+ const state = { fleet: {}, classic: {}, snap: "{}", tab: "fleet",
193
+ right: { fleet: { fmt: "yaml", edit: false }, instances: { fmt: "yaml", edit: false }, classic: { fmt: "yaml", edit: false } } };
194
+
195
+ // The object each tab's right-pane mirrors.
196
+ function modelOf(tab) {
197
+ if (tab === "fleet") return { defaults: state.fleet.defaults || {}, channels: state.fleet.channels || (state.fleet.channel ? [state.fleet.channel] : []) };
198
+ if (tab === "instances") return state.fleet.instances || {};
199
+ return { defaults: state.classic.defaults || {} };
200
+ }
201
+ const channelIds = () => (state.fleet.channels || (state.fleet.channel ? [state.fleet.channel] : [])).map(c => c.id || c.type).filter(Boolean);
202
+
203
+ // ── Chip list widget ──
204
+ function chipList(arr, onChange, placeholder) {
205
+ const box = el("div"); const chips = el("div", { class: "chips" });
206
+ const redraw = () => { chips.innerHTML = ""; arr.forEach((v, i) => chips.append(el("span", { class: "chip" }, String(v), el("span", { class: "x", onclick: () => { arr.splice(i, 1); redraw(); onChange(); } }, "×")))); };
207
+ const inp = el("input", { type: "text", placeholder: placeholder || "add…" });
208
+ const add = () => { const v = inp.value.trim(); if (v) { arr.push(/^-?\d+$/.test(v) ? Number(v) : v); inp.value = ""; redraw(); onChange(); } };
209
+ inp.addEventListener("keydown", e => { if (e.key === "Enter") { e.preventDefault(); add(); } });
210
+ box.append(chips, el("div", { class: "chip-add" }, inp, el("button", { class: "sm", onclick: add }, "+ Add")));
211
+ redraw(); return box;
212
+ }
213
+ function select(value, options, onChange) {
214
+ const s = el("select", { onchange: () => onChange(s.value) });
215
+ for (const o of options) { const opt = el("option", {}, o); opt.value = o; if (o === value) opt.selected = true; s.append(opt); }
216
+ return s;
217
+ }
218
+
219
+ // ── Right pane (shared) ──
220
+ function renderRight(tab) {
221
+ const host = $(tab + "-right"); host.innerHTML = "";
222
+ const rs = state.right[tab]; const model = modelOf(tab);
223
+ const msg = el("div", { class: "msg" });
224
+ const seg = el("div", { class: "seg" },
225
+ el("button", { class: "sm" + (rs.fmt === "yaml" ? " on" : ""), onclick: () => { rs.fmt = "yaml"; renderRight(tab); } }, "YAML"),
226
+ el("button", { class: "sm" + (rs.fmt === "json" ? " on" : ""), onclick: () => { rs.fmt = "json"; renderRight(tab); } }, "JSON"));
227
+ const editBtn = el("button", { class: "sm", onclick: () => { rs.edit = !rs.edit; renderRight(tab); } }, rs.edit ? "View" : "Edit");
228
+ const copyBtn = el("button", { class: "sm", onclick: () => { navigator.clipboard.writeText(serialize(model, rs.fmt)).then(() => { msg.innerHTML = `<span class="s">Copied</span>`; }); } }, "Copy");
229
+ const dlBtn = el("button", { class: "sm", onclick: () => download(tab, rs.fmt) }, "Download");
230
+ const toolbar = el("div", { class: "right-toolbar" }, seg, editBtn, copyBtn, dlBtn);
231
+ if (tab === "fleet") toolbar.append(el("button", { class: "sm", onclick: () => download("full", "yaml") }, "⬇ full fleet.yaml"));
232
+ host.append(el("div", { class: "card" }, el("h2", {}, "Config " + (rs.edit ? "(editing)" : "(preview)")), toolbar, (() => {
233
+ if (rs.edit) {
234
+ const ta = el("textarea"); ta.value = serialize(model, rs.fmt);
235
+ const apply = el("button", { class: "primary sm", onclick: () => {
236
+ let parsed; try { parsed = deserialize(ta.value, rs.fmt); } catch (e) { msg.innerHTML = `<span class="e">✗ ${esc(e.message)}</span>`; return; }
237
+ applyModel(tab, parsed); rs.edit = false; renderTab(tab); msg.innerHTML = `<span class="s">Applied to form (not yet saved)</span>`;
238
+ } }, "Apply → form");
239
+ const box = el("div"); box.append(ta, el("div", { class: "row-actions" }, apply, msg)); return box;
240
+ }
241
+ const pre = el("pre", { class: "preview" }); pre.textContent = serialize(model, rs.fmt);
242
+ const box = el("div"); box.append(pre, msg); return box;
243
+ })()));
244
+ }
245
+ // Parse from the right pane back into left-form state.
246
+ function applyModel(tab, parsed) {
247
+ if (!parsed || typeof parsed !== "object") return;
248
+ if (tab === "fleet") { if (parsed.defaults) state.fleet.defaults = parsed.defaults; if (parsed.channels) { state.fleet.channels = parsed.channels; delete state.fleet.channel; } }
249
+ else if (tab === "instances") { state.fleet.instances = parsed; }
250
+ else { if (parsed.defaults) state.classic.defaults = parsed.defaults; }
251
+ }
252
+ function download(which, fmt) {
253
+ let name, content;
254
+ if (which === "full") { name = "fleet.yaml"; content = toYaml(fleetForDownload()); }
255
+ else if (which === "fleet") { name = "fleet-section." + fmt; content = serialize(modelOf("fleet"), fmt); }
256
+ else if (which === "instances") { name = "instances." + fmt; content = serialize(modelOf("instances"), fmt); }
257
+ else { name = "classicBot." + fmt; content = serialize(modelOf("classic"), fmt); }
258
+ const blob = new Blob([content], { type: "text/plain" }); const a = el("a", { href: URL.createObjectURL(blob), download: name }); a.click(); URL.revokeObjectURL(a.href);
259
+ }
260
+ function fleetForDownload() {
261
+ const f = {};
262
+ if (state.fleet.channels || state.fleet.channel) f.channels = state.fleet.channels || [state.fleet.channel];
263
+ if (state.fleet.defaults) f.defaults = state.fleet.defaults;
264
+ if (state.fleet.instances) f.instances = state.fleet.instances;
265
+ return f;
266
+ }
267
+
268
+ // ── Fleet left ──
269
+ function renderFleetLeft() {
270
+ const host = $("fleet-left"); host.innerHTML = "";
271
+ state.fleet.defaults = state.fleet.defaults || {};
272
+ const d = state.fleet.defaults; d.startup = d.startup || {};
273
+ const sync = () => renderRight("fleet");
274
+
275
+ // Defaults card
276
+ const dc = el("div", { class: "card" }, el("h2", {}, "Fleet Defaults"));
277
+ dc.append(el("label", {}, "Backend"), select(d.backend || "claude-code", BACKENDS, v => { d.backend = v; sync(); }));
278
+ const model = el("input", { type: "text", value: d.model || "", placeholder: "e.g. opus, sonnet", oninput: e => { d.model = e.target.value || undefined; sync(); } });
279
+ dc.append(el("label", {}, "Model"), model);
280
+ const g2 = el("div", { class: "grid2" });
281
+ const conc = el("input", { type: "number", value: d.startup.concurrency ?? "", placeholder: "e.g. 3", oninput: e => { d.startup.concurrency = e.target.value === "" ? undefined : Number(e.target.value); sync(); } });
282
+ const stag = el("input", { type: "number", value: d.startup.stagger_delay_ms ?? "", placeholder: "ms", oninput: e => { d.startup.stagger_delay_ms = e.target.value === "" ? undefined : Number(e.target.value); sync(); } });
283
+ g2.append(el("div", {}, el("label", {}, "Startup concurrency"), conc), el("div", {}, el("label", {}, "Stagger delay (ms)"), stag));
284
+ dc.append(g2);
285
+ const dmsg = el("div", { class: "msg" });
286
+ dc.append(el("div", { class: "row-actions" }, el("button", { class: "primary", onclick: async () => { renderResult(dmsg, await api("/api/settings/fleet/defaults", { method: "PUT", body: JSON.stringify(d) })); } }, "Save defaults"), dmsg));
287
+ host.append(dc);
288
+
289
+ // Channels card
290
+ state.fleet.channels = state.fleet.channels || (state.fleet.channel ? [state.fleet.channel] : []);
291
+ const cc = el("div", { class: "card" }, el("h2", {}, el("span", { class: "grow" }, "Channels"), el("button", { class: "sm", onclick: () => { state.fleet.channels.push({ type: "discord", bot_token_env: "" }); renderTab("fleet"); } }, "+ Add channel")));
292
+ state.fleet.channels.forEach((ch, i) => {
293
+ ch.access = ch.access || { mode: "locked", allowed_users: [] };
294
+ ch.access.allowed_users = ch.access.allowed_users || [];
295
+ const card = el("div", { class: "card", style: "background:var(--bg)" });
296
+ card.append(el("h2", {}, el("span", { class: "grow" }, "#" + (i + 1) + " " + esc(ch.id || ch.type || "channel")), el("button", { class: "sm danger", onclick: () => { state.fleet.channels.splice(i, 1); renderTab("fleet"); } }, "🗑 Delete")));
297
+ const gg = el("div", { class: "grid2" });
298
+ gg.append(el("div", {}, el("label", {}, "Type"), select(ch.type || "discord", CH_TYPES, v => { ch.type = v; renderTab("fleet"); })),
299
+ el("div", {}, el("label", {}, "id (adapter id)"), el("input", { type: "text", value: ch.id || "", placeholder: "optional", oninput: e => { ch.id = e.target.value || undefined; sync(); } })));
300
+ card.append(gg);
301
+ card.append(el("label", {}, "bot_token_env"), el("input", { type: "text", value: ch.bot_token_env || "", placeholder: "e.g. DISCORD_BOT_TOKEN", oninput: e => { ch.bot_token_env = e.target.value; sync(); } }));
302
+ card.append(el("label", {}, "group_id"), el("input", { type: "text", value: ch.group_id ?? "", placeholder: "guild / supergroup id", oninput: e => { ch.group_id = e.target.value || undefined; sync(); } }));
303
+ card.append(el("label", {}, "access.allowed_users"), chipList(ch.access.allowed_users, sync, "user id"));
304
+ cc.append(card);
305
+ });
306
+ const cmsg = el("div", { class: "msg" });
307
+ cc.append(el("div", { class: "row-actions" }, el("button", { class: "primary", onclick: async () => { const res = await api("/api/settings/fleet/channels", { method: "PUT", body: JSON.stringify(state.fleet.channels) }); renderResult(cmsg, res); if (res.ok) reload(); } }, "Save channels"), cmsg));
308
+ host.append(cc);
309
+ }
310
+
311
+ // ── Instances left ──
312
+ let expanded = null;
313
+ function renderInstancesLeft() {
314
+ const host = $("instances-left"); host.innerHTML = "";
315
+ state.fleet.instances = state.fleet.instances || {};
316
+ const insts = state.fleet.instances;
317
+ const card = el("div", { class: "card" }, el("h2", {}, el("span", { class: "grow" }, "Instances")));
318
+ const addName = el("input", { type: "text", placeholder: "new instance name", style: "flex:0 0 200px" });
319
+ const addMsg = el("div", { class: "msg" });
320
+ card.append(el("div", { class: "row-actions" }, addName, el("button", { class: "primary sm", onclick: async () => {
321
+ const name = addName.value.trim(); if (!name) { addMsg.innerHTML = `<span class="e">✗ name required</span>`; return; }
322
+ const res = await api(`/api/settings/fleet/instances/${encodeURIComponent(name)}`, { method: "POST", body: JSON.stringify({ working_directory: "" }) });
323
+ renderResult(addMsg, res); if (res.ok) { addName.value = ""; reload(); }
324
+ } }, "+ Add"), addMsg));
325
+ const tbl = el("table"); tbl.append(el("thead", {}, el("tr", {}, ...["Name", "Backend", "Channel", "General"].map(h => el("th", {}, h)))));
326
+ const tb = el("tbody");
327
+ for (const name of Object.keys(insts)) {
328
+ const inst = insts[name];
329
+ const row = el("tr", { class: "inst-row", onclick: () => { expanded = expanded === name ? null : name; renderInstancesLeft(); } },
330
+ el("td", {}, name), el("td", {}, inst.backend || "—"), el("td", {}, inst.channel_id || "—"), el("td", {}, inst.general_topic ? "✓" : ""));
331
+ tb.append(row);
332
+ if (expanded === name) tb.append(instEditRow(name, inst));
333
+ }
334
+ tbl.append(tb); card.append(tbl); host.append(card);
335
+ }
336
+ function instEditRow(name, inst) {
337
+ const td = el("td", { colspan: "4" });
338
+ const sync = () => renderRight("instances");
339
+ td.append(el("label", {}, "Backend"), select(inst.backend || "claude-code", BACKENDS, v => { inst.backend = v; sync(); }));
340
+ td.append(el("label", {}, "channel_id"), select(inst.channel_id || "", ["", ...channelIds()], v => { inst.channel_id = v || undefined; sync(); }));
341
+ td.append(el("label", {}, "working_directory"), el("input", { type: "text", value: inst.working_directory || "", oninput: e => { inst.working_directory = e.target.value; sync(); } }));
342
+ td.append(el("label", {}, "description"), el("input", { type: "text", value: inst.description || "", oninput: e => { inst.description = e.target.value || undefined; sync(); } }));
343
+ const msg = el("div", { class: "msg" });
344
+ td.append(el("div", { class: "row-actions" },
345
+ el("button", { class: "primary sm", onclick: async () => { renderResult(msg, await api(`/api/settings/fleet/instances/${encodeURIComponent(name)}`, { method: "PATCH", body: JSON.stringify(inst) })); } }, "Save"),
346
+ el("button", { class: "sm danger", onclick: async () => { if (!confirm(`Delete "${name}"?`)) return; const res = await api(`/api/settings/fleet/instances/${encodeURIComponent(name)}`, { method: "DELETE" }); if (res.ok) { expanded = null; reload(); } else renderResult(msg, res); } }, "🗑 Delete"), msg));
347
+ return el("tr", { class: "edit-row" }, td);
348
+ }
349
+
350
+ // ── ClassicBot left ──
351
+ function renderClassicLeft() {
352
+ const host = $("classic-left"); host.innerHTML = "";
353
+ state.classic.defaults = state.classic.defaults || {};
354
+ const d = state.classic.defaults;
355
+ d.admin_users = d.admin_users || []; d.allowed_guilds = d.allowed_guilds || [];
356
+ const sync = () => renderRight("classic");
357
+ const card = el("div", { class: "card" }, el("h2", {}, "ClassicBot Defaults"));
358
+ card.append(el("label", {}, "Default backend"), select(d.backend || "claude-code", BACKENDS, v => { d.backend = v; sync(); }));
359
+ card.append(el("label", {}, "Admin users"), chipList(d.admin_users, sync, "user id"));
360
+ card.append(el("label", {}, "Allowed guilds"), chipList(d.allowed_guilds, sync, "guild id"));
361
+ const msg = el("div", { class: "msg" });
362
+ card.append(el("div", { class: "row-actions" }, el("button", { class: "primary", onclick: async () => { renderResult(msg, await api("/api/settings/classic/defaults", { method: "PUT", body: JSON.stringify(d) })); } }, "Save classic defaults"), msg));
363
+ host.append(card);
364
+ }
365
+
366
+ function renderTab(tab) {
367
+ if (tab === "fleet") renderFleetLeft();
368
+ else if (tab === "instances") renderInstancesLeft();
369
+ else renderClassicLeft();
370
+ renderRight(tab);
371
+ }
372
+
373
+ document.querySelectorAll(".tab").forEach(t => t.onclick = () => {
374
+ state.tab = t.dataset.tab;
375
+ document.querySelectorAll(".tab").forEach(x => x.classList.toggle("active", x === t));
376
+ document.querySelectorAll(".panel").forEach(p => p.classList.toggle("active", p.id === "panel-" + state.tab));
377
+ renderTab(state.tab);
378
+ });
379
+
380
+ async function reload() {
381
+ const f = await api("/api/settings/fleet");
382
+ if (!f.ok) { $("banner").style.display = "block"; $("banner").textContent = "Failed to load fleet config (check the token in the URL)."; return; }
383
+ state.fleet = f.body || {};
384
+ const c = await api("/api/settings/classic"); state.classic = c.body || {};
385
+ renderTab(state.tab);
386
+ }
387
+ $("reloadBtn").onclick = async () => {
388
+ const res = await api("/api/settings/reload", { method: "POST" });
389
+ $("banner").style.display = "block"; $("banner").textContent = res.ok ? "✓ Reload signal sent — instances reconciling." : "Reload failed.";
390
+ setTimeout(() => $("banner").style.display = "none", 4000);
391
+ };
392
+
393
+ reload();
394
+ })();
395
+ </script>
396
+ </body>
397
+ </html>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@songsid/agend",
3
- "version": "2.0.11-beta.17",
3
+ "version": "2.0.11-beta.19",
4
4
  "description": "Multi-agent fleet daemon — run any coding CLI (Claude, Gemini, Codex, OpenCode) from Telegram",
5
5
  "type": "module",
6
6
  "bin": {