@songsid/agend 2.0.11-beta.30 → 2.0.11-beta.32

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.
@@ -72,35 +72,70 @@
72
72
  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; }
73
73
  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; }
74
74
  .banner { display: none; padding: 10px 24px; font-size: 13px; background: #ecfdf5; color: var(--green); border-bottom: 1px solid var(--border); }
75
+ .help-btn { width: 34px; height: 34px; border-radius: 50%; border: 1px solid var(--border); background: #f3f4f6; cursor: pointer; font-size: 16px; display: inline-flex; align-items: center; justify-content: center; padding: 0; }
76
+ .help-btn:hover { background: var(--accent); border-color: var(--accent); }
77
+ .help-backdrop { position: fixed; inset: 0; background: rgba(0,0,0,.3); display: none; z-index: 40; }
78
+ .help-backdrop.open { display: block; }
79
+ .help-panel { position: fixed; top: 0; right: 0; height: 100%; width: 380px; max-width: 90vw; background: var(--bg); border-left: 1px solid var(--border); box-shadow: -8px 0 24px rgba(0,0,0,.12); transform: translateX(100%); transition: transform .2s ease; z-index: 50; overflow-y: auto; padding: 20px 22px; }
80
+ .help-panel.open { transform: translateX(0); }
81
+ .help-panel h3 { font-size: 16px; margin-bottom: 8px; }
82
+ .help-panel h4 { font-size: 14px; margin: 16px 0 6px; color: var(--accent); }
83
+ .help-panel ul { margin: 0 0 8px 18px; }
84
+ .help-panel li { font-size: 13px; margin: 3px 0; }
85
+ .help-panel .close { position: absolute; top: 14px; right: 16px; cursor: pointer; color: var(--muted); font-size: 22px; line-height: 1; background: none; border: none; }
86
+ .help-panel .tip { margin-top: 16px; padding: 10px 12px; background: var(--card); border: 1px solid var(--border); border-radius: var(--radius-sm); font-size: 13px; }
75
87
  </style>
76
88
  </head>
77
89
  <body>
78
90
  <header>
79
- <h1>⚙️ AgEnD Settings</h1>
91
+ <h1 data-i18n="title">⚙️ AgEnD Settings</h1>
92
+ <span id="verBadge" title="What's New" style="font-size:12px;color:var(--muted);cursor:pointer;padding:2px 8px;border:1px solid var(--border);border-radius:12px">v…</span>
80
93
  <div class="grow"></div>
81
- <button id="reloadBtn" class="primary" title="Apply saved config to running instances (SIGHUP)">↻ Reload Fleet</button>
94
+ <button class="help-btn" id="langBtn" title="Language">🌐</button>
95
+ <button class="help-btn" id="helpBtn" title="Help">ℹ️</button>
96
+ <button id="reloadBtn" class="primary" data-i18n="reloadFleet" title="Apply saved config to running instances (SIGHUP)">↻ Reload Fleet</button>
82
97
  </header>
98
+ <div class="help-backdrop" id="helpBackdrop"></div>
99
+ <aside class="help-panel" id="helpPanel">
100
+ <button class="close" id="helpClose" title="Close">×</button>
101
+ <div id="helpContent"></div>
102
+ </aside>
83
103
  <div class="banner" id="banner"></div>
84
104
  <main>
85
105
  <section>
86
- <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>
106
+ <div class="sec-head"><h2 data-i18n="myAgents">🤖 My Agents</h2><div class="grow"></div><input type="text" id="agentSearch" data-i18n-ph="searchPh" placeholder="🔍 Search…" style="width:180px;margin-right:8px"><button class="sm" id="newAgentBtn" data-i18n="new">+ New</button></div>
87
107
  <div id="newAgent"></div>
88
108
  <div class="card" id="agents"></div>
89
109
  </section>
90
110
 
111
+ <section id="classicSection">
112
+ <div class="sec-head"><h2 data-i18n="classicChannels">🎮 ClassicBot Channels</h2></div>
113
+ <div class="card" id="classicList"></div>
114
+ </section>
115
+
91
116
  <section>
92
- <div class="sec-head"><h2>🔌 Bots</h2><div class="grow"></div><button class="sm" id="newBotBtn">+ Add</button></div>
117
+ <div class="sec-head"><h2 data-i18n="bots">🔌 Bots</h2><div class="grow"></div><button class="sm" id="newBotBtn" data-i18n="add">+ Add</button></div>
93
118
  <div id="newBot"></div>
94
119
  <div class="card" id="bots"></div>
95
120
  </section>
96
121
 
97
122
  <section>
98
- <div class="sec-head"><h2>⚙️ General Settings</h2></div>
123
+ <div class="sec-head"><h2 data-i18n="general">⚙️ General Settings</h2></div>
99
124
  <div class="card" id="general"></div>
100
125
  </section>
101
126
 
102
127
  <section>
103
- <div class="sec-head"><h2>📋 Advanced</h2><div class="grow"></div><button class="sm" id="advToggle">▶ Expand</button></div>
128
+ <div class="sec-head"><h2 data-i18n="autoUpdate">🔄 Auto Update</h2></div>
129
+ <div class="card" id="autoUpdate"></div>
130
+ </section>
131
+
132
+ <section>
133
+ <div class="sec-head"><h2 data-i18n="whatsNew">🆕 What's New</h2><div class="grow"></div><button class="sm" id="wnToggle" data-i18n="expand">▶ Expand</button></div>
134
+ <div class="card"><div class="adv-body" id="wnBody"></div></div>
135
+ </section>
136
+
137
+ <section>
138
+ <div class="sec-head"><h2 data-i18n="advanced">📋 Advanced</h2><div class="grow"></div><button class="sm" id="advToggle" data-i18n="expand">▶ Expand</button></div>
104
139
  <div class="card"><div class="adv-body" id="advBody"></div></div>
105
140
  </section>
106
141
  </main>
@@ -112,6 +147,83 @@
112
147
  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; };
113
148
  const esc = (s) => String(s).replace(/[&<>]/g, c => ({ "&":"&amp;","<":"&lt;",">":"&gt;" }[c]));
114
149
  const BACKENDS = ["claude-code", "kiro-cli", "codex", "opencode", "gemini-cli", "antigravity"];
150
+
151
+ // ── i18n ──
152
+ const I18N = {
153
+ en: {
154
+ title: "⚙️ AgEnD Settings", reloadFleet: "↻ Reload Fleet", myAgents: "🤖 My Agents", bots: "🔌 Bots", classicChannels: "🎮 ClassicBot Channels", noClassic: "No ClassicBot channels — start one with /start in a chat.",
155
+ general: "⚙️ General Settings", autoUpdate: "🔄 Auto Update", whatsNew: "🆕 What's New", advanced: "📋 Advanced",
156
+ expand: "▶ Expand", collapse: "▼ Collapse", new: "+ New", add: "+ Add", searchPh: "🔍 Search…",
157
+ edit: "Edit", close: "Close", save: "Save", cancel: "Cancel", create: "Create", start: "Start", stop: "Stop",
158
+ backend: "Backend", model: "Model", workingDir: "Working directory", channelBinding: "Channel binding",
159
+ description: "Description", systemPrompt: "System prompt", tags: "Tags", generalTopicField: "General topic (fleet dispatcher)",
160
+ language: "Language (bot replies)", defaultBackend: "Default backend", defaultModel: "Default model", allowedUsers: "Allowed users (primary bot)",
161
+ classicAdmins: "ClassicBot admins", allowedGuilds: "Allowed guilds", type: "Type", adapterId: "Adapter id",
162
+ tokenEnv: "Bot token env var", groupId: "Group / guild id", nameField: "Name",
163
+ envHint: "Put the actual token in ~/.agend/.env under this env var, then Reload Fleet.",
164
+ noAgents: "No agents yet — click + New.", noBots: "No bots configured.", online: "✅ Online",
165
+ enable: "Enabled", schedule: "Schedule", channel: "Channel", daily: "Daily", weekly: "Weekly", manual: "Manual (off)",
166
+ beta: "Beta", stable: "Stable", currentVersion: "Current version", lastChecked: "Last checked", never: "never",
167
+ helpTitle: "📖 Help", tipReload: "💡 Click [↻ Reload Fleet] after changes to apply them.",
168
+ wnIntro: "Recent highlights:",
169
+ },
170
+ "zh-TW": {
171
+ title: "⚙️ AgEnD 設定", reloadFleet: "↻ 重新載入", myAgents: "🤖 我的 Agent", bots: "🔌 機器人", classicChannels: "🎮 ClassicBot 頻道", noClassic: "沒有 ClassicBot 頻道 — 在聊天中用 /start 建立。",
172
+ general: "⚙️ 一般設定", autoUpdate: "🔄 自動更新", whatsNew: "🆕 更新重點", advanced: "📋 進階",
173
+ expand: "▶ 展開", collapse: "▼ 收合", new: "+ 新增", add: "+ 加入", searchPh: "🔍 搜尋…",
174
+ edit: "編輯", close: "關閉", save: "儲存", cancel: "取消", create: "建立", start: "啟動", stop: "停止",
175
+ backend: "後端", model: "模型", workingDir: "工作目錄", channelBinding: "頻道綁定",
176
+ description: "描述", systemPrompt: "System prompt", tags: "標籤", generalTopicField: "General topic(fleet 分派器)",
177
+ language: "語言(bot 回覆)", defaultBackend: "預設後端", defaultModel: "預設模型", allowedUsers: "允許使用者(主 bot)",
178
+ classicAdmins: "ClassicBot 管理員", allowedGuilds: "允許的伺服器", type: "類型", adapterId: "Adapter id",
179
+ tokenEnv: "Bot token 環境變數", groupId: "群組 / guild id", nameField: "名稱",
180
+ envHint: "把實際 token 放到 ~/.agend/.env 的這個環境變數下,然後點重新載入。",
181
+ noAgents: "還沒有 agent — 點 + 新增。", noBots: "尚未設定機器人。", online: "✅ 上線",
182
+ enable: "啟用", schedule: "排程", channel: "頻道", daily: "每日", weekly: "每週", manual: "手動(關閉)",
183
+ beta: "Beta", stable: "穩定版", currentVersion: "目前版本", lastChecked: "上次檢查", never: "從未",
184
+ helpTitle: "📖 使用說明", tipReload: "💡 修改後點 [↻ 重新載入] 讓設定生效。",
185
+ wnIntro: "近期更新重點:",
186
+ },
187
+ };
188
+ const HELP = {
189
+ en: [["🤖 My Agents", ["[+ New] add an agent (name, backend, working directory)", "[Edit] change settings (backend, model, tags, systemPrompt)", "[Start/Stop] run or stop an agent", "🗑️ delete an agent (shows on hover, needs confirm)", "🔍 search — live-filter the list"]],
190
+ ["🔌 Bots", ["[+ Add] add a Discord/TG bot", "🗑️ remove a bot"]],
191
+ ["⚙️ General Settings", ["Edit global backend/model, allowed users, ClassicBot admins", "Click [Save]"]],
192
+ ["🔄 Auto Update", ["Enable a scheduled `agend update` (daily/weekly)", "Pick beta or stable channel"]],
193
+ ["📋 Advanced", ["Edit YAML/JSON directly", "[⬇ fleet.yaml] download a backup"]]],
194
+ "zh-TW": [["🤖 我的 Agent", ["[+ 新增] 新增 agent(name、backend、working directory)", "[編輯] 修改設定(backend、model、tags、systemPrompt)", "[啟動/停止] 啟動或停止 agent", "🗑️ 刪除 agent(hover 顯示,需確認)", "🔍 搜尋 — 即時過濾列表"]],
195
+ ["🔌 機器人", ["[+ 加入] 加入 Discord/TG bot", "🗑️ 移除 bot"]],
196
+ ["⚙️ 一般設定", ["修改全域 backend/model、允許使用者、ClassicBot 管理員", "點 [儲存]"]],
197
+ ["🔄 自動更新", ["啟用排程 `agend update`(每日/每週)", "選 beta 或穩定版頻道"]],
198
+ ["📋 進階", ["直接編輯 YAML/JSON", "[⬇ fleet.yaml] 下載備份"]]],
199
+ };
200
+ const WHATSNEW = [
201
+ "Settings redesign — user-flow sections, white theme, i18n (EN / 繁中)",
202
+ "Auto Update scheduler + What's New + info help panel",
203
+ "Same-channel multi-bot ClassicBot; per-bot cancel-button + react fixes",
204
+ "/view avatar & ctx% fixes; config validator + `agend validate` + validate_config MCP tool",
205
+ ];
206
+ let lang = localStorage.getItem("agend_lang") || (navigator.language && navigator.language.toLowerCase().startsWith("zh") ? "zh-TW" : "en");
207
+ if (!I18N[lang]) lang = "en";
208
+ const t = (k) => (I18N[lang] && I18N[lang][k]) || I18N.en[k] || k;
209
+ function applyStaticI18n() {
210
+ document.querySelectorAll("[data-i18n]").forEach(e => { e.textContent = t(e.getAttribute("data-i18n")); });
211
+ document.querySelectorAll("[data-i18n-ph]").forEach(e => { e.setAttribute("placeholder", t(e.getAttribute("data-i18n-ph"))); });
212
+ // toggle labels that carry expand/collapse state
213
+ for (const [id, body] of [["advToggle", "advBody"], ["wnToggle", "wnBody"]]) {
214
+ const b = $(body); if (b) $(id).textContent = b.classList.contains("open") ? t("collapse") : t("expand");
215
+ }
216
+ renderHelp();
217
+ }
218
+ function renderHelp() {
219
+ const host = $("helpContent"); if (!host) return; host.innerHTML = "";
220
+ host.append(el("h3", {}, t("helpTitle")));
221
+ for (const [h, items] of (HELP[lang] || HELP.en)) {
222
+ host.append(el("h4", {}, h));
223
+ const ul = el("ul"); for (const it of items) ul.append(el("li", {}, it)); host.append(ul);
224
+ }
225
+ host.append(el("div", { class: "tip" }, t("tipReload")));
226
+ }
115
227
  const CH_TYPES = ["discord", "telegram"];
116
228
 
117
229
  // ── YAML (config subset; round-trip verified) ──
@@ -150,7 +262,7 @@
150
262
  }
