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

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,65 @@
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
 
91
111
  <section>
92
- <div class="sec-head"><h2>🔌 Bots</h2><div class="grow"></div><button class="sm" id="newBotBtn">+ Add</button></div>
112
+ <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
113
  <div id="newBot"></div>
94
114
  <div class="card" id="bots"></div>
95
115
  </section>
96
116
 
97
117
  <section>
98
- <div class="sec-head"><h2>⚙️ General Settings</h2></div>
118
+ <div class="sec-head"><h2 data-i18n="general">⚙️ General Settings</h2></div>
99
119
  <div class="card" id="general"></div>
100
120
  </section>
101
121
 
102
122
  <section>
103
- <div class="sec-head"><h2>📋 Advanced</h2><div class="grow"></div><button class="sm" id="advToggle">▶ Expand</button></div>
123
+ <div class="sec-head"><h2 data-i18n="autoUpdate">🔄 Auto Update</h2></div>
124
+ <div class="card" id="autoUpdate"></div>
125
+ </section>
126
+
127
+ <section>
128
+ <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>
129
+ <div class="card"><div class="adv-body" id="wnBody"></div></div>
130
+ </section>
131
+
132
+ <section>
133
+ <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
134
  <div class="card"><div class="adv-body" id="advBody"></div></div>
105
135
  </section>
106
136
  </main>
@@ -112,6 +142,83 @@
112
142
  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
143
  const esc = (s) => String(s).replace(/[&<>]/g, c => ({ "&":"&amp;","<":"&lt;",">":"&gt;" }[c]));
114
144
  const BACKENDS = ["claude-code", "kiro-cli", "codex", "opencode", "gemini-cli", "antigravity"];
