@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.
- package/dist/channel/adapters/discord.js +16 -15
- package/dist/channel/adapters/discord.js.map +1 -1
- package/dist/fleet-manager.d.ts +2 -0
- package/dist/fleet-manager.js +100 -74
- package/dist/fleet-manager.js.map +1 -1
- package/dist/locale.d.ts +19 -0
- package/dist/locale.js +137 -0
- package/dist/locale.js.map +1 -0
- package/dist/quickstart.js +7 -0
- package/dist/quickstart.js.map +1 -1
- package/dist/scheduler/scheduler.d.ts +1 -1
- package/dist/scheduler/scheduler.js +15 -4
- package/dist/scheduler/scheduler.js.map +1 -1
- package/dist/settings-api.d.ts +30 -0
- package/dist/settings-api.js +48 -0
- package/dist/settings-api.js.map +1 -1
- package/dist/topic-commands.js +18 -17
- package/dist/topic-commands.js.map +1 -1
- package/dist/types.d.ts +2 -0
- package/dist/ui/settings.html +197 -42
- package/dist/ui/view.html +60 -9
- package/package.json +1 -1
package/dist/ui/settings.html
CHANGED
|
@@ -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
|
|
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
|
|
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 => ({ "&":"&","<":"<",">":">" }[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 ? "
|
|
199
|
-
el("button", { class: "sm", onclick: () => toggleAgent(name, running) }, running ? "
|
|
200
|
-
el("button", { class: "sm danger del-btn", title: "
|
|
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 = "
|
|
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", {}, "
|
|
245
|
-
el("label", {}, "
|
|
246
|
-
el("div", { class: "grid2" }, el("div", {}, el("label", {}, "
|
|
247
|
-
el("label", {}, "
|
|
248
|
-
el("label", {}, "
|
|
249
|
-
el("label", { style: "display:flex;align-items:center;gap:8px;cursor:pointer" }, fGeneral, el("span", {},
|
|
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
|
-
} }, "
|
|
261
|
-
el("button", { class: "sm ghost", onclick: () => { editing = null; renderAgents(); } }, "
|
|
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", {}, "
|
|
287
|
-
el("div", { class: "grid2" }, el("div", {}, el("label", {}, "
|
|
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 = "
|
|
293
|
-
} }, "
|
|
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 = "
|
|
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 ? "
|
|
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", {}, "
|
|
339
|
-
el("div", { class: "grid2" }, el("div", {}, el("label", {}, "
|
|
340
|
-
el("div", { class: "hint" }, "
|
|
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 = "
|
|
347
|
-
} }, "
|
|
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", {}, "
|
|
368
|
-
el("div", { class: "field" }, el("label", {}, "
|
|
369
|
-
el("div", { class: "field" }, el("label", {},
|
|
370
|
-
el("div", { class: "field" }, el("label", {}, "
|
|
371
|
-
el("div", { class: "field" }, el("label", {}, "
|
|
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
|
-
} }, "
|
|
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 = () => {
|
|
449
|
-
$("newBotBtn").onclick = () => {
|
|
450
|
-
$("advToggle").onclick = () => { const b = $("advBody"); const open = !b.classList.contains("open"); b.classList.toggle("open", open); $("advToggle").textContent = open ? "
|
|
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
|
-
<
|
|
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);
|