agim-cli 1.2.2 → 1.2.17

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 (186) hide show
  1. package/README.md +18 -5
  2. package/README.zh-CN.md +20 -7
  3. package/dist/cli.js +42 -0
  4. package/dist/cli.js.map +1 -1
  5. package/dist/core/commands/builtin.d.ts.map +1 -1
  6. package/dist/core/commands/builtin.js +2 -0
  7. package/dist/core/commands/builtin.js.map +1 -1
  8. package/dist/core/commands/router.js +1 -1
  9. package/dist/core/commands/router.js.map +1 -1
  10. package/dist/core/commands/web.d.ts +3 -0
  11. package/dist/core/commands/web.d.ts.map +1 -0
  12. package/dist/core/commands/web.js +28 -0
  13. package/dist/core/commands/web.js.map +1 -0
  14. package/dist/core/intent.d.ts +11 -2
  15. package/dist/core/intent.d.ts.map +1 -1
  16. package/dist/core/intent.js +26 -4
  17. package/dist/core/intent.js.map +1 -1
  18. package/dist/core/memory.js.map +1 -1
  19. package/dist/core/render-router.d.ts.map +1 -1
  20. package/dist/core/render-router.js +3 -2
  21. package/dist/core/render-router.js.map +1 -1
  22. package/dist/core/router.d.ts.map +1 -1
  23. package/dist/core/router.js +8 -1
  24. package/dist/core/router.js.map +1 -1
  25. package/dist/core/types.d.ts +3 -0
  26. package/dist/core/types.d.ts.map +1 -1
  27. package/dist/plugins/agents/acp/acp-client.d.ts.map +1 -1
  28. package/dist/plugins/agents/acp/acp-client.js +7 -0
  29. package/dist/plugins/agents/acp/acp-client.js.map +1 -1
  30. package/dist/plugins/agents/acp/discovery.d.ts.map +1 -1
  31. package/dist/plugins/agents/acp/discovery.js +4 -0
  32. package/dist/plugins/agents/acp/discovery.js.map +1 -1
  33. package/dist/plugins/agents/acp/url-guard.d.ts +44 -0
  34. package/dist/plugins/agents/acp/url-guard.d.ts.map +1 -0
  35. package/dist/plugins/agents/acp/url-guard.js +109 -0
  36. package/dist/plugins/agents/acp/url-guard.js.map +1 -0
  37. package/dist/web/env-mask.d.ts +21 -0
  38. package/dist/web/env-mask.d.ts.map +1 -0
  39. package/dist/web/env-mask.js +44 -0
  40. package/dist/web/env-mask.js.map +1 -0
  41. package/dist/web/public/assets/a2a-Dk2fSs33.js +7 -0
  42. package/dist/web/public/assets/a2a-Dk2fSs33.js.map +1 -0
  43. package/dist/web/public/assets/activity-eiIPshcV.js +7 -0
  44. package/dist/web/public/assets/activity-eiIPshcV.js.map +1 -0
  45. package/dist/web/public/assets/admins-DlbQYdW_.js +12 -0
  46. package/dist/web/public/assets/admins-DlbQYdW_.js.map +1 -0
  47. package/dist/web/public/assets/agents-BMI1WbZj.js +12 -0
  48. package/dist/web/public/assets/agents-BMI1WbZj.js.map +1 -0
  49. package/dist/web/public/assets/approvals-DlXS_sKD.js +10 -0
  50. package/dist/web/public/assets/approvals-DlXS_sKD.js.map +1 -0
  51. package/dist/web/public/assets/audit-C8I8xC_6.js +2 -0
  52. package/dist/web/public/assets/audit-C8I8xC_6.js.map +1 -0
  53. package/dist/web/public/assets/bgjobs-PFYinH7D.js +7 -0
  54. package/dist/web/public/assets/bgjobs-PFYinH7D.js.map +1 -0
  55. package/dist/web/public/assets/brain-DEEJttEL.js +7 -0
  56. package/dist/web/public/assets/brain-DEEJttEL.js.map +1 -0
  57. package/dist/web/public/assets/briefcase-BlMy8gI6.js +7 -0
  58. package/dist/web/public/assets/briefcase-BlMy8gI6.js.map +1 -0
  59. package/dist/web/public/assets/browser-ponyfill-BOcGq8h9.js +3 -0
  60. package/dist/web/public/assets/browser-ponyfill-BOcGq8h9.js.map +1 -0
  61. package/dist/web/public/assets/chevron-right-DmABPvoA.js +7 -0
  62. package/dist/web/public/assets/chevron-right-DmABPvoA.js.map +1 -0
  63. package/dist/web/public/assets/circle-check-C0Qpg1vL.js +7 -0
  64. package/dist/web/public/assets/circle-check-C0Qpg1vL.js.map +1 -0
  65. package/dist/web/public/assets/circle-check-big-C8LG3beV.js +7 -0
  66. package/dist/web/public/assets/circle-check-big-C8LG3beV.js.map +1 -0
  67. package/dist/web/public/assets/circle-x-D_cRHcHK.js +7 -0
  68. package/dist/web/public/assets/circle-x-D_cRHcHK.js.map +1 -0
  69. package/dist/web/public/assets/confirm-dialog-Baz_xFle.js +2 -0
  70. package/dist/web/public/assets/confirm-dialog-Baz_xFle.js.map +1 -0
  71. package/dist/web/public/assets/data-table--I_ktDF4.js +17 -0
  72. package/dist/web/public/assets/data-table--I_ktDF4.js.map +1 -0
  73. package/dist/web/public/assets/dialog-DZpoEskO.js +6 -0
  74. package/dist/web/public/assets/dialog-DZpoEskO.js.map +1 -0
  75. package/dist/web/public/assets/download-DbFGHwZ5.js +7 -0
  76. package/dist/web/public/assets/download-DbFGHwZ5.js.map +1 -0
  77. package/dist/web/public/assets/email-BB1Hq8eE.js +7 -0
  78. package/dist/web/public/assets/email-BB1Hq8eE.js.map +1 -0
  79. package/dist/web/public/assets/empty-state-DXNa90pP.js +2 -0
  80. package/dist/web/public/assets/empty-state-DXNa90pP.js.map +1 -0
  81. package/dist/web/public/assets/env-Bqrb9XkC.js +2 -0
  82. package/dist/web/public/assets/env-Bqrb9XkC.js.map +1 -0
  83. package/dist/web/public/assets/external-link-nhnJN0qg.js +7 -0
  84. package/dist/web/public/assets/external-link-nhnJN0qg.js.map +1 -0
  85. package/dist/web/public/assets/eye-IKkn_oUo.js +12 -0
  86. package/dist/web/public/assets/eye-IKkn_oUo.js.map +1 -0
  87. package/dist/web/public/assets/facts-C7Qy9vTw.js +2 -0
  88. package/dist/web/public/assets/facts-C7Qy9vTw.js.map +1 -0
  89. package/dist/web/public/assets/health-CMRdeNEW.js +2 -0
  90. package/dist/web/public/assets/health-CMRdeNEW.js.map +1 -0
  91. package/dist/web/public/assets/hot-Bh5Nrc7i.js +17 -0
  92. package/dist/web/public/assets/hot-Bh5Nrc7i.js.map +1 -0
  93. package/dist/web/public/assets/index-CpGWCLE5.js +166 -0
  94. package/dist/web/public/assets/index-CpGWCLE5.js.map +1 -0
  95. package/dist/web/public/assets/index-GpceOxum.css +1 -0
  96. package/dist/web/public/assets/installed-FYLkPij2.js +7 -0
  97. package/dist/web/public/assets/installed-FYLkPij2.js.map +1 -0
  98. package/dist/web/public/assets/jobs-BmqLUzHp.js +2 -0
  99. package/dist/web/public/assets/jobs-BmqLUzHp.js.map +1 -0
  100. package/dist/web/public/assets/layout-9Gp_myEd.js +2 -0
  101. package/dist/web/public/assets/layout-9Gp_myEd.js.map +1 -0
  102. package/dist/web/public/assets/layout-BZaHqf69.js +2 -0
  103. package/dist/web/public/assets/layout-BZaHqf69.js.map +1 -0
  104. package/dist/web/public/assets/layout-CXsUyEpG.js +2 -0
  105. package/dist/web/public/assets/layout-CXsUyEpG.js.map +1 -0
  106. package/dist/web/public/assets/layout-DFxtpNut.js +2 -0
  107. package/dist/web/public/assets/layout-DFxtpNut.js.map +1 -0
  108. package/dist/web/public/assets/layout-d8qxPKQk.js +2 -0
  109. package/dist/web/public/assets/layout-d8qxPKQk.js.map +1 -0
  110. package/dist/web/public/assets/loader-circle-JaKY-xMt.js +7 -0
  111. package/dist/web/public/assets/loader-circle-JaKY-xMt.js.map +1 -0
  112. package/dist/web/public/assets/map-pin-hFFSWZ3B.js +7 -0
  113. package/dist/web/public/assets/map-pin-hFFSWZ3B.js.map +1 -0
  114. package/dist/web/public/assets/memos-EhjMUvVZ.js +12 -0
  115. package/dist/web/public/assets/memos-EhjMUvVZ.js.map +1 -0
  116. package/dist/web/public/assets/messengers-BRV1IVGX.js +7 -0
  117. package/dist/web/public/assets/messengers-BRV1IVGX.js.map +1 -0
  118. package/dist/web/public/assets/network-DtCI2ZUU.js +7 -0
  119. package/dist/web/public/assets/network-DtCI2ZUU.js.map +1 -0
  120. package/dist/web/public/assets/outbox-CxUbMp6o.js +7 -0
  121. package/dist/web/public/assets/outbox-CxUbMp6o.js.map +1 -0
  122. package/dist/web/public/assets/pagination-CkZY8YNa.js +17 -0
  123. package/dist/web/public/assets/pagination-CkZY8YNa.js.map +1 -0
  124. package/dist/web/public/assets/persona-B6TFMSnI.js +2 -0
  125. package/dist/web/public/assets/persona-B6TFMSnI.js.map +1 -0
  126. package/dist/web/public/assets/play-BxRcWaH5.js +7 -0
  127. package/dist/web/public/assets/play-BxRcWaH5.js.map +1 -0
  128. package/dist/web/public/assets/policy-ndE1Y8zD.js +2 -0
  129. package/dist/web/public/assets/policy-ndE1Y8zD.js.map +1 -0
  130. package/dist/web/public/assets/react-C9F3QeMB.js +33 -0
  131. package/dist/web/public/assets/react-C9F3QeMB.js.map +1 -0
  132. package/dist/web/public/assets/refresh-ccw-Bx817_KW.js +7 -0
  133. package/dist/web/public/assets/refresh-ccw-Bx817_KW.js.map +1 -0
  134. package/dist/web/public/assets/reminders-XynkGQc5.js +17 -0
  135. package/dist/web/public/assets/reminders-XynkGQc5.js.map +1 -0
  136. package/dist/web/public/assets/save-CqMcATrh.js +7 -0
  137. package/dist/web/public/assets/save-CqMcATrh.js.map +1 -0
  138. package/dist/web/public/assets/schedules-VM02w_Om.js +7 -0
  139. package/dist/web/public/assets/schedules-VM02w_Om.js.map +1 -0
  140. package/dist/web/public/assets/search-Ba-e1t1P.js +7 -0
  141. package/dist/web/public/assets/search-Ba-e1t1P.js.map +1 -0
  142. package/dist/web/public/assets/service-C-wnwJ-b.js +7 -0
  143. package/dist/web/public/assets/service-C-wnwJ-b.js.map +1 -0
  144. package/dist/web/public/assets/status-badge-CsdJ6k8Q.js +2 -0
  145. package/dist/web/public/assets/status-badge-CsdJ6k8Q.js.map +1 -0
  146. package/dist/web/public/assets/subtasks-mGRKpF0G.js +7 -0
  147. package/dist/web/public/assets/subtasks-mGRKpF0G.js.map +1 -0
  148. package/dist/web/public/assets/table-vmLMgj6_.js +2 -0
  149. package/dist/web/public/assets/table-vmLMgj6_.js.map +1 -0
  150. package/dist/web/public/assets/topn-nu66Fotx.js +7 -0
  151. package/dist/web/public/assets/topn-nu66Fotx.js.map +1 -0
  152. package/dist/web/public/assets/trash-2-ZIitN_U3.js +7 -0
  153. package/dist/web/public/assets/trash-2-ZIitN_U3.js.map +1 -0
  154. package/dist/web/public/assets/use-event-stream-BGeFcayX.js +2 -0
  155. package/dist/web/public/assets/use-event-stream-BGeFcayX.js.map +1 -0
  156. package/dist/web/public/assets/use-memory-DgEqHEca.js +2 -0
  157. package/dist/web/public/assets/use-memory-DgEqHEca.js.map +1 -0
  158. package/dist/web/public/assets/use-observability-CQev_A8e.js +2 -0
  159. package/dist/web/public/assets/use-observability-CQev_A8e.js.map +1 -0
  160. package/dist/web/public/assets/use-settings-CU-UcrVD.js +2 -0
  161. package/dist/web/public/assets/use-settings-CU-UcrVD.js.map +1 -0
  162. package/dist/web/public/assets/use-skills-Dr77CXLA.js +2 -0
  163. package/dist/web/public/assets/use-skills-Dr77CXLA.js.map +1 -0
  164. package/dist/web/public/assets/use-workspace-PNv9Z4de.js +2 -0
  165. package/dist/web/public/assets/use-workspace-PNv9Z4de.js.map +1 -0
  166. package/dist/web/public/assets/useQuery-BTyugXYV.js +2 -0
  167. package/dist/web/public/assets/useQuery-BTyugXYV.js.map +1 -0
  168. package/dist/web/public/assets/vector-w-Ea3pg6.js +2 -0
  169. package/dist/web/public/assets/vector-w-Ea3pg6.js.map +1 -0
  170. package/dist/web/public/assets/viewer-DKA7QP9U.js +12 -0
  171. package/dist/web/public/assets/viewer-DKA7QP9U.js.map +1 -0
  172. package/dist/web/public/assets/workspace-DVLZca7t.js +17 -0
  173. package/dist/web/public/assets/workspace-DVLZca7t.js.map +1 -0
  174. package/dist/web/public/assets/workspaces-DYZsMmY-.js +7 -0
  175. package/dist/web/public/assets/workspaces-DYZsMmY-.js.map +1 -0
  176. package/dist/web/public/assets/x-Ru3rHT82.js +7 -0
  177. package/dist/web/public/assets/x-Ru3rHT82.js.map +1 -0
  178. package/dist/web/public/favicon.svg +4 -0
  179. package/dist/web/public/index.html +37 -928
  180. package/dist/web/public/manifest.webmanifest +19 -0
  181. package/dist/web/public/tasks.html +307 -5
  182. package/dist/web/public/vendor/chart.umd.min.js +20 -0
  183. package/dist/web/server.d.ts.map +1 -1
  184. package/dist/web/server.js +640 -60
  185. package/dist/web/server.js.map +1 -1
  186. package/package.json +4 -4
