cli-jaw 1.5.0 → 1.6.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 (230) hide show
  1. package/README.ko.md +26 -19
  2. package/README.md +50 -22
  3. package/README.zh-CN.md +26 -19
  4. package/dist/bin/cli-jaw.js +5 -1
  5. package/dist/bin/cli-jaw.js.map +1 -1
  6. package/dist/bin/commands/dispatch.js +57 -0
  7. package/dist/bin/commands/dispatch.js.map +1 -0
  8. package/dist/lib/mcp-sync.js +1 -1
  9. package/dist/lib/mcp-sync.js.map +1 -1
  10. package/dist/lib/upload.js +14 -1
  11. package/dist/lib/upload.js.map +1 -1
  12. package/dist/server.js +57 -42
  13. package/dist/server.js.map +1 -1
  14. package/dist/src/agent/events.js +72 -13
  15. package/dist/src/agent/events.js.map +1 -1
  16. package/dist/src/agent/spawn.js +71 -29
  17. package/dist/src/agent/spawn.js.map +1 -1
  18. package/dist/src/cli/acp-client.js +4 -2
  19. package/dist/src/cli/acp-client.js.map +1 -1
  20. package/dist/src/cli/commands.js +6 -6
  21. package/dist/src/cli/commands.js.map +1 -1
  22. package/dist/src/cli/handlers.js +11 -9
  23. package/dist/src/cli/handlers.js.map +1 -1
  24. package/dist/src/cli/registry.js +7 -1
  25. package/dist/src/cli/registry.js.map +1 -1
  26. package/dist/src/core/config.js +15 -8
  27. package/dist/src/core/config.js.map +1 -1
  28. package/dist/src/core/db.js +18 -3
  29. package/dist/src/core/db.js.map +1 -1
  30. package/dist/src/core/employees.js +34 -0
  31. package/dist/src/core/employees.js.map +1 -0
  32. package/dist/src/discord/bot.js +72 -9
  33. package/dist/src/discord/bot.js.map +1 -1
  34. package/dist/src/discord/commands.js +12 -8
  35. package/dist/src/discord/commands.js.map +1 -1
  36. package/dist/src/orchestrator/distribute.js +57 -35
  37. package/dist/src/orchestrator/distribute.js.map +1 -1
  38. package/dist/src/orchestrator/gateway.js +3 -1
  39. package/dist/src/orchestrator/gateway.js.map +1 -1
  40. package/dist/src/orchestrator/pipeline.js +120 -27
  41. package/dist/src/orchestrator/pipeline.js.map +1 -1
  42. package/dist/src/orchestrator/research.js +5 -5
  43. package/dist/src/orchestrator/research.js.map +1 -1
  44. package/dist/src/orchestrator/scope.js +55 -0
  45. package/dist/src/orchestrator/scope.js.map +1 -0
  46. package/dist/src/orchestrator/state-machine.js +23 -21
  47. package/dist/src/orchestrator/state-machine.js.map +1 -1
  48. package/dist/src/orchestrator/worker-registry.js +9 -2
  49. package/dist/src/orchestrator/worker-registry.js.map +1 -1
  50. package/dist/src/prompt/builder.js +76 -37
  51. package/dist/src/prompt/builder.js.map +1 -1
  52. package/dist/src/prompt/templates/a1-system.md +40 -0
  53. package/dist/src/prompt/templates/employee.md +12 -4
  54. package/dist/src/prompt/templates/orchestration.md +17 -1
  55. package/dist/src/telegram/bot.js +7 -1
  56. package/dist/src/telegram/bot.js.map +1 -1
  57. package/package.json +4 -2
  58. package/public/assets/fonts/GeistVF.woff2 +0 -0
  59. package/public/assets/fonts/JetBrainsMono-Variable.woff2 +0 -0
  60. package/public/assets/providers/claude-color.svg +1 -0
  61. package/public/assets/providers/claude.svg +1 -0
  62. package/public/assets/providers/copilot-color.svg +1 -0
  63. package/public/assets/providers/copilot.svg +1 -0
  64. package/public/assets/providers/discord.svg +1 -0
  65. package/public/assets/providers/gemini-color.svg +1 -0
  66. package/public/assets/providers/gemini.svg +1 -0
  67. package/public/assets/providers/openai.svg +1 -0
  68. package/public/assets/providers/opencode.svg +1 -0
  69. package/public/assets/providers/telegram.svg +1 -0
  70. package/public/css/chat.css +79 -51
  71. package/public/css/diagram.css +348 -0
  72. package/public/css/layout.css +38 -11
  73. package/public/css/markdown.css +64 -18
  74. package/public/css/modals.css +3 -3
  75. package/public/css/orc-state.css +9 -9
  76. package/public/css/sidebar.css +37 -3
  77. package/public/css/tool-ui.css +46 -11
  78. package/public/css/variables.css +63 -63
  79. package/public/dist/assets/api-Bbmmo0o1.js +1 -0
  80. package/public/dist/assets/architecture-YZFGNWBL-BxiHqtOH.js +1 -0
  81. package/public/dist/assets/architectureDiagram-Q4EWVU46-CJTGDw5p.js +1 -0
  82. package/public/dist/assets/blockDiagram-DXYQGD6D-Btl5Y-Er.js +1 -0
  83. package/public/dist/assets/c4Diagram-AHTNJAMY-sDfjW4ZF.js +1 -0
  84. package/public/dist/assets/classDiagram-6PBFFD2Q-ByCgggL2.js +1 -0
  85. package/public/dist/assets/classDiagram-v2-HSJHXN6E-t1amaXkU.js +1 -0
  86. package/public/dist/assets/constants-C8_0OtK2.js +1 -0
  87. package/public/dist/assets/cose-bilkent-S5V4N54A-eOSFGdaE.js +1 -0
  88. package/public/dist/assets/dagre-KV5264BT-OCB5j8Q6.js +1 -0
  89. package/public/dist/assets/diagram-5BDNPKRD-C-4fgK5P.js +1 -0
  90. package/public/dist/assets/diagram-G4DWMVQ6-C6vmHKDV.js +1 -0
  91. package/public/dist/assets/diagram-MMDJMWI5-BMCo_wmt.js +1 -0
  92. package/public/dist/assets/diagram-TYMM5635-DaCLdttJ.js +1 -0
  93. package/public/dist/assets/employees-D5n7mX5v.js +39 -0
  94. package/public/dist/assets/erDiagram-SMLLAGMA-BiTRdm5s.js +1 -0
  95. package/public/dist/assets/flowDiagram-DWJPFMVM-sv6jVi0d.js +1 -0
  96. package/public/dist/assets/ganttDiagram-T4ZO3ILL-jCP3OW23.js +1 -0
  97. package/public/dist/assets/gitGraph-7Q5UKJZL-BIeKLxpm.js +1 -0
  98. package/public/dist/assets/gitGraphDiagram-UUTBAWPF-CokylnbW.js +1 -0
  99. package/public/dist/assets/index-CLd0BsAu.js +49 -0
  100. package/public/dist/assets/index-D6ci1wCN.css +1 -0
  101. package/public/dist/assets/info-OMHHGYJF-CIEl5dWF.js +1 -0
  102. package/public/dist/assets/infoDiagram-42DDH7IO-BBnK1Bh2.js +1 -0
  103. package/public/dist/assets/ishikawaDiagram-UXIWVN3A-qXIz0VhS.js +1 -0
  104. package/public/dist/assets/journeyDiagram-VCZTEJTY-BdrOLof3.js +1 -0
  105. package/public/dist/assets/kanban-definition-6JOO6SKY-CcX7CE04.js +1 -0
  106. package/public/dist/assets/mermaid.core-BoxIvw7E.js +1 -0
  107. package/public/dist/assets/mindmap-definition-QFDTVHPH-CdXARskX.js +1 -0
  108. package/public/dist/assets/packet-4T2RLAQJ-Bz4ZwYZ3.js +1 -0
  109. package/public/dist/assets/pie-ZZUOXDRM-AtGL3wZ2.js +1 -0
  110. package/public/dist/assets/pieDiagram-DEJITSTG-B5SOq9Yr.js +1 -0
  111. package/public/dist/assets/quadrantDiagram-34T5L4WZ-D0uNWgpI.js +1 -0
  112. package/public/dist/assets/radar-PYXPWWZC-AbJSfeqB.js +1 -0
  113. package/public/dist/assets/render-C8N0rp4L.js +25 -0
  114. package/public/dist/assets/requirementDiagram-MS252O5E-62td6IQ-.js +1 -0
  115. package/public/dist/assets/sankeyDiagram-XADWPNL6-Crg_02iC.js +1 -0
  116. package/public/dist/assets/sequenceDiagram-FGHM5R23-fbCocZHj.js +1 -0
  117. package/public/dist/assets/settings-BJR-IGM5.js +41 -0
  118. package/public/dist/assets/settings-Dm6OnPmY.js +1 -0
  119. package/public/dist/assets/skills-D6jv9AIs.js +1 -0
  120. package/public/dist/assets/skills-uywskdFh.js +12 -0
  121. package/public/dist/assets/slash-commands-CCDonT40.js +1 -0
  122. package/public/dist/assets/{slash-commands-DMsE88bu.js → slash-commands-DWvL-VDU.js} +1 -1
  123. package/public/dist/assets/stateDiagram-FHFEXIEX-Gy3DhXxQ.js +1 -0
  124. package/public/dist/assets/stateDiagram-v2-QKLJ7IA2-DJc-FW9O.js +1 -0
  125. package/public/dist/assets/timeline-definition-GMOUNBTQ-B3cA9DgY.js +1 -0
  126. package/public/dist/assets/treeView-SZITEDCU-Cn58DIbB.js +1 -0
  127. package/public/dist/assets/treemap-W4RFUUIX-DPcYjDzH.js +1 -0
  128. package/public/dist/assets/ui-C1daR00l.js +1 -0
  129. package/public/dist/assets/ui-D-oFkXed.js +131 -0
  130. package/public/dist/assets/vendor-icons-1Ec7ZWcT.js +1 -0
  131. package/public/dist/assets/{vendor-mermaid-COidH9HB.js → vendor-mermaid-lvHqQdfg.js} +4 -4
  132. package/public/dist/assets/vennDiagram-DHZGUBPP-DkBfxilo.js +1 -0
  133. package/public/dist/assets/wardley-RL74JXVD-6Dg0PJ4H.js +1 -0
  134. package/public/dist/assets/wardleyDiagram-NUSXRM2D-Dsntl_vy.js +1 -0
  135. package/public/dist/assets/ws-Dnn8HG8B.js +2 -0
  136. package/public/dist/assets/xychartDiagram-5P7HB3ND-B_McB5GE.js +1 -0
  137. package/public/dist/index.html +63 -55
  138. package/public/index.html +62 -53
  139. package/public/js/constants.ts +8 -2
  140. package/public/js/diagram/iframe-renderer.ts +559 -0
  141. package/public/js/diagram/types.ts +129 -0
  142. package/public/js/diagram/widget-validator.ts +82 -0
  143. package/public/js/features/chat.ts +24 -12
  144. package/public/js/features/employees.ts +3 -2
  145. package/public/js/features/heartbeat.ts +4 -3
  146. package/public/js/features/memory.ts +4 -3
  147. package/public/js/features/process-block.ts +12 -11
  148. package/public/js/features/settings-cli-status.ts +10 -7
  149. package/public/js/features/settings-core.ts +13 -3
  150. package/public/js/features/settings-discord.ts +9 -0
  151. package/public/js/features/settings-mcp.ts +8 -7
  152. package/public/js/features/settings-stt.ts +4 -4
  153. package/public/js/features/settings-templates.ts +10 -8
  154. package/public/js/features/settings-types.ts +1 -1
  155. package/public/js/features/settings.ts +1 -1
  156. package/public/js/features/sidebar.ts +37 -7
  157. package/public/js/features/skills.ts +4 -3
  158. package/public/js/features/theme.ts +4 -0
  159. package/public/js/features/tool-ui.ts +6 -5
  160. package/public/js/features/voice-recorder.ts +2 -1
  161. package/public/js/icons.ts +257 -0
  162. package/public/js/main.ts +9 -3
  163. package/public/js/provider-icons.ts +88 -0
  164. package/public/js/render.ts +493 -30
  165. package/public/js/streaming-render.ts +3 -3
  166. package/public/js/ui.ts +38 -18
  167. package/public/js/ws.ts +17 -10
  168. package/public/locales/en.json +89 -88
  169. package/public/locales/ko.json +89 -88
  170. package/scripts/release-1.6.0.sh +69 -0
  171. package/scripts/release-preview.sh +1 -1
  172. package/dist/lib/token-keepalive.js +0 -34
  173. package/dist/lib/token-keepalive.js.map +0 -1
  174. package/public/dist/assets/api-BlPw3bUI.js +0 -1
  175. package/public/dist/assets/architecture-YZFGNWBL-BkS7SZQi.js +0 -1
  176. package/public/dist/assets/architectureDiagram-Q4EWVU46-Dl3iBKeR.js +0 -1
  177. package/public/dist/assets/blockDiagram-DXYQGD6D-DPr4D17p.js +0 -1
  178. package/public/dist/assets/c4Diagram-AHTNJAMY-CfoxJtDk.js +0 -1
  179. package/public/dist/assets/classDiagram-6PBFFD2Q-BOAdHnnB.js +0 -1
  180. package/public/dist/assets/classDiagram-v2-HSJHXN6E-B2QCJXWC.js +0 -1
  181. package/public/dist/assets/constants-DshMUJbo.js +0 -1
  182. package/public/dist/assets/cose-bilkent-S5V4N54A-CA14jk7w.js +0 -1
  183. package/public/dist/assets/dagre-KV5264BT-BVwNaSwC.js +0 -1
  184. package/public/dist/assets/diagram-5BDNPKRD-CHqIAvdc.js +0 -1
  185. package/public/dist/assets/diagram-G4DWMVQ6-DxvsCvTP.js +0 -1
  186. package/public/dist/assets/diagram-MMDJMWI5-DqOPO7dl.js +0 -1
  187. package/public/dist/assets/diagram-TYMM5635-C9xMWPQn.js +0 -1
  188. package/public/dist/assets/employees-CFRlsbHm.js +0 -39
  189. package/public/dist/assets/erDiagram-SMLLAGMA-BTmpfRkm.js +0 -1
  190. package/public/dist/assets/flowDiagram-DWJPFMVM-DZBSQAKA.js +0 -1
  191. package/public/dist/assets/ganttDiagram-T4ZO3ILL-TAEMCRbT.js +0 -1
  192. package/public/dist/assets/gitGraph-7Q5UKJZL-bbxndRNA.js +0 -1
  193. package/public/dist/assets/gitGraphDiagram-UUTBAWPF-CBxtx0MB.js +0 -1
  194. package/public/dist/assets/index-1Gg-6jeC.css +0 -1
  195. package/public/dist/assets/index-CQHqXjrK.js +0 -49
  196. package/public/dist/assets/info-OMHHGYJF-DRXq_Ywr.js +0 -1
  197. package/public/dist/assets/infoDiagram-42DDH7IO-C7FFwX4m.js +0 -1
  198. package/public/dist/assets/ishikawaDiagram-UXIWVN3A-DEZJE79j.js +0 -1
  199. package/public/dist/assets/journeyDiagram-VCZTEJTY-ZHLiY6GV.js +0 -1
  200. package/public/dist/assets/kanban-definition-6JOO6SKY-NP7pY7ML.js +0 -1
  201. package/public/dist/assets/mermaid.core-CHuluSlD.js +0 -1
  202. package/public/dist/assets/mindmap-definition-QFDTVHPH-Bd1OgfN_.js +0 -1
  203. package/public/dist/assets/packet-4T2RLAQJ-CGu8ST97.js +0 -1
  204. package/public/dist/assets/pie-ZZUOXDRM-CDG65mhS.js +0 -1
  205. package/public/dist/assets/pieDiagram-DEJITSTG-BbMBHLDM.js +0 -1
  206. package/public/dist/assets/quadrantDiagram-34T5L4WZ-CDCDgI1e.js +0 -1
  207. package/public/dist/assets/radar-PYXPWWZC-1gS-6ylu.js +0 -1
  208. package/public/dist/assets/render-YHvL5VM_.js +0 -6
  209. package/public/dist/assets/requirementDiagram-MS252O5E-DkmvOtYz.js +0 -1
  210. package/public/dist/assets/sankeyDiagram-XADWPNL6-GNcXIYsL.js +0 -1
  211. package/public/dist/assets/sequenceDiagram-FGHM5R23-BFrw2n6x.js +0 -1
  212. package/public/dist/assets/settings-BQF_u5px.js +0 -37
  213. package/public/dist/assets/settings-C5Q9IPjJ.js +0 -1
  214. package/public/dist/assets/skills-9Pd2rOw-.js +0 -1
  215. package/public/dist/assets/skills-BI7sQAdk.js +0 -12
  216. package/public/dist/assets/slash-commands-FMpJzlDB.js +0 -1
  217. package/public/dist/assets/stateDiagram-FHFEXIEX-BLgKnllT.js +0 -1
  218. package/public/dist/assets/stateDiagram-v2-QKLJ7IA2-CbzuWsSa.js +0 -1
  219. package/public/dist/assets/timeline-definition-GMOUNBTQ-CcZTJ3gL.js +0 -1
  220. package/public/dist/assets/treeView-SZITEDCU-BDqBsaH1.js +0 -1
  221. package/public/dist/assets/treemap-W4RFUUIX-BaWHA3Di.js +0 -1
  222. package/public/dist/assets/ui-BukgLHuh.js +0 -29
  223. package/public/dist/assets/ui-Bvz1JfTE.js +0 -1
  224. package/public/dist/assets/vennDiagram-DHZGUBPP-BWV_j1bh.js +0 -1
  225. package/public/dist/assets/wardley-RL74JXVD-C7gKA3d7.js +0 -1
  226. package/public/dist/assets/wardleyDiagram-NUSXRM2D-BUoq_wug.js +0 -1
  227. package/public/dist/assets/ws-S_AZgx7L.js +0 -2
  228. package/public/dist/assets/xychartDiagram-5P7HB3ND-PvT5Ec82.js +0 -1
  229. /package/public/dist/assets/{api-B8XKQ4OT.js → api-Ci-lgwRp.js} +0 -0
  230. /package/public/dist/assets/{locale-BjoAcbis.js → locale-DIXc-_34.js} +0 -0
