@songsid/agend 2.1.7-beta.5 → 2.1.7-beta.7
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/backend/codex.d.ts +34 -9
- package/dist/backend/codex.js +197 -33
- package/dist/backend/codex.js.map +1 -1
- package/dist/backend/grok.js +51 -0
- package/dist/backend/grok.js.map +1 -1
- package/dist/channel/adapters/discord.d.ts +11 -0
- package/dist/channel/adapters/discord.js +16 -2
- package/dist/channel/adapters/discord.js.map +1 -1
- package/dist/channel/types.d.ts +2 -0
- package/dist/cli.js +79 -132
- package/dist/cli.js.map +1 -1
- package/dist/completion-install.d.ts +32 -0
- package/dist/completion-install.js +125 -1
- package/dist/completion-install.js.map +1 -1
- package/dist/config-validator.js +45 -0
- package/dist/config-validator.js.map +1 -1
- package/dist/daemon.d.ts +4 -1
- package/dist/daemon.js +23 -6
- package/dist/daemon.js.map +1 -1
- package/dist/delivery-outbox.d.ts +109 -0
- package/dist/delivery-outbox.js +306 -10
- package/dist/delivery-outbox.js.map +1 -1
- package/dist/fleet-manager.d.ts +67 -7
- package/dist/fleet-manager.js +215 -59
- package/dist/fleet-manager.js.map +1 -1
- package/dist/general-knowledge/skills/cross-instance-messaging/SKILL.md +10 -0
- package/dist/instance-config-impact.js +5 -0
- package/dist/instance-config-impact.js.map +1 -1
- package/dist/instance-lifecycle.d.ts +10 -2
- package/dist/instance-lifecycle.js +50 -32
- package/dist/instance-lifecycle.js.map +1 -1
- package/dist/instructions.d.ts +17 -0
- package/dist/instructions.js +24 -1
- package/dist/instructions.js.map +1 -1
- package/dist/locale.js +2 -0
- package/dist/locale.js.map +1 -1
- package/dist/ls-rows.d.ts +50 -0
- package/dist/ls-rows.js +152 -0
- package/dist/ls-rows.js.map +1 -0
- package/dist/outbound-handlers.d.ts +1 -1
- package/dist/outbound-handlers.js +6 -7
- package/dist/outbound-handlers.js.map +1 -1
- package/dist/outbound-schemas.d.ts +2 -1
- package/dist/outbound-schemas.js +3 -2
- package/dist/outbound-schemas.js.map +1 -1
- package/dist/quickstart.js +9 -3
- package/dist/quickstart.js.map +1 -1
- package/dist/remote-credential-check.d.ts +26 -7
- package/dist/remote-credential-check.js +157 -10
- package/dist/remote-credential-check.js.map +1 -1
- package/dist/settings-api.d.ts +17 -0
- package/dist/settings-api.js +62 -1
- package/dist/settings-api.js.map +1 -1
- package/dist/status-emojis.d.ts +118 -0
- package/dist/status-emojis.js +206 -0
- package/dist/status-emojis.js.map +1 -0
- package/dist/types.d.ts +12 -0
- package/dist/ui/dashboard.html +7 -5
- package/dist/ui/settings.html +168 -6
- package/dist/ui/view.html +138 -45
- package/dist/usage/usage-api.js +30 -0
- package/dist/usage/usage-api.js.map +1 -1
- package/package.json +1 -1
package/dist/ui/dashboard.html
CHANGED
|
@@ -182,8 +182,10 @@
|
|
|
182
182
|
}
|
|
183
183
|
.detail-row { display: flex; align-items: center; gap: 10px; font-size: 13px; padding: 6px 0; }
|
|
184
184
|
.detail-label { color: var(--text-muted); min-width: 100px; font-family: 'IBM Plex Mono', monospace; font-size: 11px; }
|
|
185
|
-
|
|
186
|
-
.progress-
|
|
185
|
+
/* Readable at a glance: the percentage is bold and the bar thick (usage readability). */
|
|
186
|
+
.progress-bar { display: inline-block; width: 120px; height: 8px; background: rgba(255,255,255,0.07); border-radius: 4px; vertical-align: middle; margin-left: 10px; overflow: hidden; }
|
|
187
|
+
.progress-fill { display: block; height: 100%; border-radius: 4px; transition: width 0.4s cubic-bezier(0.4,0,0.2,1); }
|
|
188
|
+
.pct-val { font-size: 15px; font-weight: 700; font-variant-numeric: tabular-nums; }
|
|
187
189
|
.activity-item { font-size: 12px; padding: 6px 0; border-bottom: 1px solid rgba(255,255,255,0.03); font-family: 'IBM Plex Mono', monospace; }
|
|
188
190
|
.act-time { color: var(--text-muted); margin-right: 8px; }
|
|
189
191
|
.act-event { color: var(--warn); margin-right: 4px; }
|
|
@@ -472,9 +474,9 @@ async function loadDetail() {
|
|
|
472
474
|
<div class="detail-row"><span class="detail-label">Model</span><span>${esc(model)}${modelSource}</span></div>
|
|
473
475
|
${effortLine}
|
|
474
476
|
<div class="detail-row"><span class="detail-label">Cost</span><span>$${cost.toFixed(2)} <span style="color:var(--text-muted);font-size:11px">(session accumulated)</span></span></div>
|
|
475
|
-
<div class="detail-row"><span class="detail-label">Context</span><span>${ctxDisplay} ${ctxBar}</span></div>
|
|
476
|
-
<div class="detail-row"><span class="detail-label">Rate 5h</span><span>${Math.round(rl5)}
|
|
477
|
-
<div class="detail-row"><span class="detail-label">Rate 7d</span><span>${Math.round(rl7)}
|
|
477
|
+
<div class="detail-row"><span class="detail-label">Context</span><span><span class="pct-val">${ctxDisplay}</span> ${ctxBar}</span></div>
|
|
478
|
+
<div class="detail-row"><span class="detail-label">Rate 5h</span><span><span class="pct-val">${Math.round(rl5)}%</span> <span class="progress-bar"><span class="progress-fill" style="width:${Math.min(rl5,100)}%;background:${rl5>90?'var(--error)':rl5>70?'var(--warn)':'var(--accent)'}"></span></span></span></div>
|
|
479
|
+
<div class="detail-row"><span class="detail-label">Rate 7d</span><span><span class="pct-val">${Math.round(rl7)}%</span> <span class="progress-bar"><span class="progress-fill" style="width:${Math.min(rl7,100)}%;background:${rl7>90?'var(--error)':rl7>70?'var(--warn)':'var(--accent)'}"></span></span></span></div></div>`;
|
|
478
480
|
if (d.recent_activity?.length) {
|
|
479
481
|
h += `<div class="card"><h3>Recent Activity</h3>`;
|
|
480
482
|
for (const a of d.recent_activity) { const t=a.timestamp?new Date(a.timestamp.replace(" ","T")+"Z").toLocaleTimeString([],{hour:"2-digit",minute:"2-digit"}):""; h+=`<div class="activity-item"><span class="act-time">${t}</span><span class="act-event">${esc(a.event)}</span>${esc(a.summary||"")}</div>`; }
|
package/dist/ui/settings.html
CHANGED
|
@@ -145,6 +145,18 @@
|
|
|
145
145
|
.pending-bar .grow { display:none; }
|
|
146
146
|
.pending-bar button { flex:1; }
|
|
147
147
|
}
|
|
148
|
+
/* #1005 status-emoji editor */
|
|
149
|
+
.se-preview { display: flex; flex-wrap: wrap; gap: 6px 14px; padding: 8px 10px; border: 1px solid var(--border); border-radius: var(--radius-sm); background: #fff; margin-bottom: 6px; }
|
|
150
|
+
.se-item { display: inline-flex; align-items: center; gap: 5px; font-size: 12px; color: var(--muted); }
|
|
151
|
+
.se-glyph { font-size: 20px; line-height: 1; min-width: 22px; text-align: center; }
|
|
152
|
+
.se-glyph img { width: 22px; height: 22px; object-fit: contain; vertical-align: middle; }
|
|
153
|
+
.se-row { display: grid; grid-template-columns: 120px 1fr auto auto; gap: 6px; align-items: center; margin: 4px 0; }
|
|
154
|
+
.se-row label { margin: 0; }
|
|
155
|
+
.se-picker { grid-column: 1 / -1; border: 1px solid var(--border); border-radius: var(--radius-sm); padding: 8px; background: #fff; }
|
|
156
|
+
.se-grid { display: flex; flex-wrap: wrap; gap: 4px; margin: 4px 0 8px; }
|
|
157
|
+
.se-grid button { padding: 3px 6px; font-size: 18px; line-height: 1.2; }
|
|
158
|
+
.se-grid button img { width: 22px; height: 22px; object-fit: contain; vertical-align: middle; }
|
|
159
|
+
.se-grid button.off { opacity: .45; }
|
|
148
160
|
</style>
|
|
149
161
|
</head>
|
|
150
162
|
<body>
|
|
@@ -238,6 +250,11 @@
|
|
|
238
250
|
backend: "Backend", model: "Model", workingDir: "Working directory", channelBinding: "Channel binding",
|
|
239
251
|
description: "Description", systemPrompt: "System prompt", tags: "Tags", generalTopicField: "General topic (fleet dispatcher)",
|
|
240
252
|
runtimeResources: "Runtime & Resources", autoPause: "Auto-pause after", minutes: "minutes", inheritDefault: "Inherit from defaults",
|
|
253
|
+
statusEmojis: "Status emojis", statusEmojisInstance: "This agent's status emojis (override the connection's)", statusEmojisChannel: "Status emojis for this connection's bots",
|
|
254
|
+
se_received: "Received", se_queued: "Queued", se_processing: "Processing", se_delivered: "Delivered", se_failed: "Failed", se_progress_prefix: "Progress prefix",
|
|
255
|
+
se_src_instance: "agent", se_src_platform: "connection", se_pick: "Pick…", se_useDefault: "Use the default", se_fallback: "the default is used instead",
|
|
256
|
+
se_unicode: "Emoji", se_telegramOnly: "Telegram allows only these reactions", se_serverEmojis: "This server's emojis", se_noServerEmojis: "This server has no custom emojis.", se_unavailable: "unavailable",
|
|
257
|
+
se_hint: "Empty keeps the default. The preview is resolved by AgEnD exactly as the bot will react.",
|
|
241
258
|
detection: "Detection", startup: "Startup", backendSection: "Backend", advancedSection: "Advanced & Access", hangEnabled: "Hang detector enabled", hangTimeout: "Stuck timeout", agentMode: "Agent mode", toolSet: "Tool set", toolProgress: "Tool progress", replyCompletionGuard: "Require a delivered reply", replyGuardSupported: "Active for Claude Code in MCP mode.", replyGuardUnsupported: "Stored but inactive: {0} in {1} mode does not support reply-drop recovery.", logLevel: "Log level", concurrency: "Startup concurrency", staggerDelay: "Stagger delay", lightweight: "Lightweight mode", modelFailover: "Fallback model", displayName: "Display name", pause: "Pause", wake: "Wake", accessMode: "Access mode", affected: "Affected instances",
|
|
242
259
|
language: "Language (bot replies)", defaultBackend: "Default backend", defaultModel: "Default model", allowedUsers: "Allowed users (primary bot)",
|
|
243
260
|
classicAdmins: "ClassicBot admins", allowedGuilds: "Allowed guilds", type: "Type", adapterId: "Adapter id",
|
|
@@ -282,6 +299,11 @@
|
|
|
282
299
|
mustNonNegative: "Must be 0 or greater", mustPositive: "Must be greater than 0", nonnegativeInteger: "Must be a non-negative integer", failoverEmpty: "Empty list disables failover", concurrencyRange: "Must be an integer from 1 to 20", concurrencyWarning: "May exceed CPU capacity on smaller hosts", staggerRange: "Must be between 0 and 30000", staggerWarning: "Very low values can cause startup spikes",
|
|
283
300
|
},
|
|
284
301
|
"zh-TW": {
|
|
302
|
+
statusEmojis: "狀態 emoji", statusEmojisInstance: "此 Agent 的狀態 emoji(覆蓋連線設定)", statusEmojisChannel: "此連線 bot 的狀態 emoji",
|
|
303
|
+
se_received: "已收到", se_queued: "排隊中", se_processing: "處理中", se_delivered: "已送達", se_failed: "失敗", se_progress_prefix: "進度前綴",
|
|
304
|
+
se_src_instance: "Agent", se_src_platform: "連線", se_pick: "選擇…", se_useDefault: "使用預設", se_fallback: "改用預設值",
|
|
305
|
+
se_unicode: "Emoji", se_telegramOnly: "Telegram 只允許這些反應", se_serverEmojis: "此伺服器的 emoji", se_noServerEmojis: "此伺服器沒有自訂 emoji。", se_unavailable: "無法使用",
|
|
306
|
+
se_hint: "留空即使用預設值。預覽由 AgEnD 依 bot 實際 react 的方式解析。",
|
|
285
307
|
title: "⚙️ AgEnD 設定", reloadFleet: "↻ 重新載入", myAgents: "🤖 我的 Agent", bots: "🔌 機器人", classicChannels: "🎮 ClassicBot 頻道", noClassic: "沒有 ClassicBot 頻道 — 在聊天中用 /start 建立。",
|
|
286
308
|
general: "⚙️ 一般設定", whatsNew: "🆕 更新重點", advanced: "📋 進階",
|
|
287
309
|
expand: "▶ 展開", collapse: "▼ 收合", new: "+ 新增", add: "+ 加入", searchPh: "🔍 搜尋…",
|
|
@@ -451,6 +473,118 @@
|
|
|
451
473
|
return box;
|
|
452
474
|
}
|
|
453
475
|
|
|
476
|
+
// ── Status emojis (#1005) ──
|
|
477
|
+
// The page never resolves or checks an emoji itself: /status-emojis/preview
|
|
478
|
+
// runs the code the bots react with, so what shows here is what gets
|
|
479
|
+
// stamped, and a picked server emoji is stored in the form they react with.
|
|
480
|
+
let statusEmojiCatalog = null;
|
|
481
|
+
async function loadStatusEmojiCatalog() {
|
|
482
|
+
if (statusEmojiCatalog) return statusEmojiCatalog;
|
|
483
|
+
const r = await api("/api/settings/status-emojis");
|
|
484
|
+
statusEmojiCatalog = r.ok ? r.body : { keys: [], builtins: {}, telegram_allowed: [], suggestions: [] };
|
|
485
|
+
return statusEmojiCatalog;
|
|
486
|
+
}
|
|
487
|
+
const guildEmojiLists = new Map();
|
|
488
|
+
function fetchGuildEmojis(channelId, refresh) {
|
|
489
|
+
if (refresh || !guildEmojiLists.has(channelId)) {
|
|
490
|
+
guildEmojiLists.set(channelId, api(`/api/settings/status-emojis/guild-emojis?channel=${encodeURIComponent(channelId)}${refresh ? "&refresh=1" : ""}`)
|
|
491
|
+
.then(r => r.ok ? r.body : { error: r.body?.error || `HTTP ${r.status}` }));
|
|
492
|
+
}
|
|
493
|
+
return guildEmojiLists.get(channelId);
|
|
494
|
+
}
|
|
495
|
+
// no-referrer: the settings URL can carry the web token.
|
|
496
|
+
const emojiImg = (url, alt) => el("img", { src: url, alt, referrerpolicy: "no-referrer", loading: "lazy" });
|
|
497
|
+
const emojiGlyph = (entry) => el("span", { class: "se-glyph" }, entry.image_url ? emojiImg(entry.image_url, entry.display) : entry.value);
|
|
498
|
+
/** Keys in catalog order, blanks dropped — so an unchanged map compares equal. */
|
|
499
|
+
function orderedStatusMap(map, keys) {
|
|
500
|
+
const out = {};
|
|
501
|
+
for (const key of keys) { const v = map?.[key]; if (typeof v === "string" && v.trim()) out[key] = v.trim(); }
|
|
502
|
+
return Object.keys(out).length ? out : null;
|
|
503
|
+
}
|
|
504
|
+
/**
|
|
505
|
+
* Editor for one status_emojis map. `scope.platform()` and
|
|
506
|
+
* `scope.channelId()` say where it applies; `scope.previewBody(map)` is what
|
|
507
|
+
* the preview resolves (the channel map, or an instance override on top of
|
|
508
|
+
* its channel's).
|
|
509
|
+
*/
|
|
510
|
+
function statusEmojiEditor(own, scope) {
|
|
511
|
+
const values = { ...(own || {}) };
|
|
512
|
+
let keys = [];
|
|
513
|
+
const box = el("div", { class: "se" });
|
|
514
|
+
const preview = el("div", { class: "se-preview" }, el("span", { class: "hint" }, "…"));
|
|
515
|
+
const problems = el("div", { class: "feedback" });
|
|
516
|
+
const rows = el("div", { class: "se-rows" });
|
|
517
|
+
let timer = null, seq = 0;
|
|
518
|
+
const refresh = () => { clearTimeout(timer); timer = setTimeout(runPreview, 150); };
|
|
519
|
+
async function runPreview() {
|
|
520
|
+
const mine = ++seq;
|
|
521
|
+
const r = await api("/api/settings/status-emojis/preview", { method: "POST", body: JSON.stringify(scope.previewBody(orderedStatusMap(values, keys) || {})) });
|
|
522
|
+
if (mine !== seq) return;
|
|
523
|
+
preview.innerHTML = "";
|
|
524
|
+
if (!r.ok) { problems.className = "feedback error"; problems.textContent = `✗ ${r.body?.error || "preview failed"}`; return; }
|
|
525
|
+
for (const entry of r.body.entries) {
|
|
526
|
+
preview.append(el("span", { class: "se-item", title: `${entry.key}: ${entry.value}` }, emojiGlyph(entry),
|
|
527
|
+
el("span", {}, t(`se_${entry.key}`)),
|
|
528
|
+
entry.source === "builtin" ? null : el("span", { class: "tag" }, t(`se_src_${entry.source}`))));
|
|
529
|
+
}
|
|
530
|
+
problems.className = r.body.problems.length ? "feedback error" : "feedback";
|
|
531
|
+
problems.textContent = r.body.problems.map(p => `⚠ ${t(`se_${p.key}`)}: ${p.problem} — ${t("se_fallback")}`).join(" ");
|
|
532
|
+
}
|
|
533
|
+
function openPicker(row, key, input) {
|
|
534
|
+
row.querySelector(".se-picker")?.remove();
|
|
535
|
+
const picker = el("div", { class: "se-picker" });
|
|
536
|
+
const choose = (value) => { input.value = value; values[key] = value; picker.remove(); refresh(); };
|
|
537
|
+
const grid = (items) => { const g = el("div", { class: "se-grid" }); for (const it of items) g.append(it); return g; };
|
|
538
|
+
const unicodeButton = (emoji) => el("button", { type: "button", class: "sm", title: emoji, onclick: () => choose(emoji) }, emoji);
|
|
539
|
+
const platform = scope.platform();
|
|
540
|
+
const cat = statusEmojiCatalog;
|
|
541
|
+
// progress_prefix is message text, so Telegram's reaction set does not bind it.
|
|
542
|
+
const unicode = platform === "telegram" && key !== "progress_prefix" ? cat.telegram_allowed : cat.suggestions;
|
|
543
|
+
picker.append(el("div", { class: "hint" }, platform === "telegram" && key !== "progress_prefix" ? t("se_telegramOnly") : t("se_unicode")), grid(unicode.map(unicodeButton)));
|
|
544
|
+
const channelId = scope.channelId();
|
|
545
|
+
if (platform === "discord" && channelId) {
|
|
546
|
+
const guildBox = el("div");
|
|
547
|
+
const load = async (refreshList) => {
|
|
548
|
+
guildBox.innerHTML = ""; guildBox.append(el("div", { class: "hint" }, "…"));
|
|
549
|
+
const list = await fetchGuildEmojis(channelId, refreshList);
|
|
550
|
+
guildBox.innerHTML = "";
|
|
551
|
+
if (list.error) { guildBox.append(el("div", { class: "feedback error" }, `✗ ${list.error}`)); return; }
|
|
552
|
+
if (!list.emojis.length) { guildBox.append(el("div", { class: "hint" }, t("se_noServerEmojis"))); return; }
|
|
553
|
+
// An unavailable emoji (e.g. the server lost the Boost tier it
|
|
554
|
+
// needs) cannot be reacted with; picking it would preview fine and
|
|
555
|
+
// then fail silently on every status. Shown, but not pickable.
|
|
556
|
+
guildBox.append(grid(list.emojis.map(e => el("button", {
|
|
557
|
+
type: "button", class: "sm" + (e.available ? "" : " off"), title: `:${e.name}:${e.available ? "" : ` (${t("se_unavailable")})`}`,
|
|
558
|
+
...(e.available ? {} : { disabled: "disabled", "aria-disabled": "true" }),
|
|
559
|
+
onclick: () => { if (e.available) choose(e.value); },
|
|
560
|
+
}, emojiImg(e.image_url, `:${e.name}:`)))));
|
|
561
|
+
};
|
|
562
|
+
picker.append(el("div", { class: "hint", style: "display:flex;gap:8px;align-items:center" }, t("se_serverEmojis"),
|
|
563
|
+
el("button", { type: "button", class: "sm ghost", onclick: () => load(true) }, "↻")), guildBox);
|
|
564
|
+
load(false);
|
|
565
|
+
}
|
|
566
|
+
picker.append(el("div", { class: "actions" }, el("button", { type: "button", class: "sm ghost", onclick: () => picker.remove() }, t("close"))));
|
|
567
|
+
row.append(picker);
|
|
568
|
+
}
|
|
569
|
+
(async () => {
|
|
570
|
+
const cat = await loadStatusEmojiCatalog();
|
|
571
|
+
keys = cat.keys;
|
|
572
|
+
const builtins = cat.builtins[scope.platform() === "telegram" ? "telegram" : "discord"] || {};
|
|
573
|
+
for (const key of keys) {
|
|
574
|
+
const input = el("input", { type: "text", value: values[key] || "", placeholder: builtins[key] || "", autocomplete: "off" });
|
|
575
|
+
input.addEventListener("input", () => { values[key] = input.value; refresh(); });
|
|
576
|
+
const row = el("div", { class: "se-row" });
|
|
577
|
+
row.append(el("label", {}, t(`se_${key}`)), input,
|
|
578
|
+
el("button", { type: "button", class: "sm", onclick: () => openPicker(row, key, input) }, t("se_pick")),
|
|
579
|
+
el("button", { type: "button", class: "sm ghost", title: t("se_useDefault"), onclick: () => { input.value = ""; values[key] = ""; refresh(); } }, "×"));
|
|
580
|
+
rows.append(row);
|
|
581
|
+
}
|
|
582
|
+
runPreview();
|
|
583
|
+
})();
|
|
584
|
+
box.append(preview, problems, rows, el("div", { class: "hint" }, t("se_hint")));
|
|
585
|
+
return { box, refresh, value: () => orderedStatusMap(values, keys), baseline: () => orderedStatusMap(own, keys) };
|
|
586
|
+
}
|
|
587
|
+
|
|
454
588
|
/**
|
|
455
589
|
* One modal edits one object, and closing it stages a change — it never
|
|
456
590
|
* writes. Applying stays where it was: the Apply button on the pending bar.
|
|
@@ -980,6 +1114,15 @@
|
|
|
980
1114
|
const fallbackValue = Array.isArray(inst.model_failover) ? (inst.model_failover[0] || "") : (defaults.model_failover?.[0] || "");
|
|
981
1115
|
const failover = overrideControl(inst, "model_failover", el("input", { type: "text", value: fallbackValue, placeholder: "e.g. sonnet" }), defaults.model_failover?.[0] || "");
|
|
982
1116
|
const displayName = overrideControl(inst, "display_name", el("input", { type: "text", value: inst.display_name ?? defaults.display_name ?? "", placeholder: name }), defaults.display_name || "");
|
|
1117
|
+
// The override sits on top of the bound channel's map, so the preview is
|
|
1118
|
+
// resolved against whichever channel the form currently binds.
|
|
1119
|
+
const boundChannelId = () => fChan.value || channelIds()[0] || null;
|
|
1120
|
+
const statusEmojis = statusEmojiEditor(inst.status_emojis, {
|
|
1121
|
+
platform: () => chById(boundChannelId())?.type,
|
|
1122
|
+
channelId: boundChannelId,
|
|
1123
|
+
previewBody: map => ({ channel_id: boundChannelId(), instance_config: map }),
|
|
1124
|
+
});
|
|
1125
|
+
fChan.addEventListener("change", statusEmojis.refresh);
|
|
983
1126
|
const autoFeedback = el("div", { class: "feedback" });
|
|
984
1127
|
const hangFeedback = el("div", { class: "feedback" });
|
|
985
1128
|
const failoverFeedback = el("div", { class: "feedback" });
|
|
@@ -1016,6 +1159,7 @@
|
|
|
1016
1159
|
lightweight: lightweight.toggle.checked ? null : lightweight.input.checked,
|
|
1017
1160
|
model_failover: failover.toggle.checked ? null : (failover.input.value.trim() ? [failover.input.value.trim()] : []),
|
|
1018
1161
|
display_name: displayName.toggle.checked ? null : displayName.input.value.trim(),
|
|
1162
|
+
status_emojis: statusEmojis.value(),
|
|
1019
1163
|
};
|
|
1020
1164
|
const patch = changedFields(nextPatch, {
|
|
1021
1165
|
backend: inst.backend || defaults.backend || "claude-code",
|
|
@@ -1036,6 +1180,7 @@
|
|
|
1036
1180
|
lightweight: hasOwn(inst, "lightweight") ? inst.lightweight : null,
|
|
1037
1181
|
model_failover: hasOwn(inst, "model_failover") ? inst.model_failover : null,
|
|
1038
1182
|
display_name: hasOwn(inst, "display_name") ? inst.display_name : null,
|
|
1183
|
+
status_emojis: statusEmojis.baseline(),
|
|
1039
1184
|
});
|
|
1040
1185
|
state.fleet.instances[name] = { ...inst, ...patch };
|
|
1041
1186
|
for (const [key, value] of Object.entries(patch)) if (value === null) delete state.fleet.instances[name][key];
|
|
@@ -1074,6 +1219,8 @@
|
|
|
1074
1219
|
el("label", {}, t("systemPrompt"), impact("instance.systemPrompt")), fSys,
|
|
1075
1220
|
el("label", {}, t("tags"), impact("instance.tags")), chipList(tags, null, "tag"),
|
|
1076
1221
|
el("label", { style: "display:flex;align-items:center;gap:8px;cursor:pointer" }, fGeneral, el("span", {}, t("generalTopicField")), impact("instance.general_topic"))),
|
|
1222
|
+
drawer(t("statusEmojis"),
|
|
1223
|
+
el("label", {}, t("statusEmojisInstance"), impact("instance.status_emojis")), statusEmojis.box),
|
|
1077
1224
|
el("div", { class: "actions" }, msg),
|
|
1078
1225
|
);
|
|
1079
1226
|
validate();
|
|
@@ -1093,7 +1240,7 @@
|
|
|
1093
1240
|
"instance.tool_set", "instance.tool_progress",
|
|
1094
1241
|
"instance.reply_completion_guard", "instance.log_level",
|
|
1095
1242
|
"instance.lightweight", "instance.model_failover", "instance.systemPrompt",
|
|
1096
|
-
"instance.tags", "instance.general_topic",
|
|
1243
|
+
"instance.tags", "instance.general_topic", "instance.status_emojis",
|
|
1097
1244
|
];
|
|
1098
1245
|
async function toggleAgent(name, running) {
|
|
1099
1246
|
if (running) await api(`/stop/${encodeURIComponent(name)}`, { method: "POST" });
|
|
@@ -1328,6 +1475,11 @@
|
|
|
1328
1475
|
});
|
|
1329
1476
|
secretFeedback.className = "feedback"; secretFeedback.textContent = "Queued — click Apply changes.";
|
|
1330
1477
|
};
|
|
1478
|
+
const statusEmojis = statusEmojiEditor(ch.options?.status_emojis, {
|
|
1479
|
+
platform: () => type,
|
|
1480
|
+
channelId: () => connectionId,
|
|
1481
|
+
previewBody: map => ({ channel_id: connectionId, platform: type, channel_config: map }),
|
|
1482
|
+
});
|
|
1331
1483
|
const validateAccess = () => setValidation(fMode, accessFeedback, "", fMode.value === "locked" && users.length === 0 ? t("accessLockedEmpty") : "");
|
|
1332
1484
|
fMode.addEventListener("change", validateAccess);
|
|
1333
1485
|
box.append(
|
|
@@ -1340,6 +1492,8 @@
|
|
|
1340
1492
|
el("label", {}, type === "discord" ? "General channel id" : "Forum topic id"), fGeneral,
|
|
1341
1493
|
el("div", { class: "hint" }, "The target is checked with the existing bot token before any binding is changed."),
|
|
1342
1494
|
el("div", { class: "actions" }, bindingVerify, bindingFeedback)),
|
|
1495
|
+
drawer(t("statusEmojis"),
|
|
1496
|
+
el("label", {}, t("statusEmojisChannel"), impact("fleet.channel.options.status_emojis")), statusEmojis.box),
|
|
1343
1497
|
drawer(t("advancedSection"),
|
|
1344
1498
|
el("div", { class: "field" }, el("label", {}, t("tokenEnv")), el("div", { class: "sub" }, ch.bot_token_env || "(no token env)")),
|
|
1345
1499
|
el("div", { class: "field" },
|
|
@@ -1352,7 +1506,7 @@
|
|
|
1352
1506
|
validateAccess();
|
|
1353
1507
|
return {
|
|
1354
1508
|
box,
|
|
1355
|
-
impacts: ["fleet.channel.access.mode", "fleet.channel.access.allowed_users", "fleet.channels"],
|
|
1509
|
+
impacts: ["fleet.channel.access.mode", "fleet.channel.access.allowed_users", "fleet.channels", "fleet.channel.options.status_emojis"],
|
|
1356
1510
|
// Staged, not saved: the old form wrote channels straight to the API from
|
|
1357
1511
|
// inside the editor, which is the one thing a modal must not do.
|
|
1358
1512
|
stage: () => {
|
|
@@ -1361,13 +1515,21 @@
|
|
|
1361
1515
|
const stagedAllowedUsers = [...users];
|
|
1362
1516
|
const nextChannels = structuredClone(originalChannels);
|
|
1363
1517
|
nextChannels[i] = { ...nextChannels[i], access: { ...nextChannels[i].access, mode: stagedAccessMode, allowed_users: stagedAllowedUsers } };
|
|
1364
|
-
|
|
1518
|
+
const accessChanged = !sameValue(nextChannels, originalChannels);
|
|
1519
|
+
const stagedStatusEmojis = statusEmojis.value();
|
|
1520
|
+
const emojisChanged = !sameValue(stagedStatusEmojis, statusEmojis.baseline());
|
|
1521
|
+
if (emojisChanged) {
|
|
1522
|
+
const options = { ...(nextChannels[i].options || {}) };
|
|
1523
|
+
if (stagedStatusEmojis) options.status_emojis = stagedStatusEmojis; else delete options.status_emojis;
|
|
1524
|
+
nextChannels[i] = { ...nextChannels[i], options };
|
|
1525
|
+
}
|
|
1526
|
+
if (!accessChanged && !emojisChanged) return true;
|
|
1365
1527
|
stageChange(`connections:${ch.id || chLabel(i)}`, {
|
|
1366
|
-
label: `Update ${ch.id || chLabel(i)} access`,
|
|
1367
|
-
impact: impactOf("fleet.channels"),
|
|
1528
|
+
label: `Update ${ch.id || chLabel(i)} ${accessChanged && emojisChanged ? "access & status emojis" : accessChanged ? "access" : "status emojis"}`,
|
|
1529
|
+
impact: impactOf(accessChanged ? "fleet.channels" : "fleet.channel.options.status_emojis"),
|
|
1368
1530
|
// Confirm the immutable staged snapshot, not live form controls
|
|
1369
1531
|
// that the user may edit again before pressing global Apply.
|
|
1370
|
-
confirm: () => confirmAccessChange(previousAccess, stagedAccessMode, stagedAllowedUsers),
|
|
1532
|
+
confirm: () => !accessChanged || confirmAccessChange(previousAccess, stagedAccessMode, stagedAllowedUsers),
|
|
1371
1533
|
cancelMessage: t("accessChangeCancelled"),
|
|
1372
1534
|
apply: () => api("/api/settings/fleet/channels", { method: "PUT", body: JSON.stringify(nextChannels) }),
|
|
1373
1535
|
});
|