@songsid/agend 2.0.11-beta.27 → 2.0.11-beta.28

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.
Files changed (2) hide show
  1. package/dist/ui/settings.html +328 -319
  2. package/package.json +1 -1
@@ -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 { --bg:#f6f8fa; --panel:#ffffff; --border:#d0d7de; --fg:#1f2328; --dim:#656d76; --accent:#0969da; --err:#cf222e; --warn:#9a6700; --ok:#1a7f37; }
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); }
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 { background: #f6f8fa; color: var(--fg); border: 1px solid var(--border); border-radius: 6px; padding: 6px 12px; font-size: 13px; cursor: pointer; }
19
- button:hover { background: #eaeef2; border-color: #afb8c1; }
20
- button.primary { background: var(--accent); color: #ffffff; border-color: var(--accent); font-weight: 600; }
21
- button.primary:hover { background: #0860ca; border-color: #0860ca; }
22
- button.sm { padding: 3px 8px; font-size: 12px; }
23
- button.danger { background: var(--err); color: #ffffff; border-color: var(--err); }
24
- button.danger:hover { background: #a40e26; border-color: #a40e26; }
25
- main { max-width: 1200px; margin: 0 auto; padding: 18px; }
26
- .panel { display: none; } .panel.active { display: block; }
27
- .split { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; align-items: start; }
28
- @media (max-width: 900px) { .split { grid-template-columns: 1fr; } }
29
- .col { min-width: 0; }
30
- .card { background: var(--panel); border: 1px solid var(--border); border-radius: 8px; padding: 14px; margin-bottom: 14px; }
31
- .card h2 { font-size: 13px; color: var(--accent); margin-bottom: 10px; display:flex; align-items:center; gap:8px; }
32
- .card h2 .grow { flex:1; }
33
- label { display: block; font-size: 12px; color: var(--dim); margin: 8px 0 3px; }
34
- 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; }
35
- textarea { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12px; line-height: 1.5; resize: vertical; min-height: 420px; }
36
- .chips { display: flex; flex-wrap: wrap; gap: 6px; margin: 4px 0; }
37
- .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; }
38
- .chip span.x { cursor: pointer; color: var(--dim); font-weight: 700; } .chip span.x:hover { color: var(--err); }
39
- .chip-add { display: flex; gap: 6px; margin-top: 4px; } .chip-add input { flex: 1; }
40
- table { width: 100%; border-collapse: collapse; font-size: 13px; }
41
- th, td { text-align: left; padding: 7px 8px; border-bottom: 1px solid var(--border); }
42
- th { color: var(--dim); font-size: 11px; text-transform: uppercase; letter-spacing: .3px; }
43
- tr.inst-row { cursor: pointer; } tr.inst-row:hover td { background: rgba(0,0,0,.03); }
44
- .edit-row td { background: rgba(9,105,218,.06); }
45
- .grid2 { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
46
- .right-toolbar { display: flex; gap: 6px; align-items: center; margin-bottom: 8px; flex-wrap: wrap; }
47
- .right-toolbar .seg { display: flex; border: 1px solid var(--border); border-radius: 6px; overflow: hidden; }
48
- .right-toolbar .seg button { border: none; border-radius: 0; }
49
- .right-toolbar .seg button.on { background: var(--accent); color: #ffffff; }
50
- 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; }
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>⚙ AgEnD Settings</h1>
60
- <div class="tabs">
61
- <div class="tab active" data-tab="fleet">Fleet</div>
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 class="panel active" id="panel-fleet"><div class="split">
71
- <div class="col" id="fleet-left"></div>
72
- <div class="col" id="fleet-right"></div>
73
- </div></section>
74
- <section class="panel" id="panel-instances"><div class="split">
75
- <div class="col" id="instances-left"></div>
76
- <div class="col" id="instances-right"></div>
77
- </div></section>
78
- <section class="panel" id="panel-classic"><div class="split">
79
- <div class="col" id="classic-left"></div>
80
- <div class="col" id="classic-right"></div>
81
- </div></section>
82
- </main>
79
+ <section>
80
+ <div class="sec-head"><h2>🤖 My Agents</h2><div class="grow"></div><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 => ({ "&":"&amp;","<":"&lt;",">":"&gt;" }[c]));
91
108
  const BACKENDS = ["claude-code", "kiro-cli", "codex", "opencode", "gemini-cli", "antigravity"];
92
- const CH_TYPES = ["telegram", "discord"];
109
+ const CH_TYPES = ["discord", "telegram"];
93
110
 
94
- // ── YAML dump/parse (config subset; round-trip verified) ──
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
- if (v === null || v === undefined) return "null";
99
- if (typeof v === "boolean" || typeof v === "number") return String(v);
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 parseBlock() {
149
- if (pos >= lines.length) return null;
150
- const first = lines[pos];
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,274 @@
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 renderResult(msgEl, res) {
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 ? esc(e.path) + ": " : ""}${esc(e.message)}</span>`).join("\n");
189
- 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");
190
- if (res.ok) html = `<span class="s">✓ Saved</span>` + (html ? "\n" + 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
- msgEl.innerHTML = html;
143
+ elm.innerHTML = html;
193
144
  }
194
145
 
195
146
  // ── State ──
196
- const state = { fleet: {}, classic: {}, snap: "{}", tab: "fleet",
197
- right: { fleet: { fmt: "yaml", edit: false }, instances: { fmt: "yaml", edit: false }, classic: { fmt: "yaml", edit: false } } };
198
-
199
- // The object each tab's right-pane mirrors.
200
- function modelOf(tab) {
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, onChange) {
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
- // ── Right pane (shared) ──
224
- function renderRight(tab) {
225
- const host = $(tab + "-right"); host.innerHTML = "";
226
- const rs = state.right[tab]; const model = modelOf(tab);
227
- const msg = el("div", { class: "msg" });
228
- const seg = el("div", { class: "seg" },
229
- el("button", { class: "sm" + (rs.fmt === "yaml" ? " on" : ""), onclick: () => { rs.fmt = "yaml"; renderRight(tab); } }, "YAML"),
230
- el("button", { class: "sm" + (rs.fmt === "json" ? " on" : ""), onclick: () => { rs.fmt = "json"; renderRight(tab); } }, "JSON"));
231
- const editBtn = el("button", { class: "sm", onclick: () => { rs.edit = !rs.edit; renderRight(tab); } }, rs.edit ? "View" : "Edit");
232
- const copyBtn = el("button", { class: "sm", onclick: () => { navigator.clipboard.writeText(serialize(model, rs.fmt)).then(() => { msg.innerHTML = `<span class="s">Copied</span>`; }); } }, "Copy");
233
- const dlBtn = el("button", { class: "sm", onclick: () => download(tab, rs.fmt) }, "Download");
234
- const toolbar = el("div", { class: "right-toolbar" }, seg, editBtn, copyBtn, dlBtn);
235
- if (tab === "fleet") toolbar.append(el("button", { class: "sm", onclick: () => download("full", "yaml") }, "⬇ full fleet.yaml"));
236
- host.append(el("div", { class: "card" }, el("h2", {}, "Config " + (rs.edit ? "(editing)" : "(preview)")), toolbar, (() => {
237
- if (rs.edit) {
238
- const ta = el("textarea"); ta.value = serialize(model, rs.fmt);
239
- const apply = el("button", { class: "primary sm", onclick: () => {
240
- let parsed; try { parsed = deserialize(ta.value, rs.fmt); } catch (e) { msg.innerHTML = `<span class="e">✗ ${esc(e.message)}</span>`; return; }
241
- applyModel(tab, parsed); rs.edit = false; renderTab(tab); msg.innerHTML = `<span class="s">Applied to form (not yet saved)</span>`;
242
- } }, "Apply → form");
243
- const box = el("div"); box.append(ta, el("div", { class: "row-actions" }, apply, msg)); return box;
244
- }
245
- const pre = el("pre", { class: "preview" }); pre.textContent = serialize(model, rs.fmt);
246
- const box = el("div"); box.append(pre, msg); return box;
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;
248
172
  }
249
- // Parse from the right pane back into left-form state.
250
- function applyModel(tab, parsed) {
251
- if (!parsed || typeof parsed !== "object") return;
252
- if (tab === "fleet") { if (parsed.defaults) state.fleet.defaults = parsed.defaults; if (parsed.channels) { state.fleet.channels = parsed.channels; delete state.fleet.channel; } }
253
- else if (tab === "instances") { state.fleet.instances = parsed; }
254
- else { if (parsed.defaults) state.classic.defaults = parsed.defaults; }
173
+ function renderAgents() {
174
+ const host = $("agents"); host.innerHTML = "";
175
+ const insts = state.fleet.instances || {};
176
+ const names = Object.keys(insts);
177
+ if (!names.length) { host.className = "card empty"; host.textContent = "No agents yet — click + New."; return; }
178
+ host.className = "card";
179
+ for (const name of names) {
180
+ const inst = insts[name];
181
+ const st = state.live[name]?.status || "stopped";
182
+ const running = st === "running";
183
+ const item = el("div", { class: "item" });
184
+ const chTag = agentChannelLabel(inst);
185
+ item.append(el("div", { class: "item-row" },
186
+ el("span", { class: "dot " + st }),
187
+ el("span", { class: "name" }, name),
188
+ chTag ? el("span", { class: "tag" + (chTag.includes("persona") ? " persona" : chTag.includes("telegram") ? " tg" : "") }, chTag) : null,
189
+ el("span", { class: "sub", style: "margin:0" }, inst.backend || state.fleet.defaults?.backend || "claude-code"),
190
+ el("span", { class: "spacer" }),
191
+ el("button", { class: "sm ghost", onclick: () => { editing = editing === name ? null : name; renderAgents(); } }, editing === name ? "Close" : "Edit"),
192
+ el("button", { class: "sm", onclick: () => toggleAgent(name, running) }, running ? "Stop" : "Start"),
193
+ el("button", { class: "sm danger", onclick: () => delAgent(name) }, "🗑️"),
194
+ ));
195
+ if (inst.working_directory) item.append(el("div", { class: "sub" }, inst.working_directory));
196
+ if (editing === name) item.append(agentEditForm(name, inst));
197
+ host.append(item);
198
+ }
255
199
  }
256
- function download(which, fmt) {
257
- let name, content;
258
- if (which === "full") { name = "fleet.yaml"; content = toYaml(fleetForDownload()); }
259
- else if (which === "fleet") { name = "fleet-section." + fmt; content = serialize(modelOf("fleet"), fmt); }
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);
200
+ function agentEditForm(name, inst) {
201
+ const box = el("div", { class: "edit" });
202
+ const fBackend = select(inst.backend || state.fleet.defaults?.backend || "claude-code", BACKENDS);
203
+ const fModel = el("input", { type: "text", value: inst.model || "", placeholder: "(inherit default)" });
204
+ const fWd = el("input", { type: "text", value: inst.working_directory || "" });
205
+ const fChan = select(inst.channel_id || "", ["", ...channelIds()]);
206
+ const fDesc = el("input", { type: "text", value: inst.description || "" });
207
+ const msg = el("span", { class: "msg" });
208
+ box.append(
209
+ el("div", { class: "grid2" }, el("div", {}, el("label", {}, "Backend"), fBackend), el("div", {}, el("label", {}, "Model"), fModel)),
210
+ el("label", {}, "Working directory"), fWd,
211
+ el("div", { class: "grid2" }, el("div", {}, el("label", {}, "Channel binding"), fChan), el("div", {}, el("label", {}, "Description"), fDesc)),
212
+ el("div", { class: "actions" }, el("button", { class: "primary sm", onclick: async () => {
213
+ const patch = { backend: fBackend.value, model: fModel.value || undefined, working_directory: fWd.value, channel_id: fChan.value || undefined, description: fDesc.value || undefined };
214
+ const res = await api(`/api/settings/fleet/instances/${encodeURIComponent(name)}`, { method: "PATCH", body: JSON.stringify(patch) });
215
+ resultMsg(msg, res); if (res.ok) await reload();
216
+ } }, "Save"), msg),
217
+ );
218
+ return box;
263
219
  }
264
- function fleetForDownload() {
265
- const f = {};
266
- if (state.fleet.channels || state.fleet.channel) f.channels = state.fleet.channels || [state.fleet.channel];
267
- if (state.fleet.defaults) f.defaults = state.fleet.defaults;
268
- if (state.fleet.instances) f.instances = state.fleet.instances;
269
- return f;
220
+ async function toggleAgent(name, running) {
221
+ if (running) await api(`/stop/${encodeURIComponent(name)}`, { method: "POST" });
222
+ else await api(`/api/instance/${encodeURIComponent(name)}/start`, { method: "POST" });
223
+ setTimeout(reloadLive, 800);
224
+ }
225
+ async function delAgent(name) {
226
+ if (!confirm(`Delete agent "${name}"? (config removed; running daemon stops on next Reload)`)) return;
227
+ const res = await api(`/api/settings/fleet/instances/${encodeURIComponent(name)}`, { method: "DELETE" });
228
+ if (res.ok) await reload(); else alert((res.body && res.body.error) || "Delete failed");
229
+ }
230
+ function renderNewAgent(open) {
231
+ const host = $("newAgent"); host.innerHTML = "";
232
+ if (!open) return;
233
+ const box = el("div", { class: "card", style: "margin-bottom:12px" });
234
+ const fName = el("input", { type: "text", placeholder: "agent name" });
235
+ const fBackend = select(state.fleet.defaults?.backend || "claude-code", BACKENDS);
236
+ const fWd = el("input", { type: "text", placeholder: "~/Projects/… (optional)" });
237
+ const fChan = select("", ["", ...channelIds()]);
238
+ const msg = el("span", { class: "msg" });
239
+ box.append(el("div", { class: "edit", style: "border:none" },
240
+ el("div", { class: "grid2" }, el("div", {}, el("label", {}, "Name"), fName), el("div", {}, el("label", {}, "Backend"), fBackend)),
241
+ el("div", { class: "grid2" }, el("div", {}, el("label", {}, "Working directory"), fWd), el("div", {}, el("label", {}, "Channel binding"), fChan)),
242
+ el("div", { class: "actions" }, el("button", { class: "primary sm", onclick: async () => {
243
+ const name = fName.value.trim(); if (!name) { msg.innerHTML = `<span class="e">✗ name required</span>`; return; }
244
+ const body = { working_directory: fWd.value, backend: fBackend.value, channel_id: fChan.value || undefined };
245
+ const res = await api(`/api/settings/fleet/instances/${encodeURIComponent(name)}`, { method: "POST", body: JSON.stringify(body) });
246
+ resultMsg(msg, res); if (res.ok) { $("newAgentBtn").textContent = "+ New"; renderNewAgent(false); await reload(); }
247
+ } }, "Create"), el("button", { class: "sm ghost", onclick: () => { $("newAgentBtn").textContent = "+ New"; renderNewAgent(false); } }, "Cancel"), msg),
248
+ ));
249
+ host.append(box);
270
250
  }
271
251
 
272
- // ── Fleet left ──
273
- function renderFleetLeft() {
274
- const host = $("fleet-left"); host.innerHTML = "";
275
- state.fleet.defaults = state.fleet.defaults || {};
276
- const d = state.fleet.defaults; d.startup = d.startup || {};
277
- const sync = () => renderRight("fleet");
278
-
279
- // Defaults card
280
- const dc = el("div", { class: "card" }, el("h2", {}, "Fleet Defaults"));
281
- dc.append(el("label", {}, "Backend"), select(d.backend || "claude-code", BACKENDS, v => { d.backend = v; sync(); }));
282
- const model = el("input", { type: "text", value: d.model || "", placeholder: "e.g. opus, sonnet", oninput: e => { d.model = e.target.value || undefined; sync(); } });
283
- dc.append(el("label", {}, "Model"), model);
284
- const g2 = el("div", { class: "grid2" });
285
- 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(); } });
286
- 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(); } });
287
- g2.append(el("div", {}, el("label", {}, "Startup concurrency"), conc), el("div", {}, el("label", {}, "Stagger delay (ms)"), stag));
288
- dc.append(g2);
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);
252
+ // ── Bots ──
253
+ function renderBots() {
254
+ const host = $("bots"); host.innerHTML = ""; const chs = channels();
255
+ if (!chs.length) { host.className = "card empty"; host.textContent = "No bots configured."; return; }
256
+ host.className = "card";
257
+ chs.forEach((ch, i) => {
258
+ const item = el("div", { class: "item" });
259
+ const type = ch.type || "?";
260
+ item.append(el("div", { class: "item-row" },
261
+ el("span", { class: "name" }, type === "telegram" ? "Telegram" : "Discord"),
262
+ el("span", { class: "tag" + (i === 0 ? "" : " persona") + (type === "telegram" ? " tg" : "") }, chLabel(i)),
263
+ el("span", { class: "sub", style: "margin:0" }, ch.bot_token_env || "(no token env)"),
264
+ el("span", { class: "spacer" }),
265
+ el("span", { style: "color:var(--green);font-size:13px" }, state.fleetUp ? " Online" : ""),
266
+ el("button", { class: "sm danger", onclick: () => delBot(i) }, "🗑️"),
267
+ ));
268
+ host.append(item);
309
269
  });
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
270
  }
314
-
315
- // ── Instances left ──
316
- let expanded = null;
317
- function renderInstancesLeft() {
318
- const host = $("instances-left"); host.innerHTML = "";
319
- state.fleet.instances = state.fleet.instances || {};
320
- const insts = state.fleet.instances;
321
- const card = el("div", { class: "card" }, el("h2", {}, el("span", { class: "grow" }, "Instances")));
322
- const addName = el("input", { type: "text", placeholder: "new instance name", style: "flex:0 0 200px" });
323
- const addMsg = el("div", { class: "msg" });
324
- card.append(el("div", { class: "row-actions" }, addName, el("button", { class: "primary sm", onclick: async () => {
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);
271
+ async function saveChannels(msgEl) {
272
+ const res = await api("/api/settings/fleet/channels", { method: "PUT", body: JSON.stringify(channels()) });
273
+ if (msgEl) resultMsg(msgEl, res);
274
+ return res;
275
+ }
276
+ async function delBot(i) {
277
+ const chs = channels(); const ch = chs[i];
278
+ if (!confirm(`Remove bot "${ch.type} (${chLabel(i)})"? (the token in .env is left untouched)`)) return;
279
+ chs.splice(i, 1);
280
+ state.fleet.channels = chs; delete state.fleet.channel;
281
+ const res = await saveChannels(); if (res.ok) await reload(); else alert((res.body && res.body.error) || "Failed");
339
282
  }
340
- function instEditRow(name, inst) {
341
- const td = el("td", { colspan: "4" });
342
- const sync = () => renderRight("instances");
343
- td.append(el("label", {}, "Backend"), select(inst.backend || "claude-code", BACKENDS, v => { inst.backend = v; sync(); }));
344
- td.append(el("label", {}, "channel_id"), select(inst.channel_id || "", ["", ...channelIds()], v => { inst.channel_id = v || undefined; sync(); }));
345
- td.append(el("label", {}, "working_directory"), el("input", { type: "text", value: inst.working_directory || "", oninput: e => { inst.working_directory = e.target.value; sync(); } }));
346
- td.append(el("label", {}, "description"), el("input", { type: "text", value: inst.description || "", oninput: e => { inst.description = e.target.value || undefined; sync(); } }));
347
- const msg = el("div", { class: "msg" });
348
- td.append(el("div", { class: "row-actions" },
349
- el("button", { class: "primary sm", onclick: async () => { renderResult(msg, await api(`/api/settings/fleet/instances/${encodeURIComponent(name)}`, { method: "PATCH", body: JSON.stringify(inst) })); } }, "Save"),
350
- 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));
351
- return el("tr", { class: "edit-row" }, td);
283
+ function renderNewBot(open) {
284
+ const host = $("newBot"); host.innerHTML = ""; if (!open) return;
285
+ const box = el("div", { class: "card", style: "margin-bottom:12px" });
286
+ const fType = select("discord", CH_TYPES);
287
+ const fId = el("input", { type: "text", placeholder: "adapter id (e.g. persona1)" });
288
+ const fEnv = el("input", { type: "text", placeholder: "BOT_TOKEN_ENV" });
289
+ const fGroup = el("input", { type: "text", placeholder: "guild / supergroup id" });
290
+ const msg = el("span", { class: "msg" });
291
+ box.append(el("div", { class: "edit", style: "border:none" },
292
+ el("div", { class: "grid2" }, el("div", {}, el("label", {}, "Type"), fType), el("div", {}, el("label", {}, "Adapter id"), fId)),
293
+ el("div", { class: "grid2" }, el("div", {}, el("label", {}, "Bot token env var"), fEnv), el("div", {}, el("label", {}, "Group / guild id"), fGroup)),
294
+ el("div", { class: "hint" }, "Put the actual token in ~/.agend/.env under this env var, then Reload Fleet."),
295
+ el("div", { class: "actions" }, el("button", { class: "primary sm", onclick: async () => {
296
+ if (!fEnv.value.trim()) { msg.innerHTML = `<span class="e">✗ token env required</span>`; return; }
297
+ const chs = channels();
298
+ 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: [] } });
299
+ state.fleet.channels = chs; delete state.fleet.channel;
300
+ const res = await saveChannels(msg); if (res.ok) { $("newBotBtn").textContent = "+ Add"; renderNewBot(false); await reload(); }
301
+ } }, "Save"), el("button", { class: "sm ghost", onclick: () => { $("newBotBtn").textContent = "+ Add"; renderNewBot(false); } }, "Cancel"), msg),
302
+ ));
303
+ host.append(box);
352
304
  }
353
305
 
354
- // ── ClassicBot left ──
355
- function renderClassicLeft() {
356
- const host = $("classic-left"); host.innerHTML = "";
306
+ // ── General Settings ──
307
+ function renderGeneral() {
308
+ const host = $("general"); host.innerHTML = "";
309
+ state.fleet.defaults = state.fleet.defaults || {};
357
310
  state.classic.defaults = state.classic.defaults || {};
358
- const d = state.classic.defaults;
359
- d.admin_users = d.admin_users || []; d.allowed_guilds = d.allowed_guilds || [];
360
- const sync = () => renderRight("classic");
361
- const card = el("div", { class: "card" }, el("h2", {}, "ClassicBot Defaults"));
362
- card.append(el("label", {}, "Default backend"), select(d.backend || "claude-code", BACKENDS, v => { d.backend = v; sync(); }));
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
- }
311
+ const d = state.fleet.defaults, c = state.classic.defaults;
312
+ const primary = channels()[0];
313
+ if (primary) primary.access = primary.access || { mode: "locked", allowed_users: [] };
314
+ const allowedUsers = primary ? (primary.access.allowed_users = primary.access.allowed_users || []) : [];
315
+ c.admin_users = c.admin_users || []; c.allowed_guilds = c.allowed_guilds || [];
369
316
 
370
- function renderTab(tab) {
371
- if (tab === "fleet") renderFleetLeft();
372
- else if (tab === "instances") renderInstancesLeft();
373
- else renderClassicLeft();
374
- renderRight(tab);
317
+ const fBackend = select(d.backend || "claude-code", BACKENDS);
318
+ const fModel = el("input", { type: "text", value: d.model || "", placeholder: "e.g. claude-opus-4-8" });
319
+ const msg = el("span", { class: "msg" });
320
+ host.append(
321
+ el("div", { class: "field" }, el("label", {}, "Default backend"), fBackend),
322
+ el("div", { class: "field" }, el("label", {}, "Default model"), fModel),
323
+ el("div", { class: "field" }, el("label", {}, "Allowed users (primary bot)"), chipList(allowedUsers, null, "user id")),
324
+ el("div", { class: "field" }, el("label", {}, "ClassicBot admins"), chipList(c.admin_users, null, "user id")),
325
+ el("div", { class: "field" }, el("label", {}, "Allowed guilds"), chipList(c.allowed_guilds, null, "guild id")),
326
+ el("div", { class: "field" }, el("div", { class: "actions" }, el("button", { class: "primary", onclick: async () => {
327
+ d.backend = fBackend.value; d.model = fModel.value || undefined;
328
+ const r1 = await api("/api/settings/fleet/defaults", { method: "PUT", body: JSON.stringify({ backend: d.backend, model: d.model }) });
329
+ let r2 = { ok: true, body: {} };
330
+ if (primary) r2 = await saveChannels();
331
+ 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 }) });
332
+ const bad = [r1, r2, r3].find(r => !r.ok);
333
+ resultMsg(msg, bad || { ok: true, body: { warnings: [...(r1.body?.warnings || []), ...(r3.body?.warnings || [])] } });
334
+ } }, "Save"), msg)),
335
+ );
375
336
  }
376
337
 
377
- document.querySelectorAll(".tab").forEach(t => t.onclick = () => {
378
- state.tab = t.dataset.tab;
379
- document.querySelectorAll(".tab").forEach(x => x.classList.toggle("active", x === t));
380
- document.querySelectorAll(".panel").forEach(p => p.classList.toggle("active", p.id === "panel-" + state.tab));
381
- renderTab(state.tab);
382
- });
338
+ // ── Advanced (YAML/JSON) ──
339
+ const fleetModel = () => ({ ...(state.fleet.channels ? { channels: state.fleet.channels } : state.fleet.channel ? { channel: state.fleet.channel } : {}), defaults: state.fleet.defaults || {}, instances: state.fleet.instances || {} });
340
+ function renderAdvanced() {
341
+ const host = $("advBody"); host.innerHTML = "";
342
+ const fmt = state.advFmt; const model = fleetModel();
343
+ const ser = fmt === "yaml" ? toYaml(model) : JSON.stringify(model, null, 2);
344
+ const msg = el("div", { class: "msg", style: "margin-top:8px" });
345
+ const toolbar = el("div", { class: "toolbar" },
346
+ el("div", { class: "seg" },
347
+ el("button", { class: "sm" + (fmt === "yaml" ? " on" : ""), onclick: () => { state.advFmt = "yaml"; renderAdvanced(); } }, "YAML"),
348
+ el("button", { class: "sm" + (fmt === "json" ? " on" : ""), onclick: () => { state.advFmt = "json"; renderAdvanced(); } }, "JSON")),
349
+ el("button", { class: "sm", onclick: () => { state.advEdit = !state.advEdit; renderAdvanced(); } }, state.advEdit ? "View" : "Edit"),
350
+ el("button", { class: "sm", onclick: () => navigator.clipboard.writeText(ser).then(() => { msg.innerHTML = `<span class="s">Copied</span>`; }) }, "Copy"),
351
+ 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"),
352
+ );
353
+ host.append(toolbar);
354
+ if (state.advEdit) {
355
+ const ta = el("textarea"); ta.value = ser;
356
+ host.append(ta, el("div", { class: "actions" }, el("button", { class: "primary sm", onclick: async () => {
357
+ 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; }
358
+ await applyFullModel(parsed, msg);
359
+ } }, "Apply & Save"), msg));
360
+ } else {
361
+ const pre = el("pre", { class: "preview" }); pre.textContent = ser; host.append(pre, msg);
362
+ }
363
+ }
364
+ // Apply the edited full model: PUT defaults + channels, and diff instances.
365
+ async function applyFullModel(m, msg) {
366
+ if (!m || typeof m !== "object") { msg.innerHTML = `<span class="e">✗ expected a mapping</span>`; return; }
367
+ const results = [];
368
+ if (m.defaults) results.push(await api("/api/settings/fleet/defaults", { method: "PUT", body: JSON.stringify(m.defaults) }));
369
+ const chs = m.channels || (m.channel ? [m.channel] : null);
370
+ if (chs) results.push(await api("/api/settings/fleet/channels", { method: "PUT", body: JSON.stringify(chs) }));
371
+ if (m.instances && typeof m.instances === "object") {
372
+ const cur = state.fleet.instances || {};
373
+ for (const [n, cfg] of Object.entries(m.instances)) {
374
+ const exists = !!cur[n];
375
+ results.push(await api(`/api/settings/fleet/instances/${encodeURIComponent(n)}`, { method: exists ? "PATCH" : "POST", body: JSON.stringify(cfg) }));
376
+ }
377
+ for (const n of Object.keys(cur)) if (!(n in m.instances)) results.push(await api(`/api/settings/fleet/instances/${encodeURIComponent(n)}`, { method: "DELETE" }));
378
+ }
379
+ const bad = results.find(r => !r.ok);
380
+ resultMsg(msg, bad || { ok: true, body: {} });
381
+ if (!bad) { state.advEdit = false; await reload(); }
382
+ }
383
383
 
384
+ // ── Load / render ──
385
+ function renderAll() { renderAgents(); renderBots(); renderGeneral(); renderAdvanced(); }
386
+ async function reloadLive() {
387
+ const f = await api("/api/fleet");
388
+ state.fleetUp = f.ok;
389
+ state.live = {}; if (f.ok && f.body?.instances) for (const i of f.body.instances) state.live[i.name] = { status: i.status };
390
+ renderAgents(); renderBots();
391
+ }
384
392
  async function reload() {
385
393
  const f = await api("/api/settings/fleet");
386
- if (!f.ok) { $("banner").style.display = "block"; $("banner").textContent = "Failed to load fleet config (check the token in the URL)."; return; }
394
+ 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
395
  state.fleet = f.body || {};
388
396
  const c = await api("/api/settings/classic"); state.classic = c.body || {};
389
- renderTab(state.tab);
397
+ await reloadLive();
398
+ renderAll();
390
399
  }
391
- $("reloadBtn").onclick = async () => {
392
- const res = await api("/api/settings/reload", { method: "POST" });
393
- $("banner").style.display = "block"; $("banner").textContent = res.ok ? "✓ Reload signal sent — instances reconciling." : "Reload failed.";
394
- setTimeout(() => $("banner").style.display = "none", 4000);
395
- };
400
+
401
+ $("newAgentBtn").onclick = () => { const open = $("newAgentBtn").textContent === "+ New"; $("newAgentBtn").textContent = open ? "Cancel" : "+ New"; renderNewAgent(open); };
402
+ $("newBotBtn").onclick = () => { const open = $("newBotBtn").textContent === "+ Add"; $("newBotBtn").textContent = open ? "Cancel" : "+ Add"; renderNewBot(open); };
403
+ $("advToggle").onclick = () => { const b = $("advBody"); const open = !b.classList.contains("open"); b.classList.toggle("open", open); $("advToggle").textContent = open ? "▼ Collapse" : "▶ Expand"; };
404
+ $("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
405
 
397
406
  reload();
398
407
  })();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@songsid/agend",
3
- "version": "2.0.11-beta.27",
3
+ "version": "2.0.11-beta.28",
4
4
  "description": "Multi-agent fleet daemon — run any coding CLI (Claude, Gemini, Codex, OpenCode) from Telegram",
5
5
  "type": "module",
6
6
  "bin": {