145
+
146
+ // ── i18n ──
147
+ const I18N = {
148
+ en: {
149
+ title: "⚙️ AgEnD Settings", reloadFleet: "↻ Reload Fleet", myAgents: "🤖 My Agents", bots: "🔌 Bots",
150
+ general: "⚙️ General Settings", autoUpdate: "🔄 Auto Update", whatsNew: "🆕 What's New", advanced: "📋 Advanced",
151
+ expand: "▶ Expand", collapse: "▼ Collapse", new: "+ New", add: "+ Add", searchPh: "🔍 Search…",
152
+ edit: "Edit", close: "Close", save: "Save", cancel: "Cancel", create: "Create", start: "Start", stop: "Stop",
153
+ backend: "Backend", model: "Model", workingDir: "Working directory", channelBinding: "Channel binding",
154
+ description: "Description", systemPrompt: "System prompt", tags: "Tags", generalTopicField: "General topic (fleet dispatcher)",
155
+ language: "Language (bot replies)", defaultBackend: "Default backend", defaultModel: "Default model", allowedUsers: "Allowed users (primary bot)",
156
+ classicAdmins: "ClassicBot admins", allowedGuilds: "Allowed guilds", type: "Type", adapterId: "Adapter id",
157
+ tokenEnv: "Bot token env var", groupId: "Group / guild id", nameField: "Name",
158
+ envHint: "Put the actual token in ~/.agend/.env under this env var, then Reload Fleet.",
159
+ noAgents: "No agents yet — click + New.", noBots: "No bots configured.", online: "✅ Online",
160
+ enable: "Enabled", schedule: "Schedule", channel: "Channel", daily: "Daily", weekly: "Weekly", manual: "Manual (off)",
161
+ beta: "Beta", stable: "Stable", currentVersion: "Current version", lastChecked: "Last checked", never: "never",
162
+ helpTitle: "📖 Help", tipReload: "💡 Click [↻ Reload Fleet] after changes to apply them.",
163
+ wnIntro: "Recent highlights:",
164
+ },
165
+ "zh-TW": {
166
+ title: "⚙️ AgEnD 設定", reloadFleet: "↻ 重新載入", myAgents: "🤖 我的 Agent", bots: "🔌 機器人",
167
+ general: "⚙️ 一般設定", autoUpdate: "🔄 自動更新", whatsNew: "🆕 更新重點", advanced: "📋 進階",
168
+ expand: "▶ 展開", collapse: "▼ 收合", new: "+ 新增", add: "+ 加入", searchPh: "🔍 搜尋…",
169
+ edit: "編輯", close: "關閉", save: "儲存", cancel: "取消", create: "建立", start: "啟動", stop: "停止",
170
+ backend: "後端", model: "模型", workingDir: "工作目錄", channelBinding: "頻道綁定",
171
+ description: "描述", systemPrompt: "System prompt", tags: "標籤", generalTopicField: "General topic(fleet 分派器)",
172
+ language: "語言(bot 回覆)", defaultBackend: "預設後端", defaultModel: "預設模型", allowedUsers: "允許使用者(主 bot)",
173
+ classicAdmins: "ClassicBot 管理員", allowedGuilds: "允許的伺服器", type: "類型", adapterId: "Adapter id",
174
+ tokenEnv: "Bot token 環境變數", groupId: "群組 / guild id", nameField: "名稱",
175
+ envHint: "把實際 token 放到 ~/.agend/.env 的這個環境變數下,然後點重新載入。",
176
+ noAgents: "還沒有 agent — 點 + 新增。", noBots: "尚未設定機器人。", online: "✅ 上線",
177
+ enable: "啟用", schedule: "排程", channel: "頻道", daily: "每日", weekly: "每週", manual: "手動(關閉)",
178
+ beta: "Beta", stable: "穩定版", currentVersion: "目前版本", lastChecked: "上次檢查", never: "從未",
179
+ helpTitle: "📖 使用說明", tipReload: "💡 修改後點 [↻ 重新載入] 讓設定生效。",
180
+ wnIntro: "近期更新重點:",
181
+ },
182
+ };
183
+ const HELP = {
184
+ 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"]],
185
+ ["🔌 Bots", ["[+ Add] add a Discord/TG bot", "🗑️ remove a bot"]],
186
+ ["⚙️ General Settings", ["Edit global backend/model, allowed users, ClassicBot admins", "Click [Save]"]],
187
+ ["🔄 Auto Update", ["Enable a scheduled `agend update` (daily/weekly)", "Pick beta or stable channel"]],
188
+ ["📋 Advanced", ["Edit YAML/JSON directly", "[⬇ fleet.yaml] download a backup"]]],
189
+ "zh-TW": [["🤖 我的 Agent", ["[+ 新增] 新增 agent(name、backend、working directory)", "[編輯] 修改設定(backend、model、tags、systemPrompt)", "[啟動/停止] 啟動或停止 agent", "🗑️ 刪除 agent(hover 顯示,需確認)", "🔍 搜尋 — 即時過濾列表"]],
190
+ ["🔌 機器人", ["[+ 加入] 加入 Discord/TG bot", "🗑️ 移除 bot"]],
191
+ ["⚙️ 一般設定", ["修改全域 backend/model、允許使用者、ClassicBot 管理員", "點 [儲存]"]],
192
+ ["🔄 自動更新", ["啟用排程 `agend update`(每日/每週)", "選 beta 或穩定版頻道"]],
193
+ ["📋 進階", ["直接編輯 YAML/JSON", "[⬇ fleet.yaml] 下載備份"]]],
194
+ };
195
+ const WHATSNEW = [
196
+ "Settings redesign — user-flow sections, white theme, i18n (EN / 繁中)",
197
+ "Auto Update scheduler + What's New + info help panel",
198
+ "Same-channel multi-bot ClassicBot; per-bot cancel-button + react fixes",
199
+ "/view avatar & ctx% fixes; config validator + `agend validate` + validate_config MCP tool",
200
+ ];
201
+ let lang = localStorage.getItem("agend_lang") || (navigator.language && navigator.language.toLowerCase().startsWith("zh") ? "zh-TW" : "en");
202
+ if (!I18N[lang]) lang = "en";
203
+ const t = (k) => (I18N[lang] && I18N[lang][k]) || I18N.en[k] || k;
204
+ function applyStaticI18n() {
205
+ document.querySelectorAll("[data-i18n]").forEach(e => { e.textContent = t(e.getAttribute("data-i18n")); });
206
+ document.querySelectorAll("[data-i18n-ph]").forEach(e => { e.setAttribute("placeholder", t(e.getAttribute("data-i18n-ph"))); });
207
+ // toggle labels that carry expand/collapse state
208
+ for (const [id, body] of [["advToggle", "advBody"], ["wnToggle", "wnBody"]]) {
209
+ const b = $(body); if (b) $(id).textContent = b.classList.contains("open") ? t("collapse") : t("expand");
210
+ }
211
+ renderHelp();
212
+ }
213
+ function renderHelp() {
214
+ const host = $("helpContent"); if (!host) return; host.innerHTML = "";
215
+ host.append(el("h3", {}, t("helpTitle")));
216
+ for (const [h, items] of (HELP[lang] || HELP.en)) {
217
+ host.append(el("h4", {}, h));
218
+ const ul = el("ul"); for (const it of items) ul.append(el("li", {}, it)); host.append(ul);
219
+ }
220
+ host.append(el("div", { class: "tip" }, t("tipReload")));
221
+ }
115
222
  const CH_TYPES = ["discord", "telegram"];
