@songsid/agend 2.0.11-beta.27 → 2.0.11-beta.29
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/settings.html +358 -319
- package/package.json +1 -1
package/dist/ui/settings.html
CHANGED
|
@@ -6,174 +6,125 @@
|
|
|
6
6
|
<title>AgEnD Settings</title>
|
|
7
7
|
<style>
|
|
8
8
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
9
|
-
:root {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
9
|
+
:root {
|
|
10
|
+
--bg:#ffffff; --card:#f9fafb; --border:#e5e7eb; --fg:#111827; --muted:#6b7280;
|
|
11
|
+
--accent:#2563eb; --accent-hover:#1d4ed8; --green:#059669; --red:#dc2626;
|
|
12
|
+
--radius:8px; --radius-sm:6px;
|
|
13
|
+
}
|
|
14
|
+
body { background: var(--bg); color: var(--fg); font-family: Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; font-size: 14px; line-height: 1.5; padding-bottom: 60px; }
|
|
15
|
+
header { display: flex; align-items: center; gap: 12px; padding: 16px 24px; border-bottom: 1px solid var(--border); position: sticky; top: 0; background: var(--bg); z-index: 20; }
|
|
16
|
+
header h1 { font-size: 18px; font-weight: 700; }
|
|
17
|
+
.grow { flex: 1; }
|
|
18
|
+
main { max-width: 860px; margin: 0 auto; padding: 24px; }
|
|
19
|
+
section { margin-bottom: 28px; }
|
|
20
|
+
.sec-head { display: flex; align-items: center; gap: 10px; margin-bottom: 12px; }
|
|
21
|
+
.sec-head h2 { font-size: 16px; font-weight: 600; }
|
|
22
|
+
.sec-head .grow { flex: 1; }
|
|
23
|
+
.card { background: var(--card); border: 1px solid var(--border); border-radius: var(--radius); overflow: hidden; }
|
|
24
|
+
.card.empty { padding: 16px; color: var(--muted); }
|
|
25
|
+
.item { padding: 14px 16px; border-bottom: 1px solid var(--border); }
|
|
26
|
+
.item:last-child { border-bottom: none; }
|
|
27
|
+
.item-row { display: flex; align-items: center; gap: 10px; }
|
|
28
|
+
.dot { width: 9px; height: 9px; border-radius: 50%; flex: 0 0 9px; background: #9ca3af; }
|
|
29
|
+
.dot.running { background: var(--green); } .dot.crashed { background: var(--red); } .dot.stopped { background: #9ca3af; }
|
|
30
|
+
.name { font-weight: 600; }
|
|
31
|
+
.tag { font-size: 12px; color: var(--muted); background: #eef2ff; color: #3730a3; padding: 1px 7px; border-radius: 10px; }
|
|
32
|
+
.tag.persona { background: #fdf2f8; color: #9d174d; }
|
|
33
|
+
.tag.tg { background: #e0f2fe; color: #075985; }
|
|
34
|
+
.sub { font-size: 13px; color: var(--muted); margin-top: 3px; margin-left: 19px; }
|
|
17
35
|
.spacer { flex: 1; }
|
|
18
|
-
button {
|
|
19
|
-
button:hover { background: #
|
|
20
|
-
button.primary { background: var(--accent); color: #
|
|
21
|
-
button.primary:hover { background:
|
|
22
|
-
button.
|
|
23
|
-
button.
|
|
24
|
-
button.danger
|
|
25
|
-
|
|
26
|
-
.
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
.
|
|
31
|
-
.
|
|
32
|
-
.
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
.
|
|
37
|
-
.
|
|
38
|
-
.
|
|
39
|
-
.
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
.
|
|
45
|
-
.
|
|
46
|
-
.
|
|
47
|
-
.
|
|
48
|
-
|
|
49
|
-
.
|
|
50
|
-
|
|
51
|
-
.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); }
|
|
52
|
-
.row-actions { display: flex; gap: 6px; margin-top: 10px; align-items: center; flex-wrap: wrap; }
|
|
53
|
-
.banner { padding: 9px 20px; font-size: 13px; background: rgba(210,153,34,.12); border-bottom: 1px solid var(--border); color: var(--warn); display: none; }
|
|
54
|
-
code { background: var(--bg); padding: 1px 4px; border-radius: 4px; font-size: 12px; }
|
|
36
|
+
button { font-family: inherit; font-size: 13px; border-radius: var(--radius-sm); padding: 6px 12px; border: 1px solid var(--border); background: #fff; color: var(--fg); cursor: pointer; }
|
|
37
|
+
button:hover { background: #f3f4f6; }
|
|
38
|
+
button.primary { background: var(--accent); color: #fff; border-color: var(--accent); font-weight: 600; }
|
|
39
|
+
button.primary:hover { background: var(--accent-hover); }
|
|
40
|
+
button.ghost { border-color: transparent; background: transparent; color: var(--muted); }
|
|
41
|
+
button.ghost:hover { background: #f3f4f6; color: var(--fg); }
|
|
42
|
+
button.danger { color: var(--red); }
|
|
43
|
+
button.danger:hover { background: #fef2f2; }
|
|
44
|
+
button.sm { padding: 4px 9px; font-size: 12px; }
|
|
45
|
+
label { display: block; font-size: 12px; font-weight: 500; color: var(--muted); margin: 10px 0 4px; }
|
|
46
|
+
input[type=text], select { width: 100%; font-family: inherit; font-size: 13px; padding: 8px 10px; border: 1px solid var(--border); border-radius: var(--radius-sm); background: #fff; color: var(--fg); }
|
|
47
|
+
input:focus, select:focus { outline: 2px solid #bfdbfe; border-color: var(--accent); }
|
|
48
|
+
.edit { padding: 14px 16px; background: #fff; border-top: 1px solid var(--border); }
|
|
49
|
+
.grid2 { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
|
|
50
|
+
.actions { display: flex; gap: 8px; margin-top: 12px; align-items: center; flex-wrap: wrap; }
|
|
51
|
+
.chips { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 4px; }
|
|
52
|
+
.chip { display: inline-flex; align-items: center; gap: 6px; background: #fff; border: 1px solid var(--border); border-radius: 14px; padding: 3px 6px 3px 10px; font-size: 12px; }
|
|
53
|
+
.chip .x { cursor: pointer; color: var(--muted); font-weight: 700; } .chip .x:hover { color: var(--red); }
|
|
54
|
+
.chip-add { display: flex; gap: 6px; margin-top: 6px; max-width: 320px; }
|
|
55
|
+
.field { padding: 12px 16px; border-bottom: 1px solid var(--border); }
|
|
56
|
+
.field:last-child { border-bottom: none; }
|
|
57
|
+
.msg { font-size: 12px; margin-left: 4px; }
|
|
58
|
+
.msg .e { color: var(--red); } .msg .w { color: #b45309; } .msg .s { color: var(--green); }
|
|
59
|
+
.hint { font-size: 12px; color: var(--muted); margin-top: 6px; }
|
|
60
|
+
.adv-body { display: none; padding: 14px 16px; }
|
|
61
|
+
.adv-body.open { display: block; }
|
|
62
|
+
.toolbar { display: flex; gap: 6px; align-items: center; margin-bottom: 8px; flex-wrap: wrap; }
|
|
63
|
+
.seg { display: inline-flex; border: 1px solid var(--border); border-radius: var(--radius-sm); overflow: hidden; }
|
|
64
|
+
.seg button { border: none; border-radius: 0; }
|
|
65
|
+
.seg button.on { background: var(--accent); color: #fff; }
|
|
66
|
+
textarea { width: 100%; min-height: 380px; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12px; line-height: 1.5; border: 1px solid var(--border); border-radius: var(--radius-sm); padding: 10px; color: var(--fg); background: #fff; resize: vertical; }
|
|
67
|
+
pre.preview { background: #f9fafb; border: 1px solid var(--border); border-radius: var(--radius-sm); padding: 12px; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12px; line-height: 1.5; overflow: auto; max-height: 480px; white-space: pre; min-height: 380px; }
|
|
68
|
+
.banner { display: none; padding: 10px 24px; font-size: 13px; background: #ecfdf5; color: var(--green); border-bottom: 1px solid var(--border); }
|
|
55
69
|
</style>
|
|
56
70
|
</head>
|
|
57
71
|
<body>
|
|
58
72
|
<header>
|
|
59
|
-
<h1
|
|
60
|
-
<div class="
|
|
61
|
-
|
|
62
|
-
<div class="tab" data-tab="instances">Instances</div>
|
|
63
|
-
<div class="tab" data-tab="classic">ClassicBot</div>
|
|
64
|
-
</div>
|
|
65
|
-
<div class="spacer"></div>
|
|
66
|
-
<button id="reloadBtn" title="Apply saved config to running instances (SIGHUP)">↻ Reload fleet</button>
|
|
73
|
+
<h1>⚙️ AgEnD Settings</h1>
|
|
74
|
+
<div class="grow"></div>
|
|
75
|
+
<button id="reloadBtn" class="primary" title="Apply saved config to running instances (SIGHUP)">↻ Reload Fleet</button>
|
|
67
76
|
</header>
|
|
68
77
|
<div class="banner" id="banner"></div>
|
|
69
78
|
<main>
|
|
70
|
-
<section
|
|
71
|
-
<div class="
|
|
72
|
-
<div
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
<div class="
|
|
80
|
-
|
|
81
|
-
</div></section>
|
|
82
|
-
</main>
|
|
79
|
+
<section>
|
|
80
|
+
<div class="sec-head"><h2>🤖 My Agents</h2><div class="grow"></div><input type="text" id="agentSearch" placeholder="🔍 Search…" style="width:180px;margin-right:8px"><button class="sm" id="newAgentBtn">+ New</button></div>
|
|
81
|
+
<div id="newAgent"></div>
|
|
82
|
+
<div class="card" id="agents"></div>
|
|
83
|
+
</section>
|
|
84
|
+
|
|
85
|
+
<section>
|
|
86
|
+
<div class="sec-head"><h2>🔌 Bots</h2><div class="grow"></div><button class="sm" id="newBotBtn">+ Add</button></div>
|
|
87
|
+
<div id="newBot"></div>
|
|
88
|
+
<div class="card" id="bots"></div>
|
|
89
|
+
</section>
|
|
83
90
|
|
|
91
|
+
<section>
|
|
92
|
+
<div class="sec-head"><h2>⚙️ General Settings</h2></div>
|
|
93
|
+
<div class="card" id="general"></div>
|
|
94
|
+
</section>
|
|
95
|
+
|
|
96
|
+
<section>
|
|
97
|
+
<div class="sec-head"><h2>📋 Advanced</h2><div class="grow"></div><button class="sm" id="advToggle">▶ Expand</button></div>
|
|
98
|
+
<div class="card"><div class="adv-body" id="advBody"></div></div>
|
|
99
|
+
</section>
|
|
100
|
+
</main>
|
|
84
101
|
<script>
|
|
85
102
|
(() => {
|
|
86
103
|
"use strict";
|
|
87
104
|
const token = new URLSearchParams(location.search).get("token") || "";
|
|
88
105
|
const $ = (id) => document.getElementById(id);
|
|
89
|
-
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; };
|
|
106
|
+
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) if (c != null) e.append(c); return e; };
|
|
90
107
|
const esc = (s) => String(s).replace(/[&<>]/g, c => ({ "&":"&","<":"<",">":">" }[c]));
|
|
91
108
|
const BACKENDS = ["claude-code", "kiro-cli", "codex", "opencode", "gemini-cli", "antigravity"];
|
|
92
|
-
const CH_TYPES = ["
|
|
109
|
+
const CH_TYPES = ["discord", "telegram"];
|
|
93
110
|
|
|
94
|
-
// ── YAML
|
|
111
|
+
// ── YAML (config subset; round-trip verified) ──
|
|
95
112
|
function toYaml(value, indent = 0) {
|
|
96
113
|
const pad = " ".repeat(indent);
|
|
97
|
-
const scalar = (v) => {
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
const s = String(v);
|
|
101
|
-
if (s === "") return '""';
|
|
102
|
-
if (/^(true|false|null|~|-?\d+(\.\d+)?)$/i.test(s) || /[:#\[\]{}&*!|>'"%@`]/.test(s) || /^[\s-]/.test(s) || /\s$/.test(s) || s.includes("\n"))
|
|
103
|
-
return '"' + s.replace(/\\/g, "\\\\").replace(/"/g, '\\"') + '"';
|
|
104
|
-
return s;
|
|
105
|
-
};
|
|
106
|
-
if (Array.isArray(value)) {
|
|
107
|
-
if (value.length === 0) return "[]";
|
|
108
|
-
return value.map(item => {
|
|
109
|
-
if (item !== null && typeof item === "object") {
|
|
110
|
-
const dumped = toYaml(item, indent + 1); const lines = dumped.split("\n");
|
|
111
|
-
const first = lines[0].slice((indent + 1) * 2);
|
|
112
|
-
return pad + "- " + first + (lines.length > 1 ? "\n" + lines.slice(1).join("\n") : "");
|
|
113
|
-
}
|
|
114
|
-
return pad + "- " + scalar(item);
|
|
115
|
-
}).join("\n");
|
|
116
|
-
}
|
|
117
|
-
if (value !== null && typeof value === "object") {
|
|
118
|
-
const keys = Object.keys(value); if (keys.length === 0) return "{}";
|
|
119
|
-
return keys.map(k => {
|
|
120
|
-
const v = value[k];
|
|
121
|
-
if (v !== null && typeof v === "object" && (Array.isArray(v) ? v.length : Object.keys(v).length)) return pad + k + ":\n" + toYaml(v, indent + 1);
|
|
122
|
-
if (v !== null && typeof v === "object") return pad + k + ": " + (Array.isArray(v) ? "[]" : "{}");
|
|
123
|
-
return pad + k + ": " + scalar(v);
|
|
124
|
-
}).join("\n");
|
|
125
|
-
}
|
|
114
|
+
const scalar = (v) => { if (v === null || v === undefined) return "null"; if (typeof v === "boolean" || typeof v === "number") return String(v); const s = String(v); if (s === "") return '""'; if (/^(true|false|null|~|-?\d+(\.\d+)?)$/i.test(s) || /[:#\[\]{}&*!|>'"%@`]/.test(s) || /^[\s-]/.test(s) || /\s$/.test(s) || s.includes("\n")) return '"' + s.replace(/\\/g, "\\\\").replace(/"/g, '\\"') + '"'; return s; };
|
|
115
|
+
if (Array.isArray(value)) { if (!value.length) return "[]"; return value.map(item => { if (item !== null && typeof item === "object") { const d = toYaml(item, indent + 1); const lines = d.split("\n"); return pad + "- " + lines[0].slice((indent + 1) * 2) + (lines.length > 1 ? "\n" + lines.slice(1).join("\n") : ""); } return pad + "- " + scalar(item); }).join("\n"); }
|
|
116
|
+
if (value !== null && typeof value === "object") { const keys = Object.keys(value); if (!keys.length) return "{}"; return keys.map(k => { const v = value[k]; if (v !== null && typeof v === "object" && (Array.isArray(v) ? v.length : Object.keys(v).length)) return pad + k + ":\n" + toYaml(v, indent + 1); if (v !== null && typeof v === "object") return pad + k + ": " + (Array.isArray(v) ? "[]" : "{}"); return pad + k + ": " + scalar(v); }).join("\n"); }
|
|
126
117
|
return pad + scalar(value);
|
|
127
118
|
}
|
|
128
|
-
function parseScalar(s) {
|
|
129
|
-
s = s.trim();
|
|
130
|
-
if (s === "" || s === "~" || s === "null") return null;
|
|
131
|
-
if (s === "true") return true; if (s === "false") return false;
|
|
132
|
-
if (/^-?\d+$/.test(s)) return parseInt(s, 10);
|
|
133
|
-
if (/^-?\d+\.\d+$/.test(s)) return parseFloat(s);
|
|
134
|
-
if (s.startsWith('"') && s.endsWith('"')) return s.slice(1, -1).replace(/\\"/g, '"').replace(/\\\\/g, "\\");
|
|
135
|
-
if (s.startsWith("'") && s.endsWith("'")) return s.slice(1, -1);
|
|
136
|
-
if (s === "[]") return []; if (s === "{}") return {};
|
|
137
|
-
return s;
|
|
138
|
-
}
|
|
119
|
+
function parseScalar(s) { s = s.trim(); if (s === "" || s === "~" || s === "null") return null; if (s === "true") return true; if (s === "false") return false; if (/^-?\d+$/.test(s)) return parseInt(s, 10); if (/^-?\d+\.\d+$/.test(s)) return parseFloat(s); if (s.startsWith('"') && s.endsWith('"')) return s.slice(1, -1).replace(/\\"/g, '"').replace(/\\\\/g, "\\"); if (s.startsWith("'") && s.endsWith("'")) return s.slice(1, -1); if (s === "[]") return []; if (s === "{}") return {}; return s; }
|
|
139
120
|
function fromYaml(text) {
|
|
140
121
|
const lines = [];
|
|
141
|
-
for (const raw of text.replace(/\r/g, "").split("\n")) {
|
|
142
|
-
let inS = false, inD = false, out = "";
|
|
143
|
-
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; }
|
|
144
|
-
if (out.trim() === "") continue;
|
|
145
|
-
lines.push({ indent: out.match(/^ */)[0].length, text: out.trim() });
|
|
146
|
-
}
|
|
122
|
+
for (const raw of text.replace(/\r/g, "").split("\n")) { let inS = false, inD = false, out = ""; 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; } if (out.trim() === "") continue; lines.push({ indent: out.match(/^ */)[0].length, text: out.trim() }); }
|
|
147
123
|
let pos = 0;
|
|
148
|
-
function
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
if (first.text.startsWith("- ")) {
|
|
152
|
-
const arr = [];
|
|
153
|
-
while (pos < lines.length && lines[pos].indent === first.indent && lines[pos].text.startsWith("- ")) {
|
|
154
|
-
const at = lines[pos].indent, rest = lines[pos].text.slice(2);
|
|
155
|
-
if (rest.includes(":") && !/^".*"$/.test(rest) && !rest.startsWith("[") && !rest.startsWith("{")) { lines[pos] = { indent: at + 2, text: rest }; arr.push(parseMap(at + 2)); }
|
|
156
|
-
else { arr.push(parseScalar(rest)); pos++; }
|
|
157
|
-
}
|
|
158
|
-
return arr;
|
|
159
|
-
}
|
|
160
|
-
return parseMap(first.indent);
|
|
161
|
-
}
|
|
162
|
-
function parseMap(indent) {
|
|
163
|
-
const obj = {};
|
|
164
|
-
while (pos < lines.length && lines[pos].indent === indent) {
|
|
165
|
-
const line = lines[pos].text, ci = line.indexOf(":");
|
|
166
|
-
if (ci < 0) { pos++; continue; }
|
|
167
|
-
const key = line.slice(0, ci).trim(), val = line.slice(ci + 1).trim();
|
|
168
|
-
if (val === "") { pos++; if (pos < lines.length && lines[pos].indent > indent) obj[key] = parseBlock(); else obj[key] = null; }
|
|
169
|
-
else { obj[key] = parseScalar(val); pos++; }
|
|
170
|
-
}
|
|
171
|
-
return obj;
|
|
172
|
-
}
|
|
173
|
-
return parseBlock();
|
|
124
|
+
function block() { if (pos >= lines.length) return null; const first = lines[pos]; if (first.text.startsWith("- ")) { const arr = []; while (pos < lines.length && lines[pos].indent === first.indent && lines[pos].text.startsWith("- ")) { const at = lines[pos].indent, rest = lines[pos].text.slice(2); if (rest.includes(":") && !/^".*"$/.test(rest) && !rest.startsWith("[") && !rest.startsWith("{")) { lines[pos] = { indent: at + 2, text: rest }; arr.push(map(at + 2)); } else { arr.push(parseScalar(rest)); pos++; } } return arr; } return map(first.indent); }
|
|
125
|
+
function map(indent) { const obj = {}; while (pos < lines.length && lines[pos].indent === indent) { const line = lines[pos].text, ci = line.indexOf(":"); if (ci < 0) { pos++; continue; } const key = line.slice(0, ci).trim(), val = line.slice(ci + 1).trim(); if (val === "") { pos++; if (pos < lines.length && lines[pos].indent > indent) obj[key] = block(); else obj[key] = null; } else { obj[key] = parseScalar(val); pos++; } } return obj; }
|
|
126
|
+
return block();
|
|
174
127
|
}
|
|
175
|
-
const serialize = (obj, fmt) => fmt === "yaml" ? toYaml(obj) : JSON.stringify(obj, null, 2);
|
|
176
|
-
const deserialize = (text, fmt) => fmt === "yaml" ? fromYaml(text) : JSON.parse(text || "{}");
|
|
177
128
|
|
|
178
129
|
// ── API ──
|
|
179
130
|
async function api(path, opts = {}) {
|
|
@@ -183,216 +134,304 @@
|
|
|
183
134
|
let body = null; try { body = await r.json(); } catch {}
|
|
184
135
|
return { ok: r.ok, status: r.status, body };
|
|
185
136
|
}
|
|
186
|
-
function
|
|
137
|
+
function resultMsg(elm, res) {
|
|
187
138
|
const b = res.body || {}; let html = "";
|
|
188
|
-
if (b.errors && b.errors.length) html += b.errors.map(e => `<span class="e">✗ ${e.path ?
|
|
189
|
-
if (b.warnings && b.warnings.length) html +=
|
|
190
|
-
if (res.ok) html = `<span class="s">✓ Saved</span>` + (html ? "
|
|
139
|
+
if (b.errors && b.errors.length) html += b.errors.map(e => `<span class="e">✗ ${esc(e.path ? e.path + ": " : "")}${esc(e.message)}</span>`).join(" ");
|
|
140
|
+
if (b.warnings && b.warnings.length) html += " " + b.warnings.map(w => `<span class="w">⚠ ${esc(w.message)}</span>`).join(" ");
|
|
141
|
+
if (res.ok) html = `<span class="s">✓ Saved</span>` + (html ? " " + html : "");
|
|
191
142
|
else if (!html) html = `<span class="e">✗ ${esc((b && b.error) || ("HTTP " + res.status))}</span>`;
|
|
192
|
-
|
|
143
|
+
elm.innerHTML = html;
|
|
193
144
|
}
|
|
194
145
|
|
|
195
146
|
// ── State ──
|
|
196
|
-
const state = { fleet: {}, classic: {},
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
function
|
|
201
|
-
if (tab === "fleet") return { defaults: state.fleet.defaults || {}, channels: state.fleet.channels || (state.fleet.channel ? [state.fleet.channel] : []) };
|
|
202
|
-
if (tab === "instances") return state.fleet.instances || {};
|
|
203
|
-
return { defaults: state.classic.defaults || {} };
|
|
204
|
-
}
|
|
205
|
-
const channelIds = () => (state.fleet.channels || (state.fleet.channel ? [state.fleet.channel] : [])).map(c => c.id || c.type).filter(Boolean);
|
|
147
|
+
const state = { fleet: {}, classic: {}, live: {}, fleetUp: false, advFmt: "yaml", advEdit: false };
|
|
148
|
+
const channels = () => state.fleet.channels || (state.fleet.channel ? [state.fleet.channel] : []);
|
|
149
|
+
const channelIds = () => channels().map(c => c.id || c.type).filter(Boolean);
|
|
150
|
+
const chLabel = (i) => i === 0 ? "primary" : "persona";
|
|
151
|
+
function chById(id) { return channels().find(c => (c.id || c.type) === id); }
|
|
206
152
|
|
|
207
|
-
// ── Chip list widget ──
|
|
208
153
|
function chipList(arr, onChange, placeholder) {
|
|
209
154
|
const box = el("div"); const chips = el("div", { class: "chips" });
|
|
210
|
-
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(); } }, "×")))); };
|
|
155
|
+
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 && onChange(); } }, "×")))); };
|
|
211
156
|
const inp = el("input", { type: "text", placeholder: placeholder || "add…" });
|
|
212
|
-
const add = () => { const v = inp.value.trim(); if (v) { arr.push(/^-?\d+$/.test(v) ? Number(v) : v); inp.value = ""; redraw(); onChange(); } };
|
|
157
|
+
const add = () => { const v = inp.value.trim(); if (v) { arr.push(/^-?\d+$/.test(v) ? Number(v) : v); inp.value = ""; redraw(); onChange && onChange(); } };
|
|
213
158
|
inp.addEventListener("keydown", e => { if (e.key === "Enter") { e.preventDefault(); add(); } });
|
|
214
159
|
box.append(chips, el("div", { class: "chip-add" }, inp, el("button", { class: "sm", onclick: add }, "+ Add")));
|
|
215
160
|
redraw(); return box;
|
|
216
161
|
}
|
|
217
|
-
function select(value, options,
|
|
218
|
-
const s = el("select", { onchange: () => onChange(s.value) });
|
|
219
|
-
for (const o of options) { const opt = el("option", {}, o); opt.value = o; if (o === value) opt.selected = true; s.append(opt); }
|
|
220
|
-
return s;
|
|
221
|
-
}
|
|
162
|
+
function select(value, options) { const s = el("select"); for (const o of options) { const opt = el("option", {}, o); opt.value = o; if (o === value) opt.selected = true; s.append(opt); } return s; }
|
|
222
163
|
|
|
223
|
-
// ──
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
const
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
164
|
+
// ── My Agents ──
|
|
165
|
+
let editing = null;
|
|
166
|
+
function agentChannelLabel(inst) {
|
|
167
|
+
if (inst.general_topic) return "general";
|
|
168
|
+
if (!inst.channel_id) return "";
|
|
169
|
+
const idx = channels().findIndex(c => (c.id || c.type) === inst.channel_id);
|
|
170
|
+
const ch = channels()[idx];
|
|
171
|
+
return ch ? `${ch.type} (${chLabel(idx)})` : inst.channel_id;
|
|
172
|
+
}
|
|
173
|
+
// Display name: profile display_name → else strip the -t<topicId> suffix.
|
|
174
|
+
const shortName = (name, inst) => (inst && inst.display_name) || name.replace(/-t\d+$/, "");
|
|
175
|
+
const STATUS_TEXT = { running: "🟢 Running", crashed: "🔴 Crashed", stopped: "⚪ Stopped" };
|
|
176
|
+
const STATUS_ORDER = { running: 0, crashed: 1, stopped: 2 };
|
|
177
|
+
function renderAgents() {
|
|
178
|
+
const host = $("agents"); host.innerHTML = "";
|
|
179
|
+
const insts = state.fleet.instances || {};
|
|
180
|
+
const filter = ($("agentSearch")?.value || "").trim().toLowerCase();
|
|
181
|
+
let names = Object.keys(insts);
|
|
182
|
+
if (filter) names = names.filter(n => n.toLowerCase().includes(filter) || shortName(n, insts[n]).toLowerCase().includes(filter));
|
|
183
|
+
// Group by status: Running first, then Crashed, then Stopped; name within.
|
|
184
|
+
names.sort((a, b) => {
|
|
185
|
+
const sa = STATUS_ORDER[state.live[a]?.status || "stopped"], sb = STATUS_ORDER[state.live[b]?.status || "stopped"];
|
|
186
|
+
return sa !== sb ? sa - sb : a.localeCompare(b);
|
|
187
|
+
});
|
|
188
|
+
if (!Object.keys(insts).length) { host.className = "card empty"; host.textContent = "No agents yet — click + New."; return; }
|
|
189
|
+
if (!names.length) { host.className = "card empty"; host.textContent = "No agents match \"" + filter + "\"."; return; }
|
|
190
|
+
host.className = "card";
|
|
191
|
+
for (const name of names) {
|
|
192
|
+
const inst = insts[name];
|
|
193
|
+
const st = state.live[name]?.status || "stopped";
|
|
194
|
+
const running = st === "running";
|
|
195
|
+
const short = shortName(name, inst);
|
|
196
|
+
const item = el("div", { class: "item" });
|
|
197
|
+
const chTag = agentChannelLabel(inst);
|
|
198
|
+
item.append(el("div", { class: "item-row" },
|
|
199
|
+
el("span", { class: "dot " + st }),
|
|
200
|
+
el("span", { class: "name", title: name }, short),
|
|
201
|
+
short !== name ? el("span", { class: "sub", style: "margin:0;font-size:11px" }, name) : null,
|
|
202
|
+
el("span", { style: "font-size:12px;color:var(--muted)" }, STATUS_TEXT[st] || st),
|
|
203
|
+
chTag ? el("span", { class: "tag" + (chTag.includes("persona") ? " persona" : chTag.includes("telegram") ? " tg" : "") }, chTag) : null,
|
|
204
|
+
el("span", { class: "sub", style: "margin:0" }, inst.backend || state.fleet.defaults?.backend || "claude-code"),
|
|
205
|
+
el("span", { class: "spacer" }),
|
|
206
|
+
el("button", { class: "sm ghost", onclick: () => { editing = editing === name ? null : name; renderAgents(); } }, editing === name ? "Close" : "Edit"),
|
|
207
|
+
el("button", { class: "sm", onclick: () => toggleAgent(name, running) }, running ? "Stop" : "Start"),
|
|
208
|
+
el("button", { class: "sm danger", onclick: () => delAgent(name) }, "🗑️"),
|
|
209
|
+
));
|
|
210
|
+
if (inst.working_directory) item.append(el("div", { class: "sub" }, inst.working_directory));
|
|
211
|
+
if (editing === name) item.append(agentEditForm(name, inst));
|
|
212
|
+
host.append(item);
|
|
213
|
+
}
|
|
248
214
|
}
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
215
|
+
function agentEditForm(name, inst) {
|
|
216
|
+
const box = el("div", { class: "edit" });
|
|
217
|
+
const fBackend = select(inst.backend || state.fleet.defaults?.backend || "claude-code", BACKENDS);
|
|
218
|
+
const fModel = el("input", { type: "text", value: inst.model || "", placeholder: "(inherit default)" });
|
|
219
|
+
const fWd = el("input", { type: "text", value: inst.working_directory || "" });
|
|
220
|
+
const fChan = select(inst.channel_id || "", ["", ...channelIds()]);
|
|
221
|
+
const fDesc = el("input", { type: "text", value: inst.description || "" });
|
|
222
|
+
const fSys = el("input", { type: "text", value: inst.systemPrompt || "", placeholder: "e.g. file:prompts/dev.md" });
|
|
223
|
+
const tags = [...(inst.tags || [])];
|
|
224
|
+
const fGeneral = el("input", { type: "checkbox" }); fGeneral.checked = !!inst.general_topic;
|
|
225
|
+
const msg = el("span", { class: "msg" });
|
|
226
|
+
box.append(
|
|
227
|
+
el("div", { class: "grid2" }, el("div", {}, el("label", {}, "Backend"), fBackend), el("div", {}, el("label", {}, "Model"), fModel)),
|
|
228
|
+
el("label", {}, "Working directory"), fWd,
|
|
229
|
+
el("div", { class: "grid2" }, el("div", {}, el("label", {}, "Channel binding"), fChan), el("div", {}, el("label", {}, "Description"), fDesc)),
|
|
230
|
+
el("label", {}, "System prompt"), fSys,
|
|
231
|
+
el("label", {}, "Tags"), chipList(tags, null, "tag"),
|
|
232
|
+
el("label", { style: "display:flex;align-items:center;gap:8px;cursor:pointer" }, fGeneral, el("span", {}, "General topic (fleet dispatcher)")),
|
|
233
|
+
el("div", { class: "actions" },
|
|
234
|
+
el("button", { class: "primary sm", onclick: async () => {
|
|
235
|
+
const patch = {
|
|
236
|
+
backend: fBackend.value, model: fModel.value || undefined,
|
|
237
|
+
working_directory: fWd.value, channel_id: fChan.value || undefined,
|
|
238
|
+
description: fDesc.value || undefined, systemPrompt: fSys.value || undefined,
|
|
239
|
+
tags: tags.length ? tags : undefined, general_topic: fGeneral.checked,
|
|
240
|
+
};
|
|
241
|
+
const res = await api(`/api/settings/fleet/instances/${encodeURIComponent(name)}`, { method: "PATCH", body: JSON.stringify(patch) });
|
|
242
|
+
resultMsg(msg, res); if (res.ok) await reload();
|
|
243
|
+
} }, "Save"),
|
|
244
|
+
el("button", { class: "sm ghost", onclick: () => { editing = null; renderAgents(); } }, "Cancel"),
|
|
245
|
+
msg),
|
|
246
|
+
);
|
|
247
|
+
return box;
|
|
255
248
|
}
|
|
256
|
-
function
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
else if (which === "instances") { name = "instances." + fmt; content = serialize(modelOf("instances"), fmt); }
|
|
261
|
-
else { name = "classicBot." + fmt; content = serialize(modelOf("classic"), fmt); }
|
|
262
|
-
const blob = new Blob([content], { type: "text/plain" }); const a = el("a", { href: URL.createObjectURL(blob), download: name }); a.click(); URL.revokeObjectURL(a.href);
|
|
249
|
+
async function toggleAgent(name, running) {
|
|
250
|
+
if (running) await api(`/stop/${encodeURIComponent(name)}`, { method: "POST" });
|
|
251
|
+
else await api(`/api/instance/${encodeURIComponent(name)}/start`, { method: "POST" });
|
|
252
|
+
setTimeout(reloadLive, 800);
|
|
263
253
|
}
|
|
264
|
-
function
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
if (
|
|
268
|
-
|
|
269
|
-
|
|
254
|
+
async function delAgent(name) {
|
|
255
|
+
if (!confirm(`Delete agent "${name}"? (config removed; running daemon stops on next Reload)`)) return;
|
|
256
|
+
const res = await api(`/api/settings/fleet/instances/${encodeURIComponent(name)}`, { method: "DELETE" });
|
|
257
|
+
if (res.ok) await reload(); else alert((res.body && res.body.error) || "Delete failed");
|
|
258
|
+
}
|
|
259
|
+
function renderNewAgent(open) {
|
|
260
|
+
const host = $("newAgent"); host.innerHTML = "";
|
|
261
|
+
if (!open) return;
|
|
262
|
+
const box = el("div", { class: "card", style: "margin-bottom:12px" });
|
|
263
|
+
const fName = el("input", { type: "text", placeholder: "agent name" });
|
|
264
|
+
const fBackend = select(state.fleet.defaults?.backend || "claude-code", BACKENDS);
|
|
265
|
+
const fWd = el("input", { type: "text", placeholder: "~/Projects/… (optional)" });
|
|
266
|
+
const fChan = select("", ["", ...channelIds()]);
|
|
267
|
+
const msg = el("span", { class: "msg" });
|
|
268
|
+
box.append(el("div", { class: "edit", style: "border:none" },
|
|
269
|
+
el("div", { class: "grid2" }, el("div", {}, el("label", {}, "Name"), fName), el("div", {}, el("label", {}, "Backend"), fBackend)),
|
|
270
|
+
el("div", { class: "grid2" }, el("div", {}, el("label", {}, "Working directory"), fWd), el("div", {}, el("label", {}, "Channel binding"), fChan)),
|
|
271
|
+
el("div", { class: "actions" }, el("button", { class: "primary sm", onclick: async () => {
|
|
272
|
+
const name = fName.value.trim(); if (!name) { msg.innerHTML = `<span class="e">✗ name required</span>`; return; }
|
|
273
|
+
const body = { working_directory: fWd.value, backend: fBackend.value, channel_id: fChan.value || undefined };
|
|
274
|
+
const res = await api(`/api/settings/fleet/instances/${encodeURIComponent(name)}`, { method: "POST", body: JSON.stringify(body) });
|
|
275
|
+
resultMsg(msg, res); if (res.ok) { $("newAgentBtn").textContent = "+ New"; renderNewAgent(false); await reload(); }
|
|
276
|
+
} }, "Create"), el("button", { class: "sm ghost", onclick: () => { $("newAgentBtn").textContent = "+ New"; renderNewAgent(false); } }, "Cancel"), msg),
|
|
277
|
+
));
|
|
278
|
+
host.append(box);
|
|
270
279
|
}
|
|
271
280
|
|
|
272
|
-
// ──
|
|
273
|
-
function
|
|
274
|
-
const host = $("
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
const dmsg = el("div", { class: "msg" });
|
|
290
|
-
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));
|
|
291
|
-
host.append(dc);
|
|
292
|
-
|
|
293
|
-
// Channels card
|
|
294
|
-
state.fleet.channels = state.fleet.channels || (state.fleet.channel ? [state.fleet.channel] : []);
|
|
295
|
-
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")));
|
|
296
|
-
state.fleet.channels.forEach((ch, i) => {
|
|
297
|
-
ch.access = ch.access || { mode: "locked", allowed_users: [] };
|
|
298
|
-
ch.access.allowed_users = ch.access.allowed_users || [];
|
|
299
|
-
const card = el("div", { class: "card", style: "background:var(--bg)" });
|
|
300
|
-
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")));
|
|
301
|
-
const gg = el("div", { class: "grid2" });
|
|
302
|
-
gg.append(el("div", {}, el("label", {}, "Type"), select(ch.type || "discord", CH_TYPES, v => { ch.type = v; renderTab("fleet"); })),
|
|
303
|
-
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(); } })));
|
|
304
|
-
card.append(gg);
|
|
305
|
-
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(); } }));
|
|
306
|
-
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(); } }));
|
|
307
|
-
card.append(el("label", {}, "access.allowed_users"), chipList(ch.access.allowed_users, sync, "user id"));
|
|
308
|
-
cc.append(card);
|
|
281
|
+
// ── Bots ──
|
|
282
|
+
function renderBots() {
|
|
283
|
+
const host = $("bots"); host.innerHTML = ""; const chs = channels();
|
|
284
|
+
if (!chs.length) { host.className = "card empty"; host.textContent = "No bots configured."; return; }
|
|
285
|
+
host.className = "card";
|
|
286
|
+
chs.forEach((ch, i) => {
|
|
287
|
+
const item = el("div", { class: "item" });
|
|
288
|
+
const type = ch.type || "?";
|
|
289
|
+
item.append(el("div", { class: "item-row" },
|
|
290
|
+
el("span", { class: "name" }, type === "telegram" ? "Telegram" : "Discord"),
|
|
291
|
+
el("span", { class: "tag" + (i === 0 ? "" : " persona") + (type === "telegram" ? " tg" : "") }, chLabel(i)),
|
|
292
|
+
el("span", { class: "sub", style: "margin:0" }, ch.bot_token_env || "(no token env)"),
|
|
293
|
+
el("span", { class: "spacer" }),
|
|
294
|
+
el("span", { style: "color:var(--green);font-size:13px" }, state.fleetUp ? "✅ Online" : "—"),
|
|
295
|
+
el("button", { class: "sm danger", onclick: () => delBot(i) }, "🗑️"),
|
|
296
|
+
));
|
|
297
|
+
host.append(item);
|
|
309
298
|
});
|
|
310
|
-
const cmsg = el("div", { class: "msg" });
|
|
311
|
-
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));
|
|
312
|
-
host.append(cc);
|
|
313
299
|
}
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
const
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
const name = addName.value.trim(); if (!name) { addMsg.innerHTML = `<span class="e">✗ name required</span>`; return; }
|
|
326
|
-
const res = await api(`/api/settings/fleet/instances/${encodeURIComponent(name)}`, { method: "POST", body: JSON.stringify({ working_directory: "" }) });
|
|
327
|
-
renderResult(addMsg, res); if (res.ok) { addName.value = ""; reload(); }
|
|
328
|
-
} }, "+ Add"), addMsg));
|
|
329
|
-
const tbl = el("table"); tbl.append(el("thead", {}, el("tr", {}, ...["Name", "Backend", "Channel", "General"].map(h => el("th", {}, h)))));
|
|
330
|
-
const tb = el("tbody");
|
|
331
|
-
for (const name of Object.keys(insts)) {
|
|
332
|
-
const inst = insts[name];
|
|
333
|
-
const row = el("tr", { class: "inst-row", onclick: () => { expanded = expanded === name ? null : name; renderInstancesLeft(); } },
|
|
334
|
-
el("td", {}, name), el("td", {}, inst.backend || "—"), el("td", {}, inst.channel_id || "—"), el("td", {}, inst.general_topic ? "✓" : ""));
|
|
335
|
-
tb.append(row);
|
|
336
|
-
if (expanded === name) tb.append(instEditRow(name, inst));
|
|
337
|
-
}
|
|
338
|
-
tbl.append(tb); card.append(tbl); host.append(card);
|
|
300
|
+
async function saveChannels(msgEl) {
|
|
301
|
+
const res = await api("/api/settings/fleet/channels", { method: "PUT", body: JSON.stringify(channels()) });
|
|
302
|
+
if (msgEl) resultMsg(msgEl, res);
|
|
303
|
+
return res;
|
|
304
|
+
}
|
|
305
|
+
async function delBot(i) {
|
|
306
|
+
const chs = channels(); const ch = chs[i];
|
|
307
|
+
if (!confirm(`Remove bot "${ch.type} (${chLabel(i)})"? (the token in .env is left untouched)`)) return;
|
|
308
|
+
chs.splice(i, 1);
|
|
309
|
+
state.fleet.channels = chs; delete state.fleet.channel;
|
|
310
|
+
const res = await saveChannels(); if (res.ok) await reload(); else alert((res.body && res.body.error) || "Failed");
|
|
339
311
|
}
|
|
340
|
-
function
|
|
341
|
-
const
|
|
342
|
-
const
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
const msg = el("
|
|
348
|
-
|
|
349
|
-
el("
|
|
350
|
-
el("
|
|
351
|
-
|
|
312
|
+
function renderNewBot(open) {
|
|
313
|
+
const host = $("newBot"); host.innerHTML = ""; if (!open) return;
|
|
314
|
+
const box = el("div", { class: "card", style: "margin-bottom:12px" });
|
|
315
|
+
const fType = select("discord", CH_TYPES);
|
|
316
|
+
const fId = el("input", { type: "text", placeholder: "adapter id (e.g. persona1)" });
|
|
317
|
+
const fEnv = el("input", { type: "text", placeholder: "BOT_TOKEN_ENV" });
|
|
318
|
+
const fGroup = el("input", { type: "text", placeholder: "guild / supergroup id" });
|
|
319
|
+
const msg = el("span", { class: "msg" });
|
|
320
|
+
box.append(el("div", { class: "edit", style: "border:none" },
|
|
321
|
+
el("div", { class: "grid2" }, el("div", {}, el("label", {}, "Type"), fType), el("div", {}, el("label", {}, "Adapter id"), fId)),
|
|
322
|
+
el("div", { class: "grid2" }, el("div", {}, el("label", {}, "Bot token env var"), fEnv), el("div", {}, el("label", {}, "Group / guild id"), fGroup)),
|
|
323
|
+
el("div", { class: "hint" }, "Put the actual token in ~/.agend/.env under this env var, then Reload Fleet."),
|
|
324
|
+
el("div", { class: "actions" }, el("button", { class: "primary sm", onclick: async () => {
|
|
325
|
+
if (!fEnv.value.trim()) { msg.innerHTML = `<span class="e">✗ token env required</span>`; return; }
|
|
326
|
+
const chs = channels();
|
|
327
|
+
chs.push({ id: fId.value.trim() || undefined, type: fType.value, bot_token_env: fEnv.value.trim(), group_id: fGroup.value.trim() || undefined, access: { mode: "locked", allowed_users: [] } });
|
|
328
|
+
state.fleet.channels = chs; delete state.fleet.channel;
|
|
329
|
+
const res = await saveChannels(msg); if (res.ok) { $("newBotBtn").textContent = "+ Add"; renderNewBot(false); await reload(); }
|
|
330
|
+
} }, "Save"), el("button", { class: "sm ghost", onclick: () => { $("newBotBtn").textContent = "+ Add"; renderNewBot(false); } }, "Cancel"), msg),
|
|
331
|
+
));
|
|
332
|
+
host.append(box);
|
|
352
333
|
}
|
|
353
334
|
|
|
354
|
-
// ──
|
|
355
|
-
function
|
|
356
|
-
const host = $("
|
|
335
|
+
// ── General Settings ──
|
|
336
|
+
function renderGeneral() {
|
|
337
|
+
const host = $("general"); host.innerHTML = "";
|
|
338
|
+
state.fleet.defaults = state.fleet.defaults || {};
|
|
357
339
|
state.classic.defaults = state.classic.defaults || {};
|
|
358
|
-
const d = state.classic.defaults;
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
const
|
|
362
|
-
|
|
363
|
-
card.append(el("label", {}, "Admin users"), chipList(d.admin_users, sync, "user id"));
|
|
364
|
-
card.append(el("label", {}, "Allowed guilds"), chipList(d.allowed_guilds, sync, "guild id"));
|
|
365
|
-
const msg = el("div", { class: "msg" });
|
|
366
|
-
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));
|
|
367
|
-
host.append(card);
|
|
368
|
-
}
|
|
340
|
+
const d = state.fleet.defaults, c = state.classic.defaults;
|
|
341
|
+
const primary = channels()[0];
|
|
342
|
+
if (primary) primary.access = primary.access || { mode: "locked", allowed_users: [] };
|
|
343
|
+
const allowedUsers = primary ? (primary.access.allowed_users = primary.access.allowed_users || []) : [];
|
|
344
|
+
c.admin_users = c.admin_users || []; c.allowed_guilds = c.allowed_guilds || [];
|
|
369
345
|
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
346
|
+
const fBackend = select(d.backend || "claude-code", BACKENDS);
|
|
347
|
+
const fModel = el("input", { type: "text", value: d.model || "", placeholder: "e.g. claude-opus-4-8" });
|
|
348
|
+
const msg = el("span", { class: "msg" });
|
|
349
|
+
host.append(
|
|
350
|
+
el("div", { class: "field" }, el("label", {}, "Default backend"), fBackend),
|
|
351
|
+
el("div", { class: "field" }, el("label", {}, "Default model"), fModel),
|
|
352
|
+
el("div", { class: "field" }, el("label", {}, "Allowed users (primary bot)"), chipList(allowedUsers, null, "user id")),
|
|
353
|
+
el("div", { class: "field" }, el("label", {}, "ClassicBot admins"), chipList(c.admin_users, null, "user id")),
|
|
354
|
+
el("div", { class: "field" }, el("label", {}, "Allowed guilds"), chipList(c.allowed_guilds, null, "guild id")),
|
|
355
|
+
el("div", { class: "field" }, el("div", { class: "actions" }, el("button", { class: "primary", onclick: async () => {
|
|
356
|
+
d.backend = fBackend.value; d.model = fModel.value || undefined;
|
|
357
|
+
const r1 = await api("/api/settings/fleet/defaults", { method: "PUT", body: JSON.stringify({ backend: d.backend, model: d.model }) });
|
|
358
|
+
let r2 = { ok: true, body: {} };
|
|
359
|
+
if (primary) r2 = await saveChannels();
|
|
360
|
+
const r3 = await api("/api/settings/classic/defaults", { method: "PUT", body: JSON.stringify({ backend: c.backend, admin_users: c.admin_users, allowed_guilds: c.allowed_guilds }) });
|
|
361
|
+
const bad = [r1, r2, r3].find(r => !r.ok);
|
|
362
|
+
resultMsg(msg, bad || { ok: true, body: { warnings: [...(r1.body?.warnings || []), ...(r3.body?.warnings || [])] } });
|
|
363
|
+
} }, "Save"), msg)),
|
|
364
|
+
);
|
|
375
365
|
}
|
|
376
366
|
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
367
|
+
// ── Advanced (YAML/JSON) ──
|
|
368
|
+
const fleetModel = () => ({ ...(state.fleet.channels ? { channels: state.fleet.channels } : state.fleet.channel ? { channel: state.fleet.channel } : {}), defaults: state.fleet.defaults || {}, instances: state.fleet.instances || {} });
|
|
369
|
+
function renderAdvanced() {
|
|
370
|
+
const host = $("advBody"); host.innerHTML = "";
|
|
371
|
+
const fmt = state.advFmt; const model = fleetModel();
|
|
372
|
+
const ser = fmt === "yaml" ? toYaml(model) : JSON.stringify(model, null, 2);
|
|
373
|
+
const msg = el("div", { class: "msg", style: "margin-top:8px" });
|
|
374
|
+
const toolbar = el("div", { class: "toolbar" },
|
|
375
|
+
el("div", { class: "seg" },
|
|
376
|
+
el("button", { class: "sm" + (fmt === "yaml" ? " on" : ""), onclick: () => { state.advFmt = "yaml"; renderAdvanced(); } }, "YAML"),
|
|
377
|
+
el("button", { class: "sm" + (fmt === "json" ? " on" : ""), onclick: () => { state.advFmt = "json"; renderAdvanced(); } }, "JSON")),
|
|
378
|
+
el("button", { class: "sm", onclick: () => { state.advEdit = !state.advEdit; renderAdvanced(); } }, state.advEdit ? "View" : "Edit"),
|
|
379
|
+
el("button", { class: "sm", onclick: () => navigator.clipboard.writeText(ser).then(() => { msg.innerHTML = `<span class="s">Copied</span>`; }) }, "Copy"),
|
|
380
|
+
el("button", { class: "sm", onclick: () => { const blob = new Blob([toYaml(fleetModel())], { type: "text/plain" }); const a = el("a", { href: URL.createObjectURL(blob), download: "fleet.yaml" }); a.click(); URL.revokeObjectURL(a.href); } }, "⬇ fleet.yaml"),
|
|
381
|
+
);
|
|
382
|
+
host.append(toolbar);
|
|
383
|
+
if (state.advEdit) {
|
|
384
|
+
const ta = el("textarea"); ta.value = ser;
|
|
385
|
+
host.append(ta, el("div", { class: "actions" }, el("button", { class: "primary sm", onclick: async () => {
|
|
386
|
+
let parsed; try { parsed = fmt === "yaml" ? fromYaml(ta.value) : JSON.parse(ta.value || "{}"); } catch (e) { msg.innerHTML = `<span class="e">✗ ${esc(e.message)}</span>`; return; }
|
|
387
|
+
await applyFullModel(parsed, msg);
|
|
388
|
+
} }, "Apply & Save"), msg));
|
|
389
|
+
} else {
|
|
390
|
+
const pre = el("pre", { class: "preview" }); pre.textContent = ser; host.append(pre, msg);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
// Apply the edited full model: PUT defaults + channels, and diff instances.
|
|
394
|
+
async function applyFullModel(m, msg) {
|
|
395
|
+
if (!m || typeof m !== "object") { msg.innerHTML = `<span class="e">✗ expected a mapping</span>`; return; }
|
|
396
|
+
const results = [];
|
|
397
|
+
if (m.defaults) results.push(await api("/api/settings/fleet/defaults", { method: "PUT", body: JSON.stringify(m.defaults) }));
|
|
398
|
+
const chs = m.channels || (m.channel ? [m.channel] : null);
|
|
399
|
+
if (chs) results.push(await api("/api/settings/fleet/channels", { method: "PUT", body: JSON.stringify(chs) }));
|
|
400
|
+
if (m.instances && typeof m.instances === "object") {
|
|
401
|
+
const cur = state.fleet.instances || {};
|
|
402
|
+
for (const [n, cfg] of Object.entries(m.instances)) {
|
|
403
|
+
const exists = !!cur[n];
|
|
404
|
+
results.push(await api(`/api/settings/fleet/instances/${encodeURIComponent(n)}`, { method: exists ? "PATCH" : "POST", body: JSON.stringify(cfg) }));
|
|
405
|
+
}
|
|
406
|
+
for (const n of Object.keys(cur)) if (!(n in m.instances)) results.push(await api(`/api/settings/fleet/instances/${encodeURIComponent(n)}`, { method: "DELETE" }));
|
|
407
|
+
}
|
|
408
|
+
const bad = results.find(r => !r.ok);
|
|
409
|
+
resultMsg(msg, bad || { ok: true, body: {} });
|
|
410
|
+
if (!bad) { state.advEdit = false; await reload(); }
|
|
411
|
+
}
|
|
383
412
|
|
|
413
|
+
// ── Load / render ──
|
|
414
|
+
function renderAll() { renderAgents(); renderBots(); renderGeneral(); renderAdvanced(); }
|
|
415
|
+
async function reloadLive() {
|
|
416
|
+
const f = await api("/api/fleet");
|
|
417
|
+
state.fleetUp = f.ok;
|
|
418
|
+
state.live = {}; if (f.ok && f.body?.instances) for (const i of f.body.instances) state.live[i.name] = { status: i.status };
|
|
419
|
+
renderAgents(); renderBots();
|
|
420
|
+
}
|
|
384
421
|
async function reload() {
|
|
385
422
|
const f = await api("/api/settings/fleet");
|
|
386
|
-
if (!f.ok) { $("banner").style.display = "block"; $("banner").textContent = "Failed to load
|
|
423
|
+
if (!f.ok) { $("banner").style.display = "block"; $("banner").style.background = "#fef2f2"; $("banner").style.color = "var(--red)"; $("banner").textContent = "Failed to load config — check the token in the URL."; return; }
|
|
387
424
|
state.fleet = f.body || {};
|
|
388
425
|
const c = await api("/api/settings/classic"); state.classic = c.body || {};
|
|
389
|
-
|
|
426
|
+
await reloadLive();
|
|
427
|
+
renderAll();
|
|
390
428
|
}
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
};
|
|
429
|
+
|
|
430
|
+
$("agentSearch").oninput = () => renderAgents();
|
|
431
|
+
$("newAgentBtn").onclick = () => { const open = $("newAgentBtn").textContent === "+ New"; $("newAgentBtn").textContent = open ? "Cancel" : "+ New"; renderNewAgent(open); };
|
|
432
|
+
$("newBotBtn").onclick = () => { const open = $("newBotBtn").textContent === "+ Add"; $("newBotBtn").textContent = open ? "Cancel" : "+ Add"; renderNewBot(open); };
|
|
433
|
+
$("advToggle").onclick = () => { const b = $("advBody"); const open = !b.classList.contains("open"); b.classList.toggle("open", open); $("advToggle").textContent = open ? "▼ Collapse" : "▶ Expand"; };
|
|
434
|
+
$("reloadBtn").onclick = async () => { const r = await api("/api/settings/reload", { method: "POST" }); const b = $("banner"); b.style.display = "block"; b.style.background = "#ecfdf5"; b.style.color = "var(--green)"; b.textContent = r.ok ? "✓ Reload signal sent — instances reconciling." : "Reload failed."; setTimeout(() => b.style.display = "none", 4000); setTimeout(reloadLive, 1500); };
|
|
396
435
|
|
|
397
436
|
reload();
|
|
398
437
|
})();
|