@@ -1,936 +1,45 @@
1
1
  <!DOCTYPE html>
2
- <html lang="en">
2
+ <html lang="zh-CN">
3
3
  <head>
4
- <meta charset="UTF-8">
5
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>Agim Agent Chat</title>
7
- <!-- Shared utilities: theme manager (applies before first paint), error
8
- boundary (surfaces silent script failures), i18n + api helpers. -->
9
- <script src="/_app.js"></script>
4
+ <meta charset="UTF-8" />
5
+ <!-- viewport-fit=cover so iOS safe-area-inset-* actually has values to
6
+ expose; initial-scale=1 + maximum-scale=1 keeps mobile chrome from
7
+ auto-zooming on focused inputs (we control text size ourselves). -->
8
+ <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover, maximum-scale=1" />
9
+ <meta name="theme-color" content="#f7f8fa" media="(prefers-color-scheme: light)" />
10
+ <meta name="theme-color" content="#0e0f12" media="(prefers-color-scheme: dark)" />
11
+ <meta name="description" content="agim — multi-agent gateway console" />
12
+ <meta name="apple-mobile-web-app-capable" content="yes" />
13
+ <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
14
+ <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
15
+ <link rel="manifest" href="/manifest.webmanifest" />
16
+ <title>Agim</title>
17
+ <!--
18
+ Synchronous theme bootstrap. Reads localStorage 'agim-theme' BEFORE
19
+ the React bundle parses + renders, so the first paint already has
20
+ the right `data-theme` attribute on <html>. Without this, the page
21
+ flashes light-themed for ~80ms on dark-mode setups (the JS render
22
+ happens after first paint).
23
+
24
+ Mirrors the same key + state machine that agim-cli's old _app.js
25
+ used — keeps user preference continuous across the v1 → v2
26
+ upgrade.
27
+ -->
10
28
  <script>