151
263
 
152
264
  // ── State ──
153
- const state = { fleet: {}, classic: {}, live: {}, fleetUp: false, advFmt: "yaml", advEdit: false, collapsedGroups: new Set() };
265
+ const state = { fleet: {}, classic: {}, classicStatus: {}, live: {}, fleetUp: false, advFmt: "yaml", advEdit: false, collapsedGroups: new Set(), updateSchedule: null, version: "", newAgentOpen: false, newBotOpen: false };
154
266
  const channels = () => state.fleet.channels || (state.fleet.channel ? [state.fleet.channel] : []);
155
267
  const channelIds = () => channels().map(c => c.id || c.type).filter(Boolean);
156
268
  const chLabel = (i) => i === 0 ? "primary" : "persona";
@@ -195,9 +307,9 @@
195
307
  el("span", { class: "sub", style: "margin:0" }, inst.backend || state.fleet.defaults?.backend || "claude-code"),
196
308
  chTag ? el("span", { class: "tag" + (chTag.includes("persona") ? " persona" : chTag.includes("telegram") ? " tg" : "") }, chTag) : null,
197
309
  el("span", { class: "spacer" }),
198
- el("button", { class: "sm ghost", onclick: () => { editing = editing === name ? null : name; renderAgents(); } }, editing === name ? "Close" : "Edit"),
199
- el("button", { class: "sm", onclick: () => toggleAgent(name, running) }, running ? "Stop" : "Start"),
200
- el("button", { class: "sm danger del-btn", title: "Delete", onclick: () => delAgent(name) }, "🗑️"),
310
+ el("button", { class: "sm ghost", onclick: () => { editing = editing === name ? null : name; renderAgents(); } }, editing === name ? t("close") : t("edit")),
311
+ el("button", { class: "sm", onclick: () => toggleAgent(name, running) }, running ? t("stop") : t("start")),
312
+ el("button", { class: "sm danger del-btn", title: t("close"), onclick: () => delAgent(name) }, "🗑️"),
201
313
  ));