116
223
 
117
224
  // ── YAML (config subset; round-trip verified) ──
@@ -150,7 +257,7 @@
150
257
  }
151
258
 
152
259
  // ── State ──
153
- const state = { fleet: {}, classic: {}, live: {}, fleetUp: false, advFmt: "yaml", advEdit: false, collapsedGroups: new Set() };
260
+ const state = { fleet: {}, classic: {}, live: {}, fleetUp: false, advFmt: "yaml", advEdit: false, collapsedGroups: new Set(), updateSchedule: null, version: "", newAgentOpen: false, newBotOpen: false };
154
261
  const channels = () => state.fleet.channels || (state.fleet.channel ? [state.fleet.channel] : []);
155
262
  const channelIds = () => channels().map(c => c.id || c.type).filter(Boolean);
156
263
  const chLabel = (i) => i === 0 ? "primary" : "persona";
@@ -195,9 +302,9 @@
195
302
  el("span", { class: "sub", style: "margin:0" }, inst.backend || state.fleet.defaults?.backend || "claude-code"),
196
303
  chTag ? el("span", { class: "tag" + (chTag.includes("persona") ? " persona" : chTag.includes("telegram") ? " tg" : "") }, chTag) : null,
197
304
  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) }, "🗑️"),
305
+ el("button", { class: "sm ghost", onclick: () => { editing = editing === name ? null : name; renderAgents(); } }, editing === name ? t("close") : t("edit")),
306
+ el("button", { class: "sm", onclick: () => toggleAgent(name, running) }, running ? t("stop") : t("start")),
307
+ el("button", { class: "sm danger del-btn", title: t("close"), onclick: () => delAgent(name) }, "🗑️"),
201
308
  ));
202
309
  if (editing === name) item.append(agentEditForm(name, inst));
203
310
  return item;
@@ -205,7 +312,7 @@
205
312
  function renderAgents() {
206
313
  const host = $("agents"); host.innerHTML = "";
207
314
  const insts = state.fleet.instances || {};
208
- if (!Object.keys(insts).length) { host.className = "card empty"; host.textContent = "No agents yet — click + New."; return; }
315
+ if (!Object.keys(insts).length) { host.className = "card empty"; host.textContent = t("noAgents"); return; }
209
316
  const filter = ($("agentSearch")?.value || "").trim().toLowerCase();
210
317
  let names = Object.keys(insts);
211
318
  if (filter) names = names.filter(n => n.toLowerCase().includes(filter) || shortName(n, insts[n]).toLowerCase().includes(filter));
@@ -241,12 +348,12 @@
241
348
  const msg = el("span", { class: "msg" });
242
349
  box.append(
243
350
  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)")),
351
+ el("div", { class: "grid2" }, el("div", {}, el("label", {}, t("backend")), fBackend), el("div", {}, el("label", {}, t("model")), fModel)),
352
+ el("label", {}, t("workingDir")), fWd,
353
+ el("div", { class: "grid2" }, el("div", {}, el("label", {}, t("channelBinding")), fChan), el("div", {}, el("label", {}, t("description")), fDesc)),
354
+ el("label", {}, t("systemPrompt")), fSys,
355
+ el("label", {}, t("tags")), chipList(tags, null, "tag"),
356
+ el("label", { style: "display:flex;align-items:center;gap:8px;cursor:pointer" }, fGeneral, el("span", {}, t("generalTopicField"))),
250
357
  el("div", { class: "actions" },
251
358
  el("button", { class: "primary sm", onclick: async () => {
252
359
  const patch = {
@@ -257,8 +364,8 @@
257
364
  };
258
365
  const res = await api(`/api/settings/fleet/instances/${encodeURIComponent(name)}`, { method: "PATCH", body: JSON.stringify(patch) });
259
366
  resultMsg(msg, res); if (res.ok) await reload();
260
- } }, "Save"),
261
- el("button", { class: "sm ghost", onclick: () => { editing = null; renderAgents(); } }, "Cancel"),
367
+ } }, t("save")),
368
+ el("button", { class: "sm ghost", onclick: () => { editing = null; renderAgents(); } }, t("cancel")),
262
369
  msg),
