@songsid/agend 2.1.7-beta.6 → 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 +8 -0
- package/dist/backend/codex.js +106 -0
- package/dist/backend/codex.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/config-validator.js +41 -0
- package/dist/config-validator.js.map +1 -1
- package/dist/daemon.d.ts +3 -0
- package/dist/daemon.js +8 -0
- package/dist/daemon.js.map +1 -1
- package/dist/fleet-manager.d.ts +59 -7
- package/dist/fleet-manager.js +167 -59
- package/dist/fleet-manager.js.map +1 -1
- 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 +30 -21
- package/dist/instance-lifecycle.js.map +1 -1
- package/dist/instructions.d.ts +11 -0
- package/dist/instructions.js +16 -1
- package/dist/instructions.js.map +1 -1
- package/dist/outbound-schemas.d.ts +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 +6 -0
- package/dist/ui/settings.html +168 -6
- package/package.json +1 -1
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configurable delivery-status emojis (#1005).
|
|
3
|
+
*
|
|
4
|
+
* One module owns how a configured status emoji is read, checked, resolved
|
|
5
|
+
* and compared, so the reaction a bot stamps, the reaction it later removes,
|
|
6
|
+
* the reactions the fleet filters out as its own, and the list the
|
|
7
|
+
* instructions tell agents to avoid all come from the same value.
|
|
8
|
+
*
|
|
9
|
+
* Resolution, per status key: instance `status_emojis` → the bound channel's
|
|
10
|
+
* `options.status_emojis` → the built-in value for that platform. The
|
|
11
|
+
* built-ins are exactly what AgEnD stamped before this was configurable.
|
|
12
|
+
*/
|
|
13
|
+
export const STATUS_EMOJI_KEYS = ["received", "queued", "processing", "delivered", "failed"];
|
|
14
|
+
export const STATUS_EMOJI_CONFIG_KEYS = [...STATUS_EMOJI_KEYS, "progress_prefix"];
|
|
15
|
+
/**
|
|
16
|
+
* Telegram accepts only this fixed reaction set (Bot API ReactionTypeEmoji).
|
|
17
|
+
* Spelled as the Bot API spells them: without the U+FE0F variation selector.
|
|
18
|
+
*/
|
|
19
|
+
export const TELEGRAM_REACTION_EMOJIS = new Set([
|
|
20
|
+
"👍", "👎", "❤", "🔥", "🥰", "👏", "😁", "🤔", "🤯", "😱", "🤬", "😢", "🎉", "🤩", "🤮", "💩", "🙏", "👌", "🕊", "🤡",
|
|
21
|
+
"🥱", "🥴", "😍", "🐳", "❤🔥", "🌚", "🌭", "💯", "🤣", "⚡", "🍌", "🏆", "💔", "🤨", "😐", "🍓", "🍾", "💋", "🖕", "😈",
|
|
22
|
+
"😴", "😭", "🤓", "👻", "👨💻", "👀", "🎃", "🙈", "😇", "😨", "🤝", "✍", "🤗", "🫡", "🎅", "🎄", "☃", "💅", "🤪", "🗿",
|
|
23
|
+
"🆒", "💘", "🙉", "🦄", "😘", "💊", "🙊", "😎", "👾", "🤷♂", "🤷", "🤷♀", "😡",
|
|
24
|
+
]);
|
|
25
|
+
const BUILTIN_DEFAULT = {
|
|
26
|
+
received: "👀", queued: "⏳", processing: "👀", delivered: "✅", failed: "❌", progress_prefix: "👀",
|
|
27
|
+
};
|
|
28
|
+
/** Telegram's smaller vocabulary: ⏳/✅/❌ are not valid Telegram reactions. */
|
|
29
|
+
const BUILTIN_TELEGRAM = {
|
|
30
|
+
received: "👀", queued: "👀", processing: "👀", delivered: "👀", failed: "👎", progress_prefix: "👀",
|
|
31
|
+
};
|
|
32
|
+
export function builtinStatusEmojis(platform) {
|
|
33
|
+
return { ...(platform === "telegram" ? BUILTIN_TELEGRAM : BUILTIN_DEFAULT) };
|
|
34
|
+
}
|
|
35
|
+
const GRAPHEMES = new Intl.Segmenter(undefined, { granularity: "grapheme" });
|
|
36
|
+
function graphemeCount(s) {
|
|
37
|
+
let n = 0;
|
|
38
|
+
for (const _ of GRAPHEMES.segment(s))
|
|
39
|
+
if (++n > 1)
|
|
40
|
+
break;
|
|
41
|
+
return n;
|
|
42
|
+
}
|
|
43
|
+
const CUSTOM_TAG_RE = /^<(a?):([A-Za-z0-9_]{2,32}):(\d{15,25})>$/;
|
|
44
|
+
const CUSTOM_BARE_RE = /^(?:(a):)?([A-Za-z0-9_]{2,32}):(\d{15,25})$/;
|
|
45
|
+
const PICTOGRAPH_RE = /\p{Extended_Pictographic}|\p{Regional_Indicator}|\u20E3/u;
|
|
46
|
+
/**
|
|
47
|
+
* Parse one configured value. Discord custom emoji may be written `<:name:id>`,
|
|
48
|
+
* `<a:name:id>` or bare `name:id`; anything else must be exactly one emoji
|
|
49
|
+
* grapheme (no spaces, no words, not two emojis). Returns null otherwise.
|
|
50
|
+
*/
|
|
51
|
+
export function normalizeEmoji(raw) {
|
|
52
|
+
if (typeof raw !== "string")
|
|
53
|
+
return null;
|
|
54
|
+
const s = raw.trim();
|
|
55
|
+
if (!s)
|
|
56
|
+
return null;
|
|
57
|
+
const custom = CUSTOM_TAG_RE.exec(s) ?? CUSTOM_BARE_RE.exec(s);
|
|
58
|
+
if (custom)
|
|
59
|
+
return { kind: "custom", animated: custom[1] === "a", name: custom[2], id: custom[3] };
|
|
60
|
+
// Exactly one grapheme: 👀✅ is two emojis that no platform can stamp as
|
|
61
|
+
// one reaction — accepting it lost that status silently. Segmenting by
|
|
62
|
+
// grapheme (not code points) keeps ZWJ sequences (👨👩👧), flags (🇹🇼) and
|
|
63
|
+
// skin tones (👍🏽) as the single emoji they are.
|
|
64
|
+
if (/[\sA-Za-z0-9<>:]/.test(s) || !PICTOGRAPH_RE.test(s) || graphemeCount(s) !== 1)
|
|
65
|
+
return null;
|
|
66
|
+
return { kind: "unicode", value: s };
|
|
67
|
+
}
|
|
68
|
+
const stripVariation = (s) => s.replace(/\uFE0F/g, "");
|
|
69
|
+
/**
|
|
70
|
+
* Why a value cannot be used on `platform`, or null when it can. Telegram has
|
|
71
|
+
* no custom emoji and only the fixed reaction set; Discord takes any emoji.
|
|
72
|
+
* `progress_prefix` is message text, not a reaction, so any emoji works there.
|
|
73
|
+
*/
|
|
74
|
+
export function statusEmojiProblem(platform, key, raw) {
|
|
75
|
+
const e = normalizeEmoji(raw);
|
|
76
|
+
if (!e)
|
|
77
|
+
return `"${String(raw)}" is not an emoji (use a single emoji, or <:name:id> for a Discord server emoji)`;
|
|
78
|
+
if (platform !== "telegram")
|
|
79
|
+
return null;
|
|
80
|
+
if (e.kind === "custom")
|
|
81
|
+
return "Telegram has no server custom emoji";
|
|
82
|
+
if (key === "progress_prefix")
|
|
83
|
+
return null;
|
|
84
|
+
if (!TELEGRAM_REACTION_EMOJIS.has(stripVariation(e.value)))
|
|
85
|
+
return `"${e.value}" is not in Telegram's allowed reaction set`;
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* The string handed to adapter.react()/unreact(). The same form must go to
|
|
90
|
+
* both, or the old status reaction is never removed when the status advances.
|
|
91
|
+
* Discord's REST route and discord.js both take `name:id` for a custom emoji;
|
|
92
|
+
* Telegram wants its own spelling of the emoji (no variation selector).
|
|
93
|
+
*/
|
|
94
|
+
export function reactionForm(platform, raw) {
|
|
95
|
+
const e = normalizeEmoji(raw);
|
|
96
|
+
if (!e)
|
|
97
|
+
return raw;
|
|
98
|
+
if (e.kind === "custom")
|
|
99
|
+
return `${e.name}:${e.id}`;
|
|
100
|
+
return platform === "telegram" ? stripVariation(e.value) : e.value;
|
|
101
|
+
}
|
|
102
|
+
/** How a status emoji reads in text: custom emoji as `:name:`, not `<:name:id>`. */
|
|
103
|
+
export function displayForm(raw) {
|
|
104
|
+
const e = normalizeEmoji(raw);
|
|
105
|
+
if (!e)
|
|
106
|
+
return raw;
|
|
107
|
+
return e.kind === "custom" ? `:${e.name}:` : e.value;
|
|
108
|
+
}
|
|
109
|
+
/** Message-text form (progress bubble): Discord renders `<:name:id>` inline. */
|
|
110
|
+
export function textForm(platform, raw) {
|
|
111
|
+
const e = normalizeEmoji(raw);
|
|
112
|
+
if (!e)
|
|
113
|
+
return raw;
|
|
114
|
+
if (e.kind === "custom")
|
|
115
|
+
return platform === "discord" ? `<${e.animated ? "a" : ""}:${e.name}:${e.id}>` : `:${e.name}:`;
|
|
116
|
+
return e.value;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Comparison key for "is this reaction one of the status emojis": a custom
|
|
120
|
+
* emoji by id (its name can be renamed and is not unique), a unicode emoji
|
|
121
|
+
* without variation selectors (👁️ and 👁 are the same reaction).
|
|
122
|
+
*/
|
|
123
|
+
export function statusMatchKey(raw) {
|
|
124
|
+
const e = normalizeEmoji(raw);
|
|
125
|
+
if (e?.kind === "custom")
|
|
126
|
+
return `id:${e.id}`;
|
|
127
|
+
return stripVariation((e?.value ?? raw).trim());
|
|
128
|
+
}
|
|
129
|
+
/** The same key computed from an inbound reaction. */
|
|
130
|
+
export function reactionMatchKey(emoji, emojiId) {
|
|
131
|
+
return emojiId ? `id:${emojiId}` : stripVariation(emoji.trim());
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Resolve every status key. An unusable value never fails a reaction: it is
|
|
135
|
+
* reported through onInvalid and that key falls through to the next layer.
|
|
136
|
+
*/
|
|
137
|
+
export function resolveStatusEmojis(input) {
|
|
138
|
+
const out = builtinStatusEmojis(input.platform);
|
|
139
|
+
const layers = [["platform", input.platformConfig], ["instance", input.instanceConfig]];
|
|
140
|
+
for (const [source, cfg] of layers) {
|
|
141
|
+
if (!cfg || typeof cfg !== "object" || Array.isArray(cfg))
|
|
142
|
+
continue;
|
|
143
|
+
for (const key of STATUS_EMOJI_CONFIG_KEYS) {
|
|
144
|
+
const value = cfg[key];
|
|
145
|
+
if (value === undefined || value === null || value === "")
|
|
146
|
+
continue;
|
|
147
|
+
const problem = statusEmojiProblem(input.platform, key, value);
|
|
148
|
+
if (problem) {
|
|
149
|
+
input.onInvalid?.(source, key, value, problem);
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
out[key] = value.trim();
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return out;
|
|
156
|
+
}
|
|
157
|
+
/** The status reactions (not progress_prefix), deduplicated, in display form. */
|
|
158
|
+
export function statusAvoidList(resolved) {
|
|
159
|
+
return [...new Set(STATUS_EMOJI_KEYS.map(k => displayForm(resolved[k])))];
|
|
160
|
+
}
|
|
161
|
+
/** Match keys for every status reaction in `resolved`. */
|
|
162
|
+
export function statusMatchKeys(resolved) {
|
|
163
|
+
return STATUS_EMOJI_KEYS.map(k => statusMatchKey(resolved[k]));
|
|
164
|
+
}
|
|
165
|
+
// ── Settings picker / preview (#1005 phase 3.2) ─────────────────────────────
|
|
166
|
+
// The page asks the server to resolve and render, so what it previews is what
|
|
167
|
+
// resolveStatusEmojis and reactionForm produce — picked == previewed == reacted.
|
|
168
|
+
/** A handful of unicode status emojis to offer on Discord (Telegram offers its whole set). */
|
|
169
|
+
export const STATUS_EMOJI_SUGGESTIONS = [
|
|
170
|
+
"👀", "⏳", "✅", "❌", "📥", "📨", "📬", "📭", "🟢", "🟡", "🔴", "⚪", "🔵", "✔️", "☑️", "✖️", "⚠️", "🚫",
|
|
171
|
+
"🔄", "⏱️", "⌛", "💬", "💭", "🧠", "⚙️", "🛠️", "🚀", "🎯", "🏁", "🎉", "👍", "👎", "👌", "🙏", "🤔", "🫡",
|
|
172
|
+
"🦊", "🍎", "🐱", "🐶", "🐼", "🦉", "🐙", "🌟", "⭐", "🔥", "💡", "📌",
|
|
173
|
+
];
|
|
174
|
+
/** Stored config form of a Discord server emoji. */
|
|
175
|
+
export function customEmojiValue(e) {
|
|
176
|
+
return `<${e.animated ? "a" : ""}:${e.name}:${e.id}>`;
|
|
177
|
+
}
|
|
178
|
+
/** Discord's public CDN renders a known custom emoji id without any token. */
|
|
179
|
+
export function emojiImageUrl(raw) {
|
|
180
|
+
const e = normalizeEmoji(raw);
|
|
181
|
+
if (e?.kind !== "custom")
|
|
182
|
+
return null;
|
|
183
|
+
return `https://cdn.discordapp.com/emojis/${e.id}.${e.animated ? "gif" : "png"}`;
|
|
184
|
+
}
|
|
185
|
+
export function previewStatusEmojis(input) {
|
|
186
|
+
const problems = [];
|
|
187
|
+
const resolved = resolveStatusEmojis({ ...input, onInvalid: (source, key, value, problem) => problems.push({ source, key, value, problem }) });
|
|
188
|
+
const usable = (cfg, key) => {
|
|
189
|
+
if (!cfg || typeof cfg !== "object" || Array.isArray(cfg))
|
|
190
|
+
return false;
|
|
191
|
+
const v = cfg[key];
|
|
192
|
+
return v !== undefined && v !== null && v !== "" && !statusEmojiProblem(input.platform, key, v);
|
|
193
|
+
};
|
|
194
|
+
const entries = STATUS_EMOJI_CONFIG_KEYS.map((key) => {
|
|
195
|
+
const value = resolved[key];
|
|
196
|
+
return {
|
|
197
|
+
key, value,
|
|
198
|
+
source: usable(input.instanceConfig, key) ? "instance" : usable(input.platformConfig, key) ? "platform" : "builtin",
|
|
199
|
+
applied: key === "progress_prefix" ? textForm(input.platform, value) : reactionForm(input.platform, value),
|
|
200
|
+
display: displayForm(value),
|
|
201
|
+
image_url: emojiImageUrl(value),
|
|
202
|
+
};
|
|
203
|
+
});
|
|
204
|
+
return { platform: input.platform, entries, problems, avoid: statusAvoidList(resolved) };
|
|
205
|
+
}
|
|
206
|
+
//# sourceMappingURL=status-emojis.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"status-emojis.js","sourceRoot":"","sources":["../src/status-emojis.ts"],"names":[],"mappings":"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"}
|
package/dist/types.d.ts
CHANGED
|
@@ -89,6 +89,12 @@ export interface InstanceConfig {
|
|
|
89
89
|
warm_cap?: number;
|
|
90
90
|
/** Agent display name (e.g. "Kuro", "Luna") — chosen by the agent itself */
|
|
91
91
|
display_name?: string;
|
|
92
|
+
/**
|
|
93
|
+
* #1005: this instance's own delivery-status emojis, overriding its
|
|
94
|
+
* channel's `options.status_emojis` per key. Keys: received, queued,
|
|
95
|
+
* processing, delivered, failed, progress_prefix.
|
|
96
|
+
*/
|
|
97
|
+
status_emojis?: import("./status-emojis.js").StatusEmojiConfig;
|
|
92
98
|
/** Human-readable description of what this instance does */
|
|
93
99
|
description?: string;
|
|
94
100
|
/** Tags for capability discovery (e.g. ["code-reviewer", "researcher", "executor"]) */
|
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
|
});
|