@@ -2,7 +2,7 @@
2
2
 
3
3
  export interface PerCliConfig { model?: string; effort?: string; fastMode?: boolean; contextWindow?: boolean; contextWindowSize?: number; contextCompactLimit?: number; }
4
4
  export interface TelegramConfig { enabled?: boolean; token?: string; allowedChatIds?: number[]; forwardAll?: boolean; }
5
- export interface DiscordConfig { enabled?: boolean; token?: string; guildId?: string; channelIds?: string[]; forwardAll?: boolean; allowBots?: boolean; }
5
+ export interface DiscordConfig { enabled?: boolean; token?: string; guildId?: string; channelIds?: string[]; forwardAll?: boolean; allowBots?: boolean; mentionOnly?: boolean; }
6
6
  export interface QuotaWindow { label: string; percent: number; resetsAt?: string | number | null; }
7
7
  export interface QuotaEntry {
8
8
  account?: { email?: string; type?: string; plan?: string; tier?: string };
@@ -1,7 +1,7 @@
1
1
  // settings.ts — barrel re-export (preserves all import paths)
2
2
  export { loadSettings, updateSettings, setPerm, getModelValue, handleModelSelect, applyCustomModel, onCliChange, saveActiveCliSettings, savePerCli } from './settings-core.js';
3
3
  export { setTelegram, setForwardAll, saveTelegramSettings } from './settings-telegram.js';
4
- export { setDiscord, setDiscordForwardAll, setDiscordAllowBots, saveDiscordSettings } from './settings-discord.js';
4
+ export { setDiscord, setDiscordForwardAll, setDiscordAllowBots, setDiscordMentionOnly, saveDiscordSettings } from './settings-discord.js';
5
5
  export { setActiveChannel, loadFallbackOrder, saveFallbackOrder } from './settings-channel.js';
6
6
  export { loadMcpServers, syncMcpServers, installMcpGlobal } from './settings-mcp.js';
7
7
  export { loadCliStatus } from './settings-cli-status.js';
@@ -1,7 +1,9 @@
1
1
  // ── Sidebar Collapse ──
2
2
  // Toggle left/right sidebars. Responsive-aware:
3
- // - Wide viewport (>900px): toggle *-collapsed classes
4
- // - Narrow viewport (≤900px): CSS auto-collapses, toggle *-expanded to override
3
+ // - Wide viewport (>900px): persist *-collapsed classes
4
+ // - Narrow viewport (≤900px): CSS auto-collapses, expanded panels are transient
5
+
6
+ import { ICONS } from '../icons.js';
5
7
 
6
8
  interface SidebarState {
7
9
  left?: boolean;
@@ -10,32 +12,60 @@ interface SidebarState {
10
12
 
11
13
  const STORAGE_KEY = 'sidebarState';
12
14
  const BREAKPOINT = 900;
15
+ const OVERLAY_BREAKPOINT = 768;
16
+
17
+ function isOverlayMode(): boolean {
18
+ return window.innerWidth <= OVERLAY_BREAKPOINT;
19
+ }
20
+
21
+ function clearExpandedPanels(): void {
22
+ document.body.classList.remove('left-expanded', 'right-expanded');
23
+ }
24
+
25
+ function toggleExpandedPanel(side: 'left' | 'right'): void {
26
+ const ownClass = `${side}-expanded`;
27
+ const otherClass = side === 'left' ? 'right-expanded' : 'left-expanded';
28
+ const willOpen = !document.body.classList.contains(ownClass);
29
+ document.body.classList.remove(otherClass);
30
+ document.body.classList.toggle(ownClass, willOpen);
31
+ }
13
32
 
14
33
  export function initSidebar(): void {
15
34
  let saved: SidebarState = {};
16
35
  try { saved = JSON.parse(localStorage.getItem(STORAGE_KEY) || '{}'); } catch { /* corrupted */ }
17
36
  if (saved.left) document.body.classList.add('left-collapsed');
18
37
  if (saved.right) document.body.classList.add('right-collapsed');
38
+ let wasOverlayMode = isOverlayMode();
19
39
 
20
40
  document.getElementById('toggleLeft')?.addEventListener('click', toggleLeft);
21
41
  document.getElementById('toggleRight')?.addEventListener('click', toggleRight);
22
42
 
23
43
  // On resize: sync classes with viewport mode
24
44
  window.addEventListener('resize', () => {
45
+ const overlayMode = isOverlayMode();
46
+
25
47
  if (window.innerWidth > BREAKPOINT) {
26
- document.body.classList.remove('left-expanded', 'right-expanded');
48
+ clearExpandedPanels();
27
49
  let s: SidebarState = {};
28
50
  try { s = JSON.parse(localStorage.getItem(STORAGE_KEY) || '{}'); } catch { /* corrupted */ }
29
51
  document.body.classList.toggle('left-collapsed', !!s.left);
30
52
  document.body.classList.toggle('right-collapsed', !!s.right);
31
53
  } else {
32
54
  document.body.classList.remove('left-collapsed', 'right-collapsed');
55
+ if (overlayMode !== wasOverlayMode) {
56
+ clearExpandedPanels();
57
+ }
33
58
  }
59
+
60
+ wasOverlayMode = overlayMode;
34
61
  syncIcons();
35
62
  });
36
63
 
37
64
  if (window.innerWidth <= BREAKPOINT) {
38
65
  document.body.classList.remove('left-collapsed', 'right-collapsed');
66
+ if (isOverlayMode()) {
67
+ clearExpandedPanels();
68
+ }
39
69
  }
40
70
  syncIcons();
41
71
  }
@@ -46,7 +76,7 @@ function isNarrow(): boolean {
46
76
 
47
77
  export function toggleLeft(): void {
48
78
  if (isNarrow()) {
49
- document.body.classList.toggle('left-expanded');
79
+ toggleExpandedPanel('left');
50
80
  } else {
51
81
  document.body.classList.toggle('left-collapsed');
52
82
  }
@@ -56,7 +86,7 @@ export function toggleLeft(): void {
56
86
 
57
87
  export function toggleRight(): void {
58
88
  if (isNarrow()) {
59
- document.body.classList.toggle('right-expanded');
89
+ toggleExpandedPanel('right');
60
90
  } else {
61
91
  document.body.classList.toggle('right-collapsed');
62
92
  }
@@ -77,8 +107,8 @@ function isRightOpen(): boolean {
77
107
  function syncIcons(): void {
78
108
  const leftBtn = document.getElementById('toggleLeft');
79
109
  const rightBtn = document.getElementById('toggleRight');
80
- if (leftBtn) leftBtn.textContent = isLeftOpen() ? '◀' : '▶';
81
- if (rightBtn) rightBtn.textContent = isRightOpen() ? '▶' : '◀';
110
+ if (leftBtn) leftBtn.innerHTML = isLeftOpen() ? ICONS.chevronLeft : ICONS.chevronRight;
111
+ if (rightBtn) rightBtn.innerHTML = isRightOpen() ? ICONS.chevronRight : ICONS.chevronLeft;
82
112
  }
83
113
 
84
114
  function save(): void {
@@ -3,6 +3,7 @@ import { state } from '../state.js';
3
3
  import { t, fetchWithLocale } from './i18n.js';
4
4
  import { apiJson } from '../api.js';
5
5
  import { escapeHtml } from '../render.js';
6
+ import { ICONS, emojiToIcon } from '../icons.js';
6
7
 
7
8
  interface SkillItem {
8
9
  id: string;
@@ -46,13 +47,13 @@ export function renderSkills(): void {
46
47
 
47
48
  list.innerHTML = filtered.map(s => {
48
49
  const reqParts: string[] = [];
49
- if (s.requires?.env) reqParts.push('🔑 ' + s.requires.env.map(e => escapeHtml(e)).join(', '));
50
- if (s.requires?.bins) reqParts.push('⚙️ ' + s.requires.bins.map(b => escapeHtml(b)).join(', '));
50
+ if (s.requires?.env) reqParts.push(`${ICONS.key} ` + s.requires.env.map(e => escapeHtml(e)).join(', '));
51
+ if (s.requires?.bins) reqParts.push(`${ICONS.settings} ` + s.requires.bins.map(b => escapeHtml(b)).join(', '));
51
52
  if (s.install) reqParts.push(escapeHtml(s.install));
52
53
  return `
53
54
  <div class="skill-card ${s.enabled ? 'enabled' : ''}">
54
55
  <div class="skill-card-header">
55
- <span class="skill-emoji">${escapeHtml(s.emoji || '🔧')}</span>
56
+ <span class="skill-emoji">${s.emoji ? emojiToIcon(s.emoji) : ICONS.tool}</span>
56
57
  <span class="skill-name">${escapeHtml(s.name || s.id)}</span>
57
58
  <button class="skill-toggle ${s.enabled ? 'on' : 'off'}"
58
59
  data-skill-id="${escapeHtml(s.id)}" data-skill-enabled="${s.enabled}"
@@ -4,6 +4,8 @@
4
4
 
5
5
  import githubDark from 'highlight.js/styles/github-dark.css?inline';
6
6
  import githubLight from 'highlight.js/styles/github.css?inline';
7
+ import { broadcastThemeToIframes } from '../diagram/iframe-renderer.js';
8
+ import { rerenderMermaidDiagrams } from '../render.js';
7
9
 
8
10
  const STORAGE_KEY = 'theme';
9
11
  let hljsStyleEl: HTMLStyleElement | null = null;
@@ -43,4 +45,6 @@ function applyTheme(theme: string): void {
43
45
  }
44
46
 
45
47
  applyHljsTheme(theme);
48
+ broadcastThemeToIframes();
49
+ rerenderMermaidDiagrams();
46
50
  }
@@ -3,6 +3,7 @@
3
3
  // Extracted from ui.ts for modularity.
4
4
 
5
5
  import { escapeHtml } from '../render.js';
6
+ import { ICONS } from '../icons.js';
6
7
 
7
8
  export interface ToolLogEntry {
8
9
  icon: string;
@@ -42,7 +43,7 @@ function renderToolItem(tl: ToolLogEntry, idx: number): string {
42
43
  <span class="tool-item-label">${label}</span>
43
44
  ${snippetHtml}
44
45
  </span>
45
- <span class="tool-item-chevron">▸</span>
46
+ <span class="tool-item-chevron">${ICONS.chevronRight}</span>
46
47
  </button>
47
48
  <div class="tool-item-details collapsed" id="${detailId}">
48
49
  <pre class="tool-item-full">${escapeHtml(detail)}</pre>
@@ -63,14 +64,14 @@ export function buildToolGroupHtml(toolLog: ToolLogEntry[]): string {
63
64
  });
64
65
 
65
66
  const summaryParts = Object.entries(counts)
66
- .map(([icon, n]) => `${escapeHtml(icon)}×${n}`)
67
+ .map(([icon, n]) => `${escapeHtml(icon)}&times;${n}`)
67
68
  .join(' ');
68
69
 
69
70
  const toolId = `td-${Date.now()}`;
70
71
 
71
72
  const logLines = toolLog.map((tl, i) => renderToolItem(tl, i)).join('');
72
73
 
73
- return `<div class="tool-group"><button class="tool-group-summary" aria-expanded="false" aria-controls="${toolId}"><span class="tool-status-dot done"></span><span class="tool-group-summary-text">${summaryParts}</span><span class="tool-group-chevron">▾</span></button><div class="tool-details collapsed" id="${toolId}">${logLines}</div></div>`;
74
+ return `<div class="tool-group"><button class="tool-group-summary" aria-expanded="false" aria-controls="${toolId}"><span class="tool-status-dot done"></span><span class="tool-group-summary-text">${summaryParts}</span><span class="tool-group-chevron">${ICONS.chevronDown}</span></button><div class="tool-details collapsed" id="${toolId}">${logLines}</div></div>`;
74
75
  }
75
76
 
76
77
  /** Bind expand/collapse click handlers for tool items within a container */
@@ -89,7 +90,7 @@ export function bindToolItemInteractions(root: HTMLElement): void {
89
90
  details.classList.toggle('collapsed', !expanding);
90
91
  wrapper.classList.toggle('expanded', expanding);
91
92
  toggle.setAttribute('aria-expanded', expanding ? 'true' : 'false');
92
- if (chevron) chevron.textContent = expanding ? '▾' : '▸';
93
+ if (chevron) chevron.innerHTML = expanding ? ICONS.chevronDown : ICONS.chevronRight;
93
94
  });
94
95
  root.dataset.toolItemBound = '1';
95
96
  }
@@ -103,7 +104,7 @@ export function renderLiveToolActivity(msgDiv: HTMLElement, label: string): void
103
104
  const content = msgDiv.querySelector('.msg-content');
104
105
  if (content) content.before(liveEl);
105
106
  }
106
- liveEl.innerHTML = `<span class="tool-status-dot running"></span><span>${escapeHtml(label)}</span>`;
107
+ liveEl.innerHTML = `<span class="tool-status-dot running"></span><span>${label}</span>`;
107
108
  }
108
109
 
109
110
  /** Clean up all live tool activity indicators */
@@ -3,6 +3,7 @@
3
3
  import { state } from '../state.js';
4
4
  import { addSystemMsg } from '../ui.js';
5
5
  import { t } from './i18n.js';
6
+ import { ICONS } from '../icons.js';
6
7
  import { sendVoiceToServer } from './chat.js';
7
8
 
8
9
  let cancelled = false;
@@ -165,7 +166,7 @@ function updateRecordingUI(recording: boolean): void {
165
166
  const cancelBtn = document.getElementById('btnVoiceCancel');
166
167
  if (btn) {
167
168
  btn.classList.toggle('recording', recording);
168
- btn.textContent = recording ? '⏹' : '🎤';
169
+ btn.innerHTML = recording ? ICONS.stop : ICONS.mic;
169
170
  btn.title = recording ? t('voice.stop') : t('voice.start');
170
171
  }
171
172
  if (cancelBtn) {
@@ -0,0 +1,257 @@
1
+ // ── Icon System ──
2
+ // Central module for all UI icons. Replaces hardcoded emoji strings with
3
+ // Lucide SVG icons and custom SVGs. Every file that needs an icon imports
4
+ // from here — never use emoji literals in UI code.
5
+
6
+ import { buildLucideSvg } from '@lucide/icons/build';
7
+ import {
8
+ CircleCheck,
9
+ CircleX,
10
+ Wrench,
11
+ SkipForward,
12
+ Brain,
13
+ HeartPulse,
14
+ Lock,
15
+ LockOpen,
16
+ KeyRound,
17
+ Settings,
18
+ FileText,
19
+ Trash2,
20
+ TriangleAlert,
21
+ Lightbulb,
22
+ Search,
23
+ Globe,
24
+ Zap,
25
+ MessageSquare,
26
+ NotebookPen,
27
+ RefreshCw,
28
+ Mic,
29
+ Package,
30
+ ClipboardList,
31
+ Bot,
32
+ Palette,
33
+ Link,
34
+ HandMetal,
35
+ Paperclip,
36
+ Save,
37
+ Gamepad2,
38
+ House,
39
+ Radio,
40
+ FolderOpen,
41
+ Pencil,
42
+ ChartBar,
43
+ Hourglass,
44
+ Square,
45
+ X,
46
+ Send,
47
+ Check,
48
+ ChevronLeft,
49
+ ChevronRight,
50
+ ChevronDown,
51
+ ArrowLeft,
52
+ ArrowRight,
53
+ Copy,
54
+ Download,
55
+ } from '@lucide/icons';
56
+
57
+ // ── Size presets ──
58
+ const S = 14; // inline / small
59
+ const M = 16; // default UI
60
+
61
+ function luc(data: Parameters<typeof buildLucideSvg>[0], size = M): string {
62
+ return buildLucideSvg(data, { size });
63
+ }
64
+
65
+ // ── Shark mascot (🦈 emoji — brand identity) ──
66
+ const SHARK_SVG = '🦈';
67
+
68
+ // ── Icon registry ──
69
+ // Keys match the semantic role, NOT the old emoji codepoint.
70
+ export const ICONS = {
71
+ // Status
72
+ check: luc(CircleCheck),
73
+ error: luc(CircleX),
74
+ warning: luc(TriangleAlert),
75
+ skip: luc(SkipForward),
76
+
77
+ // Tool activity
78
+ tool: luc(Wrench),
79
+ thinking: luc(MessageSquare),
80
+ search: luc(Search),
81
+ web: luc(Globe),
82
+ exec: luc(Zap),
83
+ compacting: luc(Package),
84
+ plan: luc(NotebookPen),
85
+
86
+ // App features
87
+ brain: luc(Brain),
88
+ heartPulse: luc(HeartPulse),
89
+ lock: luc(Lock),
90
+ lockOpen: luc(LockOpen),
91
+ key: luc(KeyRound),
92
+ settings: luc(Settings),
93
+ file: luc(FileText),
94
+ trash: luc(Trash2),
95
+ lightbulb: luc(Lightbulb),
96
+ refresh: luc(RefreshCw),
97
+ mic: luc(Mic),
98
+ clipboard: luc(ClipboardList),
99
+ robot: luc(Bot),
100
+ palette: luc(Palette),
101
+ link: luc(Link),
102
+ salute: luc(HandMetal),
103
+
104
+ // Mascot
105
+ shark: SHARK_SVG,
106
+
107
+ // HTML template icons
108
+ paperclip: luc(Paperclip),
109
+ save: luc(Save),
110
+ gamepad: luc(Gamepad2),
111
+ house: luc(House),
112
+ radio: luc(Radio),
113
+ folder: luc(FolderOpen),
114
+ pencil: luc(Pencil),
115
+ chart: luc(ChartBar),
116
+ hourglass: luc(Hourglass),
117
+ stop: luc(Square),
118
+ close: luc(X, S),
119
+ send: luc(Send),
120
+ copy: luc(Copy, S),
121
+ download: luc(Download, S),
122
+ checkSimple: luc(Check, S),
123
+ chevronLeft: luc(ChevronLeft, S),
124
+ chevronRight:luc(ChevronRight, S),
125
+ chevronDown: luc(ChevronDown, S),
126
+ arrowLeft: luc(ArrowLeft, S),
127
+ arrowRight: luc(ArrowRight, S),
128
+ } as const;
129
+
130
+ export type IconName = keyof typeof ICONS;
131
+
132
+ /** Get an icon SVG string by name, with optional size override. */
133
+ export function icon(name: IconName, size?: number): string {
134
+ if (!size || size === M) return ICONS[name];
135
+ // Regenerate at requested size
136
+ return iconMap[name]?.(size) ?? ICONS[name];
137
+ }
138
+
139
+ // ── Size-override helpers (only for icons that support it) ──
140
+ const iconMap: Partial<Record<IconName, (s: number) => string>> = {
141
+ check: (s) => luc(CircleCheck, s),
142
+ error: (s) => luc(CircleX, s),
143
+ warning: (s) => luc(TriangleAlert, s),
144
+ skip: (s) => luc(SkipForward, s),
145
+ tool: (s) => luc(Wrench, s),
146
+ thinking: (s) => luc(MessageSquare, s),
147
+ search: (s) => luc(Search, s),
148
+ web: (s) => luc(Globe, s),
149
+ exec: (s) => luc(Zap, s),
150
+ compacting: (s) => luc(Package, s),
151
+ plan: (s) => luc(NotebookPen, s),
152
+ brain: (s) => luc(Brain, s),
153
+ heartPulse: (s) => luc(HeartPulse, s),
154
+ lock: (s) => luc(Lock, s),
155
+ lockOpen: (s) => luc(LockOpen, s),
156
+ key: (s) => luc(KeyRound, s),
157
+ settings: (s) => luc(Settings, s),
158
+ file: (s) => luc(FileText, s),
159
+ trash: (s) => luc(Trash2, s),
160
+ lightbulb: (s) => luc(Lightbulb, s),
161
+ refresh: (s) => luc(RefreshCw, s),
162
+ mic: (s) => luc(Mic, s),
163
+ clipboard: (s) => luc(ClipboardList, s),
164
+ robot: (s) => luc(Bot, s),
165
+ palette: (s) => luc(Palette, s),
166
+ link: (s) => luc(Link, s),
167
+ salute: (s) => luc(HandMetal, s),
168
+ paperclip: (s) => luc(Paperclip, s),
169
+ save: (s) => luc(Save, s),
170
+ gamepad: (s) => luc(Gamepad2, s),
171
+ house: (s) => luc(House, s),
172
+ radio: (s) => luc(Radio, s),
173
+ folder: (s) => luc(FolderOpen, s),
174
+ pencil: (s) => luc(Pencil, s),
175
+ chart: (s) => luc(ChartBar, s),
176
+ copy: (s) => luc(Copy, s),
177
+ download: (s) => luc(Download, s),
178
+ };
179
+
180
+ // ── Emoji → Icon name mapping (for server protocol backward compat) ──
181
+ const EMOJI_TO_ICON: Record<string, IconName> = {
182
+ '✅': 'check',
183
+ '❌': 'error',
184
+ '🔧': 'tool',
185
+ '⏭': 'skip',
186
+ '🧠': 'brain',
187
+ '💓': 'heartPulse',
188
+ '🔒': 'lock',
189
+ '🔓': 'lockOpen',
190
+ '🔑': 'key',
191
+ '⚙': 'settings',
192
+ '⚙️': 'settings',
193
+ '📄': 'file',
194
+ '🗑': 'trash',
195
+ '🗑️': 'trash',
196
+ '⚠': 'warning',
197
+ '⚠️': 'warning',
198
+ '💡': 'lightbulb',
199
+ '🦈': 'shark',
200
+ '💭': 'thinking',
201
+ '🔍': 'search',
202
+ '🌐': 'web',
203
+ '⚡': 'exec',
204
+ '🗜': 'compacting',
205
+ '🗜️': 'compacting',
206
+ '📝': 'plan',
207
+ '📑': 'clipboard',
208
+ '🤖': 'robot',
209
+ '📋': 'clipboard',
210
+ '🔄': 'refresh',
211
+ '🎨': 'palette',
212
+ '🎤': 'mic',
213
+ '🔗': 'link',
214
+ '🫡': 'salute',
215
+ '📎': 'paperclip',
216
+ '💾': 'save',
217
+ '🎮': 'gamepad',
218
+ '🏠': 'house',
219
+ '📡': 'radio',
220
+ '📂': 'folder',
221
+ '✏️': 'pencil',
222
+ '📊': 'chart',
223
+ '🎙️': 'mic',
224
+ };
225
+
226
+ /** Convert an emoji string to its icon SVG. Falls back to the emoji if unknown. */
227
+ export function emojiToIcon(emoji: string): string {
228
+ const name = EMOJI_TO_ICON[emoji];
229
+ return name ? ICONS[name] : emoji;
230
+ }
231
+
232
+ /** Check if a string is a known status emoji. */
233
+ export function isCompletionEmoji(emoji: string): boolean {
234
+ return emoji === '✅' || emoji === '❌';
235
+ }
236
+
237
+ /** Get semantic status from an emoji. */
238
+ export function emojiToStatus(emoji: string): 'done' | 'error' | null {
239
+ if (emoji === '✅') return 'done';
240
+ if (emoji === '❌') return 'error';
241
+ return null;
242
+ }
243
+
244
+ /**
245
+ * Hydrate all `<span data-icon="NAME">` elements in a container (default: document.body).
246
+ * Call once after DOMContentLoaded to replace HTML icon placeholders with SVGs.
247
+ */
248
+ export function hydrateIcons(root: Element = document.body): void {
249
+ const els = root.querySelectorAll<HTMLElement>('[data-icon]');
250
+ for (const el of els) {
251
+ const name = el.dataset.icon as IconName;
252
+ if (name && ICONS[name]) {
253
+ el.innerHTML = ICONS[name];
254
+ el.classList.add('icon-hydrated');
255
+ }
256
+ }
257
+ }
package/public/js/main.ts CHANGED
@@ -26,7 +26,7 @@ import {
26
26
  saveActiveCliSettings, savePerCli, openPromptModal,
27
27
  closePromptModal, savePromptFromModal, syncMcpServers, installMcpGlobal,
28
28
  loadCliStatus, setTelegram, setForwardAll, saveTelegramSettings,
29
- setDiscord, setDiscordForwardAll, setDiscordAllowBots, saveDiscordSettings, setActiveChannel,
29
+ setDiscord, setDiscordForwardAll, setDiscordAllowBots, setDiscordMentionOnly, saveDiscordSettings, setActiveChannel,
30
30
  saveFallbackOrder,
31
31
  openTemplateModal, saveTemplateFromModal, closeTemplateModal, templateGoBack, toggleDevMode
32
32
  } from './features/settings.js';
@@ -53,6 +53,8 @@ import { initTheme } from './features/theme.js';
53
53
  import { initGestures } from './features/gesture.js';
54
54
  import { initI18n, setLang, getLang, t } from './features/i18n.js';
55
55
  import { toggleRecording, cancelRecording } from './features/voice-recorder.js';
56
+ import { ICONS, hydrateIcons } from './icons.js';
57
+ import { hydrateProviderIcons } from './provider-icons.js';
56
58
 
57
59
  // ── Chat Actions ──
58
60
  document.getElementById('btnSend')?.addEventListener('click', sendMessage);
@@ -96,7 +98,7 @@ document.getElementById('langToggle')?.addEventListener('click', async () => {
96
98
  const next = getLang() === 'ko' ? 'en' : 'ko';
97
99
  await setLang(next);
98
100
  const btn = document.getElementById('langToggle');
99
- if (btn) btn.textContent = `🌐 ${t('lang.' + next)}`;
101
+ if (btn) btn.innerHTML = `${ICONS.web} ${t('lang.' + next)}`;
100
102
  // Reconnect WS with new locale
101
103
  if (state.ws) { state.ws.close(); }
102
104
  });
@@ -185,6 +187,8 @@ document.getElementById('dcForwardOff')?.addEventListener('click', () => setDisc
185
187
  document.getElementById('dcForwardOn')?.addEventListener('click', () => setDiscordForwardAll(true));
186
188
  document.getElementById('dcAllowBotsOff')?.addEventListener('click', () => setDiscordAllowBots(false));
187
189
  document.getElementById('dcAllowBotsOn')?.addEventListener('click', () => setDiscordAllowBots(true));
190
+ document.getElementById('dcMentionOff')?.addEventListener('click', () => setDiscordMentionOnly(false));
191
+ document.getElementById('dcMentionOn')?.addEventListener('click', () => setDiscordMentionOnly(true));
188
192
  document.getElementById('dcToken')?.addEventListener('change', saveDiscordSettings);
189
193
  document.getElementById('dcGuildId')?.addEventListener('change', saveDiscordSettings);
190
194
  document.getElementById('dcChannelIds')?.addEventListener('change', saveDiscordSettings);
@@ -400,9 +404,11 @@ document.getElementById('basicMemoryFiles')?.addEventListener('click', (e) => {
400
404
 
401
405
  // ── Init ──
402
406
  async function bootstrap(): Promise<void> {
407
+ hydrateIcons();
408
+ hydrateProviderIcons();
403
409
  await initI18n();
404
410
  const langBtn = document.getElementById('langToggle');
405
- if (langBtn) langBtn.textContent = `🌐 ${t('lang.' + getLang())}`;
411
+ if (langBtn) langBtn.innerHTML = `${ICONS.web} ${t('lang.' + getLang())}`;
406
412
  await loadCliRegistry();
407
413
  bindPerCliControlEvents();
408
414
  connect();
@@ -0,0 +1,88 @@
1
+ // ── Provider Icons ──
2
+ // AI provider brand icons from lobehub/icons-static-svg.
3
+ // SVGs are downloaded locally for offline support and bundled via Vite ?raw.
4
+
5
+ import claudeSvg from '../assets/providers/claude-color.svg?raw';
6
+ import openaiSvg from '../assets/providers/openai.svg?raw';
7
+ import geminiSvg from '../assets/providers/gemini-color.svg?raw';
8
+ import copilotSvg from '../assets/providers/copilot-color.svg?raw';
9
+
10
+ // Mono variants for dark/light mode flexibility
11
+ import claudeMonoSvg from '../assets/providers/claude.svg?raw';
12
+ import geminiMonoSvg from '../assets/providers/gemini.svg?raw';
13
+ import copilotMonoSvg from '../assets/providers/copilot.svg?raw';
14
+
15
+ // Service icons (Discord, Telegram)
16
+ import discordSvg from '../assets/providers/discord.svg?raw';
17
+ import telegramSvg from '../assets/providers/telegram.svg?raw';
18
+ import opencodeSvg from '../assets/providers/opencode.svg?raw';
19
+
20
+ export type ProviderSlug = 'claude' | 'openai' | 'gemini' | 'copilot' | 'codex' | 'opencode' | 'discord' | 'telegram';
21
+
22
+ interface ProviderIcon {
23
+ color: string;
24
+ mono: string;
25
+ label: string;
26
+ }
27
+
28
+ const PROVIDER_ICONS: Record<ProviderSlug, ProviderIcon> = {
29
+ claude: { color: claudeSvg, mono: claudeMonoSvg, label: 'Claude' },
30
+ openai: { color: openaiSvg, mono: openaiSvg, label: 'OpenAI' },
31
+ gemini: { color: geminiSvg, mono: geminiMonoSvg, label: 'Gemini' },
32
+ copilot: { color: copilotSvg, mono: copilotMonoSvg, label: 'GitHub Copilot' },
33
+ codex: { color: openaiSvg, mono: openaiSvg, label: 'Codex (OpenAI)' },
34
+ opencode: { color: opencodeSvg, mono: opencodeSvg, label: 'OpenCode' },
35
+ discord: { color: discordSvg, mono: discordSvg, label: 'Discord' },
36
+ telegram: { color: telegramSvg, mono: telegramSvg, label: 'Telegram' },
37
+ };
38
+
39
+ /** Get a provider icon SVG string. Returns color variant by default. */
40
+ export function providerIcon(slug: string, variant: 'color' | 'mono' = 'color'): string {
41
+ const normalized = slug.toLowerCase().replace(/[-_\s]/g, '');
42
+ // Handle aliases
43
+ let key: ProviderSlug;
44
+ if (normalized === 'claude' || normalized.startsWith('claude')) key = 'claude';
45
+ else if (normalized === 'gemini' || normalized.startsWith('gemini')) key = 'gemini';
46
+ else if (normalized.startsWith('copilot') || normalized === 'githubcopilot') key = 'copilot';
47
+ else if (normalized === 'codex') key = 'codex';
48
+ else if (normalized === 'opencode') key = 'opencode';
49
+ else if (normalized === 'openai' || normalized.startsWith('gpt') || normalized.startsWith('o1') || normalized.startsWith('o3') || normalized.startsWith('o4')) key = 'openai';
50
+ else if (normalized === 'discord') key = 'discord';
51
+ else if (normalized === 'telegram') key = 'telegram';
52
+ else return '';
53
+
54
+ const entry = PROVIDER_ICONS[key];
55
+ return variant === 'mono' ? entry.mono : entry.color;
56
+ }
57
+
58
+ /**
59
+ * Hydrate all `<span data-provider="SLUG">` elements with provider SVG icons.
60
+ * Call once after DOMContentLoaded.
61
+ */
62
+ export function hydrateProviderIcons(root: Element = document.body): void {
63
+ const els = root.querySelectorAll<HTMLElement>('[data-provider]');
64
+ for (const el of els) {
65
+ const slug = el.dataset.provider || '';
66
+ const svg = providerIcon(slug);
67
+ if (svg) {
68
+ el.innerHTML = svg;
69
+ el.classList.add('cli-provider-icon');
70
+ }
71
+ }
72
+ }
73
+
74
+ /** Get a provider's display label. */
75
+ export function providerLabel(slug: string): string {
76
+ const normalized = slug.toLowerCase().replace(/[-_\s]/g, '');
77
+ let key: ProviderSlug;
78
+ if (normalized === 'claude' || normalized.startsWith('claude')) key = 'claude';
79
+ else if (normalized === 'gemini' || normalized.startsWith('gemini')) key = 'gemini';
80
+ else if (normalized.startsWith('copilot') || normalized === 'githubcopilot') key = 'copilot';
81
+ else if (normalized === 'codex') key = 'codex';
82
+ else if (normalized === 'opencode') key = 'opencode';
83
+ else if (normalized === 'openai' || normalized.startsWith('gpt') || normalized.startsWith('o1') || normalized.startsWith('o3') || normalized.startsWith('o4')) key = 'openai';
84
+ else if (normalized === 'discord') key = 'discord';
85
+ else if (normalized === 'telegram') key = 'telegram';
86
+ else return slug;
87
+ return PROVIDER_ICONS[key].label;
88
+ }