263
370
  );
264
371
  return box;
@@ -283,14 +390,14 @@
283
390
  const fChan = select("", ["", ...channelIds()]);
284
391
  const msg = el("span", { class: "msg" });
285
392
  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)),
393
+ el("div", { class: "grid2" }, el("div", {}, el("label", {}, t("nameField")), fName), el("div", {}, el("label", {}, t("backend")), fBackend)),
394
+ el("div", { class: "grid2" }, el("div", {}, el("label", {}, t("workingDir")), fWd), el("div", {}, el("label", {}, t("channelBinding")), fChan)),
288
395
  el("div", { class: "actions" }, el("button", { class: "primary sm", onclick: async () => {
289
396
  const name = fName.value.trim(); if (!name) { msg.innerHTML = `<span class="e">✗ name required</span>`; return; }
290
397
  const body = { working_directory: fWd.value, backend: fBackend.value, channel_id: fChan.value || undefined };
291
398
  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),
399
+ resultMsg(msg, res); if (res.ok) { state.newAgentOpen = false; $("newAgentBtn").textContent = t("new"); renderNewAgent(false); await reload(); }
400
+ } }, t("create")), el("button", { class: "sm ghost", onclick: () => { state.newAgentOpen = false; $("newAgentBtn").textContent = t("new"); renderNewAgent(false); } }, t("cancel")), msg),
294
401
  ));
295
402
  host.append(box);
296
403
  }
@@ -298,7 +405,7 @@
298
405
  // ── Bots ──
299
406
  function renderBots() {
300
407
  const host = $("bots"); host.innerHTML = ""; const chs = channels();
301
- if (!chs.length) { host.className = "card empty"; host.textContent = "No bots configured."; return; }
408
+ if (!chs.length) { host.className = "card empty"; host.textContent = t("noBots"); return; }
302
409
  host.className = "card";
303
410
  chs.forEach((ch, i) => {
304
411
  const item = el("div", { class: "item" });
@@ -308,7 +415,7 @@
308
415
  el("span", { class: "tag" + (i === 0 ? "" : " persona") + (type === "telegram" ? " tg" : "") }, chLabel(i)),
309
416
  el("span", { class: "sub", style: "margin:0" }, ch.bot_token_env || "(no token env)"),
310
417
  el("span", { class: "spacer" }),
311
- el("span", { style: "color:var(--green);font-size:13px" }, state.fleetUp ? "✅ Online" : "—"),
418
+ el("span", { style: "color:var(--green);font-size:13px" }, state.fleetUp ? t("online") : "—"),
312
419
  el("button", { class: "sm danger", onclick: () => delBot(i) }, "🗑️"),
313
420
  ));
314
421
  host.append(item);
@@ -335,16 +442,16 @@
335
442
  const fGroup = el("input", { type: "text", placeholder: "guild / supergroup id" });
336
443
  const msg = el("span", { class: "msg" });
337
444
  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."),
445
+ el("div", { class: "grid2" }, el("div", {}, el("label", {}, t("type")), fType), el("div", {}, el("label", {}, t("adapterId")), fId)),
446
+ el("div", { class: "grid2" }, el("div", {}, el("label", {}, t("tokenEnv")), fEnv), el("div", {}, el("label", {}, t("groupId")), fGroup)),
447
+ el("div", { class: "hint" }, t("envHint")),
341
448
  el("div", { class: "actions" }, el("button", { class: "primary sm", onclick: async () => {
342
449
  if (!fEnv.value.trim()) { msg.innerHTML = `<span class="e">✗ token env required</span>`; return; }
343
450
  const chs = channels();
344
451
  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
452
  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),
453
+ const res = await saveChannels(msg); if (res.ok) { state.newBotOpen = false; $("newBotBtn").textContent = t("add"); renderNewBot(false); await reload(); }
454
+ } }, t("save")), el("button", { class: "sm ghost", onclick: () => { state.newBotOpen = false; $("newBotBtn").textContent = t("add"); renderNewBot(false); } }, t("cancel")), msg),
348
455
  ));