11
- // i18n — detect browser language, store preference in localStorage
12
- const LANGS = { en: 'English', zh: '中文' };
13
- const savedLang = localStorage.getItem('im-hub-lang');
14
- const browserLang = navigator.language.startsWith('zh') ? 'zh' : 'en';
15
- window.__lang = savedLang && LANGS[savedLang] ? savedLang : browserLang;
16
- document.documentElement.lang = window.__lang === 'zh' ? 'zh-CN' : 'en';
17
-
18
- const T = {
19
- en: {
20
- title: 'Agim — Agent Chat',
21
- connecting: 'Connecting...',
22
- connected: 'Connected',
23
- disconnected: 'Disconnected',
24
- connError: 'Connection error',
25
- loadingAgents: 'Loading agents...',
26
- newChat: 'New Chat',
27
- welcomeTitle: 'Agim Agent Chat',
28
- welcomeDesc: 'Select an agent and start chatting. Your locally installed AI coding agents are ready.',
29
- inputPlaceholder: 'Type a message... (Enter to send, Shift+Enter for newline)',
30
- send: 'Send',
31
- you: 'You',
32
- assistant: 'Assistant',
33
- approvalTitle: 'Tool approval request',
34
- approvalToolLabel: 'Tool',
35
- approvalInputLabel: 'Input',
36
- approvalAllow: 'Allow',
37
- approvalDeny: 'Deny',
38
- approvalAllowAll: 'Allow + auto for similar',
39
- approvalAutoAllowing: 'Auto-allowing in {s}s — click Deny to reject',
40
- approvalAllowed: '✅ Allowed',
41
- approvalAllowedPinned: '✅ Allowed (auto-allow rule pinned)',
42
- approvalDenied: '❌ Denied',
43
- approvalDeniedRevoked: '❌ Denied (auto-allow rule revoked)',
44
- approvalExpired: '⏱ Timed out (auto-denied)',
45
- },
46
- zh: {
47
- title: 'Agim — Agent 对话',
48
- connecting: '连接中...',
49
- connected: '已连接',
50
- disconnected: '已断开',
51
- connError: '连接错误',
52
- loadingAgents: '加载 Agent...',
53
- newChat: '新对话',
54
- welcomeTitle: 'Agim Agent 对话',
55
- welcomeDesc: '选择一个 Agent 开始对话,本地安装的 AI 编程助手已就绪。',
56
- inputPlaceholder: '输入消息...(Enter 发送,Shift+Enter 换行)',
57
- send: '发送',
58
- you: '你',
59
- assistant: '助手',
60
- approvalTitle: '工具调用审批',
61
- approvalToolLabel: '工具',
62
- approvalInputLabel: '入参',
63
- approvalAllow: '批准',
64
- approvalDeny: '拒绝',
65
- approvalAllowAll: '批准并自动放行后续同类',
66
- approvalAutoAllowing: '将在 {s}s 内自动放行 — 点击拒绝可阻止',
67
- approvalAllowed: '✅ 已批准',
68
- approvalAllowedPinned: '✅ 已批准(已启用自动放行)',
69
- approvalDenied: '❌ 已拒绝',
70
- approvalDeniedRevoked: '❌ 已拒绝(已撤销自动放行)',
71
- approvalExpired: '⏱ 已超时(自动拒绝)',
72
- },
73
- };
74
- function t(key) { return T[window.__lang][key] || T.en[key] || key; }
75
- document.addEventListener('DOMContentLoaded', () => { document.title = t('title'); });
76
- </script>
77
- <style>
78
- /* Three-state theming. `:root` defaults to light; explicit
79
- data-theme="dark" forces dark; `prefers-color-scheme: dark` only
80
- applies when the attribute is absent (mode === 'system'). */
81
- :root {
82
- color-scheme: light dark;
83
- /* light defaults */
84
- --bg: #f8f9fb;
85
- --surface: #ffffff;
86
- --surface2: #f1f3f6;
87
- --border: #e1e4e8;
88
- --text: #1a1f2e;
89
- --text-dim: #6b7280;
90
- --accent: #6366f1;
91
- --accent-dim: #818cf8;
92
- --user-bg: #eef2ff;
93
- --assistant-bg: #ffffff;
94
- --code-bg: #f1f5f9;
95
- --green: #16a34a;
96
- --red: #dc2626;
97
- --yellow: #ca8a04;
98
- }
99
- :root[data-theme="dark"] {
100
- --bg: #0a0a0a;
101
- --surface: #141414;
102
- --surface2: #1e1e1e;
103
- --border: #2a2a2a;
104
- --text: #e5e5e5;
105
- --text-dim: #888;
106
- --accent: #6366f1;
107
- --accent-dim: #4f46e5;
108
- --user-bg: #1a1a2e;
109
- --assistant-bg: #1e1e1e;
110
- --code-bg: #0d0d0d;
111
- --green: #22c55e;
112
- --red: #ef4444;
113
- --yellow: #eab308;
114
- }
115
- @media (prefers-color-scheme: dark) {
116
- :root:not([data-theme]) {
117
- --bg: #0a0a0a;
118
- --surface: #141414;
119
- --surface2: #1e1e1e;
120
- --border: #2a2a2a;
121
- --text: #e5e5e5;
122
- --text-dim: #888;
123
- --accent-dim: #4f46e5;
124
- --user-bg: #1a1a2e;
125
- --assistant-bg: #1e1e1e;
126
- --code-bg: #0d0d0d;
127
- --green: #22c55e;
128
- --red: #ef4444;
129
- --yellow: #eab308;
130
- }
131
- }
132
-
133
- * { margin: 0; padding: 0; box-sizing: border-box; }
134
-
135
- body {
136
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
137
- background: var(--bg);
138
- color: var(--text);
139
- height: 100vh;
140
- display: flex;
141
- flex-direction: column;
142
- }
143
-
144
- /* Header */
145
- .header {
146
- display: flex;
147
- align-items: center;
148
- justify-content: space-between;
149
- padding: 12px 20px;
150
- border-bottom: 1px solid var(--border);
151
- background: var(--surface);
152
- }
153
- .header-left {
154
- display: flex;
155
- align-items: center;
156
- gap: 12px;
157
- }
158
- .logo {
159
- font-weight: 700;
160
- font-size: 16px;
161
- color: var(--accent);
162
- }
163
- .status-dot {
164
- width: 8px;
165
- height: 8px;
166
- border-radius: 50%;
167
- background: var(--yellow);
168
- display: inline-block;
169
- }
170
- .status-dot.connected { background: var(--green); }
171
- .status-dot.disconnected { background: var(--red); }
172
- .status-text { font-size: 12px; color: var(--text-dim); }
173
- .header-right { display: flex; align-items: center; gap: 10px; }
174
-
175
- /* Agent selector */
176
- .agent-select {
177
- background: var(--surface2);
178
- color: var(--text);
179
- border: 1px solid var(--border);
180
- border-radius: 6px;
181
- padding: 6px 10px;
182
- font-size: 13px;
183
- cursor: pointer;
184
- outline: none;
185
- }
186
- .agent-select:focus { border-color: var(--accent); }
187
-
188
- .btn {
189
- background: var(--surface2);
190
- color: var(--text-dim);
191
- border: 1px solid var(--border);
192
- border-radius: 6px;
193
- padding: 6px 10px;
194
- font-size: 12px;
195
- cursor: pointer;
196
- }
197
- .btn:hover { color: var(--text); border-color: var(--text-dim); }
198
-
199
- /* Language selector */
200
- .lang-select {
201
- background: var(--surface2);
202
- color: var(--text-dim);
203
- border: 1px solid var(--border);
204
- border-radius: 6px;
205
- padding: 4px 6px;
206
- font-size: 12px;
207
- cursor: pointer;
208
- outline: none;
209
- }
210
- .lang-select:hover { color: var(--text); }
211
-
212
- /* Messages */
213
- .messages {
214
- flex: 1;
215
- overflow-y: auto;
216
- padding: 20px;
217
- display: flex;
218
- flex-direction: column;
219
- gap: 16px;
220
- }
221
-
222
- .message {
223
- max-width: 85%;
224
- padding: 12px 16px;
225
- border-radius: 12px;
226
- font-size: 14px;
227
- line-height: 1.6;
228
- white-space: pre-wrap;
229
- word-break: break-word;
230
- }
231
- .message.user {
232
- align-self: flex-end;
233
- background: var(--user-bg);
234
- border-bottom-right-radius: 4px;
235
- }
236
- .message.assistant {
237
- align-self: flex-start;
238
- background: var(--assistant-bg);
239
- border-bottom-left-radius: 4px;
240
- }
241
- .message.assistant code {
242
- background: var(--code-bg);
243
- padding: 2px 6px;
244
- border-radius: 4px;
245
- font-family: 'SF Mono', 'Fira Code', 'Cascadia Code', monospace;
246
- font-size: 13px;
247
- }
248
- .message.assistant pre {
249
- background: var(--code-bg);
250
- padding: 12px 16px;
251
- border-radius: 8px;
252
- overflow-x: auto;
253
- margin: 8px 0;
254
- }
255
- .message.assistant pre code {
256
- background: none;
257
- padding: 0;
258
- }
259
-
260
- .message-label {
261
- font-size: 11px;
262
- color: var(--text-dim);
263
- margin-bottom: 4px;
264
- font-weight: 600;
265
- }
266
-
267
- /* Typing indicator */
268
- .typing {
269
- align-self: flex-start;
270
- padding: 12px 16px;
271
- color: var(--text-dim);
272
- font-size: 13px;
273
- }
274
- .typing span {
275
- animation: blink 1.4s infinite;
276
- }
277
- .typing span:nth-child(2) { animation-delay: 0.2s; }
278
- .typing span:nth-child(3) { animation-delay: 0.4s; }
279
- @keyframes blink {
280
- 0%, 60%, 100% { opacity: 0.2; }
281
- 30% { opacity: 1; }
282
- }
283
-
284
- /* Welcome screen */
285
- .welcome {
286
- flex: 1;
287
- display: flex;
288
- flex-direction: column;
289
- align-items: center;
290
- justify-content: center;
291
- gap: 16px;
292
- color: var(--text-dim);
293
- }
294
- .welcome h2 { color: var(--text); font-size: 20px; }
295
- .welcome p { font-size: 14px; max-width: 400px; text-align: center; }
296
-
297
- /* Input area */
298
- .input-area {
299
- padding: 16px 20px;
300
- border-top: 1px solid var(--border);
301
- background: var(--surface);
302
- }
303
- .input-wrapper {
304
- display: flex;
305
- gap: 8px;
306
- align-items: flex-end;
307
- }
308
- .input-wrapper textarea {
309
- flex: 1;
310
- background: var(--surface2);
311
- color: var(--text);
312
- border: 1px solid var(--border);
313
- border-radius: 10px;
314
- padding: 10px 14px;
315
- font-size: 14px;
316
- font-family: inherit;
317
- resize: none;
318
- outline: none;
319
- max-height: 150px;
320
- min-height: 42px;
321
- }
322
- .input-wrapper textarea:focus { border-color: var(--accent); }
323
- .input-wrapper textarea::placeholder { color: var(--text-dim); }
324
-
325
- .send-btn {
326
- background: var(--accent);
327
- color: white;
328
- border: none;
329
- border-radius: 10px;
330
- padding: 10px 16px;
331
- font-size: 14px;
332
- cursor: pointer;
333
- white-space: nowrap;
334
- }
335
- .send-btn:hover { background: var(--accent-dim); }
336
- .send-btn:disabled {
337
- background: var(--surface2);
338
- color: var(--text-dim);
339
- cursor: not-allowed;
340
- }
341
-
342
- /* Scrollbar */
343
- .messages::-webkit-scrollbar { width: 6px; }
344
- .messages::-webkit-scrollbar-track { background: transparent; }
345
- .messages::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; }
346
-
347
- /* In-chat HITL approval card. Mirrors the IM card's structure (Telegram
348
- inline-keyboard variant) so the cross-platform UX is consistent. */
349
- .approval-card {
350
- border: 1px solid var(--accent);
351
- background: var(--surface);
352
- border-radius: 10px;
353
- padding: 14px 16px;
354
- margin: 8px 0;
355
- max-width: 720px;
356
- box-shadow: 0 1px 3px rgba(0,0,0,.06);
357
- }
358
- .approval-card.auto-allow { border-color: var(--yellow); }
359
- .approval-card.resolved { opacity: .85; }
360
- .approval-card .ac-title {
361
- font-weight: 600;
362
- font-size: 14px;
363
- margin-bottom: 8px;
364
- display: flex;
365
- align-items: center;
366
- gap: 6px;
367
- }
368
- .approval-card .ac-row { font-size: 13px; margin: 4px 0; color: var(--text-dim); }
369
- .approval-card .ac-row b { color: var(--text); margin-right: 6px; }
370
- .approval-card .ac-input {
371
- background: var(--code-bg);
372
- border-radius: 6px;
373
- padding: 8px 10px;
374
- font: 12px/1.5 'SF Mono', Menlo, Consolas, monospace;
375
- white-space: pre-wrap;
376
- word-break: break-word;
377
- max-height: 180px;
378
- overflow: auto;
379
- margin: 6px 0 10px;
380
- color: var(--text);
381
- }
382
- .approval-card .ac-actions { display: flex; gap: 8px; flex-wrap: wrap; margin-top: 8px; }
383
- .approval-card .ac-btn {
384
- border: 1px solid var(--border);
385
- background: var(--surface2);
386
- color: var(--text);
387
- padding: 6px 14px;
388
- border-radius: 6px;
389
- cursor: pointer;
390
- font-size: 13px;
391
- }
392
- .approval-card .ac-btn:hover { border-color: var(--accent); }
393
- .approval-card .ac-btn.allow { background: var(--green); border-color: var(--green); color: #fff; }
394
- .approval-card .ac-btn.deny { background: var(--red); border-color: var(--red); color: #fff; }
395
- .approval-card .ac-btn:disabled { opacity: .5; cursor: not-allowed; }
396
- .approval-card .ac-grace {
397
- color: var(--yellow);
398
- font-size: 13px;
399
- margin-bottom: 8px;
400
- font-weight: 500;
401
- }
402
- .approval-card .ac-outcome {
403
- font-size: 13px;
404
- margin-top: 8px;
405
- padding-top: 8px;
406
- border-top: 1px solid var(--border);
407
- color: var(--text-dim);
408
- }
409
- </style>
410
- </head>
411
- <body>
412
- <div class="header">
413
- <div class="header-left">
414
- <span class="logo">Agim</span>
415
- <span class="status-dot" id="statusDot"></span>
416
- <span class="status-text" id="statusText"></span>
417
- </div>
418
- <div class="header-right">
419
- <select class="lang-select" id="langSelect">
420
- <option value="en">EN</option>
421
- <option value="zh">中文</option>
422
- </select>
423
- <select class="agent-select" id="agentSelect" disabled>
424
- <option value=""></option>
425
- </select>
426
- <button type="button" class="btn" id="newChatBtn"></button>
427
- <button class="btn" id="theme-toggle" type="button" aria-label="Toggle color theme"></button>
428
- <a class="btn" href="/tasks" title="Tasks" style="text-decoration:none;font-size:16px;line-height:1">&#9776;</a>
429
- <a class="btn" href="/reminders" title="Reminders" style="text-decoration:none;font-size:14px;line-height:1">🔔</a>
430
- <a class="btn" href="/memos" title="Memos" style="text-decoration:none;font-size:14px;line-height:1">📋</a>
431
- <a class="btn" href="/settings" title="Settings" style="text-decoration:none;font-size:16px;line-height:1">&#9881;</a>
432
- </div>
433
- </div>
434
-
435
- <div class="messages" id="messages">
436
- <div class="welcome" id="welcome">
437
- <h2 id="welcomeTitle"></h2>
438
- <p id="welcomeDesc"></p>
439
- </div>
440
- </div>
441
-
442
- <div class="input-area">
443
- <div class="input-wrapper">
444
- <textarea
445
- id="input"
446
- rows="1"
447
- disabled
448
- ></textarea>
449
- <button type="button" class="send-btn" id="sendBtn" disabled></button>
450
- </div>
451
- </div>
452
-
453
- <script>
454
- // Apply i18n to static elements
455
- function applyLang() {
456
- document.title = t('title');
457
- document.documentElement.lang = window.__lang === 'zh' ? 'zh-CN' : 'en';
458
- statusText.textContent = statusText.dataset.state ? t(statusText.dataset.state) : t('connecting');
459
- const opt = agentSelect.querySelector('option[value=""]');
460
- if (opt) opt.textContent = t('loadingAgents');
461
- newChatBtn.textContent = t('newChat');
462
- inputEl.placeholder = t('inputPlaceholder');
463
- sendBtn.textContent = t('send');
464
- const wt = document.getElementById('welcomeTitle');
465
- const wd = document.getElementById('welcomeDesc');
466
- if (wt) wt.textContent = t('welcomeTitle');
467
- if (wd) wd.textContent = t('welcomeDesc');
468
- }
469
-
470
- // State
471
- let ws = null;
472
- let currentAgent = '';
473
- let isStreaming = false;
474
- let streamingEl = null;
475
- let streamingText = '';
476
- let _reconnectTimer = null;
477
-
478
- // DOM
479
- const messagesEl = document.getElementById('messages');
480
- const welcomeEl = document.getElementById('welcome');
481
- const inputEl = document.getElementById('input');
482
- const sendBtn = document.getElementById('sendBtn');
483
- const agentSelect = document.getElementById('agentSelect');
484
- const statusDot = document.getElementById('statusDot');
485
- const statusText = document.getElementById('statusText');
486
- const newChatBtn = document.getElementById('newChatBtn');
487
- const langSelect = document.getElementById('langSelect');
488
-
489
- // Theme toggle (light / dark / system). _app.js applied the theme
490
- // synchronously in <head>; here we wire the button so clicks cycle
491
- // through the three modes and the icon/label re-renders.
492
- if (window.imhub) imhub.theme.bindToggle(document.getElementById('theme-toggle'));
493
-
494
- // Language selector
495
- langSelect.value = window.__lang;
496
- langSelect.addEventListener('change', () => {
497
- window.__lang = langSelect.value;
498
- localStorage.setItem('im-hub-lang', window.__lang);
499
- applyLang();
500
- });
501
-
502
- // Connect
503
- function connect() {
504
- const protocol = location.protocol === 'https:' ? 'wss:' : 'ws:';
505
- ws = new WebSocket(`${protocol}//${location.host}/`);
506
-
507
- ws.onopen = () => {
508
- setStatus('connected', t('connected'));
509
- inputEl.disabled = false;
510
- sendBtn.disabled = false;
511
- };
512
-
513
- ws.onmessage = (e) => {
514
- const msg = JSON.parse(e.data);
515
- handleMessage(msg);
516
- };
517
-
518
- ws.onclose = () => {
519
- setStatus('disconnected', t('disconnected'));
520
- inputEl.disabled = true;
521
- sendBtn.disabled = true;
522
- _reconnectTimer = setTimeout(connect, 3000);
523
- };
524
-
525
- ws.onerror = () => {
526
- setStatus('disconnected', t('connError'));
527
- };
528
- }
529
-
530
- // Handle server messages
531
- function handleMessage(msg) {
532
- switch (msg.type) {
533
- case 'init':
534
- agentSelect.innerHTML = '';
535
- msg.agents.forEach(name => {
536
- const opt = document.createElement('option');
537
- opt.value = name;
538
- opt.textContent = name;
539
- if (name === msg.defaultAgent) opt.selected = true;
540
- agentSelect.appendChild(opt);
541
- });
542
- agentSelect.disabled = false;
543
- currentAgent = msg.defaultAgent;
544
- break;
545
-
546
- case 'history':
547
- hideWelcome();
548
- if (msg.agent) {
549
- currentAgent = msg.agent;
550
- agentSelect.value = msg.agent;
551
- }
552
- msg.messages.forEach(m => {
553
- addMessage(m.role, m.content, false);
554
- });
555
- scrollToBottom();
556
- break;
557
-
558
- case 'agents':
559
- agentSelect.innerHTML = '';
560
- msg.agents.forEach(name => {
561
- const opt = document.createElement('option');
562
- opt.value = name;
563
- opt.textContent = name;
564
- if (name === currentAgent) opt.selected = true;
565
- agentSelect.appendChild(opt);
566
- });
567
- agentSelect.disabled = false;
568
- break;
569
-
570
- case 'chunk':
571
- if (!isStreaming) {
572
- hideWelcome();
573
- startStreaming();
574
- }
575
- streamingText += msg.text;
576
- updateStreamingEl();
577
- scrollToBottom();
578
- break;
579
-
580
- case 'done':
581
- if (isStreaming) {
582
- finishStreaming();
583
- } else if (msg.text) {
584
- hideWelcome();
585
- addMessage('assistant', msg.text);
586
- scrollToBottom();
587
- }
588
- break;
589
-
590
- case 'error':
591
- if (isStreaming) finishStreaming();
592
- addMessage('assistant', `\u274c ${msg.message}`);
593
- scrollToBottom();
594
- break;
595
-
596
- case 'agent-switched':
597
- currentAgent = msg.agent;
598
- agentSelect.value = msg.agent;
599
- break;
600
-
601
- // HITL approval messages relayed by the synthetic 'web' messenger
602
- // (registered on the server in startWebServer). approval-router
603
- // calls our messenger.sendApprovalCard / sendMessage / editApprovalCard
604
- // — those become the three WS message types below.
605
- case 'approval-text':
606
- // Fallback / receipt path. Render as a system message.
607
- hideWelcome();
608
- addSystemMessage(msg.text);
609
- scrollToBottom();
610
- break;
611
- case 'approval-card':
612
- hideWelcome();
613
- renderApprovalCard(msg.messageId, msg.prompt);
614
- scrollToBottom();
615
- break;
616
- case 'approval-card-edit':
617
- editApprovalCard(msg.messageId, msg.outcome);
618
- break;
619
- }
620
- }
621
-
622
- // Send message
623
- function sendMessage() {
624
- const text = inputEl.value.trim();
625
- if (!text || !ws || ws.readyState !== ws.OPEN) return;
626
-
627
- hideWelcome();
628
- addMessage('user', text);
629
-
630
- ws.send(JSON.stringify({
631
- type: 'message',
632
- text,
633
- agent: currentAgent,
634
- }));
635
-
636
- inputEl.value = '';
637
- autoResize();
638
- }
639
-
640
- // Streaming helpers
641
- function startStreaming() {
642
- isStreaming = true;
643
- streamingText = '';
644
-
645
- const wrapper = document.createElement('div');
646
- wrapper.innerHTML = `<div class="message-label">${t('assistant')}</div>`;
647
- streamingEl = document.createElement('div');
648
- streamingEl.className = 'message assistant';
649
- wrapper.appendChild(streamingEl);
650
- messagesEl.appendChild(wrapper);
651
- }
652
-
653
- function updateStreamingEl() {
654
- if (!streamingEl) return;
655
- streamingEl.textContent = streamingText;
656
- }
657
-
658
- function finishStreaming() {
659
- isStreaming = false;
660
- if (streamingEl) {
661
- streamingEl.innerHTML = renderMarkdown(streamingText);
662
- }
663
- streamingEl = null;
664
- saveHistory();
665
- }
666
-
667
- // Message helpers
668
- /**
669
- * System / receipt-style message bubble. Used for `approval-text` so the
670
- * "✅ Allowed" / "⏱ Timed out" notices the bus emits show up in the
671
- * conversation flow without a "user / assistant" label.
672
- */
673
- function addSystemMessage(text) {
674
- hideWelcome();
675
- const wrapper = document.createElement('div');
676
- wrapper.style.cssText = 'margin:6px 0; color:var(--text-dim); font-size:13px;';
677
- wrapper.textContent = text;
678
- messagesEl.appendChild(wrapper);
679
- }
680
-
681
- /** Active approval cards keyed by server-issued messageId, so the
682
- * `approval-card-edit` handler can find the same DOM node and
683
- * collapse it to an outcome stamp. */
684
- const approvalCards = new Map();
685
-
686
- function renderApprovalCard(messageId, prompt) {
687
- const isAuto = prompt.mode === 'auto-allow';
688
- const card = document.createElement('div');
689
- card.className = `approval-card${isAuto ? ' auto-allow' : ''}`;
690
- card.dataset.cardId = messageId;
691
- card.innerHTML = `
692
- <div class="ac-title">🔐 <span data-slot="title"></span></div>
693
- ${isAuto ? `<div class="ac-grace" data-slot="grace"></div>` : ''}
694
- <div class="ac-row"><b data-slot="tool-label"></b><code data-slot="tool"></code></div>
695
- <div class="ac-row"><b data-slot="input-label"></b></div>
696
- <pre class="ac-input" data-slot="input"></pre>
697
- <div class="ac-actions" data-slot="actions"></div>
698
- `;
699
- card.querySelector('[data-slot="title"]').textContent = t('approvalTitle');
700
- card.querySelector('[data-slot="tool-label"]').textContent = `${t('approvalToolLabel')}:`;
701
- card.querySelector('[data-slot="tool"]').textContent = prompt.toolName;
702
- card.querySelector('[data-slot="input-label"]').textContent = `${t('approvalInputLabel')}:`;
703
- card.querySelector('[data-slot="input"]').textContent = prompt.inputJson;
704
-
705
- const send = (choice) => {
706
- // Soft disable: dim and ignore further clicks for 800 ms while the
707
- // server round-trips. We deliberately do NOT hard-disable — if the
708
- // server returns an error (e.g. 'approval handler not bound'), the
709
- // card stays usable so the user can retry once they fix it. The
710
- // success path replaces the buttons via `approval-card-edit` anyway.
711
- card.querySelectorAll('button').forEach((b) => {
712
- b.style.opacity = '0.6';
713
- b.style.pointerEvents = 'none';
714
- });
715
- setTimeout(() => {
716
- card.querySelectorAll('button').forEach((b) => {
717
- b.style.opacity = '';
718
- b.style.pointerEvents = '';
719
- });
720
- }, 800);
721
- const payload = { type: 'approval-action', messageId, data: `apv:${prompt.reqId}:${choice}` };
722
- try { console.debug?.('[approval] click', payload); } catch {}
723
- if (ws.readyState !== WebSocket.OPEN) {
724
- imhub?.showError?.('WebSocket not connected — click ignored');
725
- return;
726
- }
727
- ws.send(JSON.stringify(payload));
728
- };
729
-
730
- const actions = card.querySelector('[data-slot="actions"]');
731
- if (isAuto) {
732
- // Auto-allow grace mode: only Deny is meaningful (Allow happens
733
- // automatically on grace expiry).
734
- const denyBtn = document.createElement('button');
735
- denyBtn.className = 'ac-btn deny';
736
- denyBtn.textContent = t('approvalDeny');
737
- denyBtn.onclick = () => send('n');
738
- actions.appendChild(denyBtn);
739
-
740
- // Live countdown.
741
- let s = Number(prompt.graceSeconds) || 5;
742
- const grace = card.querySelector('[data-slot="grace"]');
743
- const refreshGrace = () => { grace.textContent = t('approvalAutoAllowing').replace('{s}', String(s)); };
744
- refreshGrace();
745
- const tick = setInterval(() => {
746
- s = Math.max(0, s - 1);
747
- refreshGrace();
748
- if (s <= 0) clearInterval(tick);
749
- }, 1000);
750
- card._graceTimer = tick;
751
- } else {
752
- const allowBtn = document.createElement('button');
753
- allowBtn.className = 'ac-btn allow';
754
- allowBtn.textContent = t('approvalAllow');
755
- allowBtn.onclick = () => send('y');
756
-
757
- const denyBtn = document.createElement('button');
758
- denyBtn.className = 'ac-btn deny';
759
- denyBtn.textContent = t('approvalDeny');
760
- denyBtn.onclick = () => send('n');
761
-
762
- const allowAllBtn = document.createElement('button');
763
- allowAllBtn.className = 'ac-btn';
764
- allowAllBtn.textContent = t('approvalAllowAll');
765
- allowAllBtn.onclick = () => send('a');
766
-
767
- actions.appendChild(allowBtn);
768
- actions.appendChild(denyBtn);
769
- actions.appendChild(allowAllBtn);
770
- }
771
-
772
- messagesEl.appendChild(card);
773
- approvalCards.set(messageId, card);
774
- }
775
-
776
- function editApprovalCard(messageId, outcome) {
777
- const card = approvalCards.get(messageId);
778
- if (!card) return;
779
- card.classList.add('resolved');
780
- // Stop any countdown if this was an auto-allow card.
781
- if (card._graceTimer) { clearInterval(card._graceTimer); card._graceTimer = null; }
782
- const grace = card.querySelector('[data-slot="grace"]');
783
- if (grace) grace.remove();
784
- // Disable any remaining live buttons.
785
- card.querySelectorAll('.ac-actions button').forEach(b => { b.disabled = true; });
786
- const actions = card.querySelector('[data-slot="actions"]');
787
- if (actions) actions.remove();
788
- const stamp = document.createElement('div');
789
- stamp.className = 'ac-outcome';
790
- const KEY = {
791
- allowed: 'approvalAllowed',
792
- 'allowed-pinned': 'approvalAllowedPinned',
793
- denied: 'approvalDenied',
794
- 'denied-revoked': 'approvalDeniedRevoked',
795
- expired: 'approvalExpired',
796
- };
797
- const text = t(KEY[outcome.decision] || 'approvalAllowed');
798
- const by = outcome.byUserDisplay ? ` · ${outcome.byUserDisplay}` : '';
799
- stamp.textContent = `${text}${by}`;
800
- card.appendChild(stamp);
801
- // Keep the card around for visual continuity, but free the map slot
802
- // so a same-id replay doesn't double-edit it.
803
- approvalCards.delete(messageId);
804
- }
805
-
806
- function addMessage(role, content, save = true) {
807
- hideWelcome();
808
-
809
- const wrapper = document.createElement('div');
810
- const label = document.createElement('div');
811
- label.className = 'message-label';
812
- label.textContent = role === 'user' ? t('you') : t('assistant');
813
-
814
- const el = document.createElement('div');
815
- el.className = `message ${role}`;
816
-
817
- if (role === 'assistant') {
818
- el.innerHTML = renderMarkdown(content);
819
- } else {
820
- el.textContent = content;
821
- }
822
-
823
- wrapper.appendChild(label);
824
- wrapper.appendChild(el);
825
- messagesEl.appendChild(wrapper);
826
-
827
- if (save) saveHistory();
828
- }
829
-
830
- // Basic markdown rendering
831
- function renderMarkdown(text) {
832
- if (!text) return '';
833
- let html = escapeHtml(text);
834
-
835
- html = html.replace(/```(\w*)\n([\s\S]*?)```/g, (_, _lang, code) => {
836
- return `<pre><code>${code.trim()}</code></pre>`;
837
- });
838
-
839
- html = html.replace(/`([^`]+)`/g, '<code>$1</code>');
840
- html = html.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>');
841
- html = html.replace(/(?<!\*)\*([^*]+)\*(?!\*)/g, '<em>$1</em>');
842
-
843
- return html;
844
- }
845
-
846
- function escapeHtml(text) {
847
- return text
848
- .replace(/&/g, '&amp;')
849
- .replace(/</g, '&lt;')
850
- .replace(/>/g, '&gt;');
851
- }
852
-
853
- function hideWelcome() {
854
- if (welcomeEl) welcomeEl.remove();
855
- }
856
-
857
- function scrollToBottom() {
858
- requestAnimationFrame(() => {
859
- messagesEl.scrollTop = messagesEl.scrollHeight;
860
- });
861
- }
862
-
863
- // Status
864
- function setStatus(state, text) {
865
- statusDot.className = `status-dot ${state}`;
866
- statusText.textContent = text;
867
- statusText.dataset.state = state === 'connected' ? 'connected' : state === 'disconnected' ? 'disconnected' : 'connecting';
868
- }
869
-
870
- // LocalStorage persistence
871
- function saveHistory() {
872
- const msgs = [];
873
- messagesEl.querySelectorAll('.message').forEach(el => {
874
- const role = el.classList.contains('user') ? 'user' : 'assistant';
875
- msgs.push({ role, content: el.textContent });
876
- });
29
+ (function () {
877
30
  try {
878
- localStorage.setItem('im-hub-history', JSON.stringify(msgs));
879
- } catch {}
880
- }
881
-
882
- function loadHistory() {
883
- try {
884
- const saved = localStorage.getItem('im-hub-history');
885
- if (saved) {
886
- const msgs = JSON.parse(saved);
887
- if (msgs.length > 0) {
888
- hideWelcome();
889
- msgs.forEach(m => { addMessage(m.role, m.content, false); });
890
- scrollToBottom();
891
- }
31
+ var m = localStorage.getItem('agim-theme')
32
+ if (m === 'light' || m === 'dark') {
33
+ document.documentElement.setAttribute('data-theme', m)
892
34
  }
893
- } catch {}
894
- }
895
-
896
- // Auto-resize textarea
897
- function autoResize() {
898
- inputEl.style.height = 'auto';
899
- inputEl.style.height = `${Math.min(inputEl.scrollHeight, 150)}px`;
900
- }
901
-
902
- // Event listeners
903
- inputEl.addEventListener('input', autoResize);
904
-
905
- inputEl.addEventListener('keydown', (e) => {
906
- if (e.key === 'Enter' && !e.shiftKey && !e.isComposing) {
907
- e.preventDefault();
908
- sendMessage();
909
- }
910
- });
911
-
912
- sendBtn.addEventListener('click', sendMessage);
913
-
914
- agentSelect.addEventListener('change', () => {
915
- const agent = agentSelect.value;
916
- if (agent && agent !== currentAgent && ws?.readyState === WebSocket.OPEN) {
917
- currentAgent = agent;
918
- ws.send(JSON.stringify({ type: 'switch-agent', agent }));
919
- }
920
- });
921
-
922
- newChatBtn.addEventListener('click', () => {
923
- messagesEl.innerHTML = '';
924
- localStorage.removeItem('im-hub-history');
925
- if (ws?.readyState === WebSocket.OPEN) {
926
- ws.send(JSON.stringify({ type: 'message', text: '/new' }));
927
- }
928
- });
929
-
930
- // Init
931
- applyLang();
932
- loadHistory();
933
- connect();
35
+ } catch (e) { /* private-mode storage exceptions: noop */ }
36
+ })()
934
37
  </script>
38
+ <script type="module" crossorigin src="/assets/index-CpGWCLE5.js"></script>
39
+ <link rel="modulepreload" crossorigin href="/assets/react-C9F3QeMB.js">
40
+ <link rel="stylesheet" crossorigin href="/assets/index-GpceOxum.css">
41
+ </head>
42
+ <body>
43
+ <div id="root"></div>
935
44
  </body>
936
45
  </html>