bosun 0.36.0 → 0.36.2

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.
Files changed (98) hide show
  1. package/.env.example +98 -16
  2. package/README.md +27 -0
  3. package/agent-event-bus.mjs +5 -5
  4. package/agent-pool.mjs +129 -12
  5. package/agent-prompts.mjs +7 -1
  6. package/agent-sdk.mjs +13 -2
  7. package/agent-supervisor.mjs +2 -2
  8. package/agent-work-report.mjs +1 -1
  9. package/anomaly-detector.mjs +6 -6
  10. package/autofix.mjs +15 -15
  11. package/bosun-skills.mjs +4 -4
  12. package/bosun.schema.json +160 -4
  13. package/claude-shell.mjs +11 -11
  14. package/cli.mjs +21 -21
  15. package/codex-config.mjs +19 -19
  16. package/codex-shell.mjs +180 -29
  17. package/config-doctor.mjs +27 -2
  18. package/config.mjs +60 -7
  19. package/copilot-shell.mjs +4 -4
  20. package/error-detector.mjs +1 -1
  21. package/fleet-coordinator.mjs +2 -2
  22. package/gemini-shell.mjs +692 -0
  23. package/github-oauth-portal.mjs +1 -1
  24. package/github-reconciler.mjs +2 -2
  25. package/kanban-adapter.mjs +741 -168
  26. package/merge-strategy.mjs +25 -25
  27. package/monitor.mjs +123 -105
  28. package/opencode-shell.mjs +22 -22
  29. package/package.json +7 -1
  30. package/postinstall.mjs +22 -22
  31. package/pr-cleanup-daemon.mjs +6 -6
  32. package/prepublish-check.mjs +4 -4
  33. package/presence.mjs +2 -2
  34. package/primary-agent.mjs +85 -7
  35. package/publish.mjs +1 -1
  36. package/review-agent.mjs +1 -1
  37. package/session-tracker.mjs +11 -0
  38. package/setup-web-server.mjs +429 -21
  39. package/setup.mjs +367 -12
  40. package/shared-knowledge.mjs +1 -1
  41. package/startup-service.mjs +9 -9
  42. package/stream-resilience.mjs +58 -4
  43. package/sync-engine.mjs +2 -2
  44. package/task-assessment.mjs +9 -9
  45. package/task-cli.mjs +1 -1
  46. package/task-complexity.mjs +71 -2
  47. package/task-context.mjs +1 -2
  48. package/task-executor.mjs +104 -41
  49. package/telegram-bot.mjs +825 -494
  50. package/telegram-sentinel.mjs +28 -28
  51. package/ui/app.js +256 -23
  52. package/ui/app.monolith.js +1 -1
  53. package/ui/components/agent-selector.js +4 -3
  54. package/ui/components/chat-view.js +101 -28
  55. package/ui/components/diff-viewer.js +3 -3
  56. package/ui/components/kanban-board.js +3 -3
  57. package/ui/components/session-list.js +255 -35
  58. package/ui/components/workspace-switcher.js +3 -3
  59. package/ui/demo.html +209 -194
  60. package/ui/index.html +3 -3
  61. package/ui/modules/icon-utils.js +206 -142
  62. package/ui/modules/icons.js +2 -27
  63. package/ui/modules/settings-schema.js +29 -5
  64. package/ui/modules/streaming.js +30 -2
  65. package/ui/modules/vision-stream.js +275 -0
  66. package/ui/modules/voice-client.js +102 -9
  67. package/ui/modules/voice-fallback.js +62 -6
  68. package/ui/modules/voice-overlay.js +594 -59
  69. package/ui/modules/voice.js +31 -38
  70. package/ui/setup.html +284 -34
  71. package/ui/styles/components.css +47 -0
  72. package/ui/styles/sessions.css +75 -0
  73. package/ui/tabs/agents.js +73 -43
  74. package/ui/tabs/chat.js +37 -40
  75. package/ui/tabs/control.js +2 -2
  76. package/ui/tabs/dashboard.js +1 -1
  77. package/ui/tabs/infra.js +10 -10
  78. package/ui/tabs/library.js +8 -8
  79. package/ui/tabs/logs.js +10 -10
  80. package/ui/tabs/settings.js +20 -20
  81. package/ui/tabs/tasks.js +76 -47
  82. package/ui-server.mjs +1761 -124
  83. package/update-check.mjs +13 -13
  84. package/ve-kanban.mjs +1 -1
  85. package/whatsapp-channel.mjs +5 -5
  86. package/workflow-engine.mjs +20 -1
  87. package/workflow-nodes.mjs +904 -4
  88. package/workflow-templates/agents.mjs +321 -7
  89. package/workflow-templates/ci-cd.mjs +6 -6
  90. package/workflow-templates/github.mjs +156 -84
  91. package/workflow-templates/planning.mjs +8 -8
  92. package/workflow-templates/reliability.mjs +8 -8
  93. package/workflow-templates/security.mjs +3 -3
  94. package/workflow-templates.mjs +15 -9
  95. package/workspace-manager.mjs +85 -1
  96. package/workspace-monitor.mjs +2 -2
  97. package/workspace-registry.mjs +2 -2
  98. package/worktree-manager.mjs +1 -1
package/telegram-bot.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * telegram-bot.mjs — Two-way Telegram primary agent for bosun.
2
+ * telegram-bot.mjs — Two-way Telegram :workflow: primary agent for bosun.
3
3
  *
4
4
  * Polls Telegram Bot API for incoming messages, routes slash commands to
5
5
  * built-in handlers, and forwards free-text to the persistent primary agent.
@@ -900,17 +900,17 @@ const uiInputRequests = new Map();
900
900
  * users connecting from outside the local network.
901
901
  */