349
456
  host.append(box);
350
457
  }
@@ -362,25 +469,65 @@
362
469
 
363
470
  const fBackend = select(d.backend || "claude-code", BACKENDS);
364
471
  const fModel = el("input", { type: "text", value: d.model || "", placeholder: "e.g. claude-opus-4-8" });
472
+ const fLocale = el("select");
473
+ [["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
474
  const msg = el("span", { class: "msg" });
366
475
  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")),
476
+ el("div", { class: "field" }, el("label", {}, t("language")), fLocale),
477
+ el("div", { class: "field" }, el("label", {}, t("defaultBackend")), fBackend),
478
+ el("div", { class: "field" }, el("label", {}, t("defaultModel")), fModel),
479
+ el("div", { class: "field" }, el("label", {}, t("allowedUsers")), chipList(allowedUsers, null, "user id")),
480
+ el("div", { class: "field" }, el("label", {}, t("classicAdmins")), chipList(c.admin_users, null, "user id")),
481
+ el("div", { class: "field" }, el("label", {}, t("allowedGuilds")), chipList(c.allowed_guilds, null, "guild id")),
372
482
  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 }) });
483
+ d.backend = fBackend.value; d.model = fModel.value || undefined; d.locale = fLocale.value;
484
+ const r1 = await api("/api/settings/fleet/defaults", { method: "PUT", body: JSON.stringify({ backend: d.backend, model: d.model, locale: d.locale }) });
375
485
  let r2 = { ok: true, body: {} };
376
486
  if (primary) r2 = await saveChannels();
377
487
  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
488
  const bad = [r1, r2, r3].find(r => !r.ok);
379
489
  resultMsg(msg, bad || { ok: true, body: { warnings: [...(r1.body?.warnings || []), ...(r3.body?.warnings || [])] } });
380
- } }, "Save"), msg)),
490
+ } }, t("save")), msg)),
381
491
  );
382
492
  }
383
493
 
494
+ // ── Auto Update ──
495
+ function renderAutoUpdate() {
496
+ const host = $("autoUpdate"); host.innerHTML = "";
497
+ const u = state.updateSchedule || { enabled: false, cron: "0 4 * * *", channel: "stable" };
498
+ const CRON = { daily: "0 4 * * *", weekly: "0 4 * * 1" };
499
+ const fEnable = el("input", { type: "checkbox" }); fEnable.checked = !!u.enabled;
500
+ const schedVal = u.cron === CRON.weekly ? "weekly" : (u.enabled ? "daily" : "manual");
501
+ const fSched = select(schedVal, ["daily", "weekly", "manual"]); // labels localized below
502
+ [...fSched.children].forEach(o => { o._txt = ""; o.textContent = t(o.value === "manual" ? "manual" : o.value); });
503
+ const fChannel = select(u.channel === "beta" ? "beta" : "stable", ["stable", "beta"]);
504
+ [...fChannel.children].forEach(o => o.textContent = t(o.value));
505
+ const msg = el("span", { class: "msg" });
506
+ host.append(
507
+ el("div", { class: "field" }, el("label", { style: "display:flex;align-items:center;gap:8px;cursor:pointer" }, fEnable, el("span", {}, t("enable")))),
508
+ el("div", { class: "field" }, el("label", {}, t("schedule")), fSched),
509
+ el("div", { class: "field" }, el("label", {}, t("channel")), fChannel),
510
+ 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")}`)),
511
+ el("div", { class: "field" }, el("div", { class: "actions" }, el("button", { class: "primary", onclick: async () => {
512
+ const sched = fSched.value;
513
+ const enabled = fEnable.checked && sched !== "manual";
514
+ const cron = sched === "weekly" ? CRON.weekly : CRON.daily;
515
+ const res = await api("/api/settings/update-schedule", { method: "PUT", body: JSON.stringify({ enabled, cron, channel: fChannel.value }) });
516
+ resultMsg(msg, res); if (res.ok) { state.updateSchedule = { enabled, cron, channel: fChannel.value, lastChecked: u.lastChecked }; }
517
+ } }, t("save")), msg)),
518
+ );
519
+ }
520
+
521
+ // ── What's New ──
522
+ function renderWhatsNew() {
523
+ const host = $("wnBody"); host.innerHTML = "";
524
+ host.append(el("div", { class: "sub", style: "margin:0 0 8px" }, `${t("currentVersion")}: ${state.version || "?"}`));
525
+ host.append(el("div", { style: "font-size:13px;margin-bottom:6px" }, t("wnIntro")));
526
+ const ul = el("ul", { style: "margin:0 0 0 18px" });
527
+ for (const line of WHATSNEW) ul.append(el("li", { style: "font-size:13px;margin:4px 0" }, line));
528
+ host.append(ul);
529
+ }
530
+
384
531
  // ── Advanced (YAML/JSON) ──