202
314
  if (editing === name) item.append(agentEditForm(name, inst));
203
315
  return item;
@@ -205,7 +317,7 @@
205
317
  function renderAgents() {
206
318
  const host = $("agents"); host.innerHTML = "";
207
319
  const insts = state.fleet.instances || {};
208
- if (!Object.keys(insts).length) { host.className = "card empty"; host.textContent = "No agents yet — click + New."; return; }
320
+ if (!Object.keys(insts).length) { host.className = "card empty"; host.textContent = t("noAgents"); return; }
209
321
  const filter = ($("agentSearch")?.value || "").trim().toLowerCase();
210
322
  let names = Object.keys(insts);
211
323
  if (filter) names = names.filter(n => n.toLowerCase().includes(filter) || shortName(n, insts[n]).toLowerCase().includes(filter));
@@ -241,12 +353,12 @@
241
353
  const msg = el("span", { class: "msg" });
242
354
  box.append(
243
355
  el("div", { class: "sub", style: "margin:0 0 8px" }, "ID: " + name),
244
- el("div", { class: "grid2" }, el("div", {}, el("label", {}, "Backend"), fBackend), el("div", {}, el("label", {}, "Model"), fModel)),
245
- el("label", {}, "Working directory"), fWd,
246
- el("div", { class: "grid2" }, el("div", {}, el("label", {}, "Channel binding"), fChan), el("div", {}, el("label", {}, "Description"), fDesc)),
247
- el("label", {}, "System prompt"), fSys,
248
- el("label", {}, "Tags"), chipList(tags, null, "tag"),
249
- el("label", { style: "display:flex;align-items:center;gap:8px;cursor:pointer" }, fGeneral, el("span", {}, "General topic (fleet dispatcher)")),
356
+ el("div", { class: "grid2" }, el("div", {}, el("label", {}, t("backend")), fBackend), el("div", {}, el("label", {}, t("model")), fModel)),
357
+ el("label", {}, t("workingDir")), fWd,
358
+ el("div", { class: "grid2" }, el("div", {}, el("label", {}, t("channelBinding")), fChan), el("div", {}, el("label", {}, t("description")), fDesc)),
359
+ el("label", {}, t("systemPrompt")), fSys,
360
+ el("label", {}, t("tags")), chipList(tags, null, "tag"),
361
+ el("label", { style: "display:flex;align-items:center;gap:8px;cursor:pointer" }, fGeneral, el("span", {}, t("generalTopicField"))),
250
362
  el("div", { class: "actions" },
251
363
  el("button", { class: "primary sm", onclick: async () => {
252
364
  const patch = {
@@ -257,8 +369,8 @@
257
369
  };
258
370
  const res = await api(`/api/settings/fleet/instances/${encodeURIComponent(name)}`, { method: "PATCH", body: JSON.stringify(patch) });
259
371
  resultMsg(msg, res); if (res.ok) await reload();
260
- } }, "Save"),
261
- el("button", { class: "sm ghost", onclick: () => { editing = null; renderAgents(); } }, "Cancel"),
372
+ } }, t("save")),
373
+ el("button", { class: "sm ghost", onclick: () => { editing = null; renderAgents(); } }, t("cancel")),
262
374
  msg),