902
902
  function getBrowserUiUrl() {
903
- const base = telegramUiUrl;
904
- if (!base) return null;
905
903
  const token = getSessionToken();
906
-
907
- // 1. Prefer the cloudflare tunnel when available — it's publicly reachable
908
- // and is the only URL guaranteed to work from Telegram / mobile.
909
- const tUrl = getTunnelUrl();
910
- if (tUrl) {
911
- return appendTokenToUrl(tUrl, token) || tUrl;
904
+ const tunnelUrl = getTunnelUrl();
905
+ if (tunnelUrl) {
906
+ return appendTokenToUrl(tunnelUrl, token) || tunnelUrl;
912
907
  }
913
908
 
909
+ const base = telegramUiUrl || getTelegramUiUrl?.() || null;
910
+ if (!base) return null;
911
+
912
+ // 1. Tunnel URL already checked above.
913
+
914
914
  // 2. Fall back to configured/explicit URL
915
915
  const explicit =
916
916
  process.env.TELEGRAM_WEBAPP_URL || process.env.TELEGRAM_UI_BASE_URL || "";
@@ -959,9 +959,9 @@ function isTelegramInlineButtonUrlAllowed(inputUrl) {
959
959
  }
960
960
 
961
961
  function getBrowserUiUrlOptions({ forTelegramButtons = true } = {}) {
962
- const base = String(telegramUiUrl || "").trim();
963
- if (!base) return [];
964
-
962
+ const tunnelUrl = getTunnelUrl();
963
+ const base = String(telegramUiUrl || getTelegramUiUrl?.() || tunnelUrl || "").trim();
964
+ if (!base && !tunnelUrl) return [];
965
965
  const token = getSessionToken();
966
966
  const options = [];
967
967
  const seen = new Set();
@@ -985,32 +985,88 @@ function getBrowserUiUrlOptions({ forTelegramButtons = true } = {}) {
985
985
  parsed = null;
986
986
  }
987
987
 
988
+ if (tunnelUrl) {
989
+ let label = ":globe: Cloudflare";
990
+ try {
991
+ const host = String(new URL(tunnelUrl).hostname || "").toLowerCase();
992
+ label = host.endsWith(".trycloudflare.com")
993
+ ? ":globe: Cloudflare (Quick)"
994
+ : ":globe: Cloudflare (Permanent)";
995
+ } catch {
996
+ // keep default label
997
+ }
998
+ add(label, tunnelUrl);
999
+ }
1000
+
988
1001
  if (parsed) {
989
1002
  const localhostUrl = `${parsed.protocol}//localhost${parsed.port ? `:${parsed.port}` : ""}`;
990
- add("🖥️ Localhost", localhostUrl);
1003
+ add(":monitor: Localhost", localhostUrl);
991
1004
  }
992
1005
 
993
1006
  if (parsed) {
994
1007
  const lanIp = getLocalLanIp?.();
995
1008
  if (lanIp && parsed.port) {
996
1009
  const lanUrl = `${parsed.protocol}//${lanIp}:${parsed.port}`;
997
- add("📶 LAN", lanUrl);
1010
+ add(":chart: LAN", lanUrl);
998
1011
  }
999
1012
  }
1000
1013
 
1001
- const tunnelUrl = getTunnelUrl();
1002
- if (tunnelUrl) {
1003
- add("☁️ Cloudflare", tunnelUrl);
1014
+ if (options.length === 0 && base) {
1015
+ add(":globe: Browser URL", base);
1004
1016
  }
1017
+ return options;
1018
+ }
1005
1019
 
1006
- if (options.length === 0) {
1007
- add("🌐 Browser URL", base);
1020
+ function normalizeMeetingCallType(raw) {
1021
+ const value = String(raw || "").trim().toLowerCase();
1022
+ if (!value) return "voice";
1023
+ if (value === "video" || value === "videocall") return "video";
1024
+ if (/\bvideo\b/.test(value)) return "video";
1025
+ return "voice";
1026
+ }
1027
+
1028
+ function appendQueryParams(inputUrl, params = {}) {
1029
+ const raw = String(inputUrl || "").trim();
1030
+ if (!raw) return null;
1031
+ try {
1032
+ const url = new URL(raw);
1033
+ for (const [key, value] of Object.entries(params || {})) {
1034
+ const next = String(value ?? "").trim();
1035
+ if (!next) continue;
1036
+ url.searchParams.set(key, next);
1037
+ }
1038
+ return url.toString();
1039
+ } catch {
1040
+ return raw;
1008
1041
  }
1009
- return options;
1042
+ }
1043
+
1044
+ function buildMeetingLaunchUrl(baseUrl, callType = "voice", extra = {}) {
1045
+ const normalizedCall = normalizeMeetingCallType(callType);
1046
+ return appendQueryParams(baseUrl, {
1047
+ launch: "meeting",
1048
+ call: normalizedCall,
1049
+ autostart: "1",
1050
+ source: "telegram",
1051
+ ...extra,
1052
+ });
1053
+ }
1054
+
1055
+ function getMeetingWebAppUrl(callType = "voice", extra = {}) {
1056
+ const base = telegramWebAppUrl || getTelegramWebAppUrl(telegramUiUrl);
1057
+ if (!base) return null;
1058
+ return buildMeetingLaunchUrl(base, callType, extra);
1059
+ }
1060
+
1061
+ function getMeetingBrowserUrlOptions(callType = "voice", extra = {}) {
1062
+ return getBrowserUiUrlOptions().map((option) => ({
1063
+ label: option.label,
1064
+ url: buildMeetingLaunchUrl(option.url, callType, extra),
1065
+ }));
1010
1066
  }
1011
1067
 
1012
1068
  function syncUiUrlsFromServer() {
1013
- const currentUiUrl = getTelegramUiUrl?.() || null;
1069
+ const currentUiUrl = getTunnelUrl() || getTelegramUiUrl?.() || null;
1014
1070
  telegramUiUrl = currentUiUrl;
1015
1071
  telegramWebAppUrl = getTelegramWebAppUrl(currentUiUrl);
1016
1072
  return {
@@ -1382,9 +1438,10 @@ async function sendReply(chatId, text, options = {}) {
1382
1438
  async function sendDirect(chatId, text, options = {}) {
1383
1439
  if (!telegramToken) return null;
1384
1440
  const skipSticky = options.skipSticky;
1441
+ const normalizedText = formatTelegramIconTokens(text, { button: false });
1385
1442
 
1386
1443
  // Split long messages
1387
- const chunks = splitMessage(text, MAX_MESSAGE_LEN);
1444
+ const chunks = splitMessage(normalizedText, MAX_MESSAGE_LEN);
1388
1445
  let lastMessageId = null;
1389
1446
  for (const chunk of chunks) {
1390
1447
  const payload = {
@@ -1454,12 +1511,13 @@ async function sendDirect(chatId, text, options = {}) {
1454
1511
  */
1455
1512
  async function editDirect(chatId, messageId, text, options = {}) {
1456
1513
  if (!telegramToken || !messageId) return messageId;
1514
+ const normalizedText = formatTelegramIconTokens(text, { button: false });
1457
1515
 
1458
1516
  // Telegram editMessageText has 4096 char limit — truncate if needed
1459
1517
  const truncated =
1460
- text.length > MAX_MESSAGE_LEN
1461
- ? text.slice(0, MAX_MESSAGE_LEN - 20) + "\n\n…(truncated)"
1462
- : text;
1518
+ normalizedText.length > MAX_MESSAGE_LEN
1519
+ ? normalizedText.slice(0, MAX_MESSAGE_LEN - 20) + "\n\n…(truncated)"
1520
+ : normalizedText;
1463
1521
 
1464
1522
  const payload = {
1465
1523
  chat_id: chatId,
@@ -1784,31 +1842,31 @@ function summarizeAction(event) {
1784
1842
  const desc = summarizeCommand(item.command);
1785
1843
  const target = extractTarget(item.command);
1786
1844
  return {
1787
- icon: "",
1845
+ icon: ":zap:",
1788
1846
  text: target ? `${desc} → ${target}` : desc,
1789
1847
  phase: "running",
1790
1848
  };
1791
1849
  }
1792
1850
  case "mcp_tool_call":
1793
1851
  return {
1794
- icon: "🔌",
1852
+ icon: ":plug:",
1795
1853
  text: `MCP: ${item.server}/${item.tool}`,
1796
1854
  phase: "running",
1797
1855
  };
1798
1856
  case "reasoning":
1799
1857
  return item.text
1800
- ? { icon: "💭", text: item.text.slice(0, 200), phase: "thinking" }
1858
+ ? { icon: ":u1f4ad:", text: item.text.slice(0, 200), phase: "thinking" }
1801
1859
  : null;
1802
1860
  case "web_search":
1803
1861
  return {
1804
- icon: "🔍",
1862
+ icon: ":search:",
1805
1863
  text: `Searching: ${item.query?.slice(0, 80)}`,
1806
1864
  phase: "searching",
1807
1865
  };
1808
1866
  case "todo_list":
1809
1867
  return item.items?.length
1810
1868
  ? {
1811
- icon: "📋",
1869
+ icon: ":clipboard:",
1812
1870
  text: `Planning ${item.items.length} steps`,
1813
1871
  phase: "planning",
1814
1872
  }
@@ -1827,7 +1885,7 @@ function summarizeAction(event) {
1827
1885
  const target = extractTarget(item.command);
1828
1886
  const label = target ? `${desc} → ${target}` : desc;
1829
1887
  return {
1830
- icon: ok ? "" : "",
1888
+ icon: ok ? ":check:" : ":close:",
1831
1889
  text: label + (ok ? "" : ` (exit ${item.exit_code})`),
1832
1890
  phase: "done",
1833
1891
  };
@@ -1837,7 +1895,7 @@ function summarizeAction(event) {
1837
1895
  const fileDescs = item.changes.map((c) => {
1838
1896
  const name = shortPath(c.path);
1839
1897
  const kind =
1840
- c.kind === "add" ? "" : c.kind === "delete" ? "🗑️" : "✏️";
1898
+ c.kind === "add" ? ":plus:" : c.kind === "delete" ? ":trash:" : ":edit:";
1841
1899
  // Show line counts if available
1842
1900
  const adds = c.additions ?? c.lines_added ?? 0;
1843
1901
  const dels = c.deletions ?? c.lines_deleted ?? 0;
@@ -1845,7 +1903,7 @@ function summarizeAction(event) {
1845
1903
  return `${kind} ${name}${stats}`;
1846
1904
  });
1847
1905
  return {
1848
- icon: "📁",
1906
+ icon: ":folder:",
1849
1907
  text: fileDescs.join(", "),
1850
1908
  phase: "done",
1851
1909
  detail: "file_change",
@@ -1862,7 +1920,7 @@ function summarizeAction(event) {
1862
1920
  case "mcp_tool_call": {
1863
1921
  const ok = item.status === "completed";
1864
1922
  return {
1865
- icon: ok ? "" : "",
1923
+ icon: ok ? ":check:" : ":close:",
1866
1924
  text: `MCP ${item.server}/${item.tool}: ${ok ? "done" : "failed"}`,
1867
1925
  phase: "done",
1868
1926
  };
@@ -1878,14 +1936,14 @@ function summarizeAction(event) {
1878
1936
  case "assistant.reasoning_delta": {
1879
1937
  const text = event.data?.content || event.data?.deltaContent || "";
1880
1938
  return text
1881
- ? { icon: "💭", text: text.slice(0, 200), phase: "thinking" }
1939
+ ? { icon: ":u1f4ad:", text: text.slice(0, 200), phase: "thinking" }
1882
1940
  : null;
1883
1941
  }
1884
1942
 
1885
1943
  case "tool.execution_start": {
1886
1944
  const { toolName, input } = getCopilotToolInfo(event);
1887
1945
  return {
1888
- icon: "🛠️",
1946
+ icon: ":u1f6e0:",
1889
1947
  text: summarizeCopilotTool(toolName, input),
1890
1948
  phase: "running",
1891
1949
  };
@@ -1899,7 +1957,7 @@ function summarizeAction(event) {
1899
1957
  String(status).toLowerCase(),
1900
1958
  );
1901
1959
  return {
1902
- icon: ok ? "" : "",
1960
+ icon: ok ? ":check:" : ":close:",
1903
1961
  text: summarizeCopilotTool(toolName, input) + (ok ? "" : " (failed)"),
1904
1962
  phase: "done",
1905
1963
  };
@@ -1907,7 +1965,7 @@ function summarizeAction(event) {
1907
1965
 
1908
1966
  case "session.error":
1909
1967
  return {
1910
- icon: "",
1968
+ icon: ":close:",
1911
1969
  text: `Failed: ${event.data?.message || "unknown"}`,
1912
1970
  phase: "error",
1913
1971
  };
@@ -1915,12 +1973,12 @@ function summarizeAction(event) {
1915
1973
  case "item.updated": {
1916
1974
  const item = event.item;
1917
1975
  if (item.type === "reasoning" && item.text) {
1918
- return { icon: "💭", text: item.text.slice(0, 200), phase: "thinking" };
1976
+ return { icon: ":u1f4ad:", text: item.text.slice(0, 200), phase: "thinking" };
1919
1977
  }
1920
1978
  if (item.type === "todo_list" && item.items) {
1921
1979
  const done = item.items.filter((t) => t.completed).length;
1922
1980
  return {
1923
- icon: "📋",
1981
+ icon: ":clipboard:",
1924
1982
  text: `Progress: ${done}/${item.items.length} steps`,
1925
1983
  phase: "planning",
1926
1984
  };
@@ -1930,7 +1988,7 @@ function summarizeAction(event) {
1930
1988
 
1931
1989
  case "turn.failed":
1932
1990
  return {
1933
- icon: "",
1991
+ icon: ":close:",
1934
1992
  text: `Failed: ${event.error?.message || "unknown"}`,
1935
1993
  phase: "error",
1936
1994
  };
@@ -2073,6 +2131,158 @@ function extractGoPackages(command) {
2073
2131
  return unique.slice(0, 3).join(" ") + (unique.length > 3 ? " …" : "");
2074
2132
  }
2075
2133
 
2134
+ const TELEGRAM_ICON_TOKEN_LABELS = Object.freeze({
2135
+ check: "OK",
2136
+ close: "Close",
2137
+ alert: "Alert",
2138
+ pause: "Pause",
2139
+ play: "Run",
2140
+ stop: "Stop",
2141
+ refresh: "Refresh",
2142
+ chart: "Status",
2143
+ clipboard: "Tasks",
2144
+ bot: "Agents",
2145
+ git: "Workspaces",
2146
+ settings: "Executor",
2147
+ server: "Routing",
2148
+ folder: "Logs",
2149
+ file: "Commands",
2150
+ phone: "Control Center",
2151
+ globe: "Browser",
2152
+ heart: "Health",
2153
+ cpu: "Session",
2154
+ chat: "Ask",
2155
+ hash: "Parallel",
2156
+ repeat: "Retry",
2157
+ beaker: "Executors",
2158
+ compass: "Tasks",
2159
+ target: "Coordinator",
2160
+ workflow: "Flow",
2161
+ arrowRight: "Back",
2162
+ plus: "New",
2163
+ menu: "Menu",
2164
+ lock: "Lock",
2165
+ unlock: "Unlock",
2166
+ search: "Search",
2167
+ link: "Link",
2168
+ upload: "Upload",
2169
+ download: "Download",
2170
+ box: "SDK",
2171
+ bell: "Alerts",
2172
+ lightbulb: "Tips",
2173
+ rocket: "Start",
2174
+ home: "Home",
2175
+ pin: "Pin",
2176
+ star: "Star",
2177
+ help: "Help",
2178
+ cloud: "Cloud",
2179
+ });
2180
+
2181
+ const TELEGRAM_ICON_TOKEN_EMOJI = Object.freeze({
2182
+ check: "✅",
2183
+ close: "❌",
2184
+ alert: "⚠️",
2185
+ pause: "⏸️",
2186
+ play: "▶️",
2187
+ stop: "⏹️",
2188
+ refresh: "🔄",
2189
+ chart: "📊",
2190
+ clipboard: "📋",
2191
+ bot: "🤖",
2192
+ git: "🌿",
2193
+ settings: "⚙️",
2194
+ server: "🖧",
2195
+ folder: "📁",
2196
+ file: "📄",
2197
+ phone: "📱",
2198
+ globe: "🌐",
2199
+ heart: "❤️",
2200
+ cpu: "🧠",
2201
+ chat: "💬",
2202
+ hash: "#️⃣",
2203
+ repeat: "🔁",
2204
+ beaker: "🧪",
2205
+ compass: "🧭",
2206
+ target: "🎯",
2207
+ workflow: "🧩",
2208
+ arrowright: "➡️",
2209
+ plus: "➕",
2210
+ menu: "☰",
2211
+ lock: "🔒",
2212
+ unlock: "🔓",
2213
+ search: "🔍",
2214
+ link: "🔗",
2215
+ upload: "📤",
2216
+ download: "📥",
2217
+ box: "📦",
2218
+ bell: "🔔",
2219
+ lightbulb: "💡",
2220
+ rocket: "🚀",
2221
+ home: "🏠",
2222
+ pin: "📌",
2223
+ star: "⭐",
2224
+ help: "❓",
2225
+ cloud: "☁️",
2226
+ monitor: "🖥️",
2227
+ eye: "👁️",
2228
+ edit: "✏️",
2229
+ trash: "🗑️",
2230
+ dot: "•",
2231
+ });
2232
+
2233
+ function decodeUnicodeIconToken(name) {
2234
+ const raw = String(name || "").trim();
2235
+ const match = raw.match(/^u([0-9a-f]{4,6})$/i);
2236
+ if (!match) return "";
2237
+ const codePoint = Number.parseInt(match[1], 16);
2238
+ if (!Number.isFinite(codePoint)) return "";
2239
+ try {
2240
+ return String.fromCodePoint(codePoint);
2241
+ } catch {
2242
+ return "";
2243
+ }
2244
+ }
2245
+
2246
+ function resolveTelegramIconTokenGlyph(name) {
2247
+ const raw = String(name || "").trim();
2248
+ if (!raw) return "";
2249
+ const lowered = raw.toLowerCase();
2250
+ const squashed = lowered.replace(/[_-]+/g, "");
2251
+ const glyph = TELEGRAM_ICON_TOKEN_EMOJI[lowered]
2252
+ || TELEGRAM_ICON_TOKEN_EMOJI[squashed]
2253
+ || decodeUnicodeIconToken(lowered)
2254
+ || decodeUnicodeIconToken(squashed);
2255
+ return glyph || "";
2256
+ }
2257
+
2258
+ function humanizeIconTokenName(name) {
2259
+ const spaced = String(name || "")
2260
+ .replace(/([a-z0-9])([A-Z])/g, "$1 $2")
2261
+ .replace(/[_-]+/g, " ")
2262
+ .trim();
2263
+ if (!spaced) return "";
2264
+ return spaced
2265
+ .split(/\s+/)
2266
+ .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
2267
+ .join(" ");
2268
+ }
2269
+
2270
+ function formatTelegramIconTokens(value, { button = false } = {}) {
2271
+ if (value == null) return value;
2272
+ const str = String(value);
2273
+ return str.replace(/:([a-zA-Z][a-zA-Z0-9_-]*):/g, (_match, rawName) => {
2274
+ const tokenName = String(rawName || "");
2275
+ const glyph = resolveTelegramIconTokenGlyph(tokenName);
2276
+ if (glyph) return glyph;
2277
+ const key = tokenName.toLowerCase();
2278
+ const label = TELEGRAM_ICON_TOKEN_LABELS[tokenName]
2279
+ || TELEGRAM_ICON_TOKEN_LABELS[key]
2280
+ || humanizeIconTokenName(tokenName);
2281
+ if (!label) return "";
2282
+ return button ? label : `[${label}]`;
2283
+ });
2284
+ }
2285
+
2076
2286
  /**
2077
2287
  * Strip web_app buttons whose URL is not HTTPS — Telegram rejects non-HTTPS
2078
2288
  * web_app URLs with a 400 error. Returns the sanitized reply_markup object.
@@ -2081,7 +2291,19 @@ function sanitizeWebAppButtons(markup) {
2081
2291
  if (!markup || !markup.inline_keyboard) return markup;
2082
2292
  const filtered = markup.inline_keyboard
2083
2293
  .map((row) =>
2084
- row.filter((btn) => {
2294
+ row
2295
+ .map((btn) => {
2296
+ if (!btn || typeof btn !== "object") return btn;
2297
+ if (typeof btn.text === "string") {
2298
+ return {
2299
+ ...btn,
2300
+ text: formatTelegramIconTokens(btn.text, { button: true }),
2301
+ };
2302
+ }
2303
+ return btn;
2304
+ })
2305
+ .filter((btn) => {
2306
+ if (!btn || typeof btn !== "object") return false;
2085
2307
  if (btn.web_app && btn.web_app.url) {
2086
2308
  try {
2087
2309
  const u = new URL(btn.web_app.url);
@@ -2090,8 +2312,8 @@ function sanitizeWebAppButtons(markup) {
2090
2312
  return false;
2091
2313
  }
2092
2314
  }
2093
- return true;
2094
- }),
2315
+ return true;
2316
+ }),
2095
2317
  )
2096
2318
  .filter((row) => row.length > 0);
2097
2319
  return { ...markup, inline_keyboard: filtered };
@@ -2360,7 +2582,7 @@ async function handleCallbackQuery(query) {
2360
2582
  if (data === "cb:confirm_restart") {
2361
2583
  await sendReply(
2362
2584
  chatId,
2363
- "⚠️ Restart will stop the orchestrator process and let the monitor respawn it.\nProceed?",
2585
+ ":alert: Restart will stop the orchestrator process and let the monitor respawn it.\nProceed?",
2364
2586
  { reply_markup: buildConfirmKeyboard("cb:do_restart", "Confirm Restart") },
2365
2587
  );
2366
2588
  return;
@@ -2403,7 +2625,7 @@ async function handleCallbackQuery(query) {
2403
2625
  if (query.message?.message_id) {
2404
2626
  await deleteDirect(chatId, query.message.message_id);
2405
2627
  }
2406
- await sendReply(chatId, " That action expired. Please try again.");
2628
+ await sendReply(chatId, ":clock: That action expired. Please try again.");
2407
2629
  return;
2408
2630
  }
2409
2631
  uiTokenRegistry.delete(token);
@@ -2417,12 +2639,12 @@ async function handleCallbackQuery(query) {
2417
2639
  if (data === "fw:open") {
2418
2640
  const fwState = getFirewallState();
2419
2641
  if (!fwState || !fwState.blocked) {
2420
- await sendReply(chatId, " Port is already open or no firewall detected.");
2642
+ await sendReply(chatId, ":check: Port is already open or no firewall detected.");
2421
2643
  return;
2422
2644
  }
2423
2645
  await sendReply(
2424
2646
  chatId,
2425
- `🔧 Attempting to open port via \`${fwState.firewall}\`...\n` +
2647
+ `:settings: Attempting to open port via \`${fwState.firewall}\`...\n` +
2426
2648
  `Please enter your admin password on the server if prompted.`,
2427
2649
  { parseMode: "Markdown" },
2428
2650
  );
@@ -2430,11 +2652,11 @@ async function handleCallbackQuery(query) {
2430
2652
  Number(new URL(getTelegramUiUrl() || "http://localhost:5511").port || 5511),
2431
2653
  );
2432
2654
  if (result.success) {
2433
- await sendReply(chatId, `✅ ${result.message}\nThe Control Center should now be reachable.`);
2655
+ await sendReply(chatId, `:check: ${result.message}\nThe Control Center should now be reachable.`);
2434
2656
  } else {
2435
2657
  await sendReply(
2436
2658
  chatId,
2437
- `⚠️ Auto-fix failed.\n\n${result.message}`,
2659
+ `:alert: Auto-fix failed.\n\n${result.message}`,
2438
2660
  { parseMode: "Markdown" },
2439
2661
  );
2440
2662
  }
@@ -2456,7 +2678,7 @@ async function transcribeAndProcessVoice(voiceFile, chatId) {
2456
2678
  || "";
2457
2679
 
2458
2680
  if (!apiKey) {
2459
- await sendReply(chatId, "⚠️ Voice messages require an OpenAI API key for transcription. Set OPENAI_API_KEY in your configuration.");
2681
+ await sendReply(chatId, ":alert: Voice messages require an OpenAI API key for transcription. Set OPENAI_API_KEY in your configuration.");
2460
2682
  return;
2461
2683
  }
2462
2684
 
@@ -2505,13 +2727,13 @@ async function transcribeAndProcessVoice(voiceFile, chatId) {
2505
2727
 
2506
2728
  const { text: transcribedText } = await whisperRes.json();
2507
2729
  if (!transcribedText?.trim()) {
2508
- await sendReply(chatId, "🎙️ Could not transcribe voice message (empty result).");
2730
+ await sendReply(chatId, ":mic: Could not transcribe voice message (empty result).");
2509
2731
  return;
2510
2732
  }
2511
2733
 
2512
2734
  // 4. Show transcription and process as free text
2513
2735
  console.log(`[telegram-bot] voice transcription: "${transcribedText.slice(0, 80)}"`);
2514
- await sendReply(chatId, `🎙️ _${transcribedText}_`, { parseMode: "Markdown" });
2736
+ await sendReply(chatId, `:mic: _${transcribedText}_`, { parseMode: "Markdown" });
2515
2737
 
2516
2738
  // Route through the same free-text handler
2517
2739
  if (transcribedText.startsWith("/")) {
@@ -2576,7 +2798,7 @@ async function handleUpdate(update) {
2576
2798
  await transcribeAndProcessVoice(voiceFile, chatId);
2577
2799
  } catch (err) {
2578
2800
  console.error(`[telegram-bot] voice processing error:`, err.message);
2579
- await sendReply(chatId, `⚠️ Could not process voice message: ${err.message}`);
2801
+ await sendReply(chatId, `:alert: Could not process voice message: ${err.message}`);
2580
2802
  }
2581
2803
  return;
2582
2804
  }
@@ -2592,7 +2814,7 @@ async function handleUpdate(update) {
2592
2814
  const cmdText = text.split(/\s+/)[0].toLowerCase().replace(/@\w+/, "");
2593
2815
  if (cmdText === "/cancel") {
2594
2816
  clearPendingUiInput(chatId);
2595
- await sendReply(chatId, " Input cancelled.");
2817
+ await sendReply(chatId, ":check: Input cancelled.");
2596
2818
  return;
2597
2819
  }
2598
2820
  if (!text.startsWith("/")) {
@@ -2637,7 +2859,7 @@ async function cmdPauseTasks(chatId) {
2637
2859
  if (!executor) {
2638
2860
  return sendDirect(
2639
2861
  chatId,
2640
- "⚠️ Internal executor not enabled — nothing to pause.",
2862
+ ":alert: Internal executor not enabled — nothing to pause.",
2641
2863
  );
2642
2864
  }
2643
2865
  if (executor.isPaused()) {
@@ -2645,12 +2867,12 @@ async function cmdPauseTasks(chatId) {
2645
2867
  const dur = info.pauseDuration;
2646
2868
  return sendDirect(
2647
2869
  chatId,
2648
- `⏸ Already paused (${dur >= 60 ? Math.round(dur / 60) + "m" : dur + "s"} ago).\nUse /resumetasks to resume.`,
2870
+ `:pause: Already paused (${dur >= 60 ? Math.round(dur / 60) + "m" : dur + "s"} ago).\nUse /resumetasks to resume.`,
2649
2871
  );
2650
2872
  }
2651
2873
  executor.pause();
2652
2874
  const status = executor.getStatus();
2653
- const lines = [`⏸ *Task executor paused*`];
2875
+ const lines = [`:pause: *Task executor paused*`];
2654
2876
  if (status.activeSlots > 0) {
2655
2877
  lines.push(
2656
2878
  `\n${status.activeSlots} running task(s) will continue to completion.`,
@@ -2662,8 +2884,8 @@ async function cmdPauseTasks(chatId) {
2662
2884
  const keyboard = {
2663
2885
  inline_keyboard: [
2664
2886
  [
2665
- { text: "▶️ Resume Tasks", callback_data: "cb:confirm_resume" },
2666
- { text: "📊 Status", callback_data: "/status" },
2887
+ { text: ":play: Resume Tasks", callback_data: "cb:confirm_resume" },
2888
+ { text: ":chart: Status", callback_data: "/status" },
2667
2889
  ],
2668
2890
  ],
2669
2891
  };
@@ -2681,11 +2903,11 @@ async function cmdResumeTasks(chatId) {
2681
2903
  if (!executor) {
2682
2904
  return sendDirect(
2683
2905
  chatId,
2684
- "⚠️ Internal executor not enabled — nothing to resume.",
2906
+ ":alert: Internal executor not enabled — nothing to resume.",
2685
2907
  );
2686
2908
  }
2687
2909
  if (!executor.isPaused()) {
2688
- return sendDirect(chatId, "▶️ Executor is already running — not paused.");
2910
+ return sendDirect(chatId, ":play: Executor is already running — not paused.");
2689
2911
  }
2690
2912
  const info = executor.getPauseInfo();
2691
2913
  const dur = info.pauseDuration;
@@ -2694,14 +2916,14 @@ async function cmdResumeTasks(chatId) {
2694
2916
  const keyboard = {
2695
2917
  inline_keyboard: [
2696
2918
  [
2697
- { text: " Pause Tasks", callback_data: "cb:confirm_pause" },
2698
- { text: "📋 Tasks", callback_data: "/tasks" },
2919
+ { text: ":pause: Pause Tasks", callback_data: "cb:confirm_pause" },
2920
+ { text: ":clipboard: Tasks", callback_data: "/tasks" },
2699
2921
  ],
2700
2922
  ],
2701
2923
  };
2702
2924
  return sendDirect(
2703
2925
  chatId,
2704
- `▶️ *Task executor resumed* (was paused for ${durStr}).\nWill pick up tasks on next poll cycle.`,
2926
+ `:play: *Task executor resumed* (was paused for ${durStr}).\nWill pick up tasks on next poll cycle.`,
2705
2927
  { parse_mode: "Markdown", reply_markup: keyboard },
2706
2928
  );
2707
2929
  }
@@ -2721,7 +2943,7 @@ async function cmdRepos(chatId, _text) {
2721
2943
  return sendDirect(
2722
2944
  chatId,
2723
2945
  [
2724
- "📁 *Repositories*",
2946
+ ":folder: *Repositories*",
2725
2947
  "",
2726
2948
  `Active: \`${config.repoSlug || config.repoRoot || "current directory"}\``,
2727
2949
  "",
@@ -2748,7 +2970,7 @@ async function cmdRepos(chatId, _text) {
2748
2970
  );
2749
2971
  }
2750
2972
 
2751
- const lines = ["📁 *Repositories*", ""];
2973
+ const lines = [":folder: *Repositories*", ""];
2752
2974
  if (activeWorkspace) {
2753
2975
  lines.push(`Workspace: \`${activeWorkspace}\``, "");
2754
2976
  }
@@ -2758,7 +2980,7 @@ async function cmdRepos(chatId, _text) {
2758
2980
  repo.name === selected?.name ||
2759
2981
  repo.slug === selected?.slug ||
2760
2982
  repo.primary;
2761
- const icon = isCurrent ? "🟢" : "";
2983
+ const icon = isCurrent ? ":dot:" : ":dot:";
2762
2984
  const primary = repo.primary ? " _(primary)_" : "";
2763
2985
  lines.push(
2764
2986
  `${icon} \`${repo.name}\` — ${repo.slug || repo.path || "?"}${primary}`,
@@ -2770,7 +2992,7 @@ async function cmdRepos(chatId, _text) {
2770
2992
 
2771
2993
  return sendDirect(chatId, lines.join("\n"), { parse_mode: "Markdown" });
2772
2994
  } catch (err) {
2773
- return sendDirect(chatId, `❌ Failed to read repo config: ${err.message}`);
2995
+ return sendDirect(chatId, `:close: Failed to read repo config: ${err.message}`);
2774
2996
  }
2775
2997
  }
2776
2998
 
@@ -2802,7 +3024,7 @@ async function cmdWorkspace(chatId, text) {
2802
3024
  await sendReply(
2803
3025
  chatId,
2804
3026
  [
2805
- "🔎 Workspace Scan Complete",
3027
+ ":search: Workspace Scan Complete",
2806
3028
  `Scanned: ${merged.scanned}`,
2807
3029
  `Added: ${merged.added}`,
2808
3030
  `Updated: ${merged.updated}`,
@@ -2823,7 +3045,7 @@ async function cmdWorkspace(chatId, text) {
2823
3045
  const active = getActiveLocalWorkspace(configDir);
2824
3046
  await sendReply(
2825
3047
  chatId,
2826
- `✅ Active workspace: ${active?.name || targetId} (\`${active?.id || targetId}\`)`,
3048
+ `:check: Active workspace: ${active?.name || targetId} (\`${active?.id || targetId}\`)`,
2827
3049
  );
2828
3050
  return;
2829
3051
  }
@@ -2837,7 +3059,7 @@ async function cmdWorkspace(chatId, text) {
2837
3059
  const ws = createManagedWs(configDir, { name });
2838
3060
  await sendReply(
2839
3061
  chatId,
2840
- `✅ Created workspace: *${ws.name}* (\`${ws.id}\`)\nPath: \`${ws.path}\``,
3062
+ `:check: Created workspace: *${ws.name}* (\`${ws.id}\`)\nPath: \`${ws.path}\``,
2841
3063
  { parseMode: "Markdown" },
2842
3064
  );
2843
3065
  return;
@@ -2851,14 +3073,14 @@ async function cmdWorkspace(chatId, text) {
2851
3073
  await sendReply(chatId, "Usage: /workspace add-repo <wsId> <gitUrl> [branch]");
2852
3074
  return;
2853
3075
  }
2854
- await sendReply(chatId, `⏳ Cloning ${gitUrl} into workspace ${wsId}...`);
3076
+ await sendReply(chatId, `:clock: Cloning ${gitUrl} into workspace ${wsId}...`);
2855
3077
  const repo = addRepoToManagedWs(configDir, wsId, {
2856
3078
  url: gitUrl,
2857
3079
  branch,
2858
3080
  });
2859
3081
  await sendReply(
2860
3082
  chatId,
2861
- `✅ Added repo *${repo.name}* to workspace \`${wsId}\`\n${repo.cloned ? "Cloned from remote" : "Already existed on disk"}`,
3083
+ `:check: Added repo *${repo.name}* to workspace \`${wsId}\`\n${repo.cloned ? "Cloned from remote" : "Already existed on disk"}`,
2862
3084
  { parseMode: "Markdown" },
2863
3085
  );
2864
3086
  return;
@@ -2875,8 +3097,8 @@ async function cmdWorkspace(chatId, text) {
2875
3097
  await sendReply(
2876
3098
  chatId,
2877
3099
  removed
2878
- ? `✅ Removed repo \`${repoName}\` from workspace \`${wsId}\``
2879
- : `⚠️ Repo \`${repoName}\` not found in workspace \`${wsId}\``,
3100
+ ? `:check: Removed repo \`${repoName}\` from workspace \`${wsId}\``
3101
+ : `:alert: Repo \`${repoName}\` not found in workspace \`${wsId}\``,
2880
3102
  );
2881
3103
  return;
2882
3104
  }
@@ -2889,18 +3111,18 @@ async function cmdWorkspace(chatId, text) {
2889
3111
  await sendReply(chatId, "Usage: /workspace pull <wsId> (no active workspace)");
2890
3112
  return;
2891
3113
  }
2892
- await sendReply(chatId, `⏳ Pulling repos in workspace ${active.name}...`);
3114
+ await sendReply(chatId, `:clock: Pulling repos in workspace ${active.name}...`);
2893
3115
  const results = pullManagedWsRepos(configDir, active.id);
2894
3116
  const lines = results.map(
2895
- (r) => `${r.success ? "" : ""} ${r.name}${r.error ? ` — ${r.error}` : ""}`,
3117
+ (r) => `${r.success ? ":check:" : ":close:"} ${r.name}${r.error ? ` — ${r.error}` : ""}`,
2896
3118
  );
2897
3119
  await sendReply(chatId, ["Pull results:", ...lines].join("\n"));
2898
3120
  return;
2899
3121
  }
2900
- await sendReply(chatId, `⏳ Pulling repos in workspace ${wsId}...`);
3122
+ await sendReply(chatId, `:clock: Pulling repos in workspace ${wsId}...`);
2901
3123
  const results = pullManagedWsRepos(configDir, wsId);
2902
3124
  const lines = results.map(
2903
- (r) => `${r.success ? "" : ""} ${r.name}${r.error ? ` — ${r.error}` : ""}`,
3125
+ (r) => `${r.success ? ":check:" : ":close:"} ${r.name}${r.error ? ` — ${r.error}` : ""}`,
2904
3126
  );
2905
3127
  await sendReply(chatId, ["Pull results:", ...lines].join("\n"));
2906
3128
  return;
@@ -2916,8 +3138,8 @@ async function cmdWorkspace(chatId, text) {
2916
3138
  await sendReply(
2917
3139
  chatId,
2918
3140
  deleted
2919
- ? `✅ Removed workspace \`${wsId}\` from config (files preserved on disk)`
2920
- : `⚠️ Workspace \`${wsId}\` not found`,
3141
+ ? `:check: Removed workspace \`${wsId}\` from config (files preserved on disk)`
3142
+ : `:alert: Workspace \`${wsId}\` not found`,
2921
3143
  );
2922
3144
  return;
2923
3145
  }
@@ -2929,7 +3151,7 @@ async function cmdWorkspace(chatId, text) {
2929
3151
  await sendReply(
2930
3152
  chatId,
2931
3153
  [
2932
- "🌳 No local workspaces configured.",
3154
+ ":git: No local workspaces configured.",
2933
3155
  `Expected directory: ${resolve(configDir, "workspaces")}`,
2934
3156
  "",
2935
3157
  "Quick start:",
@@ -2941,14 +3163,14 @@ async function cmdWorkspace(chatId, text) {
2941
3163
  return;
2942
3164
  }
2943
3165
 
2944
- const lines = ["🌳 *Local Workspaces*", ""];
3166
+ const lines = [":git: *Local Workspaces*", ""];
2945
3167
  for (const workspace of workspaces) {
2946
- const marker = workspace.id === active?.id ? "🟢" : "";
3168
+ const marker = workspace.id === active?.id ? ":dot:" : ":dot:";
2947
3169
  const repoCount = Array.isArray(workspace.repos) ? workspace.repos.length : 0;
2948
3170
  lines.push(`${marker} *${workspace.name}* (\`${workspace.id}\`) — ${repoCount} repo(s)`);
2949
3171
  for (const repo of (workspace.repos || []).slice(0, 4)) {
2950
- const primary = repo.primary ? " " : "";
2951
- const exists = repo.exists === false ? "" : "";
3172
+ const primary = repo.primary ? ":star: " : "";
3173
+ const exists = repo.exists === false ? ":close:" : ":check:";
2952
3174
  lines.push(` ${exists} ${primary}${repo.name}${repo.slug ? ` (${repo.slug})` : ""}`);
2953
3175
  }
2954
3176
  if ((workspace.repos || []).length > 4) {
@@ -2964,7 +3186,7 @@ async function cmdWorkspace(chatId, text) {
2964
3186
  lines.push("• /workspace scan");
2965
3187
  await sendReply(chatId, lines.join("\n"), { parseMode: "Markdown" });
2966
3188
  } catch (err) {
2967
- await sendReply(chatId, `❌ Workspace command failed: ${err.message}`);
3189
+ await sendReply(chatId, `:close: Workspace command failed: ${err.message}`);
2968
3190
  }
2969
3191
  }
2970
3192
 
@@ -2974,13 +3196,13 @@ async function cmdWorkspace(chatId, text) {
2974
3196
  async function cmdMaxParallel(chatId, text) {
2975
3197
  const executor = _getInternalExecutor?.();
2976
3198
  if (!executor) {
2977
- return sendDirect(chatId, "⚠️ Internal executor not enabled.");
3199
+ return sendDirect(chatId, ":alert: Internal executor not enabled.");
2978
3200
  }
2979
3201
  const arg = (text || "").replace("/maxparallel", "").trim();
2980
3202
  if (arg) {
2981
3203
  const n = parseInt(arg, 10);
2982
3204
  if (isNaN(n) || n < 0 || n > 20) {
2983
- return sendDirect(chatId, "⚠️ Provide a number between 0 and 20.");
3205
+ return sendDirect(chatId, ":alert: Provide a number between 0 and 20.");
2984
3206
  }
2985
3207
  const old = executor.maxParallel;
2986
3208
  executor.maxParallel = n;
@@ -2988,18 +3210,18 @@ async function cmdMaxParallel(chatId, text) {
2988
3210
  executor.pause();
2989
3211
  return sendDirect(
2990
3212
  chatId,
2991
- `⏸ Max parallel set to 0 — executor paused. Use /maxparallel <n> to resume.`,
3213
+ `:pause: Max parallel set to 0 — executor paused. Use /maxparallel <n> to resume.`,
2992
3214
  );
2993
3215
  }
2994
3216
  if (executor.isPaused() && n > 0) {
2995
3217
  executor.resume();
2996
3218
  }
2997
- return sendDirect(chatId, `✅ Max parallel: ${old} → ${n}`);
3219
+ return sendDirect(chatId, `:check: Max parallel: ${old} → ${n}`);
2998
3220
  }
2999
3221
  const status = executor.getStatus();
3000
3222
  return sendDirect(
3001
3223
  chatId,
3002
- `📊 Max parallel: ${status.maxParallel} (active: ${status.activeSlots})`,
3224
+ `:chart: Max parallel: ${status.maxParallel} (active: ${status.activeSlots})`,
3003
3225
  );
3004
3226
  }
3005
3227
 
@@ -3013,14 +3235,14 @@ async function cmdWhatsApp(chatId) {
3013
3235
  if (!isWhatsAppEnabled()) {
3014
3236
  return sendDirect(
3015
3237
  chatId,
3016
- " WhatsApp channel is not enabled.\n\nSet WHATSAPP_ENABLED=1 in your .env to enable.",
3238
+ ":dot: WhatsApp channel is not enabled.\n\nSet WHATSAPP_ENABLED=1 in your .env to enable.",
3017
3239
  );
3018
3240
  }
3019
3241
  const status = getWhatsAppStatus();
3020
3242
  const lines = [
3021
- "📱 <b>WhatsApp Channel Status</b>",
3243
+ ":phone: <b>WhatsApp Channel Status</b>",
3022
3244
  "",
3023
- `Status: ${status.connected ? "🟢 Connected" : "🔴 Disconnected"}`,
3245
+ `Status: ${status.connected ? ":dot: Connected" : ":dot: Disconnected"}`,
3024
3246
  `Chat ID: <code>${status.chatId || "not set"}</code>`,
3025
3247
  `Pending messages: ${status.pendingMessages || 0}`,
3026
3248
  ];
@@ -3030,7 +3252,7 @@ async function cmdWhatsApp(chatId) {
3030
3252
  }
3031
3253
  return sendDirect(chatId, lines.join("\n"), { parse_mode: "HTML" });
3032
3254
  } catch (err) {
3033
- return sendDirect(chatId, `❌ WhatsApp status error: ${err.message}`);
3255
+ return sendDirect(chatId, `:close: WhatsApp status error: ${err.message}`);
3034
3256
  }
3035
3257
  }
3036
3258
 
@@ -3044,12 +3266,12 @@ async function cmdContainer(chatId) {
3044
3266
  if (!isContainerEnabled()) {
3045
3267
  return sendDirect(
3046
3268
  chatId,
3047
- " Container isolation is not enabled.\n\nSet CONTAINER_ENABLED=1 in your .env to enable.",
3269
+ ":dot: Container isolation is not enabled.\n\nSet CONTAINER_ENABLED=1 in your .env to enable.",
3048
3270
  );
3049
3271
  }
3050
3272
  const status = getContainerStatus();
3051
3273
  const lines = [
3052
- "📦 <b>Container Runtime Status</b>",
3274
+ ":box: <b>Container Runtime Status</b>",
3053
3275
  "",
3054
3276
  `Runtime: ${status.runtime || "detecting..."}`,
3055
3277
  `Active containers: ${status.activeContainers || 0}`,
@@ -3060,7 +3282,7 @@ async function cmdContainer(chatId) {
3060
3282
  }
3061
3283
  return sendDirect(chatId, lines.join("\n"), { parse_mode: "HTML" });
3062
3284
  } catch (err) {
3063
- return sendDirect(chatId, `❌ Container status error: ${err.message}`);
3285
+ return sendDirect(chatId, `:close: Container status error: ${err.message}`);
3064
3286
  }
3065
3287
  }
3066
3288
 
@@ -3071,6 +3293,11 @@ const COMMANDS = {
3071
3293
  "/app": { handler: cmdApp, desc: "Open the Control Center Mini App" },
3072
3294
  "/miniapp": { handler: cmdApp, desc: "Open the Control Center Mini App" },
3073
3295
  "/webapp": { handler: cmdApp, desc: "Open the Control Center Mini App" },
3296
+ "/call": { handler: cmdCall, desc: "Open one-click voice meeting room" },
3297
+ "/videocall": {
3298
+ handler: cmdVideoCall,
3299
+ desc: "Open one-click video meeting room",
3300
+ },
3074
3301
  "/cancel": { handler: cmdCancel, desc: "Cancel a pending input prompt" },
3075
3302
  "/ask": { handler: cmdAsk, desc: "Send prompt to agent: /ask <prompt>" },
3076
3303
  "/status": { handler: cmdStatus, desc: "Detailed orchestrator status" },
@@ -3456,6 +3683,8 @@ async function refreshMenuButton() {
3456
3683
 
3457
3684
  const FAST_COMMANDS = new Set([
3458
3685
  "/menu",
3686
+ "/call",
3687
+ "/videocall",
3459
3688
  "/background",
3460
3689
  "/status",
3461
3690
  "/weekly",
@@ -3480,11 +3709,11 @@ const FAST_COMMANDS = new Set([
3480
3709
 
3481
3710
  function getTelegramWebAppUrl(url) {
3482
3711
  // Telegram Mini App must be HTTPS and publicly reachable.
3483
- // Priority: explicit env URL -> tunnel URL -> provided URL.
3712
+ // Priority: tunnel URL (permanent hostname) -> explicit env URL -> provided URL.
3713
+ const tUrl = getTunnelUrl();
3484
3714
  const explicit =
3485
3715
  process.env.TELEGRAM_WEBAPP_URL || process.env.TELEGRAM_UI_BASE_URL || "";
3486
- const tUrl = getTunnelUrl();
3487
- const candidates = [explicit, tUrl, url];
3716
+ const candidates = [tUrl, explicit, url];
3488
3717
 
3489
3718
  for (const candidate of candidates) {
3490
3719
  const normalized = String(candidate || "")
@@ -3534,7 +3763,7 @@ async function handleCommand(text, chatId) {
3534
3763
  try {
3535
3764
  await entry.handler(chatId, cmdArgs);
3536
3765
  } catch (err) {
3537
- await sendReply(chatId, `❌ Command error: ${err.message}`);
3766
+ await sendReply(chatId, `:close: Command error: ${err.message}`);
3538
3767
  }
3539
3768
  } else {
3540
3769
  await sendReply(
@@ -3805,10 +4034,11 @@ function uiTokenAction(token) {
3805
4034
  }
3806
4035
 
3807
4036
  function uiButton(text, action) {
4037
+ const normalizedText = formatTelegramIconTokens(text, { button: true });
3808
4038
  if (typeof action === "string" && (action.startsWith("cb:") || action.startsWith("ui:"))) {
3809
- return { text, callback_data: action };
4039
+ return { text: normalizedText, callback_data: action };
3810
4040
  }
3811
- return { text, callback_data: uiCallback(action) };
4041
+ return { text: normalizedText, callback_data: uiCallback(action) };
3812
4042
  }
3813
4043
 
3814
4044
  function buildKeyboard(rows) {
@@ -3819,8 +4049,8 @@ function buildConfirmKeyboard(confirmId, confirmLabel = "Confirm") {
3819
4049
  return {
3820
4050
  inline_keyboard: [
3821
4051
  [
3822
- { text: `✅ ${confirmLabel}`, callback_data: confirmId },
3823
- { text: " Cancel", callback_data: "cb:dismiss" },
4052
+ { text: `:check: ${confirmLabel}`, callback_data: confirmId },
4053
+ { text: ":close: Cancel", callback_data: "cb:dismiss" },
3824
4054
  ],
3825
4055
  ],
3826
4056
  };
@@ -3829,12 +4059,12 @@ function buildConfirmKeyboard(confirmId, confirmLabel = "Confirm") {
3829
4059
  function buildActionConfirmKeyboard(confirmId, confirmLabel, backAction) {
3830
4060
  const rows = [
3831
4061
  [
3832
- { text: `✅ ${confirmLabel || "Confirm"}`, callback_data: confirmId },
3833
- { text: " Cancel", callback_data: "cb:dismiss" },
4062
+ { text: `:check: ${confirmLabel || "Confirm"}`, callback_data: confirmId },
4063
+ { text: ":close: Cancel", callback_data: "cb:dismiss" },
3834
4064
  ],
3835
4065
  ];
3836
4066
  if (backAction) {
3837
- rows.push([uiButton("⬅️ Back", backAction)]);
4067
+ rows.push([uiButton(":arrowRight: Back", backAction)]);
3838
4068
  }
3839
4069
  return { inline_keyboard: rows };
3840
4070
  }
@@ -3854,13 +4084,17 @@ function appendRefreshRow(keyboard, screenId, params = {}) {
3854
4084
  )
3855
4085
  : uiGoAction(screenId, params.page);
3856
4086
  const rows = keyboard.inline_keyboard || [];
4087
+ const refreshLabel = formatTelegramIconTokens(":refresh: Refresh", { button: true });
3857
4088
  const hasRefresh = rows.some((row) =>
3858
- row.some((btn) => btn?.text === "🔄 Refresh"),
4089
+ row.some((btn) =>
4090
+ btn?.text === ":refresh: Refresh"
4091
+ || btn?.text === refreshLabel
4092
+ || btn?.callback_data === action),
3859
4093
  );
3860
4094
  if (hasRefresh) return keyboard;
3861
4095
  return {
3862
4096
  ...keyboard,
3863
- inline_keyboard: [...rows, [uiButton("🔄 Refresh", action)]],
4097
+ inline_keyboard: [...rows, [uiButton(":refresh: Refresh", action)]],
3864
4098
  };
3865
4099
  }
3866
4100
 
@@ -3943,7 +4177,7 @@ async function promptUiInput(chatId, key, extra = {}) {
3943
4177
  ...extra,
3944
4178
  });
3945
4179
  const keyboard = buildKeyboard([
3946
- [{ text: " Cancel", callback_data: uiCallback("cancel") }],
4180
+ [{ text: ":close: Cancel", callback_data: uiCallback("cancel") }],
3947
4181
  ]);
3948
4182
  await sendReply(chatId, `${prompt}\n\nSend /cancel to abort.`, {
3949
4183
  reply_markup: keyboard,
@@ -3982,7 +4216,7 @@ async function promptActionConfirm(chatId, token, payload, options = {}) {
3982
4216
  async function handleUiInput(chatId, request, text) {
3983
4217
  const trimmed = String(text || "").trim();
3984
4218
  if (!trimmed) {
3985
- await sendReply(chatId, "⚠️ Input was empty. Prompt cancelled.");
4219
+ await sendReply(chatId, ":alert: Input was empty. Prompt cancelled.");
3986
4220
  return;
3987
4221
  }
3988
4222
  if (request.key === "starttask") {
@@ -4001,12 +4235,12 @@ async function handleUiInput(chatId, request, text) {
4001
4235
  }
4002
4236
  const buildCommand = request.buildCommand;
4003
4237
  if (typeof buildCommand !== "function") {
4004
- await sendReply(chatId, "⚠️ Unable to process that input.");
4238
+ await sendReply(chatId, ":alert: Unable to process that input.");
4005
4239
  return;
4006
4240
  }
4007
4241
  const command = buildCommand(trimmed, request);
4008
4242
  if (!command) {
4009
- await sendReply(chatId, "⚠️ Could not build a command from that input.");
4243
+ await sendReply(chatId, ":alert: Could not build a command from that input.");
4010
4244
  return;
4011
4245
  }
4012
4246
  if (request.confirm) {
@@ -4088,7 +4322,7 @@ function buildStartTaskCommand(taskId, sdk, model, executor) {
4088
4322
  async function promptStartTaskConfirm(chatId, details = {}) {
4089
4323
  const taskId = String(details.taskId || "").trim();
4090
4324
  if (!taskId) {
4091
- await sendReply(chatId, "⚠️ Task ID missing for manual start.");
4325
+ await sendReply(chatId, ":alert: Task ID missing for manual start.");
4092
4326
  return;
4093
4327
  }
4094
4328
  const executor = normalizeStartTaskExecutor(details.executor);
@@ -4101,7 +4335,7 @@ async function promptStartTaskConfirm(chatId, details = {}) {
4101
4335
  const sdkLabel = isVk ? "n/a" : sdk || "auto";
4102
4336
  const modelLabel = isVk ? "n/a" : model || "default";
4103
4337
  const lines = [
4104
- "🚀 *Confirm Manual Start*",
4338
+ ":rocket: *Confirm Manual Start*",
4105
4339
  "",
4106
4340
  "This will enqueue the task immediately.",
4107
4341
  "",
@@ -4115,8 +4349,8 @@ async function promptStartTaskConfirm(chatId, details = {}) {
4115
4349
  }
4116
4350
  const keyboard = buildKeyboard([
4117
4351
  [
4118
- uiButton(" Start", uiTokenAction(token)),
4119
- uiButton(" Cancel", "cancel"),
4352
+ uiButton(":check: Start", uiTokenAction(token)),
4353
+ uiButton(":close: Cancel", "cancel"),
4120
4354
  ],
4121
4355
  ]);
4122
4356
  await sendReply(chatId, lines.join("\n"), {
@@ -4128,7 +4362,7 @@ async function promptStartTaskConfirm(chatId, details = {}) {
4128
4362
  async function showStartTaskExecutorPicker(chatId, taskId) {
4129
4363
  const safeId = String(taskId || "").trim();
4130
4364
  if (!safeId) {
4131
- await sendReply(chatId, "⚠️ Task ID missing.");
4365
+ await sendReply(chatId, ":alert: Task ID missing.");
4132
4366
  return;
4133
4367
  }
4134
4368
  const mode = resolveStartTaskExecutorMode();
@@ -4136,23 +4370,23 @@ async function showStartTaskExecutorPicker(chatId, taskId) {
4136
4370
  const buttons = [];
4137
4371
  if (availability.internal || availability.vk) {
4138
4372
  buttons.push({
4139
- label: " Auto (recommended)",
4373
+ label: ":star: Auto (recommended)",
4140
4374
  executor: "auto",
4141
4375
  });
4142
4376
  }
4143
4377
  if (availability.internal) {
4144
4378
  buttons.push({
4145
- label: availability.vk ? "🧠 Internal" : "🧠 Internal (only)",
4379
+ label: availability.vk ? ":cpu: Internal" : ":cpu: Internal (only)",
4146
4380
  executor: "internal",
4147
4381
  });
4148
4382
  }
4149
4383
  if (availability.vk) {
4150
- buttons.push({ label: "☁️ VK", executor: "vk" });
4384
+ buttons.push({ label: ":globe: VK", executor: "vk" });
4151
4385
  }
4152
4386
  if (!buttons.length) {
4153
4387
  await sendReply(
4154
4388
  chatId,
4155
- "⚠️ No executors available. Check EXECUTOR_MODE configuration.",
4389
+ ":alert: No executors available. Check EXECUTOR_MODE configuration.",
4156
4390
  );
4157
4391
  return;
4158
4392
  }
@@ -4172,15 +4406,15 @@ async function showStartTaskExecutorPicker(chatId, taskId) {
4172
4406
  ),
4173
4407
  2,
4174
4408
  ),
4175
- [uiButton(" Cancel", "cancel")],
4409
+ [uiButton(":close: Cancel", "cancel")],
4176
4410
  ];
4177
4411
  const lines = [
4178
4412
  "Step 1/3 • Choose executor",
4179
4413
  "",
4180
4414
  `Task: \`${safeId}\``,
4181
4415
  `Mode: \`${mode}\``,
4182
- availability.internal ? "🧠 Internal executor available" : "🧠 Internal executor unavailable",
4183
- availability.vk ? "☁️ VK executor available" : "☁️ VK executor unavailable",
4416
+ availability.internal ? ":cpu: Internal executor available" : ":cpu: Internal executor unavailable",
4417
+ availability.vk ? ":globe: VK executor available" : ":globe: VK executor unavailable",
4184
4418
  "",
4185
4419
  "Auto picks internal if available, otherwise VK.",
4186
4420
  ];
@@ -4193,7 +4427,7 @@ async function showStartTaskExecutorPicker(chatId, taskId) {
4193
4427
  async function showStartTaskSdkPicker(chatId, taskId, executor) {
4194
4428
  const safeId = String(taskId || "").trim();
4195
4429
  if (!safeId) {
4196
- await sendReply(chatId, "⚠️ Task ID missing.");
4430
+ await sendReply(chatId, ":alert: Task ID missing.");
4197
4431
  return;
4198
4432
  }
4199
4433
  const safeExecutor = normalizeStartTaskExecutor(executor) || "internal";
@@ -4225,13 +4459,13 @@ async function showStartTaskSdkPicker(chatId, taskId, executor) {
4225
4459
  "Auto uses the current pool SDK.",
4226
4460
  ];
4227
4461
  const rows = [
4228
- [uiButton("🤖 Auto", uiTokenAction(tokenAuto))],
4462
+ [uiButton(":bot: Auto", uiTokenAction(tokenAuto))],
4229
4463
  ...chunkButtons(
4230
4464
  tokens.map((entry) => uiButton(entry.sdk, uiTokenAction(entry.token))),
4231
4465
  2,
4232
4466
  ),
4233
- [uiButton("⬅️ Back", uiTokenAction(tokenBack))],
4234
- [uiButton(" Cancel", "cancel")],
4467
+ [uiButton(":arrowRight: Back", uiTokenAction(tokenBack))],
4468
+ [uiButton(":close: Cancel", "cancel")],
4235
4469
  ];
4236
4470
  const keyboard = buildKeyboard(rows);
4237
4471
  await sendReply(chatId, lines.join("\n"), {
@@ -4245,7 +4479,7 @@ async function showStartTaskModelPicker(chatId, taskId, sdk, executor) {
4245
4479
  const safeSdk = String(sdk || "").trim();
4246
4480
  const safeExecutor = normalizeStartTaskExecutor(executor) || "internal";
4247
4481
  if (!safeId || !safeSdk) {
4248
- await sendReply(chatId, "⚠️ Missing task ID or SDK.");
4482
+ await sendReply(chatId, ":alert: Missing task ID or SDK.");
4249
4483
  return;
4250
4484
  }
4251
4485
  const modelOptions = getDefaultModelPriority()
@@ -4301,7 +4535,7 @@ async function showStartTaskModelPicker(chatId, taskId, sdk, executor) {
4301
4535
  );
4302
4536
  }
4303
4537
  rows.push([uiButton("Custom Model", uiTokenAction(tokenCustom))]);
4304
- rows.push([uiButton("⬅️ Back", uiTokenAction(tokenBack))]);
4538
+ rows.push([uiButton(":arrowRight: Back", uiTokenAction(tokenBack))]);
4305
4539
  const keyboard = buildKeyboard(rows);
4306
4540
  await sendReply(chatId, lines.join("\n"), {
4307
4541
  parseMode: "Markdown",
@@ -4312,14 +4546,14 @@ async function showStartTaskModelPicker(chatId, taskId, sdk, executor) {
4312
4546
  function uiNavRow(parent) {
4313
4547
  if (!parent) {
4314
4548
  return [
4315
- uiButton("🏠 Home", uiGoAction("home")),
4316
- uiButton(" Close", "cb:close_menu"),
4549
+ uiButton(":home: Home", uiGoAction("home")),
4550
+ uiButton(":close: Close", "cb:close_menu"),
4317
4551
  ];
4318
4552
  }
4319
4553
  return [
4320
- uiButton("⬅️ Back", uiGoAction(parent)),
4321
- uiButton("🏠 Home", uiGoAction("home")),
4322
- uiButton(" Close", "cb:close_menu"),
4554
+ uiButton(":arrowRight: Back", uiGoAction(parent)),
4555
+ uiButton(":home: Home", uiGoAction("home")),
4556
+ uiButton(":close: Close", "cb:close_menu"),
4323
4557
  ];
4324
4558
  }
4325
4559
 
@@ -4384,13 +4618,13 @@ function formatDurationMs(ms) {
4384
4618
 
4385
4619
  async function buildHomeStatusLine() {
4386
4620
  const data = await readStatusSnapshot();
4387
- if (!data) return "Status: unavailable";
4621
+ if (!data) return "Status: :close: unavailable";
4388
4622
  const counts = data.counts || {};
4389
4623
  const backlog = data.backlog_remaining ?? "?";
4390
4624
  const running = counts.running ?? 0;
4391
4625
  const review = counts.review ?? 0;
4392
4626
  const error = counts.error ?? 0;
4393
- return `▶️ Running ${running} • 👁️ Review ${review} • ⚠️ Error ${error} • 📥 Backlog ${backlog}`;
4627
+ return `:play: Running ${running} • :eye: Review ${review} • :alert: Error ${error} • :download: Backlog ${backlog}`;
4394
4628
  }
4395
4629
 
4396
4630
  async function listWorktreeNames() {
@@ -4508,10 +4742,10 @@ Object.assign(UI_SCREENS, {
4508
4742
  let executorLine = "";
4509
4743
  if (executor) {
4510
4744
  const status = executor.getStatus();
4511
- const paused = executor.isPaused?.() ? " paused" : "▶️ running";
4512
- executorLine = `⚙️ Executor: ${paused} • 🎛️ Slots ${status.activeSlots}/${status.maxParallel}`;
4745
+ const paused = executor.isPaused?.() ? ":pause: paused" : ":play: running";
4746
+ executorLine = `:settings: Executor: ${paused} • :sliders: Slots ${status.activeSlots}/${status.maxParallel}`;
4513
4747
  } else {
4514
- executorLine = `⚙️ Executor: ${_getExecutorMode?.() || "internal"}`;
4748
+ executorLine = `:settings: Executor: ${_getExecutorMode?.() || "internal"}`;
4515
4749
  }
4516
4750
  return [
4517
4751
  "Pick a section below to manage Bosun.",
@@ -4522,49 +4756,66 @@ Object.assign(UI_SCREENS, {
4522
4756
  },
4523
4757
  keyboard: () => {
4524
4758
  syncUiUrlsFromServer();
4759
+ const voiceMeetingWebAppUrl = getMeetingWebAppUrl("voice");
4760
+ const videoMeetingWebAppUrl = getMeetingWebAppUrl("video");
4761
+ const meetingRow = [
4762
+ voiceMeetingWebAppUrl
4763
+ ? {
4764
+ text: "Voice Meeting",
4765
+ web_app: { url: voiceMeetingWebAppUrl },
4766
+ }
4767
+ : uiButton("Voice Meeting", uiCmdAction("/call")),
4768
+ videoMeetingWebAppUrl
4769
+ ? {
4770
+ text: "Video Meeting",
4771
+ web_app: { url: videoMeetingWebAppUrl },
4772
+ }
4773
+ : uiButton("Video Meeting", uiCmdAction("/videocall")),
4774
+ ];
4525
4775
  const rows = [
4526
4776
  // Core Operations
4527
4777
  [
4528
- uiButton("📊 Dashboard", uiGoAction("overview")),
4529
- uiButton("🧭 Tasks", uiGoAction("tasks")),
4530
- uiButton("🤖 Agents", uiGoAction("agents")),
4778
+ uiButton(":chart: Dashboard", uiGoAction("overview")),
4779
+ uiButton(":compass: Tasks", uiGoAction("tasks")),
4780
+ uiButton(":bot: Agents", uiGoAction("agents")),
4531
4781
  ],
4532
4782
  // System & Config
4533
4783
  [
4534
- uiButton("🌳 Workspaces", uiGoAction("workspaces")),
4535
- uiButton("⚙️ Executor", uiGoAction("executor")),
4536
- uiButton("🛰 Routing", uiGoAction("routing")),
4784
+ uiButton(":git: Workspaces", uiGoAction("workspaces")),
4785
+ uiButton(":settings: Executor", uiGoAction("executor")),
4786
+ uiButton(":server: Routing", uiGoAction("routing")),
4537
4787
  ],
4538
4788
  // Monitoring & Tools
4539
4789
  [
4540
- uiButton("📁 Logs & Git", uiGoAction("logs")),
4541
- uiButton("📈 Telemetry", uiGoAction("telemetry")),
4542
- uiButton("🧠 Session", uiGoAction("session")),
4790
+ uiButton(":folder: Logs & Git", uiGoAction("logs")),
4791
+ uiButton(":chart: Telemetry", uiGoAction("telemetry")),
4792
+ uiButton(":cpu: Session", uiGoAction("session")),
4543
4793
  ],
4544
4794
  // Quick Actions
4545
4795
  [
4546
- uiButton("💬 Ask Agent", uiInputAction("ask")),
4547
- uiButton("📖 All Commands", uiCmdAction("/helpfull")),
4796
+ uiButton(":chat: Ask Agent", uiInputAction("ask")),
4797
+ uiButton(":file: All Commands", uiCmdAction("/helpfull")),
4548
4798
  ],
4549
4799
  ];
4800
+ rows.unshift(meetingRow);
4550
4801
  if (telegramWebAppUrl) {
4551
4802
  rows.unshift([
4552
4803
  {
4553
- text: "📱 Open Control Center",
4804
+ text: ":phone: Open Control Center",
4554
4805
  web_app: { url: telegramWebAppUrl },
4555
4806
  },
4556
- uiButton("", "cb:close_menu"),
4807
+ uiButton(":close:", "cb:close_menu"),
4557
4808
  ]);
4558
4809
  if (getBrowserUiUrlOptions().length > 0) {
4559
- rows.unshift([uiButton("🌐 Open in Browser", uiGoAction("browser_urls"))]);
4810
+ rows.unshift([uiButton(":globe: Open in Browser", uiGoAction("browser_urls"))]);
4560
4811
  }
4561
4812
  } else if (telegramUiUrl) {
4562
4813
  rows.unshift([
4563
- uiButton("🌐 Open in Browser", uiGoAction("browser_urls")),
4564
- uiButton("", "cb:close_menu"),
4814
+ uiButton(":globe: Open in Browser", uiGoAction("browser_urls")),
4815
+ uiButton(":close:", "cb:close_menu"),
4565
4816
  ]);
4566
4817
  } else {
4567
- rows.unshift([uiButton(" Close Menu", "cb:close_menu")]);
4818
+ rows.unshift([uiButton(":close: Close Menu", "cb:close_menu")]);
4568
4819
  }
4569
4820
  return buildKeyboard(rows);
4570
4821
  },
@@ -4598,23 +4849,23 @@ Object.assign(UI_SCREENS, {
4598
4849
  buildKeyboard([
4599
4850
  // Core Status
4600
4851
  [
4601
- uiButton("📊 Status", uiCmdAction("/status")),
4602
- uiButton("📋 Tasks", uiCmdAction("/tasks")),
4603
- uiButton("🤖 Agents", uiCmdAction("/agents")),
4852
+ uiButton(":chart: Status", uiCmdAction("/status")),
4853
+ uiButton(":clipboard: Tasks", uiCmdAction("/tasks")),
4854
+ uiButton(":bot: Agents", uiCmdAction("/agents")),
4604
4855
  ],
4605
4856
  // System Health
4606
4857
  [
4607
- uiButton("🏥 Health", uiCmdAction("/health")),
4608
- uiButton("⚠️ Anomalies", uiCmdAction("/anomalies")),
4609
- uiButton("👁 Presence", uiCmdAction("/presence")),
4858
+ uiButton(":heart: Health", uiCmdAction("/health")),
4859
+ uiButton(":alert: Anomalies", uiCmdAction("/anomalies")),
4860
+ uiButton(":eye: Presence", uiCmdAction("/presence")),
4610
4861
  ],
4611
4862
  // Deep Dives
4612
4863
  [
4613
- uiButton("🎯 Coordinator", uiCmdAction("/coordinator")),
4614
- uiButton("📝 Logs", uiCmdAction("/logs 50")),
4864
+ uiButton(":target: Coordinator", uiCmdAction("/coordinator")),
4865
+ uiButton(":edit: Logs", uiCmdAction("/logs 50")),
4615
4866
  ],
4616
4867
  [
4617
- uiButton("📈 Telemetry", uiGoAction("telemetry")),
4868
+ uiButton(":chart: Telemetry", uiGoAction("telemetry")),
4618
4869
  ],
4619
4870
  uiNavRow("home"),
4620
4871
  ]),
@@ -4627,12 +4878,12 @@ Object.assign(UI_SCREENS, {
4627
4878
  keyboard: () =>
4628
4879
  buildKeyboard([
4629
4880
  [
4630
- uiButton("📈 Summary", uiCmdAction("/telemetry")),
4631
- uiButton("🧯 Errors", uiCmdAction("/telemetry errors")),
4881
+ uiButton(":chart: Summary", uiCmdAction("/telemetry")),
4882
+ uiButton(":alert: Errors", uiCmdAction("/telemetry errors")),
4632
4883
  ],
4633
4884
  [
4634
- uiButton("🧪 Executors", uiCmdAction("/telemetry executors")),
4635
- uiButton("🚨 Alerts", uiCmdAction("/telemetry alerts")),
4885
+ uiButton(":beaker: Executors", uiCmdAction("/telemetry executors")),
4886
+ uiButton(":alert: Alerts", uiCmdAction("/telemetry alerts")),
4636
4887
  ],
4637
4888
  uiNavRow("home"),
4638
4889
  ]),
@@ -4646,21 +4897,21 @@ Object.assign(UI_SCREENS, {
4646
4897
  buildKeyboard([
4647
4898
  // Execution Controls
4648
4899
  [
4649
- { text: " Pause", callback_data: "cb:confirm_pause" },
4650
- { text: "▶️ Resume", callback_data: "cb:confirm_resume" },
4651
- { text: "🔄 Restart", callback_data: "cb:confirm_restart" },
4900
+ { text: ":pause: Pause", callback_data: "cb:confirm_pause" },
4901
+ { text: ":play: Resume", callback_data: "cb:confirm_resume" },
4902
+ { text: ":refresh: Restart", callback_data: "cb:confirm_restart" },
4652
4903
  ],
4653
4904
  // Viewing & Starting
4654
4905
  [
4655
- uiButton("📋 Active Tasks", uiCmdAction("/tasks")),
4656
- uiButton("🗂 Task Lists", uiGoAction("task_lists")),
4657
- uiButton("▶️ Start Task", uiInputAction("starttask")),
4906
+ uiButton(":clipboard: Active Tasks", uiCmdAction("/tasks")),
4907
+ uiButton(":folder: Task Lists", uiGoAction("task_lists")),
4908
+ uiButton(":play: Start Task", uiInputAction("starttask")),
4658
4909
  ],
4659
4910
  // Management
4660
4911
  [
4661
- uiButton("🗺️ Planner", uiGoAction("plan")),
4662
- uiButton("🔁 Retry", uiGoAction("retry")),
4663
- uiButton("🧹 Cleanup", uiCmdAction("/cleanup")),
4912
+ uiButton(":grid: Planner", uiGoAction("plan")),
4913
+ uiButton(":repeat: Retry", uiGoAction("retry")),
4914
+ uiButton(":trash: Cleanup", uiCmdAction("/cleanup")),
4664
4915
  ],
4665
4916
  uiNavRow("home"),
4666
4917
  ]),
@@ -4674,7 +4925,7 @@ Object.assign(UI_SCREENS, {
4674
4925
  const rows = [
4675
4926
  [
4676
4927
  uiButton(
4677
- "📥 Backlog",
4928
+ ":download: Backlog",
4678
4929
  uiTokenAction(
4679
4930
  issueUiToken({
4680
4931
  type: "go",
@@ -4684,7 +4935,7 @@ Object.assign(UI_SCREENS, {
4684
4935
  ),
4685
4936
  ),
4686
4937
  uiButton(
4687
- "📝 Draft",
4938
+ ":edit: Draft",
4688
4939
  uiTokenAction(
4689
4940
  issueUiToken({
4690
4941
  type: "go",
@@ -4696,7 +4947,7 @@ Object.assign(UI_SCREENS, {
4696
4947
  ],
4697
4948
  [
4698
4949
  uiButton(
4699
- " Todo",
4950
+ ":check: Todo",
4700
4951
  uiTokenAction(
4701
4952
  issueUiToken({
4702
4953
  type: "go",
@@ -4706,7 +4957,7 @@ Object.assign(UI_SCREENS, {
4706
4957
  ),
4707
4958
  ),
4708
4959
  uiButton(
4709
- "🚧 Active",
4960
+ ":alert: Active",
4710
4961
  uiTokenAction(
4711
4962
  issueUiToken({
4712
4963
  type: "go",
@@ -4718,7 +4969,7 @@ Object.assign(UI_SCREENS, {
4718
4969
  ],
4719
4970
  [
4720
4971
  uiButton(
4721
- "🔍 Review",
4972
+ ":search: Review",
4722
4973
  uiTokenAction(
4723
4974
  issueUiToken({
4724
4975
  type: "go",
@@ -4728,7 +4979,7 @@ Object.assign(UI_SCREENS, {
4728
4979
  ),
4729
4980
  ),
4730
4981
  uiButton(
4731
- " Blocked",
4982
+ ":ban: Blocked",
4732
4983
  uiTokenAction(
4733
4984
  issueUiToken({
4734
4985
  type: "go",
@@ -4740,7 +4991,7 @@ Object.assign(UI_SCREENS, {
4740
4991
  ],
4741
4992
  [
4742
4993
  uiButton(
4743
- "🏁 Done",
4994
+ ":flag: Done",
4744
4995
  uiTokenAction(
4745
4996
  issueUiToken({
4746
4997
  type: "go",
@@ -4813,7 +5064,7 @@ Object.assign(UI_SCREENS, {
4813
5064
  type: "starttask_executor",
4814
5065
  taskId: task.id,
4815
5066
  });
4816
- const label = `▶ ${shortenLabel(task.id || task.title || "Task", 28)}`;
5067
+ const label = `:play: ${shortenLabel(task.id || task.title || "Task", 28)}`;
4817
5068
  return [uiButton(label, uiTokenAction(token))];
4818
5069
  }),
4819
5070
  );
@@ -4823,7 +5074,7 @@ Object.assign(UI_SCREENS, {
4823
5074
  if (safePage > 0) {
4824
5075
  pager.push(
4825
5076
  uiButton(
4826
- "⬅️ Prev",
5077
+ ":arrowRight: Prev",
4827
5078
  uiTokenAction(
4828
5079
  issueUiToken({
4829
5080
  type: "go",
@@ -4837,7 +5088,7 @@ Object.assign(UI_SCREENS, {
4837
5088
  if (safePage < totalPages - 1) {
4838
5089
  pager.push(
4839
5090
  uiButton(
4840
- "Next ➡️",
5091
+ "Next :arrowRight:",
4841
5092
  uiTokenAction(
4842
5093
  issueUiToken({
4843
5094
  type: "go",
@@ -4852,8 +5103,8 @@ Object.assign(UI_SCREENS, {
4852
5103
  }
4853
5104
  rows.push(
4854
5105
  [
4855
- uiButton("🔁 Change Status", uiGoAction("task_lists")),
4856
- uiButton("▶️ Start Task", uiInputAction("starttask")),
5106
+ uiButton(":repeat: Change Status", uiGoAction("task_lists")),
5107
+ uiButton(":play: Start Task", uiInputAction("starttask")),
4857
5108
  ],
4858
5109
  );
4859
5110
  rows.push(uiNavRow("task_lists"));
@@ -4901,15 +5152,15 @@ Object.assign(UI_SCREENS, {
4901
5152
  buildKeyboard([
4902
5153
  // Status
4903
5154
  [
4904
- uiButton("📊 Status", uiCmdAction("/executor")),
4905
- uiButton("🎛️ Slots", uiCmdAction("/executor slots")),
4906
- uiButton("⚙️ Mode", uiCmdAction("/executor mode")),
5155
+ uiButton(":chart: Status", uiCmdAction("/executor")),
5156
+ uiButton(":sliders: Slots", uiCmdAction("/executor slots")),
5157
+ uiButton(":settings: Mode", uiCmdAction("/executor mode")),
4907
5158
  ],
4908
5159
  // Controls
4909
5160
  [
4910
- uiButton(" Pause", uiCmdAction("/pausetasks")),
4911
- uiButton("▶️ Resume", uiCmdAction("/resumetasks")),
4912
- uiButton("🔢 Max Parallel", uiGoAction("maxparallel")),
5161
+ uiButton(":pause: Pause", uiCmdAction("/pausetasks")),
5162
+ uiButton(":play: Resume", uiCmdAction("/resumetasks")),
5163
+ uiButton(":hash: Max Parallel", uiGoAction("maxparallel")),
4913
5164
  ],
4914
5165
  uiNavRow("home"),
4915
5166
  ]),
@@ -4947,20 +5198,20 @@ Object.assign(UI_SCREENS, {
4947
5198
  buildKeyboard([
4948
5199
  // Monitoring
4949
5200
  [
4950
- uiButton("🤖 Active Agents", uiCmdAction("/agents")),
4951
- uiButton("📂 Agent Logs", uiGoAction("agent_logs")),
4952
- uiButton("🧵 Threads", uiGoAction("threads")),
5201
+ uiButton(":bot: Active Agents", uiCmdAction("/agents")),
5202
+ uiButton(":folder: Agent Logs", uiGoAction("agent_logs")),
5203
+ uiButton(":link: Threads", uiGoAction("threads")),
4953
5204
  ],
4954
5205
  // Control
4955
5206
  [
4956
- uiButton("🧭 Steer", uiInputAction("steer")),
4957
- uiButton("🛑 Stop", uiCmdAction("/stop")),
4958
- uiButton("🛰 Background", uiGoAction("background")),
5207
+ uiButton(":compass: Steer", uiInputAction("steer")),
5208
+ uiButton(":close: Stop", uiCmdAction("/stop")),
5209
+ uiButton(":server: Background", uiGoAction("background")),
4959
5210
  ],
4960
5211
  // Context
4961
5212
  [
4962
- uiButton("🧠 History", uiCmdAction("/history")),
4963
- uiButton("📊 Status", uiCmdAction("/status")),
5213
+ uiButton(":cpu: History", uiCmdAction("/history")),
5214
+ uiButton(":chart: Status", uiCmdAction("/status")),
4964
5215
  ],
4965
5216
  uiNavRow("home"),
4966
5217
  ]),
@@ -4976,8 +5227,8 @@ Object.assign(UI_SCREENS, {
4976
5227
  uiButton("New Background Task", uiInputAction("background")),
4977
5228
  ],
4978
5229
  [
4979
- uiButton("🧭 Steer", uiInputAction("steer")),
4980
- uiButton("🛑 Stop", uiCmdAction("/stop")),
5230
+ uiButton(":compass: Steer", uiInputAction("steer")),
5231
+ uiButton(":close: Stop", uiCmdAction("/stop")),
4981
5232
  ],
4982
5233
  uiNavRow("agents"),
4983
5234
  ]),
@@ -5031,12 +5282,12 @@ Object.assign(UI_SCREENS, {
5031
5282
  const pager = [];
5032
5283
  if (safePage > 0) {
5033
5284
  pager.push(
5034
- uiButton("⬅️ Prev", uiGoAction("agent_logs", safePage - 1)),
5285
+ uiButton(":arrowRight: Prev", uiGoAction("agent_logs", safePage - 1)),
5035
5286
  );
5036
5287
  }
5037
5288
  if (safePage < totalPages - 1) {
5038
5289
  pager.push(
5039
- uiButton("Next ➡️", uiGoAction("agent_logs", safePage + 1)),
5290
+ uiButton("Next :arrowRight:", uiGoAction("agent_logs", safePage + 1)),
5040
5291
  );
5041
5292
  }
5042
5293
  if (pager.length) rows.push(pager);
@@ -5094,12 +5345,12 @@ Object.assign(UI_SCREENS, {
5094
5345
  const pager = [];
5095
5346
  if (safePage > 0) {
5096
5347
  pager.push(
5097
- uiButton("⬅️ Prev", uiGoAction("threads_kill", safePage - 1)),
5348
+ uiButton(":arrowRight: Prev", uiGoAction("threads_kill", safePage - 1)),
5098
5349
  );
5099
5350
  }
5100
5351
  if (safePage < totalPages - 1) {
5101
5352
  pager.push(
5102
- uiButton("Next ➡️", uiGoAction("threads_kill", safePage + 1)),
5353
+ uiButton("Next :arrowRight:", uiGoAction("threads_kill", safePage + 1)),
5103
5354
  );
5104
5355
  }
5105
5356
  if (pager.length) rows.push(pager);
@@ -5120,20 +5371,20 @@ Object.assign(UI_SCREENS, {
5120
5371
  buildKeyboard([
5121
5372
  // Core Routing
5122
5373
  [
5123
- uiButton("🤖 Model", uiGoAction("model")),
5124
- uiButton("📦 SDK", uiGoAction("sdk")),
5125
- uiButton("🌍 Region", uiGoAction("region")),
5374
+ uiButton(":bot: Model", uiGoAction("model")),
5375
+ uiButton(":box: SDK", uiGoAction("sdk")),
5376
+ uiButton(":globe: Region", uiGoAction("region")),
5126
5377
  ],
5127
5378
  // Task Routing
5128
5379
  [
5129
- uiButton("🎯 Route Task", uiGoAction("route_task")),
5130
- uiButton("📋 Kanban", uiGoAction("kanban")),
5131
- uiButton("♻️ Auto Backlog", uiGoAction("autobacklog")),
5380
+ uiButton(":target: Route Task", uiGoAction("route_task")),
5381
+ uiButton(":clipboard: Kanban", uiGoAction("kanban")),
5382
+ uiButton(":repeat: Auto Backlog", uiGoAction("autobacklog")),
5132
5383
  ],
5133
5384
  // Config
5134
5385
  [
5135
- uiButton("📐 Requirements", uiGoAction("requirements")),
5136
- uiButton("🏥 Health", uiCmdAction("/health")),
5386
+ uiButton(":ruler: Requirements", uiGoAction("requirements")),
5387
+ uiButton(":heart: Health", uiCmdAction("/health")),
5137
5388
  ],
5138
5389
  uiNavRow("home"),
5139
5390
  ]),
@@ -5307,12 +5558,12 @@ Object.assign(UI_SCREENS, {
5307
5558
  const pager = [];
5308
5559
  if (safePage > 0) {
5309
5560
  pager.push(
5310
- uiButton("⬅️ Prev", uiGoAction("route_workspace", safePage - 1)),
5561
+ uiButton(":arrowRight: Prev", uiGoAction("route_workspace", safePage - 1)),
5311
5562
  );
5312
5563
  }
5313
5564
  if (safePage < totalPages - 1) {
5314
5565
  pager.push(
5315
- uiButton("Next ➡️", uiGoAction("route_workspace", safePage + 1)),
5566
+ uiButton("Next :arrowRight:", uiGoAction("route_workspace", safePage + 1)),
5316
5567
  );
5317
5568
  }
5318
5569
  if (pager.length) rows.push(pager);
@@ -5362,12 +5613,12 @@ Object.assign(UI_SCREENS, {
5362
5613
  const pager = [];
5363
5614
  if (safePage > 0) {
5364
5615
  pager.push(
5365
- uiButton("⬅️ Prev", uiGoAction("route_role", safePage - 1)),
5616
+ uiButton(":arrowRight: Prev", uiGoAction("route_role", safePage - 1)),
5366
5617
  );
5367
5618
  }
5368
5619
  if (safePage < totalPages - 1) {
5369
5620
  pager.push(
5370
- uiButton("Next ➡️", uiGoAction("route_role", safePage + 1)),
5621
+ uiButton("Next :arrowRight:", uiGoAction("route_role", safePage + 1)),
5371
5622
  );
5372
5623
  }
5373
5624
  if (pager.length) rows.push(pager);
@@ -5388,27 +5639,27 @@ Object.assign(UI_SCREENS, {
5388
5639
  buildKeyboard([
5389
5640
  // Workspaces
5390
5641
  [
5391
- uiButton("📂 My Workspaces", uiGoAction("managed_workspaces")),
5392
- uiButton(" New Workspace", uiInputAction("workspace_create")),
5393
- uiButton("🎯 Switch Active", uiGoAction("workspace_switch")),
5642
+ uiButton(":folder: My Workspaces", uiGoAction("managed_workspaces")),
5643
+ uiButton(":plus: New Workspace", uiInputAction("workspace_create")),
5644
+ uiButton(":target: Switch Active", uiGoAction("workspace_switch")),
5394
5645
  ],
5395
5646
  // Worktrees
5396
5647
  [
5397
- uiButton("🌳 Worktrees", uiCmdAction("/worktrees")),
5398
- uiButton("📊 Stats", uiCmdAction("/worktrees stats")),
5399
- uiButton("🧹 Prune", uiCmdAction("/worktrees prune")),
5648
+ uiButton(":git: Worktrees", uiCmdAction("/worktrees")),
5649
+ uiButton(":chart: Stats", uiCmdAction("/worktrees stats")),
5650
+ uiButton(":trash: Prune", uiCmdAction("/worktrees prune")),
5400
5651
  ],
5401
5652
  // Repos & Shared
5402
5653
  [
5403
- uiButton("📁 Repos", uiCmdAction("/repos")),
5404
- uiButton("🔓 Release WT", uiGoAction("worktrees_release")),
5405
- uiButton("🔄 Scan Disk", uiCmdAction("/workspace scan")),
5654
+ uiButton(":folder: Repos", uiCmdAction("/repos")),
5655
+ uiButton(":unlock: Release WT", uiGoAction("worktrees_release")),
5656
+ uiButton(":refresh: Scan Disk", uiCmdAction("/workspace scan")),
5406
5657
  ],
5407
5658
  // Shared
5408
5659
  [
5409
- uiButton("📋 Shared", uiCmdAction("/shared_workspaces")),
5410
- uiButton(" Claim", uiGoAction("shared_claim")),
5411
- uiButton("🚪 Release", uiGoAction("shared_release")),
5660
+ uiButton(":clipboard: Shared", uiCmdAction("/shared_workspaces")),
5661
+ uiButton(":check: Claim", uiGoAction("shared_claim")),
5662
+ uiButton(":close: Release", uiGoAction("shared_release")),
5412
5663
  ],
5413
5664
  uiNavRow("home"),
5414
5665
  ]),
@@ -5424,7 +5675,7 @@ Object.assign(UI_SCREENS, {
5424
5675
  const active = getActiveLocalWorkspace(configDir);
5425
5676
  if (!workspaces.length) {
5426
5677
  return buildKeyboard([
5427
- [uiButton("🔄 Scan", uiCmdAction("/workspace scan"))],
5678
+ [uiButton(":refresh: Scan", uiCmdAction("/workspace scan"))],
5428
5679
  [uiButton("Type Workspace ID", uiInputAction("workspace_switch"))],
5429
5680
  uiNavRow("workspaces"),
5430
5681
  ]);
@@ -5457,7 +5708,7 @@ Object.assign(UI_SCREENS, {
5457
5708
  ],
5458
5709
  backAction: uiGoAction("workspace_switch", safePage),
5459
5710
  });
5460
- const prefix = isActive ? "🟢" : "";
5711
+ const prefix = isActive ? ":dot:" : ":dot:";
5461
5712
  return uiButton(
5462
5713
  `${prefix} ${shortenLabel(workspace.name || workspace.id)}`,
5463
5714
  uiTokenAction(token),
@@ -5470,12 +5721,12 @@ Object.assign(UI_SCREENS, {
5470
5721
  const pager = [];
5471
5722
  if (safePage > 0) {
5472
5723
  pager.push(
5473
- uiButton("⬅️ Prev", uiGoAction("workspace_switch", safePage - 1)),
5724
+ uiButton(":arrowRight: Prev", uiGoAction("workspace_switch", safePage - 1)),
5474
5725
  );
5475
5726
  }
5476
5727
  if (safePage < totalPages - 1) {
5477
5728
  pager.push(
5478
- uiButton("Next ➡️", uiGoAction("workspace_switch", safePage + 1)),
5729
+ uiButton("Next :arrowRight:", uiGoAction("workspace_switch", safePage + 1)),
5479
5730
  );
5480
5731
  }
5481
5732
  if (pager.length) rows.push(pager);
@@ -5484,7 +5735,7 @@ Object.assign(UI_SCREENS, {
5484
5735
  rows.push([
5485
5736
  uiButton("Type Workspace ID", uiInputAction("workspace_switch")),
5486
5737
  ]);
5487
- rows.push([uiButton("🔄 Scan", uiCmdAction("/workspace scan"))]);
5738
+ rows.push([uiButton(":refresh: Scan", uiCmdAction("/workspace scan"))]);
5488
5739
  rows.push(uiNavRow("workspaces"));
5489
5740
  return buildKeyboard(rows);
5490
5741
  },
@@ -5536,12 +5787,12 @@ Object.assign(UI_SCREENS, {
5536
5787
  const pager = [];
5537
5788
  if (safePage > 0) {
5538
5789
  pager.push(
5539
- uiButton("⬅️ Prev", uiGoAction("worktrees_release", safePage - 1)),
5790
+ uiButton(":arrowRight: Prev", uiGoAction("worktrees_release", safePage - 1)),
5540
5791
  );
5541
5792
  }
5542
5793
  if (safePage < totalPages - 1) {
5543
5794
  pager.push(
5544
- uiButton("Next ➡️", uiGoAction("worktrees_release", safePage + 1)),
5795
+ uiButton("Next :arrowRight:", uiGoAction("worktrees_release", safePage + 1)),
5545
5796
  );
5546
5797
  }
5547
5798
  if (pager.length) rows.push(pager);
@@ -5589,7 +5840,7 @@ Object.assign(UI_SCREENS, {
5589
5840
  detailLines,
5590
5841
  backAction: uiGoAction("shared_claim", safePage),
5591
5842
  });
5592
- const emoji = ws.availability === "available" ? "" : "🔒";
5843
+ const emoji = ws.availability === "available" ? ":check:" : ":lock:";
5593
5844
  return uiButton(
5594
5845
  `${emoji} ${shortenLabel(ws.id)}`,
5595
5846
  uiTokenAction(token),
@@ -5601,12 +5852,12 @@ Object.assign(UI_SCREENS, {
5601
5852
  const pager = [];
5602
5853
  if (safePage > 0) {
5603
5854
  pager.push(
5604
- uiButton("⬅️ Prev", uiGoAction("shared_claim", safePage - 1)),
5855
+ uiButton(":arrowRight: Prev", uiGoAction("shared_claim", safePage - 1)),
5605
5856
  );
5606
5857
  }
5607
5858
  if (safePage < totalPages - 1) {
5608
5859
  pager.push(
5609
- uiButton("Next ➡️", uiGoAction("shared_claim", safePage + 1)),
5860
+ uiButton("Next :arrowRight:", uiGoAction("shared_claim", safePage + 1)),
5610
5861
  );
5611
5862
  }
5612
5863
  if (pager.length) rows.push(pager);
@@ -5650,7 +5901,7 @@ Object.assign(UI_SCREENS, {
5650
5901
  detailLines,
5651
5902
  backAction: uiGoAction("shared_release", safePage),
5652
5903
  });
5653
- const emoji = ws.availability === "leased" ? "🔓" : "ℹ️";
5904
+ const emoji = ws.availability === "leased" ? ":unlock:" : ":help:";
5654
5905
  return uiButton(
5655
5906
  `${emoji} ${shortenLabel(ws.id)}`,
5656
5907
  uiTokenAction(token),
@@ -5662,12 +5913,12 @@ Object.assign(UI_SCREENS, {
5662
5913
  const pager = [];
5663
5914
  if (safePage > 0) {
5664
5915
  pager.push(
5665
- uiButton("⬅️ Prev", uiGoAction("shared_release", safePage - 1)),
5916
+ uiButton(":arrowRight: Prev", uiGoAction("shared_release", safePage - 1)),
5666
5917
  );
5667
5918
  }
5668
5919
  if (safePage < totalPages - 1) {
5669
5920
  pager.push(
5670
- uiButton("Next ➡️", uiGoAction("shared_release", safePage + 1)),
5921
+ uiButton("Next :arrowRight:", uiGoAction("shared_release", safePage + 1)),
5671
5922
  );
5672
5923
  }
5673
5924
  if (pager.length) rows.push(pager);
@@ -5688,8 +5939,8 @@ Object.assign(UI_SCREENS, {
5688
5939
  const active = getActiveLocalWorkspace(configDir);
5689
5940
  if (!workspaces.length) {
5690
5941
  return buildKeyboard([
5691
- [uiButton(" Create Workspace", uiInputAction("workspace_create"))],
5692
- [uiButton("🔄 Scan Disk", uiCmdAction("/workspace scan"))],
5942
+ [uiButton(":plus: Create Workspace", uiInputAction("workspace_create"))],
5943
+ [uiButton(":refresh: Scan Disk", uiCmdAction("/workspace scan"))],
5693
5944
  uiNavRow("workspaces"),
5694
5945
  ]);
5695
5946
  }
@@ -5707,7 +5958,7 @@ Object.assign(UI_SCREENS, {
5707
5958
  const repoNames = Array.isArray(ws.repos)
5708
5959
  ? ws.repos.map((r) => (typeof r === "string" ? r : r.name || r.url || "?")).join(", ")
5709
5960
  : "none";
5710
- const prefix = isActive ? "🟢" : "";
5961
+ const prefix = isActive ? ":dot:" : ":dot:";
5711
5962
  const switchToken = issueUiToken({
5712
5963
  type: "confirm_cmd",
5713
5964
  command: `/workspace switch ${ws.id}`,
@@ -5717,7 +5968,7 @@ Object.assign(UI_SCREENS, {
5717
5968
  `Name: \`${ws.name || ws.id}\``,
5718
5969
  `ID: \`${ws.id}\``,
5719
5970
  `Repos (${repoCount}): ${repoNames || "none"}`,
5720
- `Status: ${isActive ? " Active" : "Inactive"}`,
5971
+ `Status: ${isActive ? ":check: Active" : "Inactive"}`,
5721
5972
  ],
5722
5973
  backAction: uiGoAction("managed_workspaces", safePage),
5723
5974
  });
@@ -5726,27 +5977,27 @@ Object.assign(UI_SCREENS, {
5726
5977
  `${prefix} ${shortenLabel(ws.name || ws.id, 20)} (${repoCount})`,
5727
5978
  uiTokenAction(switchToken),
5728
5979
  ),
5729
- uiButton("⬇️ Pull", uiCmdAction(`/workspace pull ${ws.id}`)),
5730
- uiButton("🗑️", uiCmdAction(`/workspace delete ${ws.id}`)),
5980
+ uiButton(":download: Pull", uiCmdAction(`/workspace pull ${ws.id}`)),
5981
+ uiButton(":trash:", uiCmdAction(`/workspace delete ${ws.id}`)),
5731
5982
  ]);
5732
5983
  }
5733
5984
  if (totalPages > 1) {
5734
5985
  const pager = [];
5735
5986
  if (safePage > 0) {
5736
5987
  pager.push(
5737
- uiButton("⬅️ Prev", uiGoAction("managed_workspaces", safePage - 1)),
5988
+ uiButton(":arrowRight: Prev", uiGoAction("managed_workspaces", safePage - 1)),
5738
5989
  );
5739
5990
  }
5740
5991
  if (safePage < totalPages - 1) {
5741
5992
  pager.push(
5742
- uiButton("Next ➡️", uiGoAction("managed_workspaces", safePage + 1)),
5993
+ uiButton("Next :arrowRight:", uiGoAction("managed_workspaces", safePage + 1)),
5743
5994
  );
5744
5995
  }
5745
5996
  if (pager.length) rows.push(pager);
5746
5997
  }
5747
5998
  rows.push([
5748
- uiButton(" Create", uiInputAction("workspace_create")),
5749
- uiButton("🔄 Scan", uiCmdAction("/workspace scan")),
5999
+ uiButton(":plus: Create", uiInputAction("workspace_create")),
6000
+ uiButton(":refresh: Scan", uiCmdAction("/workspace scan")),
5750
6001
  ]);
5751
6002
  rows.push(uiNavRow("workspaces"));
5752
6003
  return buildKeyboard(rows);
@@ -5763,18 +6014,18 @@ Object.assign(UI_SCREENS, {
5763
6014
  buildKeyboard([
5764
6015
  // Logs
5765
6016
  [
5766
- uiButton("📝 System Logs", uiGoAction("logs_tail")),
5767
- uiButton("📂 Agent Logs", uiGoAction("agent_logs")),
6017
+ uiButton(":edit: System Logs", uiGoAction("logs_tail")),
6018
+ uiButton(":folder: Agent Logs", uiGoAction("agent_logs")),
5768
6019
  ],
5769
6020
  // Git
5770
6021
  [
5771
- uiButton("🌿 Branches", uiCmdAction("/branches")),
5772
- uiButton("💡 Diff", uiCmdAction("/diff")),
5773
- uiButton("🔎 Git", uiGoAction("git")),
6022
+ uiButton(":git: Branches", uiCmdAction("/branches")),
6023
+ uiButton(":lightbulb: Diff", uiCmdAction("/diff")),
6024
+ uiButton(":search: Git", uiGoAction("git")),
5774
6025
  ],
5775
6026
  // Utils
5776
6027
  [
5777
- uiButton("🖥 Shell", uiGoAction("shell")),
6028
+ uiButton(":monitor: Shell", uiGoAction("shell")),
5778
6029
  ],
5779
6030
  uiNavRow("home"),
5780
6031
  ]),
@@ -5843,8 +6094,8 @@ Object.assign(UI_SCREENS, {
5843
6094
  keyboard: () =>
5844
6095
  buildKeyboard([
5845
6096
  [
5846
- uiButton("📱 WhatsApp", uiCmdAction("/whatsapp")),
5847
- uiButton("📦 Container", uiCmdAction("/container")),
6097
+ uiButton(":phone: WhatsApp", uiCmdAction("/whatsapp")),
6098
+ uiButton(":box: Container", uiCmdAction("/container")),
5848
6099
  ],
5849
6100
  uiNavRow("home"),
5850
6101
  ]),
@@ -5857,18 +6108,18 @@ Object.assign(UI_SCREENS, {
5857
6108
  buildKeyboard([
5858
6109
  // Interaction
5859
6110
  [
5860
- uiButton("💬 Ask", uiInputAction("ask")),
5861
- uiButton("🧭 Steer", uiInputAction("steer")),
6111
+ uiButton(":chat: Ask", uiInputAction("ask")),
6112
+ uiButton(":compass: Steer", uiInputAction("steer")),
5862
6113
  ],
5863
6114
  // Context
5864
6115
  [
5865
- uiButton("🧠 History", uiCmdAction("/history")),
5866
- uiButton("🧹 Clear", "confirm_clear"),
6116
+ uiButton(":cpu: History", uiCmdAction("/history")),
6117
+ uiButton(":trash: Clear", "confirm_clear"),
5867
6118
  ],
5868
6119
  // Control
5869
6120
  [
5870
- uiButton("🛰 Background", uiGoAction("background")),
5871
- uiButton("🛑 Stop", uiCmdAction("/stop")),
6121
+ uiButton(":server: Background", uiGoAction("background")),
6122
+ uiButton(":close: Stop", uiCmdAction("/stop")),
5872
6123
  ],
5873
6124
  uiNavRow("home"),
5874
6125
  ]),
@@ -5965,7 +6216,7 @@ async function handleUiAction({ chatId, messageId, data }) {
5965
6216
  if (messageId) {
5966
6217
  await deleteDirect(chatId, messageId);
5967
6218
  }
5968
- await sendReply(chatId, " Input cancelled.");
6219
+ await sendReply(chatId, ":check: Input cancelled.");
5969
6220
  return;
5970
6221
  }
5971
6222
  if (type === "confirm_clear") {
@@ -6040,7 +6291,7 @@ async function handleUiAction({ chatId, messageId, data }) {
6040
6291
  }
6041
6292
  await sendReply(
6042
6293
  chatId,
6043
- " That option expired. Please reopen the menu.",
6294
+ ":clock: That option expired. Please reopen the menu.",
6044
6295
  );
6045
6296
  return;
6046
6297
  }
@@ -6105,7 +6356,7 @@ async function handleUiAction({ chatId, messageId, data }) {
6105
6356
  if (!executor) {
6106
6357
  await sendReply(
6107
6358
  chatId,
6108
- `⚠️ Executor "${payload.executor || "auto"}" not available (mode: ${mode}).`,
6359
+ `:alert: Executor "${payload.executor || "auto"}" not available (mode: ${mode}).`,
6109
6360
  );
6110
6361
  await showStartTaskExecutorPicker(chatId, payload.taskId);
6111
6362
  return;
@@ -6154,7 +6405,7 @@ async function handleWebAppData(raw, chatId) {
6154
6405
  }
6155
6406
 
6156
6407
  if (!payload || typeof payload !== "object") {
6157
- await sendReply(chatId, "⚠️ Web app sent an invalid payload.");
6408
+ await sendReply(chatId, ":alert: Web app sent an invalid payload.");
6158
6409
  return;
6159
6410
  }
6160
6411
 
@@ -6175,7 +6426,7 @@ async function handleWebAppData(raw, chatId) {
6175
6426
  return;
6176
6427
  }
6177
6428
 
6178
- await sendReply(chatId, "⚠️ Web app request not recognized.");
6429
+ await sendReply(chatId, ":alert: Web app request not recognized.");
6179
6430
  }
6180
6431
 
6181
6432
  // ── Built-in Command Handlers ────────────────────────────────────────────────
@@ -6418,25 +6669,41 @@ async function cmdApp(chatId) {
6418
6669
  if (!uiUrl) {
6419
6670
  await sendReply(
6420
6671
  chatId,
6421
- "⚠️ Mini App not configured. Set TELEGRAM_UI_PORT and TELEGRAM_MINIAPP_ENABLED=true in your environment.",
6672
+ ":alert: Mini App not configured. Set TELEGRAM_UI_PORT and TELEGRAM_MINIAPP_ENABLED=true in your environment.",
6422
6673
  );
6423
6674
  return;
6424
6675
  }
6425
6676
  const browserOptions = getBrowserUiUrlOptions();
6677
+ const voiceMeetingWebAppUrl = getMeetingWebAppUrl("voice", {
6678
+ chat_id: String(chatId || "").trim(),
6679
+ });
6680
+ const videoMeetingWebAppUrl = getMeetingWebAppUrl("video", {
6681
+ chat_id: String(chatId || "").trim(),
6682
+ });
6426
6683
  const rows = [];
6427
6684
  if (webAppUrl) {
6428
- rows.unshift([{ text: "📱 Open Control Center", web_app: { url: webAppUrl } }]);
6685
+ rows.unshift([{ text: ":phone: Open Control Center", web_app: { url: webAppUrl } }]);
6686
+ }
6687
+ if (voiceMeetingWebAppUrl || videoMeetingWebAppUrl) {
6688
+ rows.push([
6689
+ voiceMeetingWebAppUrl
6690
+ ? { text: "Voice Meeting", web_app: { url: voiceMeetingWebAppUrl } }
6691
+ : { text: "Voice Meeting", callback_data: "/call" },
6692
+ videoMeetingWebAppUrl
6693
+ ? { text: "Video Meeting", web_app: { url: videoMeetingWebAppUrl } }
6694
+ : { text: "Video Meeting", callback_data: "/videocall" },
6695
+ ]);
6429
6696
  }
6430
6697
  if (browserOptions.length > 0) {
6431
6698
  rows.push(...browserOptions.map((option) => [{ text: option.label, url: option.url }]));
6432
6699
  } else {
6433
- rows.push([{ text: "🌐 Open in Browser", url: getBrowserUiUrl() || uiUrl }]);
6700
+ rows.push([{ text: ":globe: Open in Browser", url: getBrowserUiUrl() || uiUrl }]);
6434
6701
  }
6435
6702
  const keyboard = { inline_keyboard: rows };
6436
6703
 
6437
6704
  await sendDirect(
6438
6705
  chatId,
6439
- "🚀 *Bosun Control Center*\n\nOpen the Mini App or access via browser:",
6706
+ ":rocket: *Bosun Control Center*\n\nOpen the Mini App or access via browser:",
6440
6707
  {
6441
6708
  parseMode: "Markdown",
6442
6709
  reply_markup: keyboard,
@@ -6444,6 +6711,70 @@ async function cmdApp(chatId) {
6444
6711
  );
6445
6712
  }
6446
6713
 
6714
+ async function cmdCall(chatId, args = "") {
6715
+ const callType = normalizeMeetingCallType(args);
6716
+ const isVideo = callType === "video";
6717
+ const label = isVideo ? "video" : "voice";
6718
+ const title = isVideo ? "*Video Meeting*" : "*Voice Meeting*";
6719
+ syncUiUrlsFromServer();
6720
+
6721
+ const webAppMeetingUrl = getMeetingWebAppUrl(callType, {
6722
+ chat_id: String(chatId || "").trim(),
6723
+ });
6724
+ const browserOptions = getMeetingBrowserUrlOptions(callType, {
6725
+ chat_id: String(chatId || "").trim(),
6726
+ });
6727
+
6728
+ const rows = [];
6729
+ if (webAppMeetingUrl) {
6730
+ rows.push([
6731
+ {
6732
+ text: isVideo ? "Start Video Meeting" : "Start Voice Meeting",
6733
+ web_app: { url: webAppMeetingUrl },
6734
+ },
6735
+ ]);
6736
+ }
6737
+ if (browserOptions.length > 0) {
6738
+ rows.push(
6739
+ ...browserOptions.map((option) => [
6740
+ {
6741
+ text: `${option.label} (${label})`,
6742
+ url: option.url,
6743
+ },
6744
+ ]),
6745
+ );
6746
+ }
6747
+
6748
+ if (rows.length === 0) {
6749
+ await sendReply(
6750
+ chatId,
6751
+ ":alert: Meeting UI is not available yet. Set TELEGRAM_WEBAPP_URL (HTTPS) or enable the UI tunnel first.",
6752
+ );
6753
+ return;
6754
+ }
6755
+
6756
+ await sendDirect(
6757
+ chatId,
6758
+ [
6759
+ title,
6760
+ "",
6761
+ "One tap opens the Bosun meeting room with your default agent + voice settings.",
6762
+ isVideo
6763
+ ? "Video mode auto-starts camera. You can switch to screen share any time."
6764
+ : "Voice mode starts instantly. You can enable camera/screen share in-call.",
6765
+ ].join("\n"),
6766
+ {
6767
+ parseMode: "Markdown",
6768
+ reply_markup: { inline_keyboard: rows },
6769
+ },
6770
+ );
6771
+ }
6772
+
6773
+ async function cmdVideoCall(chatId, args = "") {
6774
+ const normalized = String(args || "").trim();
6775
+ await cmdCall(chatId, normalized ? `video ${normalized}` : "video");
6776
+ }
6777
+
6447
6778
  async function cmdMenu(chatId) {
6448
6779
  syncUiUrlsFromServer();
6449
6780
  if (telegramApiReachable !== false) {
@@ -6460,7 +6791,7 @@ async function cmdCancel(chatId) {
6460
6791
  return;
6461
6792
  }
6462
6793
  clearPendingUiInput(chatId);
6463
- await sendReply(chatId, " Input cancelled.");
6794
+ await sendReply(chatId, ":check: Input cancelled.");
6464
6795
  }
6465
6796
 
6466
6797
  async function cmdHelp(chatId) {
@@ -6468,7 +6799,7 @@ async function cmdHelp(chatId) {
6468
6799
  }
6469
6800
 
6470
6801
  async function cmdHelpFull(chatId) {
6471
- const lines = ["🤖 Bosun Primary Agent — All Commands:\n"];
6802
+ const lines = [":bot: Bosun Primary Agent — All Commands:\n"];
6472
6803
  for (const [cmd, { desc }] of Object.entries(COMMANDS)) {
6473
6804
  lines.push(`${cmd} — ${desc}`);
6474
6805
  }
@@ -6564,7 +6895,7 @@ async function cmdTelemetry(chatId, args = "") {
6564
6895
  .sort((a, b) => b[1] - a[1])
6565
6896
  .slice(0, 8);
6566
6897
  const lines = [
6567
- "🧯 Telemetry — Top Errors",
6898
+ ":alert: Telemetry — Top Errors",
6568
6899
  `Window: last ${days}d`,
6569
6900
  "",
6570
6901
  ];
@@ -6579,7 +6910,7 @@ async function cmdTelemetry(chatId, args = "") {
6579
6910
  const summary = summarizeTelemetry(metrics, days);
6580
6911
  if (!summary) return sendReply(chatId, "No telemetry metrics found.");
6581
6912
  const lines = [
6582
- "🧪 Telemetry — Executor Mix",
6913
+ ":beaker: Telemetry — Executor Mix",
6583
6914
  `Window: last ${days}d`,
6584
6915
  "",
6585
6916
  ];
@@ -6597,7 +6928,7 @@ async function cmdTelemetry(chatId, args = "") {
6597
6928
  return sendReply(chatId, "No analyzer alerts found.");
6598
6929
  }
6599
6930
  const lines = [
6600
- "🚨 Telemetry — Recent Alerts",
6931
+ ":alert: Telemetry — Recent Alerts",
6601
6932
  `Window: last ${days}d`,
6602
6933
  "",
6603
6934
  ...alerts.slice(-10).map((a) => {
@@ -6616,7 +6947,7 @@ async function cmdTelemetry(chatId, args = "") {
6616
6947
  return sendReply(chatId, "No telemetry metrics found.");
6617
6948
  }
6618
6949
  const lines = [
6619
- "📈 Telemetry Summary",
6950
+ ":chart: Telemetry Summary",
6620
6951
  `Window: last ${days}d`,
6621
6952
  "",
6622
6953
  `Sessions: ${summary.total}`,
@@ -6663,7 +6994,7 @@ async function cmdAsk(chatId, args) {
6663
6994
  }
6664
6995
 
6665
6996
  async function cmdStatus(chatId) {
6666
- await sendReply(chatId, " Reading orchestrator status...");
6997
+ await sendReply(chatId, ":clock: Reading orchestrator status...");
6667
6998
 
6668
6999
  let statusText = "Status unavailable";
6669
7000
 
@@ -6703,7 +7034,7 @@ async function cmdStatus(chatId) {
6703
7034
  : [];
6704
7035
 
6705
7036
  const lines = [
6706
- "📊 Bosun Orchestrator Status",
7037
+ ":chart: Bosun Orchestrator Status",
6707
7038
  "",
6708
7039
  `Running: ${counts.running ?? 0}`,
6709
7040
  `Review: ${counts.review ?? 0}`,
@@ -6719,14 +7050,14 @@ async function cmdStatus(chatId) {
6719
7050
  if (errors.length > 0) {
6720
7051
  lines.push(
6721
7052
  "",
6722
- "⚠️ Error tasks:",
7053
+ ":alert: Error tasks:",
6723
7054
  ...errors.slice(0, 5).map((t) => ` - ${t}`),
6724
7055
  );
6725
7056
  }
6726
7057
  if (manualReviews.length > 0) {
6727
7058
  lines.push(
6728
7059
  "",
6729
- "👀 Manual review:",
7060
+ ":eye: Manual review:",
6730
7061
  ...manualReviews.slice(0, 5).map((t) => ` - ${t}`),
6731
7062
  );
6732
7063
  }
@@ -6825,22 +7156,22 @@ async function cmdTasks(chatId) {
6825
7156
  : dur >= 60
6826
7157
  ? Math.round(dur / 60) + "m"
6827
7158
  : dur + "s";
6828
- lines.push(`⏸ PAUSED (for ${durStr}) — /resumetasks to resume`);
7159
+ lines.push(`:pause: PAUSED (for ${durStr}) — /resumetasks to resume`);
6829
7160
  lines.push("");
6830
7161
  }
6831
7162
 
6832
7163
  if (executorStatus.slots.length > 0) {
6833
7164
  lines.push(
6834
- `📋 Active Agents (${executorStatus.activeSlots}/${executorStatus.maxParallel} slots)\n`,
7165
+ `:clipboard: Active Agents (${executorStatus.activeSlots}/${executorStatus.maxParallel} slots)\n`,
6835
7166
  );
6836
7167
 
6837
7168
  for (const slot of executorStatus.slots) {
6838
7169
  const emoji =
6839
7170
  slot.status === "running"
6840
- ? "🟢"
7171
+ ? ":dot:"
6841
7172
  : slot.status === "error"
6842
- ? ""
6843
- : "🔵";
7173
+ ? ":close:"
7174
+ : ":dot:";
6844
7175
  const runStr = formatRuntimeSeconds(slot.runningFor);
6845
7176
  const agentId =
6846
7177
  Number.isFinite(slot.agentInstanceId) && slot.agentInstanceId > 0
@@ -6853,7 +7184,7 @@ async function cmdTasks(chatId) {
6853
7184
  lines.push(`${emoji} Agent ${agentId} • ${shortBranch}`);
6854
7185
  lines.push(` ${slot.taskTitle}`);
6855
7186
  lines.push(
6856
- ` SDK: ${slot.sdk} | ⏱️ ${runStr} | Attempt #${slot.attempt} | Task ${slot.taskId.substring(0, 8)}`,
7187
+ ` SDK: ${slot.sdk} | :clock: ${runStr} | Attempt #${slot.attempt} | Task ${slot.taskId.substring(0, 8)}`,
6857
7188
  );
6858
7189
 
6859
7190
  // Git diff stats
@@ -6868,7 +7199,7 @@ async function cmdTasks(chatId) {
6868
7199
  const delMatch = diffStat.match(/(\d+) deletion/);
6869
7200
  const filesMatch = diffStat.match(/(\d+) file/);
6870
7201
  lines.push(
6871
- ` 📊 ${filesMatch?.[1] || 0} files | +${insMatch?.[1] || 0} -${delMatch?.[1] || 0}`,
7202
+ ` :chart: ${filesMatch?.[1] || 0} files | +${insMatch?.[1] || 0} -${delMatch?.[1] || 0}`,
6872
7203
  );
6873
7204
  }
6874
7205
  } catch {
@@ -6892,7 +7223,7 @@ async function cmdTasks(chatId) {
6892
7223
  reviewQueued,
6893
7224
  );
6894
7225
  if (reviewCount > 0) {
6895
- lines.push(`👀 In review: ${reviewCount} task(s)`);
7226
+ lines.push(`:eye: In review: ${reviewCount} task(s)`);
6896
7227
  if (reviewStatus) {
6897
7228
  lines.push(
6898
7229
  ` Review agent queue: active=${reviewStatus.activeReviews || 0}, queued=${reviewStatus.queuedReviews || 0}, completed=${reviewStatus.completedReviews || 0}`,
@@ -6914,7 +7245,7 @@ async function cmdTasks(chatId) {
6914
7245
  } else {
6915
7246
  // No active slots — show status summary
6916
7247
  lines.push(
6917
- `📋 No active agents (0/${executorStatus.maxParallel} slots)`,
7248
+ `:clipboard: No active agents (0/${executorStatus.maxParallel} slots)`,
6918
7249
  );
6919
7250
  const reviewAgent = _getReviewAgent?.();
6920
7251
  const reviewStatus =
@@ -6928,7 +7259,7 @@ async function cmdTasks(chatId) {
6928
7259
  Number(reviewStatus?.queuedReviews || 0),
6929
7260
  );
6930
7261
  if (reviewCount > 0) {
6931
- lines.push(`👀 In review: ${reviewCount} task(s)`);
7262
+ lines.push(`:eye: In review: ${reviewCount} task(s)`);
6932
7263
  if (reviewTaskIds.length > 0) {
6933
7264
  for (const taskId of reviewTaskIds.slice(0, 5)) {
6934
7265
  lines.push(` - ${taskId}`);
@@ -6937,7 +7268,7 @@ async function cmdTasks(chatId) {
6937
7268
  }
6938
7269
  if (executorStatus.blockedTasks?.length > 0) {
6939
7270
  lines.push(
6940
- `\n ${executorStatus.blockedTasks.length} task(s) blocked (exceeded retry limit)`,
7271
+ `\n:ban: ${executorStatus.blockedTasks.length} task(s) blocked (exceeded retry limit)`,
6941
7272
  );
6942
7273
  }
6943
7274
  lines.push("");
@@ -6962,21 +7293,21 @@ async function cmdTasks(chatId) {
6962
7293
  return;
6963
7294
  }
6964
7295
 
6965
- const lines = ["📋 Active Task Attempts\n"];
7296
+ const lines = [":clipboard: Active Task Attempts\n"];
6966
7297
 
6967
7298
  for (const [id, attempt] of Object.entries(attempts)) {
6968
7299
  if (!attempt) continue;
6969
7300
  const status = attempt.status || "unknown";
6970
7301
  const emoji =
6971
7302
  status === "running"
6972
- ? "🟢"
7303
+ ? ":dot:"
6973
7304
  : status === "review"
6974
- ? "👀"
7305
+ ? ":eye:"
6975
7306
  : status === "error"
6976
- ? ""
7307
+ ? ":close:"
6977
7308
  : status === "completed"
6978
- ? ""
6979
- : "⏸️";
7309
+ ? ":check:"
7310
+ : ":pause:";
6980
7311
  const branch = attempt.branch || "";
6981
7312
  const pr = attempt.pr_number ? ` PR#${attempt.pr_number}` : "";
6982
7313
  const title = attempt.task_title || attempt.task_id || id;
@@ -6999,7 +7330,7 @@ async function cmdTasks(chatId) {
6999
7330
  const hrs = Math.floor(mins / 60);
7000
7331
  const remMin = mins % 60;
7001
7332
  const durStr = hrs > 0 ? `${hrs}h ${remMin}m` : `${mins}m`;
7002
- lines.push(` ⏱️ Active: ${durStr}`);
7333
+ lines.push(` :clock: Active: ${durStr}`);
7003
7334
  }
7004
7335
 
7005
7336
  if (branch) {
@@ -7013,7 +7344,7 @@ async function cmdTasks(chatId) {
7013
7344
  const delMatch = diffStat.match(/(\d+) deletion/);
7014
7345
  const filesMatch = diffStat.match(/(\d+) file/);
7015
7346
  lines.push(
7016
- ` 📊 ${filesMatch?.[1] || 0} files | +${insMatch?.[1] || 0} -${delMatch?.[1] || 0}`,
7347
+ ` :chart: ${filesMatch?.[1] || 0} files | +${insMatch?.[1] || 0} -${delMatch?.[1] || 0}`,
7017
7348
  );
7018
7349
  }
7019
7350
  } catch {
@@ -7092,7 +7423,7 @@ async function cmdStartTask(chatId, args) {
7092
7423
  if (executorArg && !normalizedExecutor) {
7093
7424
  await sendReply(
7094
7425
  chatId,
7095
- `⚠️ Unknown executor "${executorArg}". Use internal or vk.`,
7426
+ `:alert: Unknown executor "${executorArg}". Use internal or vk.`,
7096
7427
  );
7097
7428
  return;
7098
7429
  }
@@ -7110,7 +7441,7 @@ async function cmdStartTask(chatId, args) {
7110
7441
  .join(" | ");
7111
7442
  await sendReply(
7112
7443
  chatId,
7113
- `⚠️ Executor "${normalizedExecutor || "auto"}" not available (mode: ${executorMode}). Available: ${options || "none"}`,
7444
+ `:alert: Executor "${normalizedExecutor || "auto"}" not available (mode: ${executorMode}). Available: ${options || "none"}`,
7114
7445
  );
7115
7446
  return;
7116
7447
  }
@@ -7125,7 +7456,7 @@ async function cmdStartTask(chatId, args) {
7125
7456
  if (typeof adapter.submitTaskAttempt !== "function") {
7126
7457
  await sendReply(
7127
7458
  chatId,
7128
- `⚠️ VK executor not available for current backend (${getKanbanBackendName()}).`,
7459
+ `:alert: VK executor not available for current backend (${getKanbanBackendName()}).`,
7129
7460
  );
7130
7461
  return;
7131
7462
  }
@@ -7142,7 +7473,7 @@ async function cmdStartTask(chatId, args) {
7142
7473
  );
7143
7474
  }
7144
7475
  const detailLines = [
7145
- `✅ VK executor submitted for ${task.title || task.id}.`,
7476
+ `:check: VK executor submitted for ${task.title || task.id}.`,
7146
7477
  attempt?.id ? `Attempt: ${attempt.id}` : null,
7147
7478
  attempt?.branch ? `Branch: ${attempt.branch}` : null,
7148
7479
  ].filter(Boolean);
@@ -7157,7 +7488,7 @@ async function cmdStartTask(chatId, args) {
7157
7488
  if (!executor) {
7158
7489
  await sendReply(
7159
7490
  chatId,
7160
- "⚠️ Manual start requires internal executor. Set EXECUTOR_MODE=internal or hybrid and restart the monitor.",
7491
+ ":alert: Manual start requires internal executor. Set EXECUTOR_MODE=internal or hybrid and restart the monitor.",
7161
7492
  );
7162
7493
  return;
7163
7494
  }
@@ -7167,19 +7498,19 @@ async function cmdStartTask(chatId, args) {
7167
7498
  }));
7168
7499
  await sendReply(
7169
7500
  chatId,
7170
- `✅ Manual start queued for ${task.title || task.id}.` +
7501
+ `:check: Manual start queued for ${task.title || task.id}.` +
7171
7502
  `\nExecutor: ${selectedExecutor}` +
7172
7503
  (sdk ? `\nSDK: ${sdk}` : "") +
7173
7504
  (model ? `\nModel: ${model}` : ""),
7174
7505
  );
7175
7506
  } catch (err) {
7176
- await sendReply(chatId, `❌ Manual start failed: ${err.message}`);
7507
+ await sendReply(chatId, `:close: Manual start failed: ${err.message}`);
7177
7508
  }
7178
7509
  }
7179
7510
 
7180
7511
  async function cmdAgents(chatId) {
7181
7512
  try {
7182
- const lines = ["🤖 Agent Fleet", ""];
7513
+ const lines = [":bot: Agent Fleet", ""];
7183
7514
  let statusSnapshot = null;
7184
7515
  if (_readStatusData) {
7185
7516
  try {
@@ -7307,7 +7638,7 @@ async function cmdAgents(chatId) {
7307
7638
  } catch (err) {
7308
7639
  await sendReply(
7309
7640
  chatId,
7310
- `❌ Failed to read agent fleet status: ${err.message}`,
7641
+ `:close: Failed to read agent fleet status: ${err.message}`,
7311
7642
  );
7312
7643
  }
7313
7644
  }
@@ -7351,7 +7682,7 @@ async function cmdAgentLogs(chatId, args) {
7351
7682
 
7352
7683
  const wtName = matches[0]; // Best match
7353
7684
  const wtPath = resolve(worktreeDir, wtName);
7354
- const lines = [`📂 Agent: ${wtName}\n`];
7685
+ const lines = [`:folder: Agent: ${wtName}\n`];
7355
7686
 
7356
7687
  // Git log (last 5 commits)
7357
7688
  try {
@@ -7361,13 +7692,13 @@ async function cmdAgentLogs(chatId, args) {
7361
7692
  timeout: 10000,
7362
7693
  }).trim();
7363
7694
  if (gitLog) {
7364
- lines.push("📝 Recent commits:");
7695
+ lines.push(":edit: Recent commits:");
7365
7696
  lines.push(gitLog);
7366
7697
  } else {
7367
- lines.push("📝 No commits yet");
7698
+ lines.push(":edit: No commits yet");
7368
7699
  }
7369
7700
  } catch {
7370
- lines.push("📝 Git log unavailable");
7701
+ lines.push(":edit: Git log unavailable");
7371
7702
  }
7372
7703
 
7373
7704
  lines.push("");
@@ -7381,15 +7712,15 @@ async function cmdAgentLogs(chatId, args) {
7381
7712
  }).trim();
7382
7713
  if (gitStatus) {
7383
7714
  const statusLines = gitStatus.split("\n");
7384
- lines.push(`📄 Working tree: ${statusLines.length} changed files`);
7715
+ lines.push(`:file: Working tree: ${statusLines.length} changed files`);
7385
7716
  lines.push(statusLines.slice(0, 15).join("\n"));
7386
7717
  if (statusLines.length > 15)
7387
7718
  lines.push(`... +${statusLines.length - 15} more`);
7388
7719
  } else {
7389
- lines.push("📄 Working tree: clean");
7720
+ lines.push(":file: Working tree: clean");
7390
7721
  }
7391
7722
  } catch {
7392
- lines.push("📄 Git status unavailable");
7723
+ lines.push(":file: Git status unavailable");
7393
7724
  }
7394
7725
 
7395
7726
  lines.push("");
@@ -7408,7 +7739,7 @@ async function cmdAgentLogs(chatId, args) {
7408
7739
  }).trim();
7409
7740
  if (diffStat) {
7410
7741
  const statLines = diffStat.split("\n");
7411
- lines.push("📊 Diff vs main:");
7742
+ lines.push(":chart: Diff vs main:");
7412
7743
  // Show only summary line (last line)
7413
7744
  lines.push(statLines[statLines.length - 1] || "(none)");
7414
7745
  }
@@ -7434,10 +7765,10 @@ async function cmdAgentLogs(chatId, args) {
7434
7765
  ? `${Math.floor(runMin / 60)}h${runMin % 60}m`
7435
7766
  : `${runMin}m`;
7436
7767
  lines.push(
7437
- `🤖 Active agent: ${slot.sdk} | Running: ${runStr} | Attempt #${slot.attempt}`,
7768
+ `:bot: Active agent: ${slot.sdk} | Running: ${runStr} | Attempt #${slot.attempt}`,
7438
7769
  );
7439
7770
  } else {
7440
- lines.push("🤖 No active agent on this branch");
7771
+ lines.push(":bot: No active agent on this branch");
7441
7772
  }
7442
7773
  }
7443
7774
 
@@ -7468,7 +7799,7 @@ async function cmdLogs(chatId, _args) {
7468
7799
 
7469
7800
  await sendReply(
7470
7801
  chatId,
7471
- `📄 Last ${numLines} lines of ${logFile}:\n\n${tail || "(empty)"}`,
7802
+ `:file: Last ${numLines} lines of ${logFile}:\n\n${tail || "(empty)"}`,
7472
7803
  );
7473
7804
  } catch (err) {
7474
7805
  await sendReply(chatId, `Error reading logs: ${err.message}`);
@@ -7485,7 +7816,7 @@ async function cmdBranches(chatId, _args) {
7485
7816
  const lines = result.split("\n").filter(Boolean).slice(0, 20);
7486
7817
  await sendReply(
7487
7818
  chatId,
7488
- `🌿 Recent branches (top 20):\n\n${lines.join("\n")}`,
7819
+ `:git: Recent branches (top 20):\n\n${lines.join("\n")}`,
7489
7820
  );
7490
7821
  } catch (err) {
7491
7822
  await sendReply(chatId, `Error listing branches: ${err.message}`);
@@ -7505,7 +7836,7 @@ async function cmdDiff(chatId, _args) {
7505
7836
  }
7506
7837
  await sendReply(
7507
7838
  chatId,
7508
- `📝 Working tree changes:\n\n${diffStat.slice(0, 3500)}`,
7839
+ `:edit: Working tree changes:\n\n${diffStat.slice(0, 3500)}`,
7509
7840
  );
7510
7841
  } catch (err) {
7511
7842
  await sendReply(chatId, `Error reading diff: ${err.message}`);
@@ -7526,7 +7857,7 @@ async function cmdDisableUnsafeAccess(chatId, _args, editMessageId) {
7526
7857
  if (ok) {
7527
7858
  await sendReply(
7528
7859
  chatId,
7529
- " *Unsafe access disabled.*\n\n"
7860
+ ":check: *Unsafe access disabled.*\n\n"
7530
7861
  + "`TELEGRAM_UI_ALLOW_UNSAFE=false` has been written to your .env file.\n\n"
7531
7862
  + "Send /restart to restart Bosun — Cloudflare tunnel will start automatically on the next boot.",
7532
7863
  { parse_mode: "Markdown" },
@@ -7534,7 +7865,7 @@ async function cmdDisableUnsafeAccess(chatId, _args, editMessageId) {
7534
7865
  } else {
7535
7866
  await sendReply(
7536
7867
  chatId,
7537
- " Could not write to .env automatically.\n\n"
7868
+ ":close: Could not write to .env automatically.\n\n"
7538
7869
  + "Please edit your .env file manually:\n"
7539
7870
  + "`TELEGRAM_UI_ALLOW_UNSAFE=false`\n\n"
7540
7871
  + "Then send /restart.",
@@ -7548,12 +7879,12 @@ async function cmdRestart(chatId, args) {
7548
7879
  if (!confirmFlag) {
7549
7880
  await sendReply(
7550
7881
  chatId,
7551
- "⚠️ Restart will stop the orchestrator process and let the monitor respawn it.\nProceed?",
7882
+ ":alert: Restart will stop the orchestrator process and let the monitor respawn it.\nProceed?",
7552
7883
  { reply_markup: buildConfirmKeyboard("cb:do_restart", "Confirm Restart") },
7553
7884
  );
7554
7885
  return;
7555
7886
  }
7556
- await sendReply(chatId, "🔄 Restarting orchestrator process...");
7887
+ await sendReply(chatId, ":refresh: Restarting orchestrator process...");
7557
7888
  try {
7558
7889
  if (_getCurrentChild) {
7559
7890
  const child = _getCurrentChild();
@@ -7568,10 +7899,10 @@ async function cmdRestart(chatId, args) {
7568
7899
  // The monitor's handleExit will auto-restart the process
7569
7900
  await sendReply(
7570
7901
  chatId,
7571
- " Restart signal sent. Monitor will auto-restart the orchestrator.",
7902
+ ":check: Restart signal sent. Monitor will auto-restart the orchestrator.",
7572
7903
  );
7573
7904
  } catch (err) {
7574
- await sendReply(chatId, `❌ Restart failed: ${err.message}`);
7905
+ await sendReply(chatId, `:close: Restart failed: ${err.message}`);
7575
7906
  }
7576
7907
  }
7577
7908
 
@@ -7579,29 +7910,29 @@ async function cmdRetry(chatId, args) {
7579
7910
  if (!_attemptFreshSessionRetry) {
7580
7911
  await sendReply(
7581
7912
  chatId,
7582
- " Fresh session retry not available (not injected from monitor).",
7913
+ ":close: Fresh session retry not available (not injected from monitor).",
7583
7914
  );
7584
7915
  return;
7585
7916
  }
7586
7917
 
7587
7918
  const reason = args?.trim() || "manual_retry_via_telegram";
7588
- await sendReply(chatId, `🔄 Attempting fresh session retry (${reason})...`);
7919
+ await sendReply(chatId, `:refresh: Attempting fresh session retry (${reason})...`);
7589
7920
 
7590
7921
  try {
7591
7922
  const started = await _attemptFreshSessionRetry(reason);
7592
7923
  if (started) {
7593
7924
  await sendReply(
7594
7925
  chatId,
7595
- " Fresh session started. New agent will pick up the task.",
7926
+ ":check: Fresh session started. New agent will pick up the task.",
7596
7927
  );
7597
7928
  } else {
7598
7929
  await sendReply(
7599
7930
  chatId,
7600
- "⚠️ Fresh session retry failed. Check logs for details (rate limit, no active attempt, or VK endpoint unavailable).",
7931
+ ":alert: Fresh session retry failed. Check logs for details (rate limit, no active attempt, or VK endpoint unavailable).",
7601
7932
  );
7602
7933
  }
7603
7934
  } catch (err) {
7604
- await sendReply(chatId, `❌ Retry error: ${err.message || err}`);
7935
+ await sendReply(chatId, `:close: Retry error: ${err.message || err}`);
7605
7936
  }
7606
7937
  }
7607
7938
 
@@ -7609,7 +7940,7 @@ async function cmdPlan(chatId, args) {
7609
7940
  if (!_triggerTaskPlanner) {
7610
7941
  await sendReply(
7611
7942
  chatId,
7612
- " Task planner not available (not injected from monitor).",
7943
+ ":close: Task planner not available (not injected from monitor).",
7613
7944
  );
7614
7945
  return;
7615
7946
  }
@@ -7633,7 +7964,7 @@ async function cmdPlan(chatId, args) {
7633
7964
  }
7634
7965
 
7635
7966
  const promptSuffix = userPrompt ? ` — "${userPrompt.slice(0, 60)}${userPrompt.length > 60 ? "…" : ""}"` : "";
7636
- await sendReply(chatId, `📋 Triggering task planner (${taskCount} tasks${promptSuffix})...`);
7967
+ await sendReply(chatId, `:clipboard: Triggering task planner (${taskCount} tasks${promptSuffix})...`);
7637
7968
 
7638
7969
  try {
7639
7970
  const result = await _triggerTaskPlanner(
@@ -7651,19 +7982,19 @@ async function cmdPlan(chatId, args) {
7651
7982
  if (result.reason === "planner_disabled") {
7652
7983
  await sendReply(
7653
7984
  chatId,
7654
- "⚠️ Task planner disabled. Set TASK_PLANNER_MODE=kanban or codex-sdk.",
7985
+ ":alert: Task planner disabled. Set TASK_PLANNER_MODE=kanban or codex-sdk.",
7655
7986
  );
7656
7987
  return;
7657
7988
  }
7658
7989
  if (result.reason === "planner_busy") {
7659
7990
  await sendReply(
7660
7991
  chatId,
7661
- "⚠️ Task planner already running. Try again in a moment.",
7992
+ ":alert: Task planner already running. Try again in a moment.",
7662
7993
  );
7663
7994
  return;
7664
7995
  }
7665
7996
  const lines = [
7666
- "⚠️ Task planner skipped — a planning task already exists.",
7997
+ ":alert: Task planner skipped — a planning task already exists.",
7667
7998
  ];
7668
7999
  if (result.taskTitle) {
7669
8000
  lines.push(`Title: ${result.taskTitle}`);
@@ -7679,7 +8010,7 @@ async function cmdPlan(chatId, args) {
7679
8010
  }
7680
8011
  if (result?.status === "created") {
7681
8012
  const lines = [
7682
- " Task planner task created.",
8013
+ ":check: Task planner task created.",
7683
8014
  result.taskTitle ? `Title: ${result.taskTitle}` : null,
7684
8015
  result.taskId ? `Task ID: ${result.taskId}` : null,
7685
8016
  result.taskUrl || null,
@@ -7698,16 +8029,16 @@ async function cmdPlan(chatId, args) {
7698
8029
  : "";
7699
8030
  await sendReply(
7700
8031
  chatId,
7701
- `✅ Task planner completed.\n${createdInfo}Output: ${result.outputPath}${artifactInfo}`,
8032
+ `:check: Task planner completed.\n${createdInfo}Output: ${result.outputPath}${artifactInfo}`,
7702
8033
  );
7703
8034
  return;
7704
8035
  }
7705
8036
  await sendReply(
7706
8037
  chatId,
7707
- `✅ Task planner triggered for ${taskCount} tasks. Check backlog shortly.`,
8038
+ `:check: Task planner triggered for ${taskCount} tasks. Check backlog shortly.`,
7708
8039
  );
7709
8040
  } catch (err) {
7710
- await sendReply(chatId, `❌ Task planner error: ${err.message || err}`);
8041
+ await sendReply(chatId, `:close: Task planner error: ${err.message || err}`);
7711
8042
  }
7712
8043
  }
7713
8044
 
@@ -7716,33 +8047,33 @@ async function cmdCleanupMerged(chatId, args) {
7716
8047
  if (!_reconcileTaskStatuses) {
7717
8048
  await sendReply(
7718
8049
  chatId,
7719
- " Cleanup not available (not injected from monitor).",
8050
+ ":close: Cleanup not available (not injected from monitor).",
7720
8051
  );
7721
8052
  return;
7722
8053
  }
7723
8054
  if (!confirmFlag) {
7724
8055
  await sendReply(
7725
8056
  chatId,
7726
- "⚠️ Cleanup will reconcile VK task statuses with PR/branch state.\nProceed?",
8057
+ ":alert: Cleanup will reconcile VK task statuses with PR/branch state.\nProceed?",
7727
8058
  { reply_markup: buildConfirmKeyboard("cb:confirm_cleanup", "Confirm Cleanup") },
7728
8059
  );
7729
8060
  return;
7730
8061
  }
7731
8062
  await sendReply(
7732
8063
  chatId,
7733
- "🧹 Reconciling VK task statuses with PR/branch state…",
8064
+ ":trash: Reconciling VK task statuses with PR/branch state…",
7734
8065
  );
7735
8066
  try {
7736
8067
  const result = await _reconcileTaskStatuses("manual-telegram");
7737
8068
  const lines = [
7738
- " Cleanup complete.",
8069
+ ":check: Cleanup complete.",
7739
8070
  `Checked: ${result?.checked ?? 0}`,
7740
8071
  `Moved to done: ${result?.movedDone ?? 0}`,
7741
8072
  `Moved to inreview: ${result?.movedReview ?? 0}`,
7742
8073
  ];
7743
8074
  await sendReply(chatId, lines.join("\n"));
7744
8075
  } catch (err) {
7745
- await sendReply(chatId, `❌ Cleanup error: ${err.message || err}`);
8076
+ await sendReply(chatId, `:close: Cleanup error: ${err.message || err}`);
7746
8077
  }
7747
8078
  }
7748
8079
 
@@ -7751,7 +8082,7 @@ async function cmdHistory(chatId) {
7751
8082
  const sessionLabel = info.sessionId || info.threadId || "(none)";
7752
8083
  const agentLabel = info.adapter || info.provider || getPrimaryAgentName();
7753
8084
  const lines = [
7754
- `🧠 Primary Agent (${agentLabel})`,
8085
+ `:cpu: Primary Agent (${agentLabel})`,
7755
8086
  "",
7756
8087
  `Session: ${sessionLabel}`,
7757
8088
  `Turns: ${info.turnCount}`,
@@ -7773,7 +8104,7 @@ async function cmdClear(chatId, args) {
7773
8104
  const sessionLabel = info.sessionId || info.threadId || "(none)";
7774
8105
  const agentLabel = info.adapter || info.provider || getPrimaryAgentName();
7775
8106
  const lines = [
7776
- "⚠️ This will clear the primary agent session and reset context.",
8107
+ ":alert: This will clear the primary agent session and reset context.",
7777
8108
  "",
7778
8109
  `Agent: ${agentLabel}`,
7779
8110
  `Session: ${sessionLabel}`,
@@ -7790,7 +8121,7 @@ async function cmdClear(chatId, args) {
7790
8121
  await resetPrimaryAgent();
7791
8122
  await sendReply(
7792
8123
  chatId,
7793
- "🧹 Agent session reset. Next message starts a fresh conversation.",
8124
+ ":trash: Agent session reset. Next message starts a fresh conversation.",
7794
8125
  );
7795
8126
  }
7796
8127
 
@@ -7849,7 +8180,7 @@ async function cmdGit(chatId, gitArgs) {
7849
8180
  } catch (err) {
7850
8181
  await sendReply(
7851
8182
  chatId,
7852
- `$ git ${args}\n\n ${err.message?.slice(0, 1500) || err}`,
8183
+ `$ git ${args}\n\n:close: ${err.message?.slice(0, 1500) || err}`,
7853
8184
  );
7854
8185
  }
7855
8186
  }
@@ -7874,7 +8205,7 @@ async function cmdShell(chatId, shellArgs) {
7874
8205
 
7875
8206
  const blockedShell = [/rm\s+-rf\s+\/(\s|$)/i];
7876
8207
  if (matchesAnyPattern(args, blockedShell)) {
7877
- await sendReply(chatId, `⛔ Blocked: '${args}' is too destructive.`);
8208
+ await sendReply(chatId, `:ban: Blocked: '${args}' is too destructive.`);
7878
8209
  return;
7879
8210
  }
7880
8211
 
@@ -7922,7 +8253,7 @@ async function cmdShell(chatId, shellArgs) {
7922
8253
  const stdout = err.stdout ? err.stdout.toString().slice(0, 1000) : "";
7923
8254
  await sendReply(
7924
8255
  chatId,
7925
- `$ ${args}\n\n ${stderr || stdout || err.message}`,
8256
+ `$ ${args}\n\n:close: ${stderr || stdout || err.message}`,
7926
8257
  );
7927
8258
  }
7928
8259
  }
@@ -8078,11 +8409,11 @@ async function cmdRegion(chatId, regionArg) {
8078
8409
  );
8079
8410
  const status = JSON.parse(result);
8080
8411
  const lines = [
8081
- "🌍 Codex Region Status",
8412
+ ":globe: Codex Region Status",
8082
8413
  "",
8083
8414
  `Active: ${status.active_region?.toUpperCase() || "unknown"}`,
8084
8415
  `Override: ${status.override || "auto"}`,
8085
- `Sweden available: ${status.sweden_available ? "" : ""}`,
8416
+ `Sweden available: ${status.sweden_available ? ":check:" : ":close:"}`,
8086
8417
  `Cooldown: ${status.cooldown_min}min`,
8087
8418
  ];
8088
8419
  if (status.switched_ago_min !== null) {
@@ -8117,13 +8448,13 @@ async function cmdRegion(chatId, regionArg) {
8117
8448
  : `. '${resolveVeKanbanPs1Path()}'; Set-RegionOverride -Region '${target}' | ConvertTo-Json`;
8118
8449
  const result = runPwsh(psCmd);
8119
8450
  const info = JSON.parse(result);
8120
- const icon = info.changed ? "" : "ℹ️";
8451
+ const icon = info.changed ? ":check:" : ":help:";
8121
8452
  await sendReply(
8122
8453
  chatId,
8123
8454
  `${icon} Region: ${info.region?.toUpperCase()}\nReason: ${info.reason}`,
8124
8455
  );
8125
8456
  } catch (err) {
8126
- await sendReply(chatId, `❌ Region switch failed: ${err.message}`);
8457
+ await sendReply(chatId, `:close: Region switch failed: ${err.message}`);
8127
8458
  }
8128
8459
  }
8129
8460
 
@@ -8140,23 +8471,23 @@ async function cmdHealth(chatId) {
8140
8471
  const arr = buildExecutorHealthEntries(executorConfig, metrics);
8141
8472
 
8142
8473
  const iconMap = {
8143
- healthy: "",
8144
- degraded: "⚠️",
8145
- cooldown: "⏸️",
8146
- disabled: "",
8474
+ healthy: ":check:",
8475
+ degraded: ":alert:",
8476
+ cooldown: ":pause:",
8477
+ disabled: ":close:",
8147
8478
  };
8148
- const lines = ["🏥 Executor Health Dashboard\n"];
8479
+ const lines = [":heart: Executor Health Dashboard\n"];
8149
8480
 
8150
8481
  if (!arr.length) {
8151
8482
  lines.push("No executor data available.");
8152
8483
  }
8153
8484
 
8154
8485
  for (const e of arr) {
8155
- const icon = iconMap[e.status] || "";
8486
+ const icon = iconMap[e.status] || ":help:";
8156
8487
  lines.push(
8157
8488
  `${icon} ${e.label} (${e.tier}/${e.region})\n` +
8158
8489
  ` Status: ${e.status} | Active: ${e.stats.active}\n` +
8159
- ` ✓${e.stats.successes} ✗${e.stats.failures} ⏱${e.stats.timeouts} 🚫${e.stats.rate_limits}`,
8490
+ ` ✓${e.stats.successes} ✗${e.stats.failures} :clock:${e.stats.timeouts} :ban:${e.stats.rate_limits}`,
8160
8491
  );
8161
8492
  }
8162
8493
 
@@ -8171,11 +8502,11 @@ async function cmdHealth(chatId) {
8171
8502
  const region = JSON.parse(regionResult);
8172
8503
  lines.push(
8173
8504
  "",
8174
- `🌍 Region: ${region.active_region?.toUpperCase()} ${region.override ? `(override: ${region.override})` : "(auto)"}`,
8505
+ `:globe: Region: ${region.active_region?.toUpperCase()} ${region.override ? `(override: ${region.override})` : "(auto)"}`,
8175
8506
  `Sweden backup: ${region.sweden_available ? "available" : "not configured"}`,
8176
8507
  );
8177
8508
  } catch {
8178
- lines.push("", "🌍 Region: unavailable");
8509
+ lines.push("", ":globe: Region: unavailable");
8179
8510
  }
8180
8511
 
8181
8512
  await sendReply(chatId, lines.join("\n"));
@@ -8228,7 +8559,7 @@ async function cmdModel(chatId, modelArg) {
8228
8559
  }
8229
8560
  }
8230
8561
  const lines = [
8231
- "🤖 Model Routing",
8562
+ ":bot: Model Routing",
8232
8563
  "",
8233
8564
  `Override: ${overrideText}`,
8234
8565
  "",
@@ -8259,10 +8590,10 @@ async function cmdModel(chatId, modelArg) {
8259
8590
  }
8260
8591
  await sendReply(
8261
8592
  chatId,
8262
- " Model override cleared. Smart routing active.",
8593
+ ":check: Model override cleared. Smart routing active.",
8263
8594
  );
8264
8595
  } catch (err) {
8265
- await sendReply(chatId, `❌ Error: ${err.message}`);
8596
+ await sendReply(chatId, `:close: Error: ${err.message}`);
8266
8597
  }
8267
8598
  return;
8268
8599
  }
@@ -8296,10 +8627,10 @@ async function cmdModel(chatId, modelArg) {
8296
8627
  );
8297
8628
  await sendReply(
8298
8629
  chatId,
8299
- `✅ Model override set: ${target}\nApplies to next 3 tasks (or 1 hour)`,
8630
+ `:check: Model override set: ${target}\nApplies to next 3 tasks (or 1 hour)`,
8300
8631
  );
8301
8632
  } catch (err) {
8302
- await sendReply(chatId, `❌ Error: ${err.message}`);
8633
+ await sendReply(chatId, `:close: Error: ${err.message}`);
8303
8634
  }
8304
8635
  }
8305
8636
 
@@ -8311,7 +8642,7 @@ async function cmdKanban(chatId, backendArg) {
8311
8642
  process.env.KANBAN_SYNC_POLICY || "internal-primary",
8312
8643
  ).toLowerCase();
8313
8644
  const lines = [
8314
- "📋 Kanban Backend Status",
8645
+ ":clipboard: Kanban Backend Status",
8315
8646
  "",
8316
8647
  `Active: ${current}`,
8317
8648
  `Sync Policy: ${syncPolicy}`,
@@ -8342,17 +8673,17 @@ async function cmdKanban(chatId, backendArg) {
8342
8673
  setKanbanBackend(target);
8343
8674
  await sendReply(
8344
8675
  chatId,
8345
- `✅ Kanban backend switched to: ${target}\nActive: ${getKanbanBackendName()}`,
8676
+ `:check: Kanban backend switched to: ${target}\nActive: ${getKanbanBackendName()}`,
8346
8677
  );
8347
8678
  } catch (err) {
8348
- await sendReply(chatId, `❌ Error switching backend: ${err.message}`);
8679
+ await sendReply(chatId, `:close: Error switching backend: ${err.message}`);
8349
8680
  }
8350
8681
  }
8351
8682
 
8352
8683
  async function cmdAutoBacklog(chatId, args) {
8353
8684
  const executor = _getInternalExecutor?.();
8354
8685
  if (!executor) {
8355
- await sendReply(chatId, "⚠️ Internal executor is not available.");
8686
+ await sendReply(chatId, ":alert: Internal executor is not available.");
8356
8687
  return;
8357
8688
  }
8358
8689
 
@@ -8366,7 +8697,7 @@ async function cmdAutoBacklog(chatId, args) {
8366
8697
  await sendReply(
8367
8698
  chatId,
8368
8699
  [
8369
- "♻️ Experimental Auto-Backlog",
8700
+ ":repeat: Experimental Auto-Backlog",
8370
8701
  "",
8371
8702
  `Enabled: ${cfg.enabled ? "yes" : "no"}`,
8372
8703
  `Min new tasks: ${cfg.minNewTasks ?? 1}`,
@@ -8390,7 +8721,7 @@ async function cmdAutoBacklog(chatId, args) {
8390
8721
  });
8391
8722
  await sendReply(
8392
8723
  chatId,
8393
- `✅ Auto-backlog ${op === "on" ? "enabled" : "disabled"}. Min=${cfg?.minNewTasks ?? 1}, Max=${cfg?.maxNewTasks ?? 2}`,
8724
+ `:check: Auto-backlog ${op === "on" ? "enabled" : "disabled"}. Min=${cfg?.minNewTasks ?? 1}, Max=${cfg?.maxNewTasks ?? 2}`,
8394
8725
  );
8395
8726
  return;
8396
8727
  }
@@ -8398,14 +8729,14 @@ async function cmdAutoBacklog(chatId, args) {
8398
8729
  if ((op === "min" || op === "max") && parts[1]) {
8399
8730
  const value = Number(parts[1]);
8400
8731
  if (!Number.isFinite(value)) {
8401
- await sendReply(chatId, `❌ Invalid ${op} value: ${parts[1]}`);
8732
+ await sendReply(chatId, `:close: Invalid ${op} value: ${parts[1]}`);
8402
8733
  return;
8403
8734
  }
8404
8735
  const patch = op === "min" ? { minNewTasks: value } : { maxNewTasks: value };
8405
8736
  const cfg = executor.setBacklogReplenishmentConfig?.(patch);
8406
8737
  await sendReply(
8407
8738
  chatId,
8408
- `✅ Auto-backlog updated. Enabled=${cfg?.enabled ? "yes" : "no"}, Min=${cfg?.minNewTasks ?? 1}, Max=${cfg?.maxNewTasks ?? 2}`,
8739
+ `:check: Auto-backlog updated. Enabled=${cfg?.enabled ? "yes" : "no"}, Min=${cfg?.minNewTasks ?? 1}, Max=${cfg?.maxNewTasks ?? 2}`,
8409
8740
  );
8410
8741
  return;
8411
8742
  }
@@ -8416,7 +8747,7 @@ async function cmdAutoBacklog(chatId, args) {
8416
8747
  async function cmdRequirements(chatId, args) {
8417
8748
  const executor = _getInternalExecutor?.();
8418
8749
  if (!executor) {
8419
- await sendReply(chatId, "⚠️ Internal executor is not available.");
8750
+ await sendReply(chatId, ":alert: Internal executor is not available.");
8420
8751
  return;
8421
8752
  }
8422
8753
  const profiles = [
@@ -8435,7 +8766,7 @@ async function cmdRequirements(chatId, args) {
8435
8766
  await sendReply(
8436
8767
  chatId,
8437
8768
  [
8438
- "📐 Project Requirements",
8769
+ ":ruler: Project Requirements",
8439
8770
  "",
8440
8771
  `Profile: ${req.profile || "feature"}`,
8441
8772
  `Notes: ${req.notes || "(none)"}`,
@@ -8451,7 +8782,7 @@ async function cmdRequirements(chatId, args) {
8451
8782
  if (!profiles.includes(profile)) {
8452
8783
  await sendReply(
8453
8784
  chatId,
8454
- `❌ Unknown requirements profile: ${input}\nValid: ${profiles.join(", ")}`,
8785
+ `:close: Unknown requirements profile: ${input}\nValid: ${profiles.join(", ")}`,
8455
8786
  );
8456
8787
  return;
8457
8788
  }
@@ -8459,7 +8790,7 @@ async function cmdRequirements(chatId, args) {
8459
8790
  const req = executor.setProjectRequirements?.({ profile });
8460
8791
  await sendReply(
8461
8792
  chatId,
8462
- `✅ Project requirements profile set to ${req?.profile || profile}`,
8793
+ `:check: Project requirements profile set to ${req?.profile || profile}`,
8463
8794
  );
8464
8795
  }
8465
8796
 
@@ -8472,7 +8803,7 @@ async function cmdThreads(chatId, subArg) {
8472
8803
  if (!confirmed) {
8473
8804
  await sendReply(
8474
8805
  chatId,
8475
- "⚠️ This will clear all thread records. Proceed?",
8806
+ ":alert: This will clear all thread records. Proceed?",
8476
8807
  {
8477
8808
  reply_markup: buildConfirmKeyboard(
8478
8809
  "cb:confirm_threads_clear",
@@ -8483,7 +8814,7 @@ async function cmdThreads(chatId, subArg) {
8483
8814
  return;
8484
8815
  }
8485
8816
  clearThreadRegistry();
8486
- await sendReply(chatId, " Thread registry cleared.");
8817
+ await sendReply(chatId, ":check: Thread registry cleared.");
8487
8818
  return;
8488
8819
  }
8489
8820
  }
@@ -8515,7 +8846,7 @@ async function cmdThreads(chatId, subArg) {
8515
8846
  return;
8516
8847
  }
8517
8848
  invalidateThread(taskKey);
8518
- await sendReply(chatId, `✅ Thread for "${taskKey}" invalidated.`);
8849
+ await sendReply(chatId, `:check: Thread for "${taskKey}" invalidated.`);
8519
8850
  return;
8520
8851
  }
8521
8852
 
@@ -8523,12 +8854,12 @@ async function cmdThreads(chatId, subArg) {
8523
8854
  if (threads.length === 0) {
8524
8855
  await sendReply(
8525
8856
  chatId,
8526
- "🧵 No active agent threads.\n\nThreads are created when tasks run via the agent pool with thread persistence.",
8857
+ ":link: No active agent threads.\n\nThreads are created when tasks run via the agent pool with thread persistence.",
8527
8858
  );
8528
8859
  return;
8529
8860
  }
8530
8861
 
8531
- const lines = [`🧵 Active Agent Threads (${threads.length})`, ""];
8862
+ const lines = [`:link: Active Agent Threads (${threads.length})`, ""];
8532
8863
 
8533
8864
  for (const t of threads) {
8534
8865
  const ageMin = Math.round(t.age / 60_000);
@@ -8585,12 +8916,12 @@ async function cmdWorktrees(chatId, args) {
8585
8916
  // Prune stale worktrees
8586
8917
  try {
8587
8918
  const result = await pruneStaleWorktrees(repoRoot);
8588
- const lines = [`🧹 Worktree prune complete:`];
8919
+ const lines = [`:trash: Worktree prune complete:`];
8589
8920
  lines.push(` Pruned: ${result.pruned}`);
8590
8921
  lines.push(` Registry evicted: ${result.evicted}`);
8591
8922
  await sendReply(chatId, lines.join("\n"));
8592
8923
  } catch (err) {
8593
- await sendReply(chatId, `❌ Prune failed: ${err.message}`);
8924
+ await sendReply(chatId, `:close: Prune failed: ${err.message}`);
8594
8925
  }
8595
8926
  return;
8596
8927
  }
@@ -8626,16 +8957,16 @@ async function cmdWorktrees(chatId, args) {
8626
8957
  if (result.success) {
8627
8958
  await sendReply(
8628
8959
  chatId,
8629
- `✅ Released worktree for "${taskKey}": ${result.path}`,
8960
+ `:check: Released worktree for "${taskKey}": ${result.path}`,
8630
8961
  );
8631
8962
  } else {
8632
8963
  await sendReply(
8633
8964
  chatId,
8634
- `⚠️ No worktree found for task key "${taskKey}"`,
8965
+ `:alert: No worktree found for task key "${taskKey}"`,
8635
8966
  );
8636
8967
  }
8637
8968
  } catch (err) {
8638
- await sendReply(chatId, `❌ Release failed: ${err.message}`);
8969
+ await sendReply(chatId, `:close: Release failed: ${err.message}`);
8639
8970
  }
8640
8971
  return;
8641
8972
  }
@@ -8643,7 +8974,7 @@ async function cmdWorktrees(chatId, args) {
8643
8974
  if (sub === "stats") {
8644
8975
  try {
8645
8976
  const stats = getWorktreeStats();
8646
- const lines = [`📊 Worktree Stats:`];
8977
+ const lines = [`:chart: Worktree Stats:`];
8647
8978
  lines.push(` Total tracked: ${stats.total}`);
8648
8979
  lines.push(` Active: ${stats.active}`);
8649
8980
  lines.push(` Stale: ${stats.stale}`);
@@ -8655,7 +8986,7 @@ async function cmdWorktrees(chatId, args) {
8655
8986
  }
8656
8987
  await sendReply(chatId, lines.join("\n"));
8657
8988
  } catch (err) {
8658
- await sendReply(chatId, `❌ Stats failed: ${err.message}`);
8989
+ await sendReply(chatId, `:close: Stats failed: ${err.message}`);
8659
8990
  }
8660
8991
  return;
8661
8992
  }
@@ -8664,11 +8995,11 @@ async function cmdWorktrees(chatId, args) {
8664
8995
  try {
8665
8996
  const worktrees = listManagedWorktrees(repoRoot);
8666
8997
  if (!worktrees || worktrees.length === 0) {
8667
- await sendReply(chatId, "🌳 No active worktrees tracked.");
8998
+ await sendReply(chatId, ":git: No active worktrees tracked.");
8668
8999
  return;
8669
9000
  }
8670
9001
 
8671
- const lines = [`🌳 Active Worktrees (${worktrees.length}):\n`];
9002
+ const lines = [`:git: Active Worktrees (${worktrees.length}):\n`];
8672
9003
  for (const wt of worktrees) {
8673
9004
  const ageMin = Math.round((wt.age || 0) / 60000);
8674
9005
  const ageStr =
@@ -8687,7 +9018,7 @@ async function cmdWorktrees(chatId, args) {
8687
9018
  );
8688
9019
  await sendReply(chatId, lines.join("\n"));
8689
9020
  } catch (err) {
8690
- await sendReply(chatId, `❌ Worktree list failed: ${err.message}`);
9021
+ await sendReply(chatId, `:close: Worktree list failed: ${err.message}`);
8691
9022
  }
8692
9023
  }
8693
9024
 
@@ -8712,7 +9043,7 @@ async function cmdExecutor(chatId, args) {
8712
9043
  if (!executor) {
8713
9044
  await sendReply(
8714
9045
  chatId,
8715
- `⚙️ Internal executor not active (mode: ${mode})`,
9046
+ `:settings: Internal executor not active (mode: ${mode})`,
8716
9047
  );
8717
9048
  return;
8718
9049
  }
@@ -8720,12 +9051,12 @@ async function cmdExecutor(chatId, args) {
8720
9051
  if (status.slots.length === 0) {
8721
9052
  await sendReply(
8722
9053
  chatId,
8723
- `⚙️ No active task slots (${status.activeSlots}/${status.maxParallel} used)`,
9054
+ `:settings: No active task slots (${status.activeSlots}/${status.maxParallel} used)`,
8724
9055
  );
8725
9056
  return;
8726
9057
  }
8727
9058
  const lines = [
8728
- `⚙️ Active Task Slots (${status.activeSlots}/${status.maxParallel}):\n`,
9059
+ `:settings: Active Task Slots (${status.activeSlots}/${status.maxParallel}):\n`,
8729
9060
  ];
8730
9061
  for (const slot of status.slots) {
8731
9062
  const runStr = formatRuntimeSeconds(slot.runningFor);
@@ -8751,26 +9082,26 @@ async function cmdExecutor(chatId, args) {
8751
9082
  if (target && ["vk", "internal", "hybrid"].includes(target)) {
8752
9083
  await sendReply(
8753
9084
  chatId,
8754
- `⚙️ Current mode: ${mode}\n` +
8755
- `ℹ️ Mode can be changed via EXECUTOR_MODE env var or config.\n` +
9085
+ `:settings: Current mode: ${mode}\n` +
9086
+ `:help: Mode can be changed via EXECUTOR_MODE env var or config.\n` +
8756
9087
  `Restart the monitor after changing to apply.`,
8757
9088
  );
8758
9089
  } else {
8759
9090
  await sendReply(
8760
9091
  chatId,
8761
- `⚙️ Current executor mode: ${mode}\n\nValid modes: vk, internal, hybrid`,
9092
+ `:settings: Current executor mode: ${mode}\n\nValid modes: vk, internal, hybrid`,
8762
9093
  );
8763
9094
  }
8764
9095
  return;
8765
9096
  }
8766
9097
 
8767
9098
  // Default: show status
8768
- const lines = [`⚙️ Executor Status\n`];
9099
+ const lines = [`:settings: Executor Status\n`];
8769
9100
  lines.push(`Mode: ${mode}`);
8770
9101
 
8771
9102
  if (executor) {
8772
9103
  const status = executor.getStatus();
8773
- lines.push(`Running: ${status.running ? " Yes" : " No"}`);
9104
+ lines.push(`Running: ${status.running ? ":check: Yes" : ":close: No"}`);
8774
9105
  lines.push(`SDK: ${status.sdk}`);
8775
9106
  lines.push(`Active Slots: ${status.activeSlots}/${status.maxParallel}`);
8776
9107
  lines.push(`Poll Interval: ${status.pollIntervalMs / 1000}s`);
@@ -8784,7 +9115,7 @@ async function cmdExecutor(chatId, args) {
8784
9115
  lines.push(`Internal executor: not active`);
8785
9116
  if (mode === "vk") {
8786
9117
  lines.push(
8787
- `\nℹ️ Using VK executor only. Set EXECUTOR_MODE=internal or hybrid to enable.`,
9118
+ `\n:help: Using VK executor only. Set EXECUTOR_MODE=internal or hybrid to enable.`,
8788
9119
  );
8789
9120
  }
8790
9121
  }
@@ -8800,7 +9131,7 @@ async function cmdSdk(chatId, sdkArg) {
8800
9131
  const primaryAgent = getPrimaryAgentName();
8801
9132
  const available = getAvailableSdks();
8802
9133
  const lines = [
8803
- "🔌 Agent SDK Status",
9134
+ ":plug: Agent SDK Status",
8804
9135
  "",
8805
9136
  `Pool SDK: ${poolSdk}`,
8806
9137
  `Primary Agent: ${primaryAgent}`,
@@ -8822,7 +9153,7 @@ async function cmdSdk(chatId, sdkArg) {
8822
9153
  resetPoolSdkCache();
8823
9154
  await sendReply(
8824
9155
  chatId,
8825
- " Agent pool SDK reset to config default.\nCurrent: " +
9156
+ ":check: Agent pool SDK reset to config default.\nCurrent: " +
8826
9157
  getPoolSdkName(),
8827
9158
  );
8828
9159
  return;
@@ -8849,10 +9180,10 @@ async function cmdSdk(chatId, sdkArg) {
8849
9180
 
8850
9181
  await sendReply(
8851
9182
  chatId,
8852
- `✅ SDK switched to: ${target}\nPool SDK: ${getPoolSdkName()}\n${primaryStatus}`,
9183
+ `:check: SDK switched to: ${target}\nPool SDK: ${getPoolSdkName()}\n${primaryStatus}`,
8853
9184
  );
8854
9185
  } catch (err) {
8855
- await sendReply(chatId, `❌ Error switching SDK: ${err.message}`);
9186
+ await sendReply(chatId, `:close: Error switching SDK: ${err.message}`);
8856
9187
  }
8857
9188
  }
8858
9189
 
@@ -8895,12 +9226,12 @@ async function cmdSharedWorkspaceClaim(chatId, rawArgs) {
8895
9226
  actor,
8896
9227
  });
8897
9228
  if (result.error) {
8898
- await sendReply(chatId, `❌ ${result.error}`);
9229
+ await sendReply(chatId, `:close: ${result.error}`);
8899
9230
  return;
8900
9231
  }
8901
9232
  await sendReply(
8902
9233
  chatId,
8903
- `✅ Claimed ${result.workspace.id} for ${result.lease.owner} (expires ${result.lease.lease_expires_at})`,
9234
+ `:check: Claimed ${result.workspace.id} for ${result.lease.owner} (expires ${result.lease.lease_expires_at})`,
8904
9235
  );
8905
9236
  }
8906
9237
 
@@ -8946,10 +9277,10 @@ async function cmdSharedWorkspaceRelease(chatId, rawArgs) {
8946
9277
  actor,
8947
9278
  });
8948
9279
  if (result.error) {
8949
- await sendReply(chatId, `❌ ${result.error}`);
9280
+ await sendReply(chatId, `:close: ${result.error}`);
8950
9281
  return;
8951
9282
  }
8952
- await sendReply(chatId, `✅ Released ${result.workspace.id}`);
9283
+ await sendReply(chatId, `:check: Released ${result.workspace.id}`);
8953
9284
  }
8954
9285
 
8955
9286
  // ── /agent — route to workspace registry ────────────────────────────────────
@@ -9462,7 +9793,7 @@ async function cmdAgent(chatId, rawArgs) {
9462
9793
  } catch (err) {
9463
9794
  await sendReply(
9464
9795
  chatId,
9465
- `❌ /agent failed: ${err.message || err}\n${infoLines.join("\\n")}`,
9796
+ `:close: /agent failed: ${err.message || err}\n${infoLines.join("\\n")}`,
9466
9797
  );
9467
9798
  }
9468
9799
  }
@@ -9474,7 +9805,7 @@ async function cmdBackground(chatId, args) {
9474
9805
  if (task) {
9475
9806
  await sendReply(
9476
9807
  chatId,
9477
- `🛰️ Background task queued: "${task.slice(0, 80)}${task.length > 80 ? "…" : ""}"`,
9808
+ `:server: Background task queued: "${task.slice(0, 80)}${task.length > 80 ? "…" : ""}"`,
9478
9809
  );
9479
9810
  safeDetach("background-free-text", () => handleFreeText(task, chatId, { background: true, isolated: true }));
9480
9811
  return;
@@ -9507,7 +9838,7 @@ async function cmdBackground(chatId, args) {
9507
9838
 
9508
9839
  await sendReply(
9509
9840
  chatId,
9510
- "🛰️ Background mode enabled for the active agent. I will post a final summary when it completes. Use /stop to cancel or /steer to adjust context.",
9841
+ ":server: Background mode enabled for the active agent. I will post a final summary when it completes. Use /stop to cancel or /steer to adjust context.",
9511
9842
  );
9512
9843
  }
9513
9844
 
@@ -9518,7 +9849,7 @@ async function cmdStop(chatId, args) {
9518
9849
  if (!confirmFlag) {
9519
9850
  await sendReply(
9520
9851
  chatId,
9521
- "⚠️ Stop will halt the active agent session. Proceed?",
9852
+ ":alert: Stop will halt the active agent session. Proceed?",
9522
9853
  { reply_markup: buildConfirmKeyboard("cb:confirm_stop", "Confirm Stop") },
9523
9854
  );
9524
9855
  return;
@@ -9538,14 +9869,14 @@ async function cmdStop(chatId, args) {
9538
9869
  }
9539
9870
  if (session.actionLog) {
9540
9871
  session.actionLog.push({
9541
- icon: "🛑",
9872
+ icon: ":close:",
9542
9873
  text: "Stop requested by user (will halt after current step)",
9543
9874
  });
9544
9875
  if (session.scheduleEdit) {
9545
9876
  session.scheduleEdit();
9546
9877
  }
9547
9878
  }
9548
- await sendReply(chatId, "🛑 Stop signal sent. Agent will halt and wait.");
9879
+ await sendReply(chatId, ":close: Stop signal sent. Agent will halt and wait.");
9549
9880
  }
9550
9881
 
9551
9882
  // ── /steer — Steering update for running agent ───────────────────────────────
@@ -9568,14 +9899,14 @@ async function cmdSteer(chatId, steerArgs) {
9568
9899
  if (result.ok) {
9569
9900
  if (session.actionLog) {
9570
9901
  session.actionLog.push({
9571
- icon: "🧭",
9902
+ icon: ":compass:",
9572
9903
  text: `Steering update delivered (${result.mode})`,
9573
9904
  });
9574
9905
  if (session.scheduleEdit) {
9575
9906
  session.scheduleEdit();
9576
9907
  }
9577
9908
  }
9578
- await sendReply(chatId, `🧭 Steering sent (${result.mode}).`);
9909
+ await sendReply(chatId, `:compass: Steering sent (${result.mode}).`);
9579
9910
  return;
9580
9911
  }
9581
9912
 
@@ -9587,7 +9918,7 @@ async function cmdSteer(chatId, steerArgs) {
9587
9918
  if (session.actionLog) {
9588
9919
  const steerStatus = result.reason || "failed";
9589
9920
  session.actionLog.push({
9590
- icon: "🧭",
9921
+ icon: ":compass:",
9591
9922
  text: `Steering queued (#${qLen}; steer failed: ${steerStatus})`,
9592
9923
  kind: "followup_queued",
9593
9924
  steerStatus,
@@ -9596,7 +9927,7 @@ async function cmdSteer(chatId, steerArgs) {
9596
9927
  session.scheduleEdit();
9597
9928
  }
9598
9929
  }
9599
- await sendReply(chatId, `🧭 Steering queued (#${qLen}).`);
9930
+ await sendReply(chatId, `:compass: Steering queued (#${qLen}).`);
9600
9931
  }
9601
9932
 
9602
9933
  // ── Free-text → Primary Agent Dispatch ───────────────────────────────────────
@@ -9628,8 +9959,8 @@ function buildStreamMessage({
9628
9959
  searchesDone,
9629
9960
  statusIcon,
9630
9961
  }) {
9631
- const header = `🔧 Agent: ${taskPreview}`;
9632
- const counter = `📊 Actions: ${totalActions} | ${phase}`;
9962
+ const header = `:settings: Agent: ${taskPreview}`;
9963
+ const counter = `:chart: Actions: ${totalActions} | ${phase}`;
9633
9964
  const separator = "────────────────────────────";
9634
9965
 
9635
9966
  // Show last N actions (keep message compact)
@@ -9648,37 +9979,37 @@ function buildStreamMessage({
9648
9979
  }
9649
9980
 
9650
9981
  if (currentThought) {
9651
- lines.push("", `💭 ${currentThought}`);
9982
+ lines.push("", `:u1f4ad: ${currentThought}`);
9652
9983
  }
9653
9984
 
9654
9985
  if (!finalResponse) {
9655
9986
  if (filesWritten?.size) {
9656
- lines.push("", "✍️ Files modified so far:");
9987
+ lines.push("", ":edit: Files modified so far:");
9657
9988
  const recent = Array.from(filesWritten.entries()).slice(-6);
9658
9989
  for (const [fpath, info] of recent) {
9659
9990
  const name = shortPath(fpath);
9660
9991
  if (info.adds || info.dels) {
9661
- lines.push(` ✏️ ${name} (+${info.adds} -${info.dels})`);
9992
+ lines.push(` :edit: ${name} (+${info.adds} -${info.dels})`);
9662
9993
  } else {
9663
- lines.push(` ✏️ ${name}`);
9994
+ lines.push(` :edit: ${name}`);
9664
9995
  }
9665
9996
  }
9666
9997
  }
9667
9998
  if (filesRead?.size) {
9668
- lines.push("", "📖 Files read so far:");
9999
+ lines.push("", ":file: Files read so far:");
9669
10000
  const recent = Array.from(filesRead.values()).slice(-6);
9670
10001
  for (const fpath of recent) {
9671
- lines.push(` 📄 ${shortPath(fpath)}`);
10002
+ lines.push(` :file: ${shortPath(fpath)}`);
9672
10003
  }
9673
10004
  }
9674
10005
  if (searchesDone) {
9675
- lines.push("", `🔎 Searches: ${searchesDone}`);
10006
+ lines.push("", `:search: Searches: ${searchesDone}`);
9676
10007
  }
9677
10008
  }
9678
10009
 
9679
10010
  if (finalResponse) {
9680
10011
  // ── Final summary block ──────────────────────────────────────
9681
- const icon = statusIcon || "";
10012
+ const icon = statusIcon || ":check:";
9682
10013
  lines.push("", separator);
9683
10014
  lines.push(`${icon} ${phase}`);
9684
10015
  lines.push("");
@@ -9689,20 +10020,20 @@ function buildStreamMessage({
9689
10020
  if (filesWritten?.size) stats.push(`${filesWritten.size} files modified`);
9690
10021
  if (searchesDone) stats.push(`${searchesDone} searches`);
9691
10022
  if (stats.length) {
9692
- lines.push(`📈 ${stats.join(" · ")}`);
10023
+ lines.push(`:chart: ${stats.join(" · ")}`);
9693
10024
  }
9694
10025
 
9695
10026
  // Files modified detail
9696
10027
  if (filesWritten?.size) {
9697
10028
  lines.push("");
9698
- lines.push("📁 Files modified:");
10029
+ lines.push(":folder: Files modified:");
9699
10030
  for (const [fpath, info] of filesWritten) {
9700
10031
  const name = shortPath(fpath);
9701
10032
  if (info.adds || info.dels) {
9702
- lines.push(` ✏️ ${name} (+${info.adds} -${info.dels})`);
10033
+ lines.push(` :edit: ${name} (+${info.adds} -${info.dels})`);
9703
10034
  } else {
9704
10035
  const kindIcon =
9705
- info.kind === "add" ? "" : info.kind === "delete" ? "🗑️" : "✏️";
10036
+ info.kind === "add" ? ":plus:" : info.kind === "delete" ? ":trash:" : ":edit:";
9706
10037
  lines.push(` ${kindIcon} ${name}`);
9707
10038
  }
9708
10039
  }
@@ -9737,13 +10068,13 @@ async function handleFreeText(text, chatId, options = {}) {
9737
10068
  // Acknowledge the follow-up in both the user's chat and update the agent message
9738
10069
  await sendDirect(
9739
10070
  chatId,
9740
- `📌 Follow-up queued (#${qLen}). Agent will process it after current action. ${steerNote}`,
10071
+ `:pin: Follow-up queued (#${qLen}). Agent will process it after current action. ${steerNote}`,
9741
10072
  );
9742
10073
 
9743
10074
  // Add follow-up indicator to the streaming message
9744
10075
  if (chatSession.actionLog) {
9745
10076
  chatSession.actionLog.push({
9746
- icon: "📌",
10077
+ icon: ":pin:",
9747
10078
  text: `Follow-up: "${text.length > 60 ? text.slice(0, 60) + "…" : text}" (${steerNote})`,
9748
10079
  kind: "followup_queued",
9749
10080
  steerStatus,
@@ -9997,7 +10328,7 @@ async function handleFreeText(text, chatId, options = {}) {
9997
10328
  if (followUps.length > 0 && !sessionState.aborted) {
9998
10329
  for (const followUp of followUps) {
9999
10330
  actionLog.push({
10000
- icon: "📌",
10331
+ icon: ":pin:",
10001
10332
  text: `Processing follow-up: "${followUp.slice(0, 60)}"`,
10002
10333
  });
10003
10334
  phase = "processing follow-up…";
@@ -10015,12 +10346,12 @@ async function handleFreeText(text, chatId, options = {}) {
10015
10346
  if (followUpResult.finalResponse) {
10016
10347
  result.finalResponse =
10017
10348
  (result.finalResponse || "") +
10018
- `\n\n📌 Follow-up result:\n${followUpResult.finalResponse}`;
10349
+ `\n\n:pin: Follow-up result:\n${followUpResult.finalResponse}`;
10019
10350
  suppressSteerFailedLines(actionLog);
10020
10351
  }
10021
10352
  } catch (err) {
10022
10353
  actionLog.push({
10023
- icon: "",
10354
+ icon: ":close:",
10024
10355
  text: `Follow-up error: ${err.message}`,
10025
10356
  });
10026
10357
  }
@@ -10041,14 +10372,14 @@ async function handleFreeText(text, chatId, options = {}) {
10041
10372
  const hasChanges = filesWritten.size > 0;
10042
10373
  let statusIcon;
10043
10374
  if (hadError) {
10044
- statusIcon = "";
10375
+ statusIcon = ":close:";
10045
10376
  phase = "Failed — needs manual review";
10046
10377
  } else if (hasChanges) {
10047
- statusIcon = "";
10378
+ statusIcon = ":check:";
10048
10379
  phase = "Completed successfully";
10049
10380
  } else {
10050
10381
  // No files changed — might be informational or might need user input
10051
- statusIcon = "";
10382
+ statusIcon = ":help:";
10052
10383
  phase = "Completed — no files changed";
10053
10384
  }
10054
10385
 
@@ -10084,7 +10415,7 @@ async function handleFreeText(text, chatId, options = {}) {
10084
10415
  filesRead,
10085
10416
  filesWritten,
10086
10417
  searchesDone: searchCount,
10087
- statusIcon: "",
10418
+ statusIcon: ":close:",
10088
10419
  });
10089
10420
  if (backgroundMode || sessionState.background) {
10090
10421
  await sendReply(chatId, finalMsg);
@@ -10232,11 +10563,11 @@ let liveDigest = {
10232
10563
  };
10233
10564
 
10234
10565
  const PRIORITY_EMOJI = {
10235
- 1: "🔴",
10236
- 2: "",
10237
- 3: "⚠️",
10238
- 4: "ℹ️",
10239
- 5: "🔹",
10566
+ 1: ":dot:",
10567
+ 2: ":close:",
10568
+ 3: ":alert:",
10569
+ 4: ":help:",
10570
+ 5: ":dot:",
10240
10571
  };
10241
10572
 
10242
10573
  /**
@@ -10254,14 +10585,14 @@ function buildLiveDigestText() {
10254
10585
  }
10255
10586
 
10256
10587
  const countParts = [];
10257
- if (counts[1] > 0) countParts.push(`🔴 ${counts[1]}`);
10258
- if (counts[2] > 0) countParts.push(`❌ ${counts[2]}`);
10259
- if (counts[3] > 0) countParts.push(`⚠️ ${counts[3]}`);
10260
- if (counts[4] > 0) countParts.push(`ℹ️ ${counts[4]}`);
10588
+ if (counts[1] > 0) countParts.push(`:dot: ${counts[1]}`);
10589
+ if (counts[2] > 0) countParts.push(`:close: ${counts[2]}`);
10590
+ if (counts[3] > 0) countParts.push(`:alert: ${counts[3]}`);
10591
+ if (counts[4] > 0) countParts.push(`:help: ${counts[4]}`);
10261
10592
 
10262
10593
  const statusLine = d.sealed
10263
- ? `📊 Digest (${startTime} → ${now}) — sealed`
10264
- : `📊 Live Digest (since ${startTime}) — updating...`;
10594
+ ? `:chart: Digest (${startTime} → ${now}) — sealed`
10595
+ : `:chart: Live Digest (since ${startTime}) — updating...`;
10265
10596
  const headerLine =
10266
10597
  countParts.length > 0
10267
10598
  ? `${statusLine}\n${countParts.join(" • ")}`
@@ -10434,7 +10765,7 @@ async function addToLiveDigest(text, priority, category) {
10434
10765
  const d = liveDigest;
10435
10766
  const now = Date.now();
10436
10767
  const timeStr = new Date(now).toISOString().slice(11, 19);
10437
- const emoji = PRIORITY_EMOJI[priority] || "ℹ️";
10768
+ const emoji = PRIORITY_EMOJI[priority] || ":help:";
10438
10769
 
10439
10770
  // Check if we need a new digest window
10440
10771
  const windowMs = liveDigestWindowSec * 1000;
@@ -10554,7 +10885,7 @@ export async function initStatusBoard() {
10554
10885
  `[telegram-bot] status board restored (msg ${saved.messageId})`,
10555
10886
  );
10556
10887
  // Edit the board straight away so it shows "restarted" state
10557
- scheduleStatusBoardEdit("🔄 Orchestrator restarting…", {});
10888
+ scheduleStatusBoardEdit(":refresh: Orchestrator restarting…", {});
10558
10889
  return;
10559
10890
  }
10560
10891
  } catch {
@@ -10564,7 +10895,7 @@ export async function initStatusBoard() {
10564
10895
  // Create the initial status board message
10565
10896
  const msgId = await sendDirect(
10566
10897
  telegramChatId,
10567
- "📡 Orchestrator starting…",
10898
+ ":server: Orchestrator starting…",
10568
10899
  { silent: true },
10569
10900
  );
10570
10901
  if (!msgId) return;
@@ -10675,20 +11006,20 @@ async function flushNotificationQueue() {
10675
11006
 
10676
11007
  // Build summary header
10677
11008
  const timestamp = new Date().toISOString().slice(11, 19);
10678
- let header = `📊 Update Summary (${timestamp})`;
11009
+ let header = `:chart: Update Summary (${timestamp})`;
10679
11010
  if (totalMessages > 0) {
10680
11011
  const parts = [];
10681
- if (counts.critical > 0) parts.push(`🔴 ${counts.critical}`);
10682
- if (counts.errors > 0) parts.push(`❌ ${counts.errors}`);
10683
- if (counts.warnings > 0) parts.push(`⚠️ ${counts.warnings}`);
10684
- if (counts.info > 0) parts.push(`ℹ️ ${counts.info}`);
11012
+ if (counts.critical > 0) parts.push(`:dot: ${counts.critical}`);
11013
+ if (counts.errors > 0) parts.push(`:close: ${counts.errors}`);
11014
+ if (counts.warnings > 0) parts.push(`:alert: ${counts.warnings}`);
11015
+ if (counts.info > 0) parts.push(`:help: ${counts.info}`);
10685
11016
  header += `\n${parts.join(" • ")}`;
10686
11017
  }
10687
11018
 
10688
11019
  // Critical messages (show all)
10689
11020
  if (counts.critical > 0) {
10690
11021
  sections.push(
10691
- `🔴 Critical:\n${messageQueue.critical.map((m) => ` • ${m.text}`).join("\n")}`,
11022
+ `:dot: Critical:\n${messageQueue.critical.map((m) => ` • ${m.text}`).join("\n")}`,
10692
11023
  );
10693
11024
  }
10694
11025
 
@@ -10700,7 +11031,7 @@ async function flushNotificationQueue() {
10700
11031
  if (counts.errors > 5) {
10701
11032
  errorTexts.push(` • ... and ${counts.errors - 5} more errors`);
10702
11033
  }
10703
- sections.push(`❌ Errors:\n${errorTexts.join("\n")}`);
11034
+ sections.push(`:close: Errors:\n${errorTexts.join("\n")}`);
10704
11035
  }
10705
11036
 
10706
11037
  // Warnings (show up to 3, then summarize)
@@ -10711,7 +11042,7 @@ async function flushNotificationQueue() {
10711
11042
  if (counts.warnings > 3) {
10712
11043
  warnTexts.push(` • ... and ${counts.warnings - 3} more warnings`);
10713
11044
  }
10714
- sections.push(`⚠️ Warnings:\n${warnTexts.join("\n")}`);
11045
+ sections.push(`:alert: Warnings:\n${warnTexts.join("\n")}`);
10715
11046
  }
10716
11047
 
10717
11048
  // Info messages (aggregate by category)
@@ -10724,7 +11055,7 @@ async function flushNotificationQueue() {
10724
11055
  const summary = Object.entries(categories)
10725
11056
  .map(([cat, count]) => ` • ${cat}: ${count}`)
10726
11057
  .join("\n");
10727
- sections.push(`ℹ️ Info:\n${summary}`);
11058
+ sections.push(`:help: Info:\n${summary}`);
10728
11059
  }
10729
11060
 
10730
11061
  // Build final message
@@ -10857,7 +11188,7 @@ export async function startTelegramBot(options = {}) {
10857
11188
  onProjectSyncAlert: async (alert) => {
10858
11189
  if (!_sendTelegramMessage) return;
10859
11190
  const text = String(alert?.message || "Project sync alert");
10860
- await _sendTelegramMessage(`⚠️ ${text}`);
11191
+ await _sendTelegramMessage(`:alert: ${text}`);
10861
11192
  },
10862
11193
  },
10863
11194
  });
@@ -10908,7 +11239,7 @@ export async function startTelegramBot(options = {}) {
10908
11239
  const port = new URL(telegramUiUrl || "http://localhost:5511").port || "5511";
10909
11240
  await sendDirect(
10910
11241
  telegramChatId,
10911
- `🔥 *Firewall Alert*\n\n` +
11242
+ `:zap: *Firewall Alert*\n\n` +
10912
11243
  `Port ${port}/tcp appears blocked by \`${fwState.firewall}\`.\n` +
10913
11244
  `The Control Center may not be reachable from your phone or LAN browser.\n\n` +
10914
11245
  `To fix, run on the server:\n\`\`\`\n${fwState.allowCmd}\n\`\`\``,
@@ -10916,7 +11247,7 @@ export async function startTelegramBot(options = {}) {
10916
11247
  parseMode: "Markdown",
10917
11248
  reply_markup: {
10918
11249
  inline_keyboard: [[
10919
- { text: "🔓 Open Port (requires admin password on server)", callback_data: "fw:open" },
11250
+ { text: ":unlock: Open Port (requires admin password on server)", callback_data: "fw:open" },
10920
11251
  ]],
10921
11252
  },
10922
11253
  },
@@ -10995,7 +11326,7 @@ export async function startTelegramBot(options = {}) {
10995
11326
  } else {
10996
11327
  await sendDirect(
10997
11328
  telegramChatId,
10998
- `🤖 Bosun primary agent online (${getPrimaryAgentName()}).\n\nType /menu for the control center or send any message to chat with the agent.\n\nRefreshing control center menu below…`,
11329
+ `:bot: Bosun primary agent online (${getPrimaryAgentName()}).\n\nType /menu for the control center or send any message to chat with the agent.\n\nRefreshing control center menu below…`,
10999
11330
  );
11000
11331
  await refreshStickyMenu(telegramChatId, "home", {});
11001
11332
 
@@ -11004,11 +11335,11 @@ export async function startTelegramBot(options = {}) {
11004
11335
  String(process.env.TELEGRAM_UI_ALLOW_UNSAFE || "").toLowerCase(),
11005
11336
  );
11006
11337
  if (_isUnsafe) {
11007
- const _tunnelMode = (process.env.TELEGRAM_UI_TUNNEL || "auto").toLowerCase();
11338
+ const _tunnelMode = (process.env.TELEGRAM_UI_TUNNEL || "named").toLowerCase();
11008
11339
  const _tunnelWanted = _tunnelMode !== "disabled" && _tunnelMode !== "off" && _tunnelMode !== "0";
11009
11340
  const title = _tunnelWanted
11010
- ? " *Unsafe UI Access + Cloudflare Tunnel conflict detected*"
11011
- : "⚠️ *Unsafe UI Access enabled*";
11341
+ ? ":ban: *Unsafe UI Access + Cloudflare Tunnel conflict detected*"
11342
+ : ":alert: *Unsafe UI Access enabled*";
11012
11343
  const body = _tunnelWanted
11013
11344
  ? "*Unsafe UI access was enabled alongside Cloudflare tunnel — the tunnel has been disabled* until unsafe access is turned off.\n\n"
11014
11345
  + "With both active, anyone on the internet could control your agents, execute code, and read your secrets.\n\n"
@@ -11024,7 +11355,7 @@ export async function startTelegramBot(options = {}) {
11024
11355
  reply_markup: {
11025
11356
  inline_keyboard: [
11026
11357
  [
11027
- { text: "🔒 Disable Unsafe Access", callback_data: "cb:do_disable_unsafe" },
11358
+ { text: ":lock: Disable Unsafe Access", callback_data: "cb:do_disable_unsafe" },
11028
11359
  ],
11029
11360
  ],
11030
11361
  },