385
532
  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
533
  function renderAdvanced() {
@@ -428,7 +575,7 @@
428
575
  }
429
576
 
430
577
  // ── Load / render ──
431
- function renderAll() { renderAgents(); renderBots(); renderGeneral(); renderAdvanced(); }
578
+ function renderAll() { renderAgents(); renderBots(); renderGeneral(); renderAutoUpdate(); renderWhatsNew(); renderAdvanced(); applyStaticI18n(); }
432
579
  async function reloadLive() {
433
580
  const f = await api("/api/fleet");
434
581
  state.fleetUp = f.ok;
@@ -440,14 +587,22 @@
440
587
  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
588
  state.fleet = f.body || {};
442
589
  const c = await api("/api/settings/classic"); state.classic = c.body || {};
590
+ 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
591
  await reloadLive();
444
592
  renderAll();
445
593
  }
446
594
 
595
+ const setHelp = (open) => { $("helpPanel").classList.toggle("open", open); $("helpBackdrop").classList.toggle("open", open); };
596
+ $("helpBtn").onclick = () => setHelp(!$("helpPanel").classList.contains("open"));
597
+ $("helpClose").onclick = () => setHelp(false);
598
+ $("helpBackdrop").onclick = () => setHelp(false);
599
+ $("langBtn").onclick = () => { lang = lang === "en" ? "zh-TW" : "en"; localStorage.setItem("agend_lang", lang); renderAll(); };
600
+ $("verBadge").onclick = () => { const b = $("wnBody"); b.classList.add("open"); $("wnToggle").textContent = t("collapse"); b.scrollIntoView({ behavior: "smooth" }); };
447
601
  $("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"; };
602
+ $("newAgentBtn").onclick = () => { state.newAgentOpen = !state.newAgentOpen; $("newAgentBtn").textContent = state.newAgentOpen ? t("cancel") : t("new"); renderNewAgent(state.newAgentOpen); };
603
+ $("newBotBtn").onclick = () => { state.newBotOpen = !state.newBotOpen; $("newBotBtn").textContent = state.newBotOpen ? t("cancel") : t("add"); renderNewBot(state.newBotOpen); };
604
+ $("advToggle").onclick = () => { const b = $("advBody"); const open = !b.classList.contains("open"); b.classList.toggle("open", open); $("advToggle").textContent = open ? t("collapse") : t("expand"); };
605
+ $("wnToggle").onclick = () => { const b = $("wnBody"); const open = !b.classList.contains("open"); b.classList.toggle("open", open); $("wnToggle").textContent = open ? t("collapse") : t("expand"); };
451
606
  $("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
607
 
453
608
  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.31",
4
4
  "description": "Multi-agent fleet daemon — run any coding CLI (Claude, Gemini, Codex, OpenCode) from Telegram",
5
5
  "type": "module",
6
6
  "bin": {