263
375
  );
264
376
  return box;
@@ -283,14 +395,14 @@
283
395
  const fChan = select("", ["", ...channelIds()]);
284
396
  const msg = el("span", { class: "msg" });
285
397
  box.append(el("div", { class: "edit", style: "border:none" },
286
- el("div", { class: "grid2" }, el("div", {}, el("label", {}, "Name"), fName), el("div", {}, el("label", {}, "Backend"), fBackend)),
287
- el("div", { class: "grid2" }, el("div", {}, el("label", {}, "Working directory"), fWd), el("div", {}, el("label", {}, "Channel binding"), fChan)),
398
+ el("div", { class: "grid2" }, el("div", {}, el("label", {}, t("nameField")), fName), el("div", {}, el("label", {}, t("backend")), fBackend)),
399
+ el("div", { class: "grid2" }, el("div", {}, el("label", {}, t("workingDir")), fWd), el("div", {}, el("label", {}, t("channelBinding")), fChan)),
288
400
  el("div", { class: "actions" }, el("button", { class: "primary sm", onclick: async () => {
289
401
  const name = fName.value.trim(); if (!name) { msg.innerHTML = `<span class="e">✗ name required</span>`; return; }
290
402
  const body = { working_directory: fWd.value, backend: fBackend.value, channel_id: fChan.value || undefined };
291
403
  const res = await api(`/api/settings/fleet/instances/${encodeURIComponent(name)}`, { method: "POST", body: JSON.stringify(body) });
292
- resultMsg(msg, res); if (res.ok) { $("newAgentBtn").textContent = "+ New"; renderNewAgent(false); await reload(); }
293
- } }, "Create"), el("button", { class: "sm ghost", onclick: () => { $("newAgentBtn").textContent = "+ New"; renderNewAgent(false); } }, "Cancel"), msg),
404
+ resultMsg(msg, res); if (res.ok) { state.newAgentOpen = false; $("newAgentBtn").textContent = t("new"); renderNewAgent(false); await reload(); }
405
+ } }, t("create")), el("button", { class: "sm ghost", onclick: () => { state.newAgentOpen = false; $("newAgentBtn").textContent = t("new"); renderNewAgent(false); } }, t("cancel")), msg),
294
406
  ));
295
407
  host.append(box);
296
408
  }
@@ -298,7 +410,7 @@
298
410
  // ── Bots ──
299
411
  function renderBots() {
300
412
  const host = $("bots"); host.innerHTML = ""; const chs = channels();
301
- if (!chs.length) { host.className = "card empty"; host.textContent = "No bots configured."; return; }
413
+ if (!chs.length) { host.className = "card empty"; host.textContent = t("noBots"); return; }
302
414
  host.className = "card";
303
415
  chs.forEach((ch, i) => {
304
416
  const item = el("div", { class: "item" });
@@ -308,12 +420,40 @@
308
420
  el("span", { class: "tag" + (i === 0 ? "" : " persona") + (type === "telegram" ? " tg" : "") }, chLabel(i)),
309
421
  el("span", { class: "sub", style: "margin:0" }, ch.bot_token_env || "(no token env)"),
310
422
  el("span", { class: "spacer" }),
311
- el("span", { style: "color:var(--green);font-size:13px" }, state.fleetUp ? "✅ Online" : "—"),
423
+ el("span", { style: "color:var(--green);font-size:13px" }, state.fleetUp ? t("online") : "—"),
312
424
  el("button", { class: "sm danger", onclick: () => delBot(i) }, "🗑️"),
313
425
  ));
314
426
  host.append(item);
315
427
  });
316
428
  }
429
+
430
+ // ── ClassicBot Channels (read-only; created dynamically via /start /stop) ──
431
+ function renderClassic() {
432
+ const host = $("classicList"); if (!host) return; host.innerHTML = "";
433
+ const chans = Object.values(state.classic?.channels || {});
434
+ if (!chans.length) { host.className = "card empty"; host.textContent = t("noClassic"); return; }
435
+ const filter = ($("agentSearch")?.value || "").trim().toLowerCase();
436
+ let list = chans;
437
+ if (filter) list = list.filter(c => String(c.name || "").toLowerCase().includes(filter) || String(c.instanceName || "").toLowerCase().includes(filter));
438
+ if (!list.length) { host.className = "card empty"; host.textContent = "—"; return; }
439
+ host.className = "card";
440
+ list.sort((a, b) => {
441
+ const sa = STATUS_ORDER[state.classicStatus[a.instanceName] || "stopped"], sb = STATUS_ORDER[state.classicStatus[b.instanceName] || "stopped"];
442
+ return sa !== sb ? sa - sb : String(a.name || "").localeCompare(String(b.name || ""));
443
+ });
444
+ for (const c of list) {
445
+ const st = state.classicStatus[c.instanceName] || "stopped";
446
+ const adapter = c.adapterId || "discord";
447
+ host.append(el("div", { class: "item" }, el("div", { class: "item-row" },
448
+ el("span", { class: "dot " + st, title: STATUS_TEXT[st] || st }),
449
+ el("span", { class: "name", title: c.instanceName || "" }, c.name || c.instanceName || "?"),
450
+ el("span", { class: "sub", style: "margin:0" }, c.backend || state.classic?.defaults?.backend || "claude-code"),
451
+ el("span", { class: "tag" + (adapter !== "discord" ? " persona" : "") }, adapter),
452
+ c.collab ? el("span", { class: "tag", style: "background:#ecfdf5;color:#065f46" }, "collab") : null,
453
+ )));
454
+ }
455
+ }
456
+
317
457
  async function saveChannels(msgEl) {
318
458
  const res = await api("/api/settings/fleet/channels", { method: "PUT", body: JSON.stringify(channels()) });
319
459
  if (msgEl) resultMsg(msgEl, res);
@@ -335,16 +475,16 @@
335
475
  const fGroup = el("input", { type: "text", placeholder: "guild / supergroup id" });
336
476
  const msg = el("span", { class: "msg" });
337
477
  box.append(el("div", { class: "edit", style: "border:none" },
338
- el("div", { class: "grid2" }, el("div", {}, el("label", {}, "Type"), fType), el("div", {}, el("label", {}, "Adapter id"), fId)),
339
- el("div", { class: "grid2" }, el("div", {}, el("label", {}, "Bot token env var"), fEnv), el("div", {}, el("label", {}, "Group / guild id"), fGroup)),
340
- el("div", { class: "hint" }, "Put the actual token in ~/.agend/.env under this env var, then Reload Fleet."),
478
+ el("div", { class: "grid2" }, el("div", {}, el("label", {}, t("type")), fType), el("div", {}, el("label", {}, t("adapterId")), fId)),
479
+ el("div", { class: "grid2" }, el("div", {}, el("label", {}, t("tokenEnv")), fEnv), el("div", {}, el("label", {}, t("groupId")), fGroup)),
480
+ el("div", { class: "hint" }, t("envHint")),
341
481
  el("div", { class: "actions" }, el("button", { class: "primary sm", onclick: async () => {
342
482
  if (!fEnv.value.trim()) { msg.innerHTML = `<span class="e">✗ token env required</span>`; return; }
343
483
  const chs = channels();
344
484
  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: [] } });
345
485
  state.fleet.channels = chs; delete state.fleet.channel;
346
- const res = await saveChannels(msg); if (res.ok) { $("newBotBtn").textContent = "+ Add"; renderNewBot(false); await reload(); }
347
- } }, "Save"), el("button", { class: "sm ghost", onclick: () => { $("newBotBtn").textContent = "+ Add"; renderNewBot(false); } }, "Cancel"), msg),
486
+ const res = await saveChannels(msg); if (res.ok) { state.newBotOpen = false; $("newBotBtn").textContent = t("add"); renderNewBot(false); await reload(); }
487
+ } }, t("save")), el("button", { class: "sm ghost", onclick: () => { state.newBotOpen = false; $("newBotBtn").textContent = t("add"); renderNewBot(false); } }, t("cancel")), msg),
348
488
  ));
349
489
  host.append(box);
350
490
  }
@@ -362,25 +502,65 @@
362
502
 
363
503
  const fBackend = select(d.backend || "claude-code", BACKENDS);
364
504
  const fModel = el("input", { type: "text", value: d.model || "", placeholder: "e.g. claude-opus-4-8" });
505
+ const fLocale = el("select");
506
+ [["en", "English"], ["zh-TW", "繁體中文 (zh-TW)"]].forEach(([v, lbl]) => { const o = el("option", {}, lbl); o.value = v; if ((d.locale || "") === v) o.selected = true; fLocale.append(o); });
365
507
  const msg = el("span", { class: "msg" });
366
508
  host.append(
367
- el("div", { class: "field" }, el("label", {}, "Default backend"), fBackend),
368
- el("div", { class: "field" }, el("label", {}, "Default model"), fModel),
369
- el("div", { class: "field" }, el("label", {}, "Allowed users (primary bot)"), chipList(allowedUsers, null, "user id")),
370
- el("div", { class: "field" }, el("label", {}, "ClassicBot admins"), chipList(c.admin_users, null, "user id")),
371
- el("div", { class: "field" }, el("label", {}, "Allowed guilds"), chipList(c.allowed_guilds, null, "guild id")),
509
+ el("div", { class: "field" }, el("label", {}, t("language")), fLocale),
510
+ el("div", { class: "field" }, el("label", {}, t("defaultBackend")), fBackend),
511
+ el("div", { class: "field" }, el("label", {}, t("defaultModel")), fModel),
512
+ el("div", { class: "field" }, el("label", {}, t("allowedUsers")), chipList(allowedUsers, null, "user id")),
513
+ el("div", { class: "field" }, el("label", {}, t("classicAdmins")), chipList(c.admin_users, null, "user id")),
514
+ el("div", { class: "field" }, el("label", {}, t("allowedGuilds")), chipList(c.allowed_guilds, null, "guild id")),
372
515
  el("div", { class: "field" }, el("div", { class: "actions" }, el("button", { class: "primary", onclick: async () => {
373
- d.backend = fBackend.value; d.model = fModel.value || undefined;
374
- const r1 = await api("/api/settings/fleet/defaults", { method: "PUT", body: JSON.stringify({ backend: d.backend, model: d.model }) });
516
+ d.backend = fBackend.value; d.model = fModel.value || undefined; d.locale = fLocale.value;
517
+ const r1 = await api("/api/settings/fleet/defaults", { method: "PUT", body: JSON.stringify({ backend: d.backend, model: d.model, locale: d.locale }) });
375
518
  let r2 = { ok: true, body: {} };
376
519
  if (primary) r2 = await saveChannels();
377
520
  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 }) });
378
521
  const bad = [r1, r2, r3].find(r => !r.ok);
379
522
  resultMsg(msg, bad || { ok: true, body: { warnings: [...(r1.body?.warnings || []), ...(r3.body?.warnings || [])] } });
380
- } }, "Save"), msg)),
523
+ } }, t("save")), msg)),
524
+ );
525
+ }
526
+
527
+ // ── Auto Update ──
528
+ function renderAutoUpdate() {
529
+ const host = $("autoUpdate"); host.innerHTML = "";
530
+ const u = state.updateSchedule || { enabled: false, cron: "0 4 * * *", channel: "stable" };
531
+ const CRON = { daily: "0 4 * * *", weekly: "0 4 * * 1" };
532
+ const fEnable = el("input", { type: "checkbox" }); fEnable.checked = !!u.enabled;
533
+ const schedVal = u.cron === CRON.weekly ? "weekly" : (u.enabled ? "daily" : "manual");
534
+ const fSched = select(schedVal, ["daily", "weekly", "manual"]); // labels localized below
535
+ [...fSched.children].forEach(o => { o._txt = ""; o.textContent = t(o.value === "manual" ? "manual" : o.value); });
536
+ const fChannel = select(u.channel === "beta" ? "beta" : "stable", ["stable", "beta"]);
537
+ [...fChannel.children].forEach(o => o.textContent = t(o.value));
538
+ const msg = el("span", { class: "msg" });
539
+ host.append(
540
+ el("div", { class: "field" }, el("label", { style: "display:flex;align-items:center;gap:8px;cursor:pointer" }, fEnable, el("span", {}, t("enable")))),
541
+ el("div", { class: "field" }, el("label", {}, t("schedule")), fSched),
542
+ el("div", { class: "field" }, el("label", {}, t("channel")), fChannel),
543
+ el("div", { class: "field" }, el("div", { class: "sub", style: "margin:0" }, `${t("currentVersion")}: ${state.version || "?"} · ${t("lastChecked")}: ${u.lastChecked ? new Date(u.lastChecked).toLocaleString() : t("never")}`)),
544
+ el("div", { class: "field" }, el("div", { class: "actions" }, el("button", { class: "primary", onclick: async () => {
545
+ const sched = fSched.value;
546
+ const enabled = fEnable.checked && sched !== "manual";
547
+ const cron = sched === "weekly" ? CRON.weekly : CRON.daily;
548
+ const res = await api("/api/settings/update-schedule", { method: "PUT", body: JSON.stringify({ enabled, cron, channel: fChannel.value }) });
549
+ resultMsg(msg, res); if (res.ok) { state.updateSchedule = { enabled, cron, channel: fChannel.value, lastChecked: u.lastChecked }; }
550
+ } }, t("save")), msg)),
381
551
  );
382
552
  }
383
553
 
554
+ // ── What's New ──
555
+ function renderWhatsNew() {
556
+ const host = $("wnBody"); host.innerHTML = "";
557
+ host.append(el("div", { class: "sub", style: "margin:0 0 8px" }, `${t("currentVersion")}: ${state.version || "?"}`));
558
+ host.append(el("div", { style: "font-size:13px;margin-bottom:6px" }, t("wnIntro")));
559
+ const ul = el("ul", { style: "margin:0 0 0 18px" });
560
+ for (const line of WHATSNEW) ul.append(el("li", { style: "font-size:13px;margin:4px 0" }, line));
561
+ host.append(ul);
562
+ }
563
+
384
564
  // ── Advanced (YAML/JSON) ──
385
565
  const fleetModel = () => ({ ...(state.fleet.channels ? { channels: state.fleet.channels } : state.fleet.channel ? { channel: state.fleet.channel } : {}), defaults: state.fleet.defaults || {}, instances: state.fleet.instances || {} });
386
566
  function renderAdvanced() {
@@ -428,26 +608,38 @@
428
608
  }
429
609
 
430
610
  // ── Load / render ──
431
- function renderAll() { renderAgents(); renderBots(); renderGeneral(); renderAdvanced(); }
611
+ function renderAll() { renderAgents(); renderClassic(); renderBots(); renderGeneral(); renderAutoUpdate(); renderWhatsNew(); renderAdvanced(); applyStaticI18n(); }
432
612
  async function reloadLive() {
433
613
  const f = await api("/api/fleet");
434
614
  state.fleetUp = f.ok;
435
615
  state.live = {}; if (f.ok && f.body?.instances) for (const i of f.body.instances) state.live[i.name] = { status: i.status };
436
- renderAgents(); renderBots();
616
+ // /api/fleet is fleet-config only; classic instance statuses come from the
617
+ // merged roster (/api/profiles, open GET).
618
+ const pr = await api("/api/profiles");
619
+ state.classicStatus = {}; if (pr.ok && Array.isArray(pr.body)) for (const r of pr.body) state.classicStatus[r.instance_name] = r.status;
620
+ renderAgents(); renderBots(); renderClassic();
437
621
  }
438
622
  async function reload() {
439
623
  const f = await api("/api/settings/fleet");
440
624
  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; }
441
625
  state.fleet = f.body || {};
442
626
  const c = await api("/api/settings/classic"); state.classic = c.body || {};
627
+ const u = await api("/api/settings/update-schedule"); if (u.ok) { state.updateSchedule = u.body; state.version = u.body.currentVersion || ""; $("verBadge").textContent = "v" + (state.version || "?"); }
443
628
  await reloadLive();
444
629
  renderAll();
445
630
  }
446
631
 
447
- $("agentSearch").oninput = () => renderAgents();
448
- $("newAgentBtn").onclick = () => { const open = $("newAgentBtn").textContent === "+ New"; $("newAgentBtn").textContent = open ? "Cancel" : "+ New"; renderNewAgent(open); };
449
- $("newBotBtn").onclick = () => { const open = $("newBotBtn").textContent === "+ Add"; $("newBotBtn").textContent = open ? "Cancel" : "+ Add"; renderNewBot(open); };
450
- $("advToggle").onclick = () => { const b = $("advBody"); const open = !b.classList.contains("open"); b.classList.toggle("open", open); $("advToggle").textContent = open ? "▼ Collapse" : "▶ Expand"; };
632
+ const setHelp = (open) => { $("helpPanel").classList.toggle("open", open); $("helpBackdrop").classList.toggle("open", open); };
633
+ $("helpBtn").onclick = () => setHelp(!$("helpPanel").classList.contains("open"));
634
+ $("helpClose").onclick = () => setHelp(false);
635
+ $("helpBackdrop").onclick = () => setHelp(false);
636
+ $("langBtn").onclick = () => { lang = lang === "en" ? "zh-TW" : "en"; localStorage.setItem("agend_lang", lang); renderAll(); };
637
+ $("verBadge").onclick = () => { const b = $("wnBody"); b.classList.add("open"); $("wnToggle").textContent = t("collapse"); b.scrollIntoView({ behavior: "smooth" }); };
638
+ $("agentSearch").oninput = () => { renderAgents(); renderClassic(); };
639
+ $("newAgentBtn").onclick = () => { state.newAgentOpen = !state.newAgentOpen; $("newAgentBtn").textContent = state.newAgentOpen ? t("cancel") : t("new"); renderNewAgent(state.newAgentOpen); };
640
+ $("newBotBtn").onclick = () => { state.newBotOpen = !state.newBotOpen; $("newBotBtn").textContent = state.newBotOpen ? t("cancel") : t("add"); renderNewBot(state.newBotOpen); };
641
+ $("advToggle").onclick = () => { const b = $("advBody"); const open = !b.classList.contains("open"); b.classList.toggle("open", open); $("advToggle").textContent = open ? t("collapse") : t("expand"); };
642
+ $("wnToggle").onclick = () => { const b = $("wnBody"); const open = !b.classList.contains("open"); b.classList.toggle("open", open); $("wnToggle").textContent = open ? t("collapse") : t("expand"); };
451
643
  $("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); };
452
644
 
453
645
  reload();
package/dist/ui/view.html CHANGED
@@ -58,10 +58,27 @@
58
58
  #edit .hint { color: var(--dim); font-size: 11px; }
59
59
  #msg { font-size: 12px; }
60
60
  .err { color: #f85149; } .ok { color: #3fb950; }
61
+ .help-btn { position: fixed; top: 10px; right: 14px; width: 34px; height: 34px; border-radius: 50%; border: 1px solid var(--border); background: var(--panel); color: var(--fg); cursor: pointer; font-size: 16px; z-index: 60; display: inline-flex; align-items: center; justify-content: center; padding: 0; }
62
+ .help-btn:hover { background: var(--accent); border-color: var(--accent); }
63
+ .help-backdrop { position: fixed; inset: 0; background: rgba(0,0,0,.5); display: none; z-index: 70; }
64
+ .help-backdrop.open { display: block; }
65
+ .help-panel { position: fixed; top: 0; right: 0; height: 100%; width: 360px; max-width: 90vw; background: var(--panel); border-left: 1px solid var(--border); box-shadow: -8px 0 24px rgba(0,0,0,.4); transform: translateX(100%); transition: transform .2s ease; z-index: 80; overflow-y: auto; padding: 20px 22px; }
66
+ .help-panel.open { transform: translateX(0); }
67
+ .help-panel h3 { font-size: 16px; margin-bottom: 8px; }
68
+ .help-panel ul { margin: 8px 0 0 18px; }
69
+ .help-panel li { font-size: 13px; margin: 6px 0; color: var(--fg); }
70
+ .help-panel .close { position: absolute; top: 14px; right: 16px; cursor: pointer; color: var(--dim); font-size: 22px; line-height: 1; background: none; border: none; }
61
71
  </style>
62
72
  </head>
63
73
  <body>
64
- <div id="sidebar"><h1>AgEnD · View</h1><div id="list"></div></div>
74
+ <button class="help-btn" id="langBtn" title="Language" style="right:56px">🌐</button>
75
+ <button class="help-btn" id="helpBtn" title="Help">ℹ️</button>
76
+ <div class="help-backdrop" id="helpBackdrop"></div>
77
+ <aside class="help-panel" id="helpPanel">
78
+ <button class="close" id="helpClose" title="Close">×</button>
79
+ <div id="helpContent"></div>
80
+ </aside>
81
+ <div id="sidebar"><h1 data-i18n="viewTitle">AgEnD · View</h1><div id="list"></div></div>
65
82
  <div id="main">
66
83
  <div id="term"><pre id="pre"></pre></div>
67
84
  <div id="card">
@@ -71,19 +88,19 @@
71
88
  <div class="meta" id="meta"></div>
72
89
  <div class="desc" id="desc"></div>
73
90
  </div>
74
- <button id="editBtn">✏️ Edit</button>
91
+ <button id="editBtn" data-i18n="editProfile">✏️ Edit</button>
75
92
  <div id="edit">
76
- <input id="fDisplay" placeholder="Display name">
77
- <input id="fRole" placeholder="Role">
78
- <textarea id="fDesc" placeholder="Description"></textarea>
93
+ <input id="fDisplay" data-i18n-ph="displayName" placeholder="Display name">
94
+ <input id="fRole" data-i18n-ph="role" placeholder="Role">
95
+ <textarea id="fDesc" data-i18n-ph="description" placeholder="Description"></textarea>
79
96
  <input id="fAvatar" type="file" accept="image/png,image/jpeg,image/gif,image/webp">
80
- <input id="fToken" type="password" placeholder="web.token (required to save)">
97
+ <input id="fToken" type="password" data-i18n-ph="tokenPh" placeholder="web.token (required to save)">
81
98
  <div class="actions">
82
- <button id="saveBtn">Save</button>
83
- <button id="cancelBtn">Cancel</button>
99
+ <button id="saveBtn" data-i18n="save">Save</button>
100
+ <button id="cancelBtn" data-i18n="cancel">Cancel</button>
84
101
  <span id="msg"></span>
85
102
  </div>
86
- <div class="hint">Viewing is open (read-only). Saving requires the web.token (~/.agend/web.token).</div>
103
+ <div class="hint" data-i18n="saveHint">Viewing is open (read-only). Saving requires the web.token (~/.agend/web.token).</div>
87
104
  </div>
88
105
  </div>
89
106
  </div>
@@ -96,6 +113,33 @@
96
113
  const urlToken = new URLSearchParams(location.search).get("token") || "";
97
114
  const webToken = () => localStorage.getItem("agend_web_token") || urlToken;
98
115
  const q = (id) => document.getElementById(id);
116
+
117
+ // ── i18n ──
118
+ const I18N = {
119
+ en: { viewTitle: "AgEnD · View", editProfile: "✏️ Edit", save: "Save", cancel: "Cancel",
120
+ displayName: "Display name", role: "Role", description: "Description", tokenPh: "web.token (required to save)",
121
+ saveHint: "Viewing is open (read-only). Saving requires the web.token (~/.agend/web.token).", helpTitle: "📖 Help",
122
+ help1: "Pick an instance on the left → its live terminal shows on the right",
123
+ help2: "Click a group header ▼/▶ to collapse/expand", help3: "✏️ Edit changes display name, role, description",
124
+ help4: "Drag to reorder (needs web.token permission)" },
125
+ "zh-TW": { viewTitle: "AgEnD · 檢視", editProfile: "✏️ 編輯", save: "儲存", cancel: "取消",
126
+ displayName: "顯示名稱", role: "角色", description: "描述", tokenPh: "web.token(儲存時需要)",
127
+ saveHint: "檢視為公開(唯讀)。儲存需要 web.token(~/.agend/web.token)。", helpTitle: "📖 使用說明",
128
+ help1: "左側選 instance → 右側顯示即時 terminal 畫面",
129
+ help2: "點 group header ▼/▶ 收合/展開", help3: "✏️ Edit 可修改顯示名稱、角色、描述",
130
+ help4: "拖曳可調整順序(需 web.token 權限)" },
131
+ };
132
+ let lang = localStorage.getItem("agend_lang") || (navigator.language && navigator.language.toLowerCase().startsWith("zh") ? "zh-TW" : "en");
133
+ if (!I18N[lang]) lang = "en";
134
+ const T = (k) => (I18N[lang] && I18N[lang][k]) || I18N.en[k] || k;
135
+ function applyI18n() {
136
+ document.querySelectorAll("[data-i18n]").forEach(e => { e.textContent = T(e.getAttribute("data-i18n")); });
137
+ document.querySelectorAll("[data-i18n-ph]").forEach(e => { e.setAttribute("placeholder", T(e.getAttribute("data-i18n-ph"))); });
138
+ const hc = q("helpContent");
139
+ if (hc) { hc.innerHTML = ""; const h3 = document.createElement("h3"); h3.textContent = T("helpTitle"); hc.append(h3);
140
+ const ul = document.createElement("ul"); for (const k of ["help1","help2","help3","help4"]) { const li = document.createElement("li"); li.textContent = T(k); ul.append(li); } hc.append(ul); }
141
+ }
142
+
99
143
  let roster = [];
100
144
  let rosterByName = new Map();
101
145
  let current = null;
@@ -467,6 +511,13 @@
467
511
  q("cancelBtn").onclick = exitEdit;
468
512
  q("saveBtn").onclick = save;
469
513
 
514
+ const setHelp = (open) => { q("helpPanel").classList.toggle("open", open); q("helpBackdrop").classList.toggle("open", open); };
515
+ q("helpBtn").onclick = () => setHelp(!q("helpPanel").classList.contains("open"));
516
+ q("helpClose").onclick = () => setHelp(false);
517
+ q("helpBackdrop").onclick = () => setHelp(false);
518
+ q("langBtn").onclick = () => { lang = lang === "en" ? "zh-TW" : "en"; localStorage.setItem("agend_lang", lang); applyI18n(); };
519
+ applyI18n();
520
+
470
521
  // ── Boot ──────────────────────────────────────────────
471
522
  loadRoster().then(() => { refreshPane(); });
472
523
  poll = setInterval(refreshPane, 800);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@songsid/agend",
3
- "version": "2.0.11-beta.30",
3
+ "version": "2.0.11-beta.32",
4
4
  "description": "Multi-agent fleet daemon — run any coding CLI (Claude, Gemini, Codex, OpenCode) from Telegram",
5
5
  "type": "module",
6
6
  "bin": {