myaiforone 1.0.0

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 (315) hide show
  1. package/README.md +113 -0
  2. package/agents/_template/CLAUDE.md +18 -0
  3. package/agents/_template/agent.json +7 -0
  4. package/agents/platform/agentcreator/CLAUDE.md +300 -0
  5. package/agents/platform/appcreator/CLAUDE.md +158 -0
  6. package/agents/platform/gym/CLAUDE.md +486 -0
  7. package/agents/platform/gym/agent.json +40 -0
  8. package/agents/platform/gym/programs/agent-building/program.json +160 -0
  9. package/agents/platform/gym/programs/automations-mastery/program.json +129 -0
  10. package/agents/platform/gym/programs/getting-started/program.json +124 -0
  11. package/agents/platform/gym/programs/mcp-integrations/program.json +116 -0
  12. package/agents/platform/gym/programs/multi-model-strategy/program.json +115 -0
  13. package/agents/platform/gym/programs/prompt-engineering/program.json +136 -0
  14. package/agents/platform/gym/souls/alex.md +12 -0
  15. package/agents/platform/gym/souls/jordan.md +12 -0
  16. package/agents/platform/gym/souls/morgan.md +12 -0
  17. package/agents/platform/gym/souls/riley.md +12 -0
  18. package/agents/platform/gym/souls/sam.md +12 -0
  19. package/agents/platform/hub/CLAUDE.md +372 -0
  20. package/agents/platform/promptcreator/CLAUDE.md +130 -0
  21. package/agents/platform/skillcreator/CLAUDE.md +163 -0
  22. package/bin/cli.js +566 -0
  23. package/config.example.json +310 -0
  24. package/dist/agent-registry.d.ts +32 -0
  25. package/dist/agent-registry.d.ts.map +1 -0
  26. package/dist/agent-registry.js +144 -0
  27. package/dist/agent-registry.js.map +1 -0
  28. package/dist/channels/discord.d.ts +17 -0
  29. package/dist/channels/discord.d.ts.map +1 -0
  30. package/dist/channels/discord.js +114 -0
  31. package/dist/channels/discord.js.map +1 -0
  32. package/dist/channels/imessage.d.ts +23 -0
  33. package/dist/channels/imessage.d.ts.map +1 -0
  34. package/dist/channels/imessage.js +214 -0
  35. package/dist/channels/imessage.js.map +1 -0
  36. package/dist/channels/slack.d.ts +19 -0
  37. package/dist/channels/slack.d.ts.map +1 -0
  38. package/dist/channels/slack.js +167 -0
  39. package/dist/channels/slack.js.map +1 -0
  40. package/dist/channels/telegram.d.ts +19 -0
  41. package/dist/channels/telegram.d.ts.map +1 -0
  42. package/dist/channels/telegram.js +274 -0
  43. package/dist/channels/telegram.js.map +1 -0
  44. package/dist/channels/types.d.ts +44 -0
  45. package/dist/channels/types.d.ts.map +1 -0
  46. package/dist/channels/types.js +18 -0
  47. package/dist/channels/types.js.map +1 -0
  48. package/dist/channels/whatsapp.d.ts +23 -0
  49. package/dist/channels/whatsapp.d.ts.map +1 -0
  50. package/dist/channels/whatsapp.js +189 -0
  51. package/dist/channels/whatsapp.js.map +1 -0
  52. package/dist/config.d.ts +134 -0
  53. package/dist/config.d.ts.map +1 -0
  54. package/dist/config.js +127 -0
  55. package/dist/config.js.map +1 -0
  56. package/dist/cron.d.ts +8 -0
  57. package/dist/cron.d.ts.map +1 -0
  58. package/dist/cron.js +35 -0
  59. package/dist/cron.js.map +1 -0
  60. package/dist/decrypt-keys.d.ts +7 -0
  61. package/dist/decrypt-keys.d.ts.map +1 -0
  62. package/dist/decrypt-keys.js +53 -0
  63. package/dist/decrypt-keys.js.map +1 -0
  64. package/dist/encrypt-keys.d.ts +8 -0
  65. package/dist/encrypt-keys.d.ts.map +1 -0
  66. package/dist/encrypt-keys.js +62 -0
  67. package/dist/encrypt-keys.js.map +1 -0
  68. package/dist/executor.d.ts +31 -0
  69. package/dist/executor.d.ts.map +1 -0
  70. package/dist/executor.js +2009 -0
  71. package/dist/executor.js.map +1 -0
  72. package/dist/gemini-executor.d.ts +27 -0
  73. package/dist/gemini-executor.d.ts.map +1 -0
  74. package/dist/gemini-executor.js +160 -0
  75. package/dist/gemini-executor.js.map +1 -0
  76. package/dist/goals.d.ts +24 -0
  77. package/dist/goals.d.ts.map +1 -0
  78. package/dist/goals.js +189 -0
  79. package/dist/goals.js.map +1 -0
  80. package/dist/gym/activity-digest.d.ts +30 -0
  81. package/dist/gym/activity-digest.d.ts.map +1 -0
  82. package/dist/gym/activity-digest.js +506 -0
  83. package/dist/gym/activity-digest.js.map +1 -0
  84. package/dist/gym/dimension-scorer.d.ts +76 -0
  85. package/dist/gym/dimension-scorer.d.ts.map +1 -0
  86. package/dist/gym/dimension-scorer.js +236 -0
  87. package/dist/gym/dimension-scorer.js.map +1 -0
  88. package/dist/gym/gym-router.d.ts +7 -0
  89. package/dist/gym/gym-router.d.ts.map +1 -0
  90. package/dist/gym/gym-router.js +718 -0
  91. package/dist/gym/gym-router.js.map +1 -0
  92. package/dist/gym/index.d.ts +11 -0
  93. package/dist/gym/index.d.ts.map +1 -0
  94. package/dist/gym/index.js +11 -0
  95. package/dist/gym/index.js.map +1 -0
  96. package/dist/heartbeat.d.ts +21 -0
  97. package/dist/heartbeat.d.ts.map +1 -0
  98. package/dist/heartbeat.js +163 -0
  99. package/dist/heartbeat.js.map +1 -0
  100. package/dist/index.d.ts +2 -0
  101. package/dist/index.d.ts.map +1 -0
  102. package/dist/index.js +254 -0
  103. package/dist/index.js.map +1 -0
  104. package/dist/keystore.d.ts +22 -0
  105. package/dist/keystore.d.ts.map +1 -0
  106. package/dist/keystore.js +178 -0
  107. package/dist/keystore.js.map +1 -0
  108. package/dist/logger.d.ts +9 -0
  109. package/dist/logger.d.ts.map +1 -0
  110. package/dist/logger.js +45 -0
  111. package/dist/logger.js.map +1 -0
  112. package/dist/memory/daily.d.ts +22 -0
  113. package/dist/memory/daily.d.ts.map +1 -0
  114. package/dist/memory/daily.js +82 -0
  115. package/dist/memory/daily.js.map +1 -0
  116. package/dist/memory/embeddings.d.ts +15 -0
  117. package/dist/memory/embeddings.d.ts.map +1 -0
  118. package/dist/memory/embeddings.js +154 -0
  119. package/dist/memory/embeddings.js.map +1 -0
  120. package/dist/memory/index.d.ts +32 -0
  121. package/dist/memory/index.d.ts.map +1 -0
  122. package/dist/memory/index.js +159 -0
  123. package/dist/memory/index.js.map +1 -0
  124. package/dist/memory/search.d.ts +21 -0
  125. package/dist/memory/search.d.ts.map +1 -0
  126. package/dist/memory/search.js +77 -0
  127. package/dist/memory/search.js.map +1 -0
  128. package/dist/memory/store.d.ts +23 -0
  129. package/dist/memory/store.d.ts.map +1 -0
  130. package/dist/memory/store.js +144 -0
  131. package/dist/memory/store.js.map +1 -0
  132. package/dist/ollama-executor.d.ts +17 -0
  133. package/dist/ollama-executor.d.ts.map +1 -0
  134. package/dist/ollama-executor.js +112 -0
  135. package/dist/ollama-executor.js.map +1 -0
  136. package/dist/openai-executor.d.ts +38 -0
  137. package/dist/openai-executor.d.ts.map +1 -0
  138. package/dist/openai-executor.js +197 -0
  139. package/dist/openai-executor.js.map +1 -0
  140. package/dist/router.d.ts +11 -0
  141. package/dist/router.d.ts.map +1 -0
  142. package/dist/router.js +185 -0
  143. package/dist/router.js.map +1 -0
  144. package/dist/test-message.d.ts +2 -0
  145. package/dist/test-message.d.ts.map +1 -0
  146. package/dist/test-message.js +60 -0
  147. package/dist/test-message.js.map +1 -0
  148. package/dist/utils/imsg-db-reader.d.ts +24 -0
  149. package/dist/utils/imsg-db-reader.d.ts.map +1 -0
  150. package/dist/utils/imsg-db-reader.js +92 -0
  151. package/dist/utils/imsg-db-reader.js.map +1 -0
  152. package/dist/utils/imsg-rpc.d.ts +25 -0
  153. package/dist/utils/imsg-rpc.d.ts.map +1 -0
  154. package/dist/utils/imsg-rpc.js +149 -0
  155. package/dist/utils/imsg-rpc.js.map +1 -0
  156. package/dist/utils/message-formatter.d.ts +3 -0
  157. package/dist/utils/message-formatter.d.ts.map +1 -0
  158. package/dist/utils/message-formatter.js +69 -0
  159. package/dist/utils/message-formatter.js.map +1 -0
  160. package/dist/web-ui.d.ts +12 -0
  161. package/dist/web-ui.d.ts.map +1 -0
  162. package/dist/web-ui.js +5784 -0
  163. package/dist/web-ui.js.map +1 -0
  164. package/dist/whatsapp-chats.d.ts +2 -0
  165. package/dist/whatsapp-chats.d.ts.map +1 -0
  166. package/dist/whatsapp-chats.js +76 -0
  167. package/dist/whatsapp-chats.js.map +1 -0
  168. package/dist/whatsapp-login.d.ts +2 -0
  169. package/dist/whatsapp-login.d.ts.map +1 -0
  170. package/dist/whatsapp-login.js +90 -0
  171. package/dist/whatsapp-login.js.map +1 -0
  172. package/dist/wiki-sync.d.ts +21 -0
  173. package/dist/wiki-sync.d.ts.map +1 -0
  174. package/dist/wiki-sync.js +147 -0
  175. package/dist/wiki-sync.js.map +1 -0
  176. package/docs/AddNewAgentGuide.md +100 -0
  177. package/docs/AddNewMcpGuide.md +72 -0
  178. package/docs/Architecture.md +795 -0
  179. package/docs/CLAUDE-AI-SETUP.md +166 -0
  180. package/docs/Setup.md +297 -0
  181. package/docs/ai-gym-architecture.md +1040 -0
  182. package/docs/ai-gym-build-plan.md +343 -0
  183. package/docs/ai-gym-onboarding.md +122 -0
  184. package/docs/appcreator_plan.md +348 -0
  185. package/docs/platform-mcp-audit.md +320 -0
  186. package/docs/server-deployment-plan.md +503 -0
  187. package/docs/superpowers/plans/2026-03-25-marketplace.md +1281 -0
  188. package/docs/superpowers/specs/2026-03-25-marketplace-design.md +287 -0
  189. package/docs/user-guide.md +2016 -0
  190. package/mcp-catalog.json +628 -0
  191. package/package.json +63 -0
  192. package/public/MyAIforOne-logomark-512.svg +16 -0
  193. package/public/MyAIforOne-logomark-transparent.svg +15 -0
  194. package/public/activity.html +314 -0
  195. package/public/admin.html +1674 -0
  196. package/public/agent-dashboard.html +670 -0
  197. package/public/api-docs.html +1106 -0
  198. package/public/automations.html +722 -0
  199. package/public/canvas.css +223 -0
  200. package/public/canvas.js +588 -0
  201. package/public/changelog.html +231 -0
  202. package/public/gym.html +2766 -0
  203. package/public/home.html +1930 -0
  204. package/public/index.html +2809 -0
  205. package/public/lab.html +1643 -0
  206. package/public/library.html +1442 -0
  207. package/public/marketplace.html +1101 -0
  208. package/public/mcp-docs.html +441 -0
  209. package/public/mini.html +390 -0
  210. package/public/monitor.html +584 -0
  211. package/public/org.html +4304 -0
  212. package/public/projects.html +734 -0
  213. package/public/settings.html +645 -0
  214. package/public/tasks.html +932 -0
  215. package/public/trainers/alex.svg +12 -0
  216. package/public/trainers/jordan.svg +12 -0
  217. package/public/trainers/morgan.svg +12 -0
  218. package/public/trainers/riley.svg +12 -0
  219. package/public/trainers/sam.svg +12 -0
  220. package/public/user-guide.html +218 -0
  221. package/registry/agents.json +3 -0
  222. package/registry/apps.json +20 -0
  223. package/registry/installed-drafts.json +3 -0
  224. package/registry/mcps.json +1084 -0
  225. package/registry/prompts/personal/mcp-test-prompt.md +6 -0
  226. package/registry/prompts/personal/memory-recall.md +6 -0
  227. package/registry/prompts/platform/brainstorm.md +15 -0
  228. package/registry/prompts/platform/code-review.md +16 -0
  229. package/registry/prompts/platform/explain.md +16 -0
  230. package/registry/prompts.json +58 -0
  231. package/registry/skills/external/brainstorming.md +5 -0
  232. package/registry/skills/external/code-review.md +40 -0
  233. package/registry/skills/external/frontend-patterns.md +642 -0
  234. package/registry/skills/external/frontend-slides.md +184 -0
  235. package/registry/skills/external/systematic-debugging.md +5 -0
  236. package/registry/skills/external/tdd.md +328 -0
  237. package/registry/skills/external/verification-before-completion.md +5 -0
  238. package/registry/skills/external/writing-plans.md +5 -0
  239. package/registry/skills/platform/ai41_app_build.md +930 -0
  240. package/registry/skills/platform/ai41_app_deploy.md +168 -0
  241. package/registry/skills/platform/ai41_app_orchestrator.md +239 -0
  242. package/registry/skills/platform/ai41_app_patterns.md +359 -0
  243. package/registry/skills/platform/ai41_app_register.md +85 -0
  244. package/registry/skills/platform/ai41_app_scaffold.md +421 -0
  245. package/registry/skills/platform/ai41_app_verify.md +107 -0
  246. package/registry/skills/platform/opProjectCreate.md +239 -0
  247. package/registry/skills/platform/op_devbrowser.md +136 -0
  248. package/registry/skills/platform/sop_brandguidelines.md +103 -0
  249. package/registry/skills/platform/sop_docx.md +117 -0
  250. package/registry/skills/platform/sop_frontenddesign.md +44 -0
  251. package/registry/skills/platform/sop_frontenddesign_v2.md +659 -0
  252. package/registry/skills/platform/sop_mcpbuilder.md +133 -0
  253. package/registry/skills/platform/sop_pdf.md +172 -0
  254. package/registry/skills/platform/sop_pptx.md +133 -0
  255. package/registry/skills/platform/sop_skillcreator.md +104 -0
  256. package/registry/skills/platform/sop_themefactory.md +128 -0
  257. package/registry/skills/platform/sop_webapptesting.md +75 -0
  258. package/registry/skills/platform/sop_webartifactsbuilder.md +97 -0
  259. package/registry/skills/platform/sop_xlsx.md +134 -0
  260. package/registry/skills.json +1055 -0
  261. package/scripts/discover-chats.sh +11 -0
  262. package/scripts/install-service-windows.ps1 +87 -0
  263. package/scripts/install-service.sh +52 -0
  264. package/scripts/seed-registry.ts +195 -0
  265. package/scripts/test-send.sh +5 -0
  266. package/scripts/tray-indicator.ps1 +35 -0
  267. package/scripts/uninstall-service-windows.ps1 +23 -0
  268. package/scripts/uninstall-service.sh +15 -0
  269. package/scripts/xbar-myagent.5s.sh +32 -0
  270. package/server/mcp-server/dist/index.d.ts +11 -0
  271. package/server/mcp-server/dist/index.js +1332 -0
  272. package/server/mcp-server/dist/lib/api-client.d.ts +165 -0
  273. package/server/mcp-server/dist/lib/api-client.js +241 -0
  274. package/server/mcp-server/index.ts +1545 -0
  275. package/server/mcp-server/lib/api-client.ts +366 -0
  276. package/server/mcp-server/tsconfig.json +14 -0
  277. package/src/agent-registry.ts +180 -0
  278. package/src/channels/discord.ts +129 -0
  279. package/src/channels/imessage.ts +261 -0
  280. package/src/channels/slack.ts +208 -0
  281. package/src/channels/telegram.ts +307 -0
  282. package/src/channels/types.ts +62 -0
  283. package/src/channels/whatsapp.ts +227 -0
  284. package/src/config.ts +281 -0
  285. package/src/cron.ts +43 -0
  286. package/src/decrypt-keys.ts +60 -0
  287. package/src/encrypt-keys.ts +70 -0
  288. package/src/executor.ts +2190 -0
  289. package/src/gemini-executor.ts +212 -0
  290. package/src/goals.ts +240 -0
  291. package/src/gym/activity-digest.ts +546 -0
  292. package/src/gym/dimension-scorer.ts +297 -0
  293. package/src/gym/gym-router.ts +801 -0
  294. package/src/gym/index.ts +19 -0
  295. package/src/heartbeat.ts +220 -0
  296. package/src/index.ts +275 -0
  297. package/src/keystore.ts +190 -0
  298. package/src/logger.ts +51 -0
  299. package/src/memory/daily.ts +101 -0
  300. package/src/memory/embeddings.ts +185 -0
  301. package/src/memory/index.ts +218 -0
  302. package/src/memory/search.ts +124 -0
  303. package/src/memory/store.ts +189 -0
  304. package/src/ollama-executor.ts +126 -0
  305. package/src/openai-executor.ts +259 -0
  306. package/src/router.ts +230 -0
  307. package/src/test-message.ts +72 -0
  308. package/src/utils/imsg-db-reader.ts +109 -0
  309. package/src/utils/imsg-rpc.ts +178 -0
  310. package/src/utils/message-formatter.ts +90 -0
  311. package/src/web-ui.ts +5778 -0
  312. package/src/whatsapp-chats.ts +91 -0
  313. package/src/whatsapp-login.ts +110 -0
  314. package/src/wiki-sync.ts +199 -0
  315. package/tsconfig.json +19 -0
@@ -0,0 +1,2809 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>MyAIforOne</title>
7
+ <link href="https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;500;600;700&family=IBM+Plex+Mono:wght@400;500;600&family=Syne:wght@600;700;800&display=swap" rel="stylesheet">
8
+ <script src="https://cdn.jsdelivr.net/npm/mammoth@1.8.0/mammoth.browser.min.js"></script>
9
+ <script src="https://cdn.jsdelivr.net/npm/jszip@3.10.1/dist/jszip.min.js"></script>
10
+ <script src="https://cdn.jsdelivr.net/npm/xlsx@0.18.5/dist/xlsx.full.min.js"></script>
11
+ <link rel="stylesheet" href="/canvas.css">
12
+ <script src="/canvas.js"></script>
13
+ <style>
14
+ *,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
15
+
16
+ /* ─── Theme Variables ─────────────────────────────────────────── */
17
+ :root{
18
+ --bg-deep:#060a13;
19
+ --bg-surface:rgba(12,18,33,0.92);
20
+ --bg-card:rgba(16,22,40,0.85);
21
+ --bg-input:rgba(0,0,0,0.35);
22
+ --border-dim:rgba(56,189,248,0.08);
23
+ --border-glow:rgba(56,189,248,0.18);
24
+ --border-active:rgba(56,189,248,0.45);
25
+ --text-primary:rgba(255,255,255,0.92);
26
+ --text-secondary:rgba(255,255,255,0.68);
27
+ --text-muted:rgba(148,163,184,0.55);
28
+ --accent:#22d3ee;
29
+ --accent-soft:#38bdf8;
30
+ --accent-bg:rgba(6,182,212,0.15);
31
+ --accent-glow:rgba(34,211,238,0.12);
32
+ --purple:rgba(139,92,246,0.7);
33
+ --purple-bg:rgba(139,92,246,0.12);
34
+ --green:#4ade80;
35
+ --green-bg:rgba(74,222,128,0.1);
36
+ --amber:#fbbf24;
37
+ --amber-bg:rgba(251,191,36,0.1);
38
+ --msg-user:rgba(56,189,248,0.08);
39
+ --msg-agent:rgba(139,92,246,0.06);
40
+ --shadow:0 2px 12px rgba(0,0,0,0.3);
41
+ --shadow-glow:0 0 20px rgba(34,211,238,0.08);
42
+ --radius:12px;
43
+ --font-sans:'DM Sans',system-ui,-apple-system,sans-serif;
44
+ --font-mono:'IBM Plex Mono',monospace;
45
+ --font-display:'Syne',sans-serif;
46
+ }
47
+
48
+ [data-theme="light"]{
49
+ --bg-deep:#f4f6f9;
50
+ --bg-surface:rgba(255,255,255,0.95);
51
+ --bg-card:rgba(255,255,255,0.9);
52
+ --bg-input:rgba(0,0,0,0.04);
53
+ --border-dim:rgba(0,0,0,0.08);
54
+ --border-glow:rgba(14,116,144,0.18);
55
+ --border-active:rgba(14,116,144,0.45);
56
+ --text-primary:rgba(15,23,42,0.92);
57
+ --text-secondary:rgba(51,65,85,0.8);
58
+ --text-muted:rgba(100,116,139,0.6);
59
+ --accent:#0891b2;
60
+ --accent-soft:#0e7490;
61
+ --accent-bg:rgba(14,116,144,0.08);
62
+ --accent-glow:rgba(14,116,144,0.06);
63
+ --purple:rgba(109,40,217,0.75);
64
+ --purple-bg:rgba(139,92,246,0.08);
65
+ --green:#16a34a;
66
+ --green-bg:rgba(22,163,74,0.08);
67
+ --amber:#d97706;
68
+ --amber-bg:rgba(217,119,6,0.08);
69
+ --msg-user:rgba(14,116,144,0.06);
70
+ --msg-agent:rgba(139,92,246,0.04);
71
+ --shadow:0 1px 8px rgba(0,0,0,0.06);
72
+ --shadow-glow:none;
73
+ }
74
+
75
+ html,body{
76
+ width:100%;height:100%;overflow:hidden;max-width:100vw;
77
+ background:var(--bg-deep);
78
+ font-family:var(--font-sans);
79
+ color:var(--text-primary);
80
+ transition:background .3s,color .3s;
81
+ }
82
+
83
+ /* ─── Topbar ──────────────────────────────────────────────────── */
84
+ .topbar{height:48px;display:flex;align-items:center;padding:0 20px;background:var(--bg-surface);border-bottom:1px solid var(--border-dim);backdrop-filter:blur(20px);-webkit-backdrop-filter:blur(20px);position:fixed;top:0;left:0;right:0;z-index:200;flex-shrink:0;overflow:hidden;max-width:100vw}
85
+ .topbar-logo{display:flex;align-items:center;gap:10px;margin-right:28px;flex-shrink:0}
86
+ .tab-group{display:flex;gap:0;flex-shrink:1;min-width:0}
87
+ .tab-btn{font-family:var(--font-sans);font-size:13px;font-weight:600;color:var(--text-muted);background:none;border:none;padding:14px 12px;cursor:pointer;position:relative;transition:color .2s;text-decoration:none;display:block;white-space:nowrap;flex-shrink:0}
88
+ .tab-btn:hover{color:var(--text-secondary)}
89
+ .tab-btn.active{color:var(--accent)}
90
+ .tab-btn.active::after{content:'';position:absolute;bottom:0;left:10px;right:10px;height:2px;background:var(--accent);border-radius:1px}
91
+ .topbar-right{margin-left:auto;display:flex;align-items:center;gap:8px;flex-shrink:0}
92
+ .gym-nav-btn{font-family:var(--font-sans);font-size:12px;font-weight:700;color:#fff;background:var(--accent);border:none;border-radius:8px;padding:6px 14px;cursor:pointer;text-decoration:none;letter-spacing:.02em;transition:opacity .2s,transform .2s;white-space:nowrap}
93
+ .gym-nav-btn:hover{opacity:.88;transform:translateY(-1px)}
94
+
95
+ /* ─── Layout ──────────────────────────────────────────────────── */
96
+ .app{display:flex;height:100vh;overflow:hidden;padding-top:48px;max-width:100vw}
97
+
98
+ .sidebar{
99
+ width:320px;min-width:320px;
100
+ background:var(--bg-surface);
101
+ border-right:1px solid var(--border-dim);
102
+ display:flex;flex-direction:column;
103
+ backdrop-filter:blur(20px);-webkit-backdrop-filter:blur(20px);
104
+ transition:all .3s ease;overflow:hidden;
105
+ }
106
+ .sidebar.collapsed{width:0;min-width:0;border-right:none;padding:0}
107
+ .sidebar-expand{
108
+ position:fixed;top:12px;left:8px;z-index:50;
109
+ width:32px;height:32px;border-radius:8px;border:1px solid var(--border-dim);
110
+ background:var(--bg-surface);color:var(--text-muted);cursor:pointer;
111
+ display:none;align-items:center;justify-content:center;font-size:16px;
112
+ transition:all .2s;
113
+ }
114
+ .sidebar-expand:hover{border-color:var(--border-glow);color:var(--text-secondary)}
115
+ .sidebar-expand.visible{display:flex}
116
+ .sidebar-collapse{
117
+ padding:8px;border-top:1px solid var(--border-dim);
118
+ display:flex;align-items:center;justify-content:center;
119
+ }
120
+ .sidebar-collapse button{
121
+ width:100%;padding:6px;border-radius:8px;border:1px solid var(--border-dim);
122
+ background:transparent;color:var(--text-muted);cursor:pointer;
123
+ font-size:11px;font-family:var(--font-sans);transition:all .2s;
124
+ }
125
+ .sidebar-collapse button:hover{border-color:var(--border-glow);color:var(--text-secondary)}
126
+
127
+ .main{flex:1;display:flex;flex-direction:column;position:relative;min-width:0;overflow:hidden}
128
+
129
+ /* ─── Sidebar Header ─────────────────────────────────────────── */
130
+ .sidebar-header{
131
+ padding:20px 20px 16px;
132
+ border-bottom:1px solid var(--border-dim);
133
+ display:flex;align-items:center;justify-content:space-between;
134
+ }
135
+ .logo-group{display:flex;align-items:center;gap:10px}
136
+ .logo-mark{width:28px;height:28px;border-radius:8px;object-fit:contain}
137
+ .logo-text{font-family:var(--font-display);font-size:15px;font-weight:700;color:var(--accent)}
138
+ /* Docs dropdown */
139
+ .docs-dropdown{position:relative;display:inline-block}
140
+ .docs-btn{display:flex;align-items:center;justify-content:center;width:34px;height:34px;border-radius:8px;border:1px solid var(--border-dim);background:transparent;color:var(--text-muted);cursor:pointer;font-size:14px;transition:all .2s}
141
+ .docs-btn:hover{border-color:var(--border-glow);color:var(--text-secondary)}
142
+ .docs-menu{display:none;position:absolute;top:40px;right:0;min-width:140px;background:var(--bg-surface);border:1px solid var(--border-glow);border-radius:8px;box-shadow:0 8px 24px rgba(0,0,0,0.4);z-index:200;padding:4px;backdrop-filter:blur(20px)}
143
+ .docs-menu.open{display:block}
144
+ .docs-menu a{display:flex;align-items:center;gap:8px;padding:7px 12px;font-size:12px;font-weight:500;color:var(--text-secondary);text-decoration:none;border-radius:6px;transition:all .15s}
145
+ .docs-menu a:hover{background:var(--accent-bg);color:var(--accent)}
146
+ .docs-menu a .dm-icon{font-size:14px;width:18px;text-align:center}
147
+ .theme-toggle,.gear-btn{
148
+ width:34px;height:34px;border-radius:8px;border:1px solid var(--border-dim);
149
+ background:transparent;color:var(--text-muted);cursor:pointer;
150
+ display:flex;align-items:center;justify-content:center;font-size:16px;
151
+ transition:all .2s;
152
+ text-decoration:none;}
153
+ .theme-toggle:hover,.gear-btn:hover{border-color:var(--border-glow);color:var(--text-secondary)}
154
+
155
+ /* ─── Status Bar ──────────────────────────────────────────────── */
156
+ .status-bar{
157
+ padding:12px 20px;border-bottom:1px solid var(--border-dim);
158
+ display:flex;align-items:center;gap:12px;
159
+ }
160
+ .status-dot{
161
+ width:8px;height:8px;border-radius:50%;background:var(--green);
162
+ box-shadow:0 0 8px rgba(74,222,128,0.4);
163
+ animation:pulse 2s ease-in-out infinite;
164
+ }
165
+ @keyframes pulse{0%,100%{opacity:1}50%{opacity:.5}}
166
+ .status-text{font-family:var(--font-mono);font-size:11px;color:var(--text-muted)}
167
+ .status-channels{margin-left:auto;display:flex;gap:4px}
168
+ .channel-pill{
169
+ font-family:var(--font-mono);font-size:9px;font-weight:500;
170
+ padding:2px 7px;border-radius:4px;
171
+ background:var(--accent-bg);color:var(--accent);
172
+ text-transform:uppercase;letter-spacing:.04em;
173
+ }
174
+
175
+ /* ─── Filter Bar ──────────────────────────────────────────────── */
176
+ .filter-bar{
177
+ padding:8px 12px;border-bottom:1px solid var(--border-dim);
178
+ display:flex;gap:6px;
179
+ }
180
+ .filter-select{
181
+ flex:1;min-width:0;font-family:var(--font-mono);font-size:10px;
182
+ padding:5px 8px;border-radius:6px;
183
+ border:1px solid var(--border-dim);background:var(--bg-input);
184
+ color:var(--text-primary);outline:none;cursor:pointer;
185
+ transition:border-color .2s;overflow:hidden;text-overflow:ellipsis;
186
+ }
187
+ .filter-select:focus{border-color:var(--accent)}
188
+
189
+ /* ─── Agent List ──────────────────────────────────────────────── */
190
+ .agent-list{flex:1;overflow-y:auto;padding:8px 12px}
191
+ .agent-list::-webkit-scrollbar{width:4px}
192
+ .agent-list::-webkit-scrollbar-thumb{background:var(--border-glow);border-radius:2px}
193
+
194
+ /* Sidebar sections */
195
+ .sidebar-section{margin-bottom:12px}
196
+ .sidebar-section-header{
197
+ display:flex;align-items:center;gap:6px;padding:4px 4px;
198
+ font-family:var(--font-mono);font-size:9px;font-weight:600;
199
+ letter-spacing:.1em;text-transform:uppercase;color:var(--text-muted);
200
+ cursor:default;user-select:none;
201
+ }
202
+ .sidebar-section-header.collapsible{cursor:pointer}
203
+ .sidebar-section-header .section-arrow{font-size:8px;transition:transform .2s}
204
+ .sidebar-section.open .section-arrow{transform:rotate(90deg)}
205
+ .sidebar-section:not(.open) .section-body{display:none}
206
+ .sidebar-section-header .section-count{font-size:9px;color:var(--text-muted);opacity:.6;margin-left:auto}
207
+ .agent-card-compact{
208
+ padding:8px 10px;margin-bottom:4px;
209
+ border-radius:8px;cursor:pointer;transition:all .15s;
210
+ display:flex;align-items:center;gap:8px;
211
+ }
212
+ .agent-card-compact:hover{background:var(--bg-input)}
213
+ .agent-card-compact.selected{background:var(--accent-bg);border-left:2px solid var(--accent)}
214
+ .agent-card-compact .compact-avatar{
215
+ width:28px;height:28px;border-radius:7px;flex-shrink:0;
216
+ background:var(--accent-bg);border:1px solid var(--border-dim);
217
+ display:flex;align-items:center;justify-content:center;
218
+ font-family:var(--font-mono);font-size:10px;font-weight:600;color:var(--accent);
219
+ }
220
+ .agent-card-compact .compact-info{flex:1;min-width:0}
221
+ .agent-card-compact .compact-name{font-size:12px;font-weight:600;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
222
+ .agent-card-compact .compact-time{font-family:var(--font-mono);font-size:9px;color:var(--text-muted)}
223
+ .thinking-pulse{
224
+ width:8px;height:8px;border-radius:50%;background:var(--amber);flex-shrink:0;
225
+ animation:think-pulse 1.2s ease-in-out infinite;
226
+ }
227
+ @keyframes think-pulse{0%,100%{opacity:1;transform:scale(1)}50%{opacity:.4;transform:scale(.7)}}
228
+
229
+ .agent-card{
230
+ padding:14px 16px;margin-bottom:8px;
231
+ background:var(--bg-card);border:1px solid var(--border-dim);
232
+ border-radius:var(--radius);cursor:pointer;
233
+ backdrop-filter:blur(16px);-webkit-backdrop-filter:blur(16px);
234
+ box-shadow:var(--shadow);transition:all .25s;position:relative;
235
+ }
236
+ .agent-card:hover{
237
+ border-color:var(--border-glow);
238
+ box-shadow:var(--shadow),var(--shadow-glow);
239
+ transform:translateY(-1px);
240
+ }
241
+ .agent-card.selected{
242
+ border-color:var(--border-active);
243
+ box-shadow:0 0 20px rgba(34,211,238,0.12),0 0 40px rgba(34,211,238,0.06);
244
+ }
245
+ .agent-card-top{display:flex;align-items:center;gap:10px;margin-bottom:6px}
246
+ .agent-avatar{
247
+ width:36px;height:36px;border-radius:10px;
248
+ background:var(--accent-bg);border:1px solid var(--accent);
249
+ display:flex;align-items:center;justify-content:center;
250
+ font-family:var(--font-mono);font-size:12px;font-weight:600;color:var(--accent);
251
+ flex-shrink:0;
252
+ }
253
+ .agent-name{font-family:var(--font-display);font-size:14px;font-weight:700;flex:1}
254
+ .agent-session-dot{
255
+ width:8px;height:8px;border-radius:50%;flex-shrink:0;
256
+ }
257
+ .agent-session-dot.active{background:var(--green);box-shadow:0 0 6px rgba(74,222,128,0.4)}
258
+ .agent-session-dot.inactive{background:var(--text-muted)}
259
+ .agent-alias{
260
+ font-family:var(--font-mono);font-size:11px;color:var(--accent);
261
+ margin-bottom:4px;display:flex;justify-content:space-between;align-items:center;
262
+ }
263
+ .agent-last-msg{
264
+ font-family:var(--font-sans);font-size:10px;color:var(--text-muted);font-weight:400;
265
+ }
266
+ .agent-desc{font-size:12px;color:var(--text-muted);line-height:1.5;margin-bottom:8px}
267
+ .agent-meta{display:flex;gap:6px;flex-wrap:wrap}
268
+ .agent-tag{
269
+ font-family:var(--font-mono);font-size:9px;font-weight:500;
270
+ padding:2px 7px;border-radius:4px;
271
+ border:1px solid;text-transform:uppercase;letter-spacing:.04em;
272
+ }
273
+ .tag-persistent{border-color:rgba(74,222,128,0.2);background:var(--green-bg);color:var(--green)}
274
+ .tag-msgs{border-color:var(--border-dim);background:transparent;color:var(--text-muted)}
275
+ .tag-mcp{border-color:rgba(139,92,246,0.2);background:var(--purple-bg);color:var(--purple)}
276
+
277
+ /* ─── Add Agent Button ────────────────────────────────────────── */
278
+ .add-agent-btn{
279
+ margin:8px 12px 16px;padding:12px;
280
+ border:1px dashed var(--border-glow);border-radius:var(--radius);
281
+ background:transparent;color:var(--text-muted);cursor:pointer;
282
+ font-family:var(--font-sans);font-size:13px;font-weight:500;
283
+ display:flex;align-items:center;justify-content:center;gap:8px;
284
+ transition:all .2s;
285
+ }
286
+ .add-agent-btn:hover{border-color:var(--accent);color:var(--accent);background:var(--accent-bg)}
287
+
288
+ /* ─── Main: Empty State ───────────────────────────────────────── */
289
+ .empty-state{
290
+ flex:1;display:flex;flex-direction:column;
291
+ align-items:center;justify-content:center;gap:12px;
292
+ color:var(--text-muted);
293
+ }
294
+ .empty-icon{font-size:48px;opacity:.3}
295
+ .empty-text{font-size:14px;font-weight:500}
296
+ .empty-sub{font-size:12px;opacity:.6}
297
+
298
+ /* ─── Main: Chat View ─────────────────────────────────────────── */
299
+ .chat-header{
300
+ padding:16px 24px;
301
+ border-bottom:1px solid var(--border-dim);
302
+ background:var(--bg-surface);
303
+ backdrop-filter:blur(20px);-webkit-backdrop-filter:blur(20px);
304
+ display:flex;align-items:center;gap:12px;
305
+ }
306
+ .chat-header-info{flex:1}
307
+ .chat-header-name{font-family:var(--font-display);font-size:16px;font-weight:700}
308
+ .chat-header-alias{font-family:var(--font-mono);font-size:11px;color:var(--accent)}
309
+ .chat-header-actions{display:flex;gap:8px}
310
+ .header-btn{
311
+ font-family:var(--font-mono);font-size:10px;font-weight:500;
312
+ padding:6px 12px;border-radius:8px;
313
+ border:1px solid var(--border-dim);background:transparent;
314
+ color:var(--text-muted);cursor:pointer;transition:all .2s;
315
+ }
316
+ .header-btn:hover{border-color:var(--border-glow);color:var(--text-secondary)}
317
+ .header-btn.danger:hover{border-color:rgba(239,68,68,.3);color:#ef4444}
318
+
319
+ /* ─── Session Tabs ──────────────────────────────────────────── */
320
+ .session-tabs{
321
+ display:flex;align-items:center;gap:0;
322
+ padding:0 16px;
323
+ border-bottom:1px solid var(--border-dim);
324
+ background:var(--bg-surface);
325
+ min-height:32px;
326
+ overflow-x:auto;
327
+ scrollbar-width:none;
328
+ }
329
+ .session-tabs::-webkit-scrollbar{display:none}
330
+ .session-tab{
331
+ display:flex;align-items:center;gap:4px;
332
+ padding:6px 10px;
333
+ font-family:var(--font-mono);font-size:11px;
334
+ color:var(--text-muted);cursor:pointer;
335
+ border-bottom:2px solid transparent;
336
+ white-space:nowrap;transition:all .15s;
337
+ position:relative;
338
+ }
339
+ .session-tab:hover{color:var(--text-secondary);background:rgba(255,255,255,.03)}
340
+ .session-tab.active{
341
+ color:var(--accent);border-bottom-color:var(--accent);
342
+ background:rgba(var(--amber-rgb,245,158,11),.05);
343
+ }
344
+ .session-tab .tab-label{max-width:120px;overflow:hidden;text-overflow:ellipsis}
345
+ .session-tab .tab-close{
346
+ display:none;font-size:13px;line-height:1;
347
+ padding:0 2px;border-radius:3px;color:var(--text-muted);
348
+ cursor:pointer;margin-left:2px;
349
+ }
350
+ .session-tab:hover .tab-close{display:inline}
351
+ .session-tab .tab-close:hover{color:#ef4444;background:rgba(239,68,68,.15)}
352
+ .session-tab .tab-dot{
353
+ width:5px;height:5px;border-radius:50%;
354
+ background:var(--accent);display:none;
355
+ }
356
+ .session-tab.has-activity .tab-dot{display:inline-block}
357
+ .session-tab-add{
358
+ padding:4px 8px;font-size:14px;line-height:1;
359
+ color:var(--text-muted);cursor:pointer;
360
+ border-radius:4px;transition:all .15s;
361
+ flex-shrink:0;
362
+ }
363
+ .session-tab-add:hover{color:var(--accent);background:rgba(255,255,255,.05)}
364
+ .tab-rename-input{
365
+ font-family:var(--font-mono);font-size:11px;
366
+ background:transparent;border:1px solid var(--accent);
367
+ color:var(--text-primary);padding:1px 4px;border-radius:3px;
368
+ outline:none;width:90px;
369
+ }
370
+
371
+ /* ─── Session Picker Modal ───────────────────────────────── */
372
+ .picker-session{
373
+ padding:12px 20px;cursor:pointer;border-bottom:1px solid var(--border-dim);
374
+ transition:background .15s;
375
+ }
376
+ .picker-session:last-child{border-bottom:none}
377
+ .picker-session:hover{background:rgba(255,255,255,.04)}
378
+ .picker-session.picker-active{background:rgba(var(--amber-rgb,245,158,11),.06)}
379
+ .picker-session-name{font-size:13px;font-weight:600;color:var(--text-primary);display:flex;align-items:center;gap:8px}
380
+ .picker-session-meta{font-size:11px;color:var(--text-muted);margin-top:3px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
381
+ .picker-badge{font-size:9px;font-weight:700;letter-spacing:.04em;text-transform:uppercase;padding:1px 5px;border-radius:3px;background:rgba(var(--amber-rgb,245,158,11),.15);color:var(--accent);}
382
+ #sessionPickerModal .modal{max-height:70vh;display:flex;flex-direction:column}
383
+ #sessionPickerList{overflow-y:auto;flex:1}
384
+ .picker-footer{padding:12px 20px;border-top:1px solid var(--border-dim)}
385
+ .picker-new-btn{
386
+ width:100%;padding:8px 16px;border-radius:8px;
387
+ border:1px dashed var(--border-glow);background:transparent;
388
+ color:var(--text-muted);font-size:12px;cursor:pointer;transition:all .15s;
389
+ }
390
+ .picker-new-btn:hover{border-color:var(--accent);color:var(--accent);background:rgba(var(--amber-rgb,245,158,11),.05)}
391
+
392
+ .chat-messages{
393
+ flex:1;overflow-y:auto;padding:20px 24px;
394
+ display:flex;flex-direction:column;gap:12px;
395
+ }
396
+ .chat-messages::-webkit-scrollbar{width:4px}
397
+ .chat-messages::-webkit-scrollbar-thumb{background:var(--border-glow);border-radius:2px}
398
+
399
+ .msg{
400
+ max-width:80%;padding:12px 16px;border-radius:14px;
401
+ font-size:13px;line-height:1.6;
402
+ animation:msgIn .3s ease-out;
403
+ }
404
+ @keyframes msgIn{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:none}}
405
+ .msg-user{
406
+ align-self:flex-end;
407
+ background:var(--msg-user);border:1px solid var(--border-glow);
408
+ border-bottom-right-radius:4px;
409
+ }
410
+ .msg-agent{
411
+ align-self:flex-start;
412
+ background:var(--msg-agent);border:1px solid rgba(139,92,246,0.1);
413
+ border-bottom-left-radius:4px;
414
+ }
415
+ .msg-agent pre{
416
+ background:var(--bg-input);border:1px solid var(--border-dim);
417
+ border-radius:8px;padding:10px 12px;margin:8px 0;
418
+ overflow-x:auto;font-family:var(--font-mono);font-size:12px;line-height:1.5;
419
+ }
420
+ .msg-agent code{font-family:var(--font-mono);font-size:12px;background:var(--bg-input);padding:1px 5px;border-radius:4px}
421
+ .msg-agent p{margin:0 0 .6em 0}.msg-agent p:last-child{margin-bottom:0}
422
+ .channel-badge{
423
+ display:inline-block;background:rgba(139,92,246,0.15);color:var(--accent);
424
+ padding:1px 5px;border-radius:3px;font-size:8px;font-weight:600;
425
+ text-transform:uppercase;letter-spacing:.05em;margin-right:4px;
426
+ }
427
+ .msg-time{
428
+ font-family:var(--font-mono);font-size:9px;color:var(--text-muted);
429
+ margin-top:4px;
430
+ }
431
+ .msg-system{
432
+ align-self:center;
433
+ font-family:var(--font-mono);font-size:11px;color:var(--text-muted);
434
+ padding:6px 14px;background:var(--bg-input);border-radius:20px;
435
+ }
436
+
437
+ .thinking{
438
+ align-self:flex-start;display:flex;align-items:center;gap:8px;
439
+ padding:12px 18px;background:var(--msg-agent);border:1px solid rgba(139,92,246,0.1);
440
+ border-radius:14px;border-bottom-left-radius:4px;
441
+ }
442
+ .thinking-dots{display:flex;gap:4px}
443
+ .thinking-dots span{
444
+ width:6px;height:6px;border-radius:50%;background:var(--purple);
445
+ animation:bounce 1.4s ease-in-out infinite;
446
+ }
447
+ .thinking-dots span:nth-child(2){animation-delay:.2s}
448
+ .thinking-dots span:nth-child(3){animation-delay:.4s}
449
+ @keyframes bounce{0%,80%,100%{transform:scale(0.6);opacity:.4}40%{transform:scale(1);opacity:1}}
450
+ .thinking-text{font-size:12px;color:var(--text-muted)}
451
+
452
+ /* ─── Streaming Activity Feed ─────────────────────────────────── */
453
+ .activity-feed{
454
+ align-self:flex-start;max-width:85%;
455
+ background:var(--bg-input);border:1px solid var(--border-dim);
456
+ border-radius:12px;padding:10px 14px;margin-bottom:4px;
457
+ animation:msgIn .2s ease-out;
458
+ }
459
+ .activity-line{
460
+ display:flex;align-items:center;gap:6px;
461
+ font-family:var(--font-mono);font-size:11px;
462
+ padding:3px 0;color:var(--text-muted);line-height:1.4;
463
+ }
464
+ .activity-line:last-child{color:var(--text-secondary)}
465
+ .activity-dot{width:6px;height:6px;border-radius:50%;flex-shrink:0}
466
+ .activity-dot.done{background:var(--green)}
467
+ .activity-dot.active{background:var(--accent);animation:pulse 1s ease-in-out infinite}
468
+ .activity-dot.pending{background:var(--text-muted);opacity:.3}
469
+ .activity-name{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
470
+ .activity-feed.collapsed .activity-lines{display:none}
471
+ .activity-feed.collapsed{padding:6px 12px}
472
+ .activity-header{
473
+ font-family:var(--font-mono);font-size:9px;font-weight:600;
474
+ text-transform:uppercase;letter-spacing:.08em;color:var(--accent);user-select:none;
475
+ margin-bottom:4px;
476
+ }
477
+
478
+ /* ─── Chat Input ──────────────────────────────────────────────── */
479
+ .chat-input-wrap{
480
+ padding:16px 24px 20px;
481
+ border-top:1px solid var(--border-dim);
482
+ background:var(--bg-surface);
483
+ backdrop-filter:blur(20px);-webkit-backdrop-filter:blur(20px);
484
+ }
485
+ .chat-input-box{
486
+ display:flex;gap:10px;align-items:flex-end;
487
+ }
488
+ .chat-textarea{
489
+ flex:1;resize:none;
490
+ font-family:var(--font-sans);font-size:14px;
491
+ padding:12px 16px;border-radius:12px;
492
+ border:1px solid var(--border-dim);
493
+ background:var(--bg-input);color:var(--text-primary);
494
+ outline:none;min-height:44px;max-height:140px;
495
+ transition:border-color .2s;line-height:1.5;
496
+ }
497
+ .chat-textarea::placeholder{color:var(--text-muted)}
498
+ .chat-textarea:focus{border-color:var(--accent)}
499
+ .send-btn{
500
+ width:44px;height:44px;border-radius:10px;border:none;
501
+ background:var(--accent);color:#fff;cursor:pointer;
502
+ display:flex;align-items:center;justify-content:center;
503
+ font-size:18px;transition:all .2s;flex-shrink:0;
504
+ }
505
+ .send-btn:hover{filter:brightness(1.1);transform:scale(1.05)}
506
+ .send-btn:disabled{opacity:.4;cursor:not-allowed;transform:none}
507
+ .mic-btn{
508
+ width:44px;height:44px;border-radius:10px;border:1px solid var(--border-dim);
509
+ background:transparent;color:var(--text-muted);cursor:pointer;
510
+ display:flex;align-items:center;justify-content:center;
511
+ font-size:18px;transition:all .2s;flex-shrink:0;
512
+ }
513
+ .mic-btn:hover{border-color:var(--border-glow);color:var(--text-secondary)}
514
+ .mic-btn.recording{
515
+ border-color:rgba(239,68,68,.5);background:rgba(239,68,68,.1);color:#ef4444;
516
+ animation:micPulse 1s ease-in-out infinite;
517
+ }
518
+ @keyframes micPulse{0%,100%{opacity:1}50%{opacity:.6}}
519
+ .clip-btn{
520
+ width:44px;height:44px;border-radius:10px;border:1px solid var(--border-dim);
521
+ background:transparent;color:var(--text-muted);cursor:pointer;
522
+ display:flex;align-items:center;justify-content:center;
523
+ font-size:18px;transition:all .2s;flex-shrink:0;
524
+ }
525
+ .clip-btn:hover{border-color:var(--border-glow);color:var(--text-secondary)}
526
+ .clip-btn.has-files{border-color:var(--accent);color:var(--accent)}
527
+ .download-btn{
528
+ display:inline-flex;align-items:center;gap:4px;
529
+ padding:3px 10px;margin:2px 0;border-radius:6px;
530
+ border:1px solid var(--border-glow);
531
+ background:var(--accent-bg);color:var(--accent);
532
+ font-family:var(--font-sans);font-size:11px;font-weight:500;
533
+ cursor:pointer;transition:all .2s;vertical-align:middle;
534
+ }
535
+ .download-btn:hover{background:var(--accent);color:#000;border-color:var(--accent)}
536
+ /* ─── Recent Files Bar ────────────────────────────────────────── */
537
+ .recent-files-bar{
538
+ display:flex;align-items:center;gap:6px;padding:4px 12px;
539
+ border-top:1px solid var(--border-dim);background:var(--bg-surface);
540
+ overflow-x:auto;flex-shrink:0;
541
+ }
542
+ .recent-files-bar:empty{display:none}
543
+ .recent-files-label{
544
+ font-family:var(--font-mono);font-size:9px;font-weight:600;
545
+ color:var(--text-muted);text-transform:uppercase;letter-spacing:.04em;
546
+ white-space:nowrap;flex-shrink:0;
547
+ }
548
+ .recent-file-chip{
549
+ display:inline-flex;align-items:center;gap:4px;
550
+ padding:2px 8px;border-radius:5px;
551
+ border:1px solid var(--border-dim);background:var(--bg-input);
552
+ font-family:var(--font-mono);font-size:10px;color:var(--text-secondary);
553
+ cursor:pointer;transition:all .15s;white-space:nowrap;flex-shrink:0;
554
+ }
555
+ .recent-file-chip:hover{border-color:var(--border-glow);color:var(--text-primary);background:var(--accent-bg)}
556
+ .recent-file-chip .chip-icon{font-size:12px}
557
+ .recent-file-chip .chip-folder{font-size:10px;opacity:.5;margin-left:2px;cursor:pointer}
558
+ .recent-file-chip .chip-folder:hover{opacity:1}
559
+
560
+ .tool-cards{margin-bottom:8px;display:flex;flex-direction:column;gap:4px}
561
+ .tool-card{
562
+ display:flex;align-items:center;gap:6px;
563
+ padding:4px 10px;border-radius:6px;
564
+ background:var(--purple-bg);border:1px solid rgba(139,92,246,0.15);
565
+ font-size:11px;color:var(--text-muted);
566
+ font-family:var(--font-mono);overflow:hidden;
567
+ }
568
+ .tool-name{
569
+ color:var(--purple);font-weight:600;white-space:nowrap;flex-shrink:0;
570
+ }
571
+ .tool-cmd{
572
+ background:rgba(0,0,0,0.2);padding:1px 5px;border-radius:3px;
573
+ color:var(--text-secondary);font-size:10px;
574
+ overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:400px;
575
+ }
576
+ .tool-detail{
577
+ color:var(--text-muted);font-size:10px;
578
+ overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:400px;
579
+ }
580
+ .file-drop-zone{
581
+ display:none;padding:12px 16px;margin-bottom:10px;
582
+ border:2px dashed var(--border-glow);border-radius:12px;
583
+ background:var(--bg-input);text-align:center;
584
+ font-size:12px;color:var(--text-muted);cursor:pointer;
585
+ transition:all .2s;min-height:60px;
586
+ display:flex;flex-direction:column;align-items:center;justify-content:center;gap:6px;
587
+ }
588
+ .file-drop-zone.hidden{display:none}
589
+ .file-drop-zone.dragover{border-color:var(--accent);background:var(--accent-bg);color:var(--accent)}
590
+ .file-drop-zone input[type=file]{display:none}
591
+ .file-list{display:flex;flex-wrap:wrap;gap:6px;width:100%;justify-content:flex-start}
592
+ .file-pill{
593
+ display:inline-flex;align-items:center;gap:6px;
594
+ padding:5px 10px;border-radius:8px;
595
+ background:var(--bg-surface);border:1px solid var(--border-dim);
596
+ font-family:var(--font-mono);font-size:10px;color:var(--text-primary);
597
+ max-width:200px;
598
+ }
599
+ .file-pill-name{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1}
600
+ .file-pill-size{color:var(--text-muted);flex-shrink:0}
601
+ .file-pill-remove{
602
+ cursor:pointer;color:var(--text-muted);font-size:14px;flex-shrink:0;
603
+ transition:color .2s;
604
+ }
605
+ .file-pill-remove:hover{color:#ef4444}
606
+ .file-pill-store{
607
+ cursor:pointer;font-size:12px;flex-shrink:0;opacity:.5;
608
+ transition:opacity .2s;
609
+ }
610
+ .file-pill-store.active{opacity:1}
611
+ .file-pill-store:hover{opacity:1}
612
+
613
+ /* ─── Modal ───────────────────────────────────────────────────── */
614
+ .modal-overlay{
615
+ position:fixed;inset:0;z-index:1000;
616
+ background:rgba(0,0,0,0.6);backdrop-filter:blur(8px);-webkit-backdrop-filter:blur(8px);
617
+ display:none;align-items:center;justify-content:center;
618
+ animation:fadeIn .2s ease-out;
619
+ }
620
+ .modal-overlay.show{display:flex}
621
+ @keyframes fadeIn{from{opacity:0}to{opacity:1}}
622
+
623
+ .modal{
624
+ width:520px;max-width:92vw;max-height:85vh;overflow-y:auto;
625
+ background:var(--bg-surface);border:1px solid var(--border-glow);
626
+ border-radius:16px;padding:0;
627
+ box-shadow:0 8px 40px rgba(0,0,0,0.4),0 0 60px rgba(34,211,238,0.06);
628
+ animation:modalIn .25s ease-out;
629
+ }
630
+ @keyframes modalIn{from{opacity:0;transform:scale(.96) translateY(8px)}to{opacity:1;transform:none}}
631
+
632
+ .modal-header{
633
+ display:flex;align-items:center;justify-content:space-between;
634
+ padding:20px 24px 16px;border-bottom:1px solid var(--border-dim);
635
+ }
636
+ .modal-title{font-family:var(--font-display);font-size:18px;font-weight:700}
637
+ .modal-close{
638
+ width:32px;height:32px;border-radius:8px;border:1px solid var(--border-dim);
639
+ background:transparent;color:var(--text-muted);cursor:pointer;
640
+ display:flex;align-items:center;justify-content:center;font-size:16px;
641
+ transition:all .2s;
642
+ }
643
+ .modal-close:hover{border-color:var(--border-glow);color:var(--text-secondary)}
644
+
645
+ .modal-body{padding:20px 24px}
646
+
647
+ .form-group{margin-bottom:16px}
648
+ .form-label{
649
+ display:block;font-family:var(--font-mono);font-size:10px;font-weight:600;
650
+ text-transform:uppercase;letter-spacing:.08em;color:var(--text-muted);
651
+ margin-bottom:6px;
652
+ }
653
+ .form-input{
654
+ width:100%;padding:10px 14px;border-radius:10px;
655
+ border:1px solid var(--border-dim);background:var(--bg-input);
656
+ color:var(--text-primary);font-family:var(--font-sans);font-size:13px;
657
+ outline:none;transition:border-color .2s;
658
+ }
659
+ .form-input:focus{border-color:var(--accent)}
660
+ .form-input::placeholder{color:var(--text-muted)}
661
+ .form-hint{font-size:11px;color:var(--text-muted);margin-top:4px}
662
+
663
+ .form-row{display:flex;gap:12px}
664
+ .form-row .form-group{flex:1}
665
+
666
+ .form-section{
667
+ font-family:var(--font-mono);font-size:10px;font-weight:600;
668
+ text-transform:uppercase;letter-spacing:.12em;color:var(--accent);
669
+ margin:20px 0 12px;padding-bottom:6px;
670
+ border-bottom:1px solid var(--border-dim);
671
+ }
672
+
673
+ .checkbox-group{display:flex;flex-wrap:wrap;gap:6px}
674
+ .checkbox-pill{
675
+ font-family:var(--font-mono);font-size:10px;font-weight:500;
676
+ padding:5px 10px;border-radius:6px;cursor:pointer;
677
+ border:1px solid var(--border-dim);background:transparent;
678
+ color:var(--text-muted);transition:all .2s;user-select:none;
679
+ }
680
+ .checkbox-pill:hover{border-color:var(--border-glow);color:var(--text-secondary)}
681
+ .checkbox-pill.checked{
682
+ border-color:var(--accent);background:var(--accent-bg);color:var(--accent);
683
+ }
684
+
685
+ .modal-footer{
686
+ display:flex;justify-content:space-between;align-items:center;
687
+ padding:16px 24px 20px;border-top:1px solid var(--border-dim);
688
+ }
689
+ .modal-footer-hint{font-size:11px;color:var(--text-muted)}
690
+ .btn-create{
691
+ padding:10px 24px;border-radius:10px;border:none;
692
+ background:var(--accent);color:#fff;cursor:pointer;
693
+ font-family:var(--font-sans);font-size:13px;font-weight:600;
694
+ transition:all .2s;
695
+ }
696
+ .btn-create:hover{filter:brightness(1.1);transform:scale(1.02)}
697
+ .btn-create:disabled{opacity:.5;cursor:not-allowed;transform:none}
698
+
699
+ .route-entry{
700
+ display:flex;gap:8px;align-items:center;margin-bottom:8px;
701
+ }
702
+ .route-entry select,.route-entry input{
703
+ padding:8px 10px;border-radius:8px;
704
+ border:1px solid var(--border-dim);background:var(--bg-input);
705
+ color:var(--text-primary);font-family:var(--font-mono);font-size:12px;
706
+ outline:none;
707
+ }
708
+ .route-entry select{width:110px}
709
+ .route-entry input{flex:1}
710
+ .route-remove{
711
+ width:28px;height:28px;border-radius:6px;border:1px solid var(--border-dim);
712
+ background:transparent;color:var(--text-muted);cursor:pointer;
713
+ display:flex;align-items:center;justify-content:center;font-size:14px;
714
+ }
715
+ .route-remove:hover{border-color:rgba(239,68,68,.3);color:#ef4444}
716
+ .add-route-btn{
717
+ font-family:var(--font-mono);font-size:11px;color:var(--accent);
718
+ background:none;border:none;cursor:pointer;padding:4px 0;
719
+ }
720
+ .add-route-btn:hover{text-decoration:underline}
721
+
722
+ /* ─── Scrollbar ───────────────────────────────────────────────── */
723
+ ::-webkit-scrollbar{width:6px}
724
+ ::-webkit-scrollbar-track{background:transparent}
725
+ ::-webkit-scrollbar-thumb{background:var(--border-glow);border-radius:3px}
726
+
727
+ /* ─── Hide Names (presentation mode) ────────────────────────── */
728
+ .name-blurred{filter:blur(8px);-webkit-filter:blur(8px);transition:filter .2s}
729
+
730
+ /* ─── Stop Button ────────────────────────────────────────────── */
731
+ .stop-btn{
732
+ width:44px;height:44px;border-radius:10px;border:none;
733
+ background:rgba(239,68,68,0.85);color:#fff;cursor:pointer;
734
+ display:flex;align-items:center;justify-content:center;
735
+ font-size:16px;transition:all .2s;flex-shrink:0;
736
+ }
737
+ .stop-btn:hover{filter:brightness(1.1);transform:scale(1.05);background:#ef4444}
738
+
739
+ /* ─── Raw Logs Drawer ────────────────────────────────────────── */
740
+ .raw-logs-btn{
741
+ width:44px;height:44px;border-radius:10px;border:1px solid var(--border-dim);
742
+ background:transparent;color:var(--text-muted);cursor:pointer;
743
+ display:flex;align-items:center;justify-content:center;font-size:18px;
744
+ transition:all .2s;flex-shrink:0;
745
+ }
746
+ .raw-logs-btn:hover{border-color:var(--border-glow);color:var(--text-secondary)}
747
+ .raw-logs-btn.active{border-color:var(--accent);color:var(--accent);background:rgba(139,92,246,0.1)}
748
+ .raw-logs-drawer{
749
+ display:none;height:200px;background:#0a0a0a;border-top:1px solid var(--border-dim);
750
+ overflow:auto;font-family:var(--font-mono);font-size:11px;line-height:1.5;
751
+ color:#8b8b8b;padding:8px 12px;
752
+ }
753
+ .raw-logs-drawer.open{display:block}
754
+ .raw-logs-header{display:flex;justify-content:space-between;align-items:center;padding:4px 0;border-bottom:1px solid rgba(255,255,255,0.06);margin-bottom:4px}
755
+ .raw-logs-mode-btn{background:none;border:1px solid var(--border-dim);color:var(--text-muted);font-size:10px;padding:2px 8px;border-radius:4px;cursor:pointer;font-family:var(--font-mono)}
756
+ .raw-logs-mode-btn:hover{border-color:var(--accent);color:var(--accent)}
757
+ .raw-logs-drawer pre{margin:0;white-space:pre-wrap;word-break:break-all}
758
+ .raw-logs-drawer .log-line{opacity:0.7}
759
+ .raw-logs-drawer .log-line:last-child{opacity:1}
760
+ .raw-logs-drawer .log-stderr{color:#ef4444}
761
+ .raw-logs-drawer .log-dim{opacity:0.4}
762
+
763
+ /* ─── Queue Button variant ───────────────────────────────────── */
764
+ .send-btn.queue-mode{
765
+ background:var(--amber);
766
+ font-size:11px;font-weight:700;font-family:var(--font-mono);
767
+ width:auto;padding:0 14px;letter-spacing:.02em;
768
+ }
769
+ .send-btn.queue-mode:hover{filter:brightness(1.1);transform:scale(1.05)}
770
+
771
+ /* ─── Message Queue ──────────────────────────────────────────── */
772
+ .msg-queue{
773
+ display:flex;flex-direction:column;gap:6px;
774
+ padding:8px 0 0;
775
+ }
776
+ .msg-queue-item{
777
+ display:flex;align-items:center;gap:8px;
778
+ padding:6px 12px;border-radius:8px;
779
+ background:var(--bg-input);border:1px solid var(--border-dim);
780
+ animation:msgIn .2s ease-out;
781
+ }
782
+ .msg-queue-text{
783
+ flex:1;font-size:12px;color:var(--text-secondary);
784
+ overflow:hidden;text-overflow:ellipsis;white-space:nowrap;
785
+ font-family:var(--font-sans);
786
+ }
787
+ .msg-queue-pos{
788
+ font-family:var(--font-mono);font-size:9px;font-weight:600;
789
+ color:var(--text-muted);flex-shrink:0;min-width:16px;text-align:center;
790
+ }
791
+ .msg-queue-actions{display:flex;gap:4px;flex-shrink:0}
792
+ .msg-queue-btn{
793
+ width:24px;height:24px;border-radius:6px;border:1px solid var(--border-dim);
794
+ background:transparent;color:var(--text-muted);cursor:pointer;
795
+ display:flex;align-items:center;justify-content:center;font-size:12px;
796
+ transition:all .2s;
797
+ }
798
+ .msg-queue-btn:hover{border-color:var(--border-glow);color:var(--text-secondary)}
799
+ .msg-queue-btn.danger:hover{border-color:rgba(239,68,68,.3);color:#ef4444}
800
+ </style>
801
+ </head>
802
+ <body>
803
+
804
+ <div class="topbar">
805
+ <a href="/" style="display:flex;align-items:center;gap:10px;margin-right:28px;text-decoration:none">
806
+ <img class="logo-mark" src="/MyAIforOne-logomark-transparent.svg" alt="MyAIforOne">
807
+ <span class="logo-text">MyAIforOne</span>
808
+ </a>
809
+ <nav class="tab-group">
810
+ <a class="tab-btn" href="/">Home</a>
811
+ <a class="tab-btn" href="/org">Agents</a>
812
+ <a class="tab-btn active" href="/ui">Chat</a>
813
+ <a class="tab-btn" href="/library">Library</a>
814
+ <a class="tab-btn" href="/lab">Lab</a>
815
+ </nav>
816
+ <div class="topbar-right">
817
+ <a class="gym-nav-btn gym-tab-link" href="/gym" style="display:none">Gym</a>
818
+ <a href="/marketplace" class="gear-btn" title="Marketplace" style="font-size:11px;font-weight:700;letter-spacing:.03em;padding:0 10px;width:auto">Marketplace</a>
819
+ <a href="/monitor" class="gear-btn" title="Monitor" style="font-size:11px;font-weight:700;letter-spacing:.03em;padding:0 10px;width:auto">Monitor</a>
820
+ <a href="/admin" class="gear-btn" title="Admin" style="font-size:11px;font-weight:700;letter-spacing:.03em;padding:0 10px;width:auto">Admin</a>
821
+ <a href="/user-guide" class="gear-btn" title="User Guide" style="font-size:11px;font-weight:700;letter-spacing:.03em;padding:0 10px;width:auto">User Guide</a>
822
+ <button onclick="window.open('/mini','minibar','width=440,height=460,resizable=yes,scrollbars=no')" title="Open Mini Bar" style="display:flex;align-items:center;justify-content:center;width:34px;height:34px;border-radius:8px;border:1px solid var(--border-dim);background:transparent;color:var(--text-muted);cursor:pointer;font-size:15px;transition:all .2s" onmouseover="this.style.borderColor='var(--border-glow)'" onmouseout="this.style.borderColor='var(--border-dim)'">⊡</button>
823
+ <button class="theme-toggle" onclick="toggleTheme()" title="Toggle theme">☀</button>
824
+ </div>
825
+ </div>
826
+
827
+ <div class="app">
828
+ <!-- Sidebar -->
829
+ <div class="sidebar">
830
+ <div class="status-bar">
831
+ <div class="status-dot"></div>
832
+ <span class="status-text" id="statusText">Loading...</span>
833
+ <div class="status-channels" id="channelPills"></div>
834
+ </div>
835
+ <div class="sidebar-filters" style="padding:8px 12px;display:flex;gap:6px;border-bottom:1px solid var(--border-dim)">
836
+ <input type="text" id="sidebarSearch" placeholder="Search..." oninput="renderAgentList()" style="flex:1;min-width:0;font-family:var(--font-sans);font-size:11px;padding:5px 8px;border-radius:6px;border:1px solid var(--border-dim);background:var(--bg-input);color:var(--text-primary);outline:none">
837
+ <select id="sidebarOrgFilter" onchange="renderAgentList()" style="min-width:0;font-family:var(--font-sans);font-size:11px;padding:5px 8px;border-radius:6px;border:1px solid var(--border-dim);background:var(--bg-input);color:var(--text-primary);outline:none;cursor:pointer;max-width:120px">
838
+ <option value="">All Orgs</option>
839
+ </select>
840
+ </div>
841
+ <div class="agent-list" id="agentList"></div>
842
+ <div class="sidebar-collapse">
843
+ <button onclick="toggleSidebar()">&#x25C0; Collapse</button>
844
+ </div>
845
+ </div>
846
+
847
+ <!-- Sidebar expand button (outside main so it survives re-renders) -->
848
+ <button class="sidebar-expand" id="sidebarExpandBtn" onclick="toggleSidebar()" title="Show sidebar">&#x2630;</button>
849
+
850
+ <!-- Main -->
851
+ <div class="main" id="mainArea">
852
+ <div class="empty-state" id="emptyState">
853
+ <div class="empty-icon">◇</div>
854
+ <div class="empty-text">Select an agent to start chatting</div>
855
+ <div class="empty-sub">Or click "+ New Agent" to create one</div>
856
+ </div>
857
+ </div>
858
+ </div>
859
+
860
+ <!-- New Agent Modal -->
861
+ <div class="modal-overlay" id="newAgentModal">
862
+ <div class="modal">
863
+ <div class="modal-header">
864
+ <div class="modal-title">Create New Agent</div>
865
+ <button class="modal-close" onclick="closeModal()">✕</button>
866
+ </div>
867
+ <div class="modal-body">
868
+ <div class="form-row">
869
+ <div class="form-group">
870
+ <label class="form-label">Agent ID</label>
871
+ <input class="form-input" id="f-agentId" placeholder="my-agent" oninput="autoAlias()">
872
+ <div class="form-hint">Lowercase, hyphens only. Used as folder name.</div>
873
+ </div>
874
+ <div class="form-group">
875
+ <label class="form-label">Mention Alias</label>
876
+ <input class="form-input" id="f-alias" placeholder="@myagent">
877
+ <div class="form-hint">How you trigger this agent in chat.</div>
878
+ </div>
879
+ </div>
880
+
881
+ <div class="form-group">
882
+ <label class="form-label">Name</label>
883
+ <input class="form-input" id="f-name" placeholder="My Agent">
884
+ </div>
885
+
886
+ <div class="form-group">
887
+ <label class="form-label">Description</label>
888
+ <input class="form-input" id="f-desc" placeholder="What does this agent do?">
889
+ </div>
890
+
891
+ <div class="form-group">
892
+ <label class="form-label">Workspace</label>
893
+ <input class="form-input" id="f-workspace" placeholder="~" value="~">
894
+ <div class="form-hint">Directory Claude can access. ~ = home directory.</div>
895
+ </div>
896
+
897
+ <div class="form-section">Session</div>
898
+ <div class="form-group">
899
+ <div class="checkbox-group">
900
+ <div class="checkbox-pill checked" id="f-persistent" onclick="togglePill(this)">Persistent Session</div>
901
+ </div>
902
+ </div>
903
+
904
+ <div class="form-section">Tools</div>
905
+ <div class="form-group">
906
+ <div class="checkbox-group" id="f-tools">
907
+ <div class="checkbox-pill checked" data-val="Read" onclick="togglePill(this)">Read</div>
908
+ <div class="checkbox-pill checked" data-val="Edit" onclick="togglePill(this)">Edit</div>
909
+ <div class="checkbox-pill checked" data-val="Write" onclick="togglePill(this)">Write</div>
910
+ <div class="checkbox-pill checked" data-val="Glob" onclick="togglePill(this)">Glob</div>
911
+ <div class="checkbox-pill checked" data-val="Grep" onclick="togglePill(this)">Grep</div>
912
+ <div class="checkbox-pill checked" data-val="Bash" onclick="togglePill(this)">Bash</div>
913
+ <div class="checkbox-pill checked" data-val="WebFetch" onclick="togglePill(this)">WebFetch</div>
914
+ <div class="checkbox-pill checked" data-val="WebSearch" onclick="togglePill(this)">WebSearch</div>
915
+ </div>
916
+ </div>
917
+
918
+ <div class="form-section">MCPs</div>
919
+ <div class="form-group">
920
+ <div class="checkbox-group" id="f-mcps"></div>
921
+ <div class="form-hint" id="f-mcps-hint">Loading available MCPs...</div>
922
+ </div>
923
+
924
+ <div class="form-section">Routes</div>
925
+ <div id="routeEntries"></div>
926
+ <button class="add-route-btn" onclick="addRouteEntry()">+ Add Route</button>
927
+ </div>
928
+ <div class="modal-footer">
929
+ <div class="modal-footer-hint">Agent files created in ~/Desktop/MyAIforOne Drive/PersonalAgents/</div>
930
+ <button class="btn-create" id="createBtn" onclick="createAgent()">Create Agent</button>
931
+ </div>
932
+ </div>
933
+ </div>
934
+
935
+ <script>
936
+ // ─── Hidden Orgs (presentation mode) ────────────────────────────
937
+ function getHiddenOrgs() {
938
+ try { return JSON.parse(localStorage.getItem('hiddenOrgs') || '[]'); } catch { return []; }
939
+ }
940
+ function isAgentOrgHidden(agent) {
941
+ const hidden = getHiddenOrgs();
942
+ if (!hidden.length) return false;
943
+ return (agent.org || []).some(o => hidden.includes(o.organization));
944
+ }
945
+
946
+ // ─── State ─────────────────────────────────────────────────────
947
+ let agents = [];
948
+ let selectedAgent = null;
949
+ let chatHistory = {}; // agentId -> [{role,text,time}]
950
+ // Per-agent streaming state
951
+ const streamState = {}; // agentId -> {isThinking, text, status, tools, jobId, queue}
952
+ function getStream(id) {
953
+ if (!streamState[id]) streamState[id] = { isThinking: false, text: '', status: '', tools: [], jobId: null, queue: [], renderDirty: false, renderRaf: null };
954
+ return streamState[id];
955
+ }
956
+ // Legacy globals — read from selected agent's state (used by renderChat)
957
+ let isThinking = false; // kept for non-streaming code paths
958
+ let streamingText = '';
959
+ let streamingStatus = '';
960
+ let streamingTools = [];
961
+
962
+ // ─── Voice read-back mode ─────────────────────────────
963
+ let voiceModeOn = false;
964
+ function toggleVoiceMode(){
965
+ voiceModeOn = !voiceModeOn;
966
+ const btn = document.getElementById('voiceModeBtn');
967
+ if(btn){ btn.textContent = voiceModeOn ? '🔊' : '🔇'; btn.style.color = voiceModeOn ? 'var(--accent)' : ''; }
968
+ if(!voiceModeOn) window.speechSynthesis?.cancel();
969
+ }
970
+ function speakText(text){
971
+ if(!voiceModeOn || !window.speechSynthesis) return;
972
+ const clean = text.replace(/\[via:[^\]]+\]/g,'').replace(/[*_`#>~]/g,'').replace(/\n+/g,' ').trim();
973
+ if(!clean) return;
974
+ window.speechSynthesis.cancel();
975
+ const u = new SpeechSynthesisUtterance(clean);
976
+ u.rate = 1.05;
977
+ window.speechSynthesis.speak(u);
978
+ }
979
+ let availableAccounts = [];
980
+ let accountOverrides = {}; // agentId -> account name (in-memory only)
981
+
982
+ // ─── Session Tabs ──────────────────────────────────────────────
983
+ // Open tabs stored in localStorage; full session list lives server-side.
984
+ // chatHistory and streamState are keyed by "agentId::tabId".
985
+ let sessionTabs = {}; // agentId -> [{id, label, createdAt}]
986
+ let activeTab = {}; // agentId -> tabId
987
+
988
+ function tabKey(agentId, tabId) { return agentId + '::' + tabId; }
989
+ function activeTabKey(agentId) { return tabKey(agentId, activeTab[agentId] || getAgentTabs(agentId)[0]?.id); }
990
+
991
+ function getAgentTabs(agentId) {
992
+ if (!sessionTabs[agentId] || !sessionTabs[agentId].length) {
993
+ const id = 'tab-' + Date.now().toString(36);
994
+ sessionTabs[agentId] = [{ id, label: 'Session 1', createdAt: Date.now() }];
995
+ activeTab[agentId] = id;
996
+ syncTabCreate(agentId, id, 'Session 1');
997
+ }
998
+ return sessionTabs[agentId];
999
+ }
1000
+
1001
+ function saveTabs() {
1002
+ try { localStorage.setItem('sessionTabs', JSON.stringify(sessionTabs)); localStorage.setItem('activeTabs', JSON.stringify(activeTab)); } catch {}
1003
+ }
1004
+ function loadTabs() {
1005
+ try {
1006
+ const t = localStorage.getItem('sessionTabs');
1007
+ const a = localStorage.getItem('activeTabs');
1008
+ if (t) sessionTabs = JSON.parse(t);
1009
+ if (a) activeTab = JSON.parse(a);
1010
+ } catch {}
1011
+ }
1012
+ loadTabs();
1013
+
1014
+ // ── Server sync helpers ──────────────────────────────────────
1015
+ async function syncTabCreate(agentId, tabId, label) {
1016
+ try { await fetch(`/api/agents/${agentId}/session-tabs`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ tabId, label }) }); } catch {}
1017
+ }
1018
+ async function syncTabRename(agentId, tabId, label) {
1019
+ try { await fetch(`/api/agents/${agentId}/session-tabs/${tabId}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ label }) }); } catch {}
1020
+ }
1021
+ async function syncTabDelete(agentId, tabId) {
1022
+ try { await fetch(`/api/agents/${agentId}/session-tabs/${tabId}`, { method: 'DELETE' }); } catch {}
1023
+ }
1024
+ async function fetchServerSessions(agentId) {
1025
+ try { const r = await fetch(`/api/agents/${agentId}/session-tabs`); const d = await r.json(); return d.tabs || []; } catch { return []; }
1026
+ }
1027
+ async function fetchTabHistory(agentId, tabId) {
1028
+ try { const r = await fetch(`/api/agents/${agentId}/session-tabs/${tabId}/history`); const d = await r.json(); return d.messages || []; } catch { return []; }
1029
+ }
1030
+
1031
+ // ── Tab operations ──────────────────────────────────────────
1032
+ function createTab(agentId, label) {
1033
+ const tabs = getAgentTabs(agentId);
1034
+ const num = tabs.length + 1;
1035
+ const id = 'tab-' + Date.now().toString(36);
1036
+ const finalLabel = label || ('Session ' + num);
1037
+ tabs.push({ id, label: finalLabel, createdAt: Date.now() });
1038
+ activeTab[agentId] = id;
1039
+ saveTabs();
1040
+ syncTabCreate(agentId, id, finalLabel);
1041
+ renderedFrameAgent = null;
1042
+ renderChat();
1043
+ scrollChat();
1044
+ }
1045
+
1046
+ function closeTab(agentId, tabId) {
1047
+ const tabs = getAgentTabs(agentId);
1048
+ if (tabs.length <= 1) {
1049
+ // Last tab — don't close, open picker instead so user can switch sessions
1050
+ openSessionPicker(agentId);
1051
+ return;
1052
+ }
1053
+ const idx = tabs.findIndex(t => t.id === tabId);
1054
+ if (idx < 0) return;
1055
+ const key = tabKey(agentId, tabId);
1056
+ const ss = streamState[key];
1057
+ if (ss && (ss.isThinking || ss.jobId)) {
1058
+ if (!confirm('This session has an active stream. Close anyway?')) return;
1059
+ }
1060
+ tabs.splice(idx, 1);
1061
+ // Keep history on server; only clear in-memory state
1062
+ delete chatHistory[key];
1063
+ delete streamState[key];
1064
+ if (activeTab[agentId] === tabId) {
1065
+ activeTab[agentId] = tabs[Math.min(idx, tabs.length - 1)].id;
1066
+ }
1067
+ saveTabs();
1068
+ // NOTE: we do NOT delete from server — session stays accessible via picker
1069
+ renderedFrameAgent = null;
1070
+ renderChat();
1071
+ scrollChat();
1072
+ }
1073
+
1074
+ function switchTab(agentId, tabId) {
1075
+ if (activeTab[agentId] === tabId) return;
1076
+ activeTab[agentId] = tabId;
1077
+ saveTabs();
1078
+ updateMessages();
1079
+ scrollChat();
1080
+ document.querySelectorAll('.session-tab').forEach(el => {
1081
+ el.classList.toggle('active', el.dataset.tabId === tabId);
1082
+ });
1083
+ const inp = document.getElementById('chatInput');
1084
+ if (inp) {
1085
+ const agent = agents.find(a => a.id === agentId);
1086
+ inp.placeholder = 'Message ' + (agent?.aliases[0] || agent?.name || '') + '...';
1087
+ inp.focus();
1088
+ }
1089
+ const ss = getStream(activeTabKey(agentId));
1090
+ updateInputButtons(ss);
1091
+ }
1092
+
1093
+ function startRenameTab(agentId, tabId) {
1094
+ const el = document.querySelector(`.session-tab[data-tab-id="${tabId}"] .tab-label`);
1095
+ if (!el) return;
1096
+ const tab = getAgentTabs(agentId).find(t => t.id === tabId);
1097
+ if (!tab) return;
1098
+ const input = document.createElement('input');
1099
+ input.className = 'tab-rename-input';
1100
+ input.value = tab.label;
1101
+ input.onblur = () => finishRenameTab(agentId, tabId, input.value);
1102
+ input.onkeydown = (e) => { if (e.key === 'Enter') input.blur(); if (e.key === 'Escape') { input.value = tab.label; input.blur(); } };
1103
+ el.replaceWith(input);
1104
+ input.focus();
1105
+ input.select();
1106
+ }
1107
+
1108
+ function finishRenameTab(agentId, tabId, newLabel) {
1109
+ const tab = getAgentTabs(agentId).find(t => t.id === tabId);
1110
+ if (tab && newLabel.trim()) {
1111
+ tab.label = newLabel.trim();
1112
+ saveTabs();
1113
+ syncTabRename(agentId, tabId, newLabel.trim());
1114
+ }
1115
+ renderTabBar(agentId);
1116
+ }
1117
+
1118
+ function renderTabBar(agentId) {
1119
+ const container = document.getElementById('sessionTabBar');
1120
+ if (!container) return;
1121
+ const tabs = getAgentTabs(agentId);
1122
+ const curTab = activeTab[agentId] || tabs[0]?.id;
1123
+ let html = tabs.map(t => {
1124
+ const isActive = t.id === curTab;
1125
+ const key = tabKey(agentId, t.id);
1126
+ const ss = streamState[key];
1127
+ const hasActivity = !isActive && ss && (ss.isThinking || ss.jobId);
1128
+ return `<div class="session-tab${isActive ? ' active' : ''}${hasActivity ? ' has-activity' : ''}" data-tab-id="${t.id}"
1129
+ onclick="switchTab('${agentId}','${t.id}')"
1130
+ ondblclick="startRenameTab('${agentId}','${t.id}')">
1131
+ <span class="tab-dot"></span>
1132
+ <span class="tab-label">${escapeHtml(t.label)}</span>
1133
+ <span class="tab-close" onclick="event.stopPropagation();closeTab('${agentId}','${t.id}')">&times;</span>
1134
+ </div>`;
1135
+ }).join('');
1136
+ html += `<div class="session-tab-add" onclick="openSessionPicker('${agentId}')" title="Sessions">+</div>`;
1137
+ container.innerHTML = html;
1138
+ }
1139
+
1140
+ // ── Session Picker ──────────────────────────────────────────
1141
+ let pickerAgentId = null;
1142
+
1143
+ async function openSessionPicker(agentId) {
1144
+ pickerAgentId = agentId;
1145
+ const modal = document.getElementById('sessionPickerModal');
1146
+ if (!modal) return;
1147
+ modal.classList.add('show');
1148
+ const list = document.getElementById('sessionPickerList');
1149
+ list.innerHTML = '<div style="padding:20px;color:var(--text-muted);text-align:center">Loading…</div>';
1150
+ const openIds = new Set((sessionTabs[agentId] || []).map(t => t.id));
1151
+ const serverSessions = await fetchServerSessions(agentId);
1152
+ if (!serverSessions.length) {
1153
+ list.innerHTML = '<div style="padding:16px 20px;color:var(--text-muted)">No sessions yet. Start a new one below.</div>';
1154
+ return;
1155
+ }
1156
+ list.innerHTML = serverSessions.map(s => {
1157
+ const isOpen = openIds.has(s.id);
1158
+ const isActive = activeTab[agentId] === s.id;
1159
+ const ago = s.lastMessageAt ? timeAgo(s.lastMessageAt) : '';
1160
+ return `<div class="picker-session${isActive ? ' picker-active' : ''}" onclick="pickSession('${agentId}','${s.id}',${JSON.stringify(s.label)})">
1161
+ <div class="picker-session-name">${escapeHtml(s.label)}${isOpen ? ' <span class="picker-badge">open</span>' : ''}</div>
1162
+ <div class="picker-session-meta">${escapeHtml(s.lastPreview || '')}${ago ? ' · ' + ago : ''}</div>
1163
+ </div>`;
1164
+ }).join('');
1165
+ }
1166
+
1167
+ function closeSessionPicker() {
1168
+ const modal = document.getElementById('sessionPickerModal');
1169
+ if (modal) modal.classList.remove('show');
1170
+ pickerAgentId = null;
1171
+ }
1172
+
1173
+ async function pickSession(agentId, tabId, label) {
1174
+ closeSessionPicker();
1175
+ const openIds = (sessionTabs[agentId] || []).map(t => t.id);
1176
+ if (openIds.includes(tabId)) {
1177
+ // Already open — just switch to it
1178
+ switchTab(agentId, tabId);
1179
+ return;
1180
+ }
1181
+ // Restore: add as a new tab, load history
1182
+ if (!sessionTabs[agentId]) sessionTabs[agentId] = [];
1183
+ sessionTabs[agentId].push({ id: tabId, label, createdAt: Date.now() });
1184
+ activeTab[agentId] = tabId;
1185
+ saveTabs();
1186
+ renderedFrameAgent = null;
1187
+ renderChat();
1188
+ // Load history from server
1189
+ const messages = await fetchTabHistory(agentId, tabId);
1190
+ if (messages.length) {
1191
+ chatHistory[tabKey(agentId, tabId)] = messages;
1192
+ updateMessages();
1193
+ scrollChat();
1194
+ }
1195
+ }
1196
+
1197
+ function createNewSession() {
1198
+ const agentId = pickerAgentId;
1199
+ closeSessionPicker();
1200
+ if (agentId) createTab(agentId);
1201
+ }
1202
+
1203
+ function timeAgo(iso) {
1204
+ const diff = Date.now() - new Date(iso).getTime();
1205
+ const m = Math.floor(diff / 60000);
1206
+ if (m < 1) return 'just now';
1207
+ if (m < 60) return m + 'm ago';
1208
+ const h = Math.floor(m / 60);
1209
+ if (h < 24) return h + 'h ago';
1210
+ return Math.floor(h / 24) + 'd ago';
1211
+ }
1212
+
1213
+ // Canvas — shared module (canvas.js loaded in <head>)
1214
+ Canvas.init({ getAgentId: () => selectedAgent, escapeHtml });
1215
+
1216
+ // ─── Recent Files Tracking ─────────────────────────────────────
1217
+ let recentFiles = []; // [{name, path, ts, tool}]
1218
+ const FILE_TOOLS = new Set(['Write', 'Edit', 'NotebookEdit']);
1219
+
1220
+ function trackFileFromTool(tool) {
1221
+ if (!tool || !tool.input) return;
1222
+ const name = tool.name;
1223
+ let filePath = null;
1224
+ if (FILE_TOOLS.has(name) && tool.input.file_path) {
1225
+ filePath = tool.input.file_path;
1226
+ } else if (name === 'Bash' && tool.input.command) {
1227
+ // Try to detect file creation from bash commands (output redirection, etc.)
1228
+ const m = tool.input.command.match(/>\s*["']?([^\s"'|&;]+\.\w+)/);
1229
+ if (m) filePath = m[1];
1230
+ }
1231
+ if (!filePath) return;
1232
+ const fileName = filePath.split('/').pop() || filePath;
1233
+ // Deduplicate by path
1234
+ const idx = recentFiles.findIndex(f => f.path === filePath);
1235
+ if (idx >= 0) {
1236
+ recentFiles[idx].ts = Date.now();
1237
+ recentFiles[idx].tool = name;
1238
+ } else {
1239
+ recentFiles.unshift({ name: fileName, path: filePath, ts: Date.now(), tool: name });
1240
+ if (recentFiles.length > 20) recentFiles.pop();
1241
+ }
1242
+ renderRecentFiles();
1243
+ }
1244
+
1245
+ function renderRecentFiles() {
1246
+ const bar = document.getElementById('recentFilesBar');
1247
+ if (!bar || recentFiles.length === 0) { if (bar) bar.innerHTML = ''; return; }
1248
+ const icons = {csv:'📊',json:'📋',md:'📝',txt:'📝',html:'🌐',js:'⚡',ts:'⚡',py:'🐍',css:'🎨',pdf:'📄',png:'🖼️',jpg:'🖼️',svg:'🖼️',xlsx:'📊',docx:'📃'};
1249
+ let html = '<span class="recent-files-label">Recent Files</span>';
1250
+ for (const f of recentFiles.slice(0, 10)) {
1251
+ const ext = (f.name.split('.').pop() || '').toLowerCase();
1252
+ const icon = icons[ext] || '📁';
1253
+ const enc = encodeURIComponent(f.path);
1254
+ html += `<span class="recent-file-chip" title="${escapeHtml(f.path)}">
1255
+ <span class="chip-icon" onclick="Canvas.openFile('${enc}')">${icon}</span>
1256
+ <span onclick="Canvas.openFile('${enc}')">${escapeHtml(f.name)}</span>
1257
+ <span class="chip-folder" onclick="event.stopPropagation();fetch('/api/open-folder',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({path:'${enc}'})})" title="Open folder">&#x1F4C2;</span>
1258
+ </span>`;
1259
+ }
1260
+ bar.innerHTML = html;
1261
+ }
1262
+ let rawLogsOpen = false;
1263
+ let rawLogsReader = null; // active fetch reader for raw stream
1264
+ let rawMode = false; // false = clean view, true = raw JSON
1265
+
1266
+ // Throttled stream rendering — batch token updates to avoid per-token formatMarkdown() calls
1267
+ function scheduleStreamRender(ss, isViewing) {
1268
+ if (!isViewing()) return;
1269
+ ss.renderDirty = true;
1270
+ if (!ss.renderRaf) {
1271
+ ss.renderRaf = requestAnimationFrame(() => {
1272
+ ss.renderRaf = null;
1273
+ if (!ss.renderDirty) return;
1274
+ ss.renderDirty = false;
1275
+ flushStreamRender(ss, isViewing);
1276
+ });
1277
+ }
1278
+ }
1279
+ function flushStreamRender(ss, isViewing) {
1280
+ if (!isViewing()) return;
1281
+ const streamEl = document.getElementById('streamingContent');
1282
+ const thinkWrap = document.getElementById('thinkingWrap');
1283
+ if (streamEl) {
1284
+ streamEl.innerHTML = formatMarkdown(ss.text);
1285
+ } else {
1286
+ if (thinkWrap) thinkWrap.innerHTML = '';
1287
+ const msgDiv = document.createElement('div');
1288
+ msgDiv.className = 'msg msg-agent';
1289
+ msgDiv.id = 'streamingMsg';
1290
+ msgDiv.innerHTML = `<div id="streamingContent">${formatMarkdown(ss.text)}</div><div class="msg-time">streaming...</div>`;
1291
+ const chatMsgs = document.getElementById('chatMessages');
1292
+ if (chatMsgs) chatMsgs.insertBefore(msgDiv, thinkWrap);
1293
+ }
1294
+ scrollChat();
1295
+ }
1296
+
1297
+ function toggleRawLogs() {
1298
+ rawLogsOpen = !rawLogsOpen;
1299
+ const drawer = document.getElementById('rawLogsDrawer');
1300
+ const btn = document.getElementById('rawLogsBtn');
1301
+ if (drawer) drawer.classList.toggle('open', rawLogsOpen);
1302
+ if (btn) btn.classList.toggle('active', rawLogsOpen);
1303
+ if (rawLogsOpen) connectRawLogs();
1304
+ }
1305
+
1306
+ function connectRawLogs() {
1307
+ const ss = getStream(activeTabKey(selectedAgent));
1308
+ if (!ss.jobId) {
1309
+ const el = document.getElementById('rawLogsContent');
1310
+ if (el) el.textContent = 'No active job. Send a message to start streaming logs.\n';
1311
+ return;
1312
+ }
1313
+ streamRawLogs(ss.jobId);
1314
+ }
1315
+
1316
+ async function streamRawLogs(jobId) {
1317
+ const el = document.getElementById('rawLogsContent');
1318
+ if (!el) return;
1319
+ el.textContent = '';
1320
+
1321
+ try {
1322
+ const res = await fetch(`/api/chat/jobs/${jobId}/raw?after=0`);
1323
+ const reader = res.body.getReader();
1324
+ rawLogsReader = reader;
1325
+ const decoder = new TextDecoder();
1326
+ let buffer = '';
1327
+
1328
+ while (true) {
1329
+ const { done, value } = await reader.read();
1330
+ if (done) break;
1331
+
1332
+ buffer += decoder.decode(value, { stream: true });
1333
+ const lines = buffer.split('\n');
1334
+ buffer = lines.pop() || '';
1335
+
1336
+ for (const line of lines) {
1337
+ if (!line.startsWith('data: ')) continue;
1338
+ const data = line.slice(6);
1339
+ if (data === '[DONE]') {
1340
+ el.textContent += '\n--- Job complete ---\n';
1341
+ break;
1342
+ }
1343
+ if (rawMode) {
1344
+ const span = document.createElement('span');
1345
+ span.className = data.startsWith('[stderr]') ? 'log-line log-stderr' : 'log-line';
1346
+ span.textContent = data + '\n';
1347
+ el.appendChild(span);
1348
+ } else {
1349
+ const fmt = formatLogLine(data);
1350
+ if (fmt) {
1351
+ const span = document.createElement('span');
1352
+ span.className = 'log-line' + (fmt.cls ? ' ' + fmt.cls : '');
1353
+ span.textContent = fmt.icon + ' ' + fmt.text + '\n';
1354
+ el.appendChild(span);
1355
+ }
1356
+ }
1357
+ // Auto-scroll
1358
+ const drawer = document.getElementById('rawLogsDrawer');
1359
+ if (drawer) drawer.scrollTop = drawer.scrollHeight;
1360
+ }
1361
+ }
1362
+ } catch { /* connection closed */ }
1363
+ }
1364
+
1365
+ function toggleRawMode() {
1366
+ rawMode = !rawMode;
1367
+ const btn = document.getElementById('rawModeBtn');
1368
+ if (btn) btn.textContent = rawMode ? '● Raw' : '○ Clean';
1369
+ }
1370
+
1371
+ function formatLogLine(data) {
1372
+ if (data.startsWith('[stderr]')) return { icon: '⚠', text: data.slice(9), cls: 'log-stderr' };
1373
+ try {
1374
+ let evt = JSON.parse(data);
1375
+ // Unwrap stream_event
1376
+ if (evt.type === 'stream_event' && evt.event) evt = evt.event;
1377
+
1378
+ // Text output
1379
+ if (evt.type === 'content_block_delta' && evt.delta?.type === 'text_delta')
1380
+ return { icon: '💬', text: evt.delta.text };
1381
+
1382
+ // Thinking
1383
+ if (evt.type === 'content_block_delta' && evt.delta?.type === 'thinking_delta')
1384
+ return { icon: '🧠', text: evt.delta.thinking };
1385
+
1386
+ // Tool starting
1387
+ if (evt.type === 'content_block_start' && evt.content_block?.type === 'tool_use')
1388
+ return { icon: '🔧', text: evt.content_block.name };
1389
+
1390
+ // Tool call complete (from assistant message)
1391
+ if (evt.type === 'assistant' && evt.message?.content) {
1392
+ const tools = evt.message.content.filter(b => b.type === 'tool_use');
1393
+ if (tools.length) {
1394
+ return { icon: '📖', text: tools.map(t => {
1395
+ const path = t.input?.file_path || t.input?.command?.slice(0, 80) || t.input?.pattern || '';
1396
+ return t.name + (path ? ' ' + path : '');
1397
+ }).join(', ') };
1398
+ }
1399
+ const texts = evt.message.content.filter(b => b.type === 'text' && b.text);
1400
+ if (texts.length) return { icon: '💬', text: texts.map(t => t.text).join(' ') };
1401
+ }
1402
+
1403
+ // Tool result
1404
+ if (evt.type === 'user' && evt.message?.content?.[0]?.type === 'tool_result')
1405
+ return { icon: '✓', text: 'tool_result', cls: 'log-dim' };
1406
+
1407
+ // Skip noise
1408
+ if (evt.type === 'content_block_delta' && evt.delta?.type === 'input_json_delta') return null;
1409
+ if (evt.type === 'content_block_stop' || evt.type === 'message_stop' || evt.type === 'message_delta') return null;
1410
+ if (evt.type === 'rate_limit_event') return null;
1411
+ if (evt.type === 'content_block_delta' && evt.delta?.type === 'signature_delta') return null;
1412
+
1413
+ // System init
1414
+ if (evt.type === 'system' && evt.subtype === 'init')
1415
+ return { icon: '●', text: 'Session started — ' + (evt.model || '') + ' | ' + (evt.mcp_servers?.length || 0) + ' MCPs' };
1416
+ if (evt.type === 'system') return { icon: '⚙', text: evt.subtype || 'system' };
1417
+
1418
+ // Result
1419
+ if (evt.type === 'result')
1420
+ return { icon: '🏁', text: 'Done — ' + (evt.duration_ms ? Math.round(evt.duration_ms/1000) + 's' : '') };
1421
+
1422
+ // Message start
1423
+ if (evt.type === 'message_start') return null;
1424
+
1425
+ // Fallback: show type
1426
+ return { icon: '·', text: evt.type || data.slice(0, 80), cls: 'log-dim' };
1427
+ } catch {
1428
+ return { icon: '·', text: data.slice(0, 120) };
1429
+ }
1430
+ }
1431
+
1432
+ // ─── Theme ─────────────────────────────────────────────────────
1433
+ function toggleTheme() {
1434
+ const html = document.documentElement;
1435
+ const current = html.getAttribute('data-theme');
1436
+ const next = current === 'light' ? '' : 'light';
1437
+ html.setAttribute('data-theme', next);
1438
+ document.querySelector('.theme-toggle').textContent = next === 'light' ? '🌙' : '☀';
1439
+ localStorage.setItem('theme', next);
1440
+ }
1441
+
1442
+ // Init theme
1443
+ (function() {
1444
+ const saved = localStorage.getItem('theme');
1445
+ const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
1446
+ const theme = saved !== null ? saved : (prefersDark ? '' : 'light');
1447
+ if (theme) document.documentElement.setAttribute('data-theme', theme);
1448
+ document.querySelector('.theme-toggle').textContent = theme === 'light' ? '🌙' : '☀';
1449
+ })();
1450
+
1451
+ // ─── API ───────────────────────────────────────────────────────
1452
+ async function fetchDashboard() {
1453
+ try {
1454
+ const res = await fetch('/api/dashboard');
1455
+ const data = await res.json();
1456
+ agents = data.agents;
1457
+ availableAccounts = data.claudeAccounts || [];
1458
+ renderStatus(data);
1459
+ buildFilterDropdowns();
1460
+ renderAgentList();
1461
+ } catch (err) {
1462
+ document.getElementById('statusText').textContent = 'Connection failed';
1463
+ }
1464
+ }
1465
+
1466
+ let filtersInitialized = false;
1467
+ function buildFilterDropdowns() {
1468
+ const orgSet = new Set();
1469
+ for (const a of agents) {
1470
+ if ((a.agentClass || 'standard') !== 'standard') continue;
1471
+ for (const o of (a.org || [])) {
1472
+ if (o.organization) orgSet.add(o.organization);
1473
+ }
1474
+ }
1475
+ const sel = document.getElementById('sidebarOrgFilter');
1476
+ if (sel) {
1477
+ const prev = sel.value;
1478
+ sel.innerHTML = '<option value="">All Orgs</option>' +
1479
+ [...orgSet].sort().map(o => `<option value="${o}">${o}</option>`).join('');
1480
+ if (prev && [...orgSet].includes(prev)) sel.value = prev;
1481
+ }
1482
+ }
1483
+
1484
+
1485
+
1486
+ function getFilteredAgents() {
1487
+ const query = (document.getElementById('sidebarSearch')?.value || '').toLowerCase().trim();
1488
+ const orgFilter = document.getElementById('sidebarOrgFilter')?.value || '';
1489
+
1490
+ let result = agents.filter(a => (a.agentClass || 'standard') === 'standard');
1491
+
1492
+ if (query) {
1493
+ result = result.filter(a =>
1494
+ (a.name || '').toLowerCase().includes(query) ||
1495
+ (a.id || '').toLowerCase().includes(query) ||
1496
+ (a.aliases || []).some(al => al.toLowerCase().includes(query)) ||
1497
+ (a.description || '').toLowerCase().includes(query)
1498
+ );
1499
+ }
1500
+
1501
+ if (orgFilter) {
1502
+ result = result.filter(a =>
1503
+ (a.org || []).some(o => o.organization === orgFilter)
1504
+ );
1505
+ }
1506
+
1507
+ return result.slice().sort((a, b) => {
1508
+ const aTime = a.lastMessage && a.lastMessage !== 'never' ? new Date(a.lastMessage).getTime() : 0;
1509
+ const bTime = b.lastMessage && b.lastMessage !== 'never' ? new Date(b.lastMessage).getTime() : 0;
1510
+ return bTime - aTime;
1511
+ });
1512
+ }
1513
+
1514
+ // Recover latest exchange from server log when streaming disconnects
1515
+ async function recoverFromLog(agentId, tk) {
1516
+ try {
1517
+ // Wait a moment for the executor to finish writing
1518
+ await new Promise(r => setTimeout(r, 3000));
1519
+ const res = await fetch(`/api/agents/${agentId}`);
1520
+ const data = await res.json();
1521
+ if (data.recentMessages && data.recentMessages.length > 0) {
1522
+ const latest = data.recentMessages[data.recentMessages.length - 1];
1523
+ if (latest.response) {
1524
+ // Check if we already have this response (avoid duplicates)
1525
+ const _hist = chatHistory[tk || agentId] || [];
1526
+ const lastInHistory = _hist[_hist.length - 1];
1527
+ if (lastInHistory?.role === 'agent' && lastInHistory.text === latest.response) return true;
1528
+ _hist.push({ role: 'agent', text: latest.response, time: latest.ts, channel: latest.channel || 'unknown' });
1529
+ return true;
1530
+ }
1531
+ }
1532
+ } catch { /* ignore */ }
1533
+ return false;
1534
+ }
1535
+
1536
+ async function fetchAgentMessages(agentId) {
1537
+ try {
1538
+ const res = await fetch(`/api/agents/${agentId}`);
1539
+ const data = await res.json();
1540
+ if (data.recentMessages && data.recentMessages.length > 0) {
1541
+ // Load server history into the default tab only (other tabs are local-only)
1542
+ const defaultKey = tabKey(agentId, 'default');
1543
+ chatHistory[defaultKey] = data.recentMessages.map(m => ([
1544
+ { role: 'user', text: m.text, time: m.ts, channel: m.channel || 'unknown' },
1545
+ { role: 'agent', text: m.response, time: m.ts, channel: m.channel || 'unknown' },
1546
+ ])).flat().filter(m => m.text);
1547
+ }
1548
+ } catch { /* ignore */ }
1549
+ }
1550
+
1551
+ async function sendMessage(agentId, text, _tabKeyOverride) {
1552
+ // Capture the tab key at send time so it stays stable even if user switches tabs during streaming
1553
+ const tk = _tabKeyOverride || activeTabKey(agentId);
1554
+ if (!chatHistory[tk]) chatHistory[tk] = [];
1555
+ chatHistory[tk].push({ role: 'user', text, time: new Date().toISOString() });
1556
+ renderChat();
1557
+ scrollChat();
1558
+
1559
+ const agent = agents.find(a => a.id === agentId);
1560
+ const useStreaming = agent?.streaming;
1561
+
1562
+ if (useStreaming) {
1563
+ // SSE streaming mode — use per-tab state so switching tabs doesn't bleed
1564
+ const ss = getStream(tk);
1565
+ ss.isThinking = true;
1566
+ ss.text = '';
1567
+ ss.status = '';
1568
+ ss.tools = [];
1569
+ const isViewing = () => selectedAgent === agentId && activeTabKey(agentId) === tk;
1570
+ if (isViewing()) renderChat();
1571
+ scrollChat();
1572
+
1573
+ try {
1574
+ // Step 1: Start job
1575
+ const senderId = tk.split('::')[1] || 'default';
1576
+ const startRes = await fetch(`/api/chat/${agentId}/stream`, {
1577
+ method: 'POST',
1578
+ headers: { 'Content-Type': 'application/json' },
1579
+ body: JSON.stringify({ text, accountOverride: accountOverrides[agentId], senderId }),
1580
+ });
1581
+ const { jobId } = await startRes.json();
1582
+ if (!jobId) throw new Error('No jobId returned');
1583
+ ss.jobId = jobId;
1584
+ // Re-render input area to show Stop button and Queue mode
1585
+ if (isViewing()) updateInputButtons(ss);
1586
+ // Auto-connect raw logs if drawer is open
1587
+ if (rawLogsOpen) streamRawLogs(jobId);
1588
+
1589
+ // Step 2: Stream events with auto-reconnect
1590
+ let lastEventId = 0;
1591
+ let isDone = false;
1592
+ let retries = 0;
1593
+ const MAX_RETRIES = 10;
1594
+ console.log(`[Stream] Starting job ${jobId} for ${agentId}`);
1595
+
1596
+ // Inactivity timeout: if no events for 60s, assume connection dead
1597
+ let lastEventTime = Date.now();
1598
+ const INACTIVITY_TIMEOUT_MS = 60_000;
1599
+ const inactivityCheck = setInterval(() => {
1600
+ if (isDone) { clearInterval(inactivityCheck); return; }
1601
+ if (Date.now() - lastEventTime > INACTIVITY_TIMEOUT_MS) {
1602
+ console.warn(`[Stream] No events for ${INACTIVITY_TIMEOUT_MS/1000}s — assuming connection dead`);
1603
+ isDone = true;
1604
+ clearInterval(inactivityCheck);
1605
+ }
1606
+ }, 10_000);
1607
+
1608
+ while (!isDone && retries < MAX_RETRIES) {
1609
+ try {
1610
+ console.log(`[Stream] Connecting to job ${jobId} after=${lastEventId} retry=${retries}`);
1611
+ const streamRes = await fetch(`/api/chat/jobs/${jobId}/stream?after=${lastEventId}`);
1612
+ if (!streamRes.ok) {
1613
+ console.warn(`[Stream] HTTP ${streamRes.status} from stream endpoint`);
1614
+ if (streamRes.status === 404) {
1615
+ // Service restarted — job is gone from memory. Post the exchange
1616
+ // back to the new server so it gets saved to conversation_log.jsonl.
1617
+ console.log(`[Stream] 404 = service restart. Recovering exchange (textLen=${ss.text.length})`);
1618
+ try {
1619
+ await fetch(`/api/agents/${agentId}/recover`, {
1620
+ method: 'POST',
1621
+ headers: { 'Content-Type': 'application/json' },
1622
+ body: JSON.stringify({ userText: text, response: ss.text }),
1623
+ });
1624
+ } catch { /* best effort */ }
1625
+ if (ss.text) {
1626
+ chatHistory[tk].push({ role: 'agent', text: ss.text, time: new Date().toISOString(), tools: ss.tools.length ? [...ss.tools] : undefined });
1627
+ ss.text = '';
1628
+ }
1629
+ isDone = true;
1630
+ break;
1631
+ }
1632
+ retries++;
1633
+ await new Promise(r => setTimeout(r, 1000));
1634
+ continue;
1635
+ }
1636
+ const reader = streamRes.body.getReader();
1637
+ const decoder = new TextDecoder();
1638
+ let buffer = '';
1639
+ let eventsThisConnection = 0;
1640
+ retries = 0; // reset on successful connect
1641
+
1642
+ while (true) {
1643
+ const { done, value } = await reader.read();
1644
+ if (done) {
1645
+ console.log(`[Stream] Reader done. Events this connection: ${eventsThisConnection}, isDone: ${isDone}, lastEventId: ${lastEventId}`);
1646
+ break;
1647
+ }
1648
+
1649
+ buffer += decoder.decode(value, { stream: true });
1650
+ const lines = buffer.split('\n');
1651
+ buffer = lines.pop() || '';
1652
+
1653
+ for (const line of lines) {
1654
+ // Track event IDs for reconnect
1655
+ if (line.startsWith('id: ')) {
1656
+ lastEventId = parseInt(line.slice(4)) + 1;
1657
+ continue;
1658
+ }
1659
+ if (!line.startsWith('data: ')) continue;
1660
+ const data = line.slice(6);
1661
+ if (data === '[DONE]') { isDone = true; console.log(`[Stream] Received [DONE] for job ${jobId}`); continue; }
1662
+ lastEventTime = Date.now(); // reset inactivity timer on any event
1663
+ eventsThisConnection++;
1664
+
1665
+ try {
1666
+ const event = JSON.parse(data);
1667
+ if (event.type === 'text') {
1668
+ ss.text += event.data;
1669
+ ss.isThinking = false;
1670
+ scheduleStreamRender(ss, isViewing);
1671
+ } else if (event.type === 'tool') {
1672
+ ss.status = event.data;
1673
+ if (event.tool) {
1674
+ const hasInput = event.tool.input && Object.keys(event.tool.input).length > 0;
1675
+ const existingIdx = hasInput ? ss.tools.findLastIndex(t => t.name === event.tool.name && (!t.input || Object.keys(t.input).length === 0)) : -1;
1676
+ if (existingIdx >= 0) {
1677
+ ss.tools[existingIdx].input = event.tool.input;
1678
+ } else {
1679
+ ss.tools.push({ name: event.tool.name, input: event.tool.input, ts: Date.now() });
1680
+ }
1681
+ // Track file creation/modification for recent files bar
1682
+ if (hasInput) trackFileFromTool(event.tool);
1683
+ }
1684
+ if (isViewing()) {
1685
+ const toolsWrap = document.getElementById('streamingToolsWrap');
1686
+ if (toolsWrap) {
1687
+ toolsWrap.innerHTML = renderActivityFeed(ss.tools, true);
1688
+ } else {
1689
+ renderChat();
1690
+ }
1691
+ scrollChat();
1692
+ }
1693
+ } else if (event.type === 'status') {
1694
+ ss.status = event.data;
1695
+ if (isViewing()) {
1696
+ const thinkWrap = document.getElementById('thinkingWrap');
1697
+ if (thinkWrap && !ss.text) {
1698
+ thinkWrap.innerHTML = `<div class="thinking"><div class="thinking-dots"><span></span><span></span><span></span></div><span class="thinking-text">${ss.status || 'Thinking...'}</span></div>`;
1699
+ }
1700
+ scrollChat();
1701
+ }
1702
+ } else if (event.type === 'heartbeat') {
1703
+ // Backend heartbeat — connection alive, timer already reset above
1704
+ } else if (event.type === 'done') {
1705
+ // Agent finished — executor's final event
1706
+ isDone = true;
1707
+ } else if (event.type === 'stopped') {
1708
+ // User stopped the stream — finalize partial response
1709
+ isDone = true;
1710
+ } else if (event.type === 'error') {
1711
+ ss.isThinking = false;
1712
+ ss.text = '';
1713
+ ss.status = '';
1714
+ chatHistory[tk].push({ role: 'agent', text: `Error: ${event.data}`, time: new Date().toISOString() });
1715
+ if (isViewing()) { renderChat(); scrollChat(); }
1716
+ isDone = true;
1717
+ }
1718
+ } catch { /* skip malformed event */ }
1719
+ }
1720
+ }
1721
+
1722
+ // Stream ended naturally — if not done, reconnect silently
1723
+ if (!isDone) {
1724
+ retries++;
1725
+ console.warn(`[Stream] Stream ended without [DONE]. Retry ${retries}/${MAX_RETRIES}. lastEventId=${lastEventId}, textLen=${ss.text.length}`);
1726
+ // Only show reconnecting after 3+ retries (indicates real problem)
1727
+ if (retries >= 3 && isViewing()) {
1728
+ const thinkWrap = document.getElementById('thinkingWrap');
1729
+ if (thinkWrap) thinkWrap.innerHTML = `<div class="thinking"><div class="thinking-dots"><span></span><span></span><span></span></div><span class="thinking-text">Reconnecting... (${retries}/${MAX_RETRIES})</span></div>`;
1730
+ }
1731
+ await new Promise(r => setTimeout(r, 500));
1732
+ }
1733
+ } catch (streamErr) {
1734
+ // Connection failed — retry silently
1735
+ retries++;
1736
+ console.error(`[Stream] Connection error:`, streamErr, `Retry ${retries}/${MAX_RETRIES}`);
1737
+ if (retries >= 3 && retries < MAX_RETRIES && isViewing()) {
1738
+ const thinkWrap = document.getElementById('thinkingWrap');
1739
+ if (thinkWrap) thinkWrap.innerHTML = `<div class="thinking"><div class="thinking-dots"><span></span><span></span><span></span></div><span class="thinking-text">Reconnecting... (${retries}/${MAX_RETRIES})</span></div>`;
1740
+ }
1741
+ if (retries < MAX_RETRIES) {
1742
+ await new Promise(r => setTimeout(r, 500 * retries));
1743
+ }
1744
+ }
1745
+ }
1746
+
1747
+ // Finalize — flush any pending throttled render
1748
+ if (ss.renderRaf) { cancelAnimationFrame(ss.renderRaf); ss.renderRaf = null; }
1749
+ if (ss.renderDirty) { ss.renderDirty = false; flushStreamRender(ss, isViewing); }
1750
+ clearInterval(inactivityCheck);
1751
+ ss.isThinking = false;
1752
+ ss.jobId = null;
1753
+ if (ss.text) {
1754
+ speakText(ss.text);
1755
+ chatHistory[tk].push({ role: 'agent', text: ss.text, time: new Date().toISOString(), tools: ss.tools.length ? [...ss.tools] : undefined });
1756
+ } else if (!isDone) {
1757
+ // Stream disconnected without completing — reload from server log
1758
+ // The executor runs to completion regardless, so the log should have the response
1759
+ await recoverFromLog(agentId, tk);
1760
+ }
1761
+ ss.text = '';
1762
+ ss.status = '';
1763
+ ss.tools = [];
1764
+ } catch (err) {
1765
+ ss.isThinking = false;
1766
+ ss.jobId = null;
1767
+ // Save partial response if any text was streamed before disconnect
1768
+ if (ss.text) {
1769
+ chatHistory[tk].push({ role: 'agent', text: ss.text, time: new Date().toISOString(), tools: ss.tools.length ? [...ss.tools] : undefined });
1770
+ } else {
1771
+ // Try to recover from server log before falling back to error
1772
+ const recovered = await recoverFromLog(agentId, tk);
1773
+ if (!recovered) {
1774
+ chatHistory[tk].push({ role: 'agent', text: `Connection lost. The agent may still be running — check back shortly.`, time: new Date().toISOString() });
1775
+ }
1776
+ }
1777
+ ss.text = '';
1778
+ ss.status = '';
1779
+ ss.tools = [];
1780
+ }
1781
+ if (isViewing()) { renderChat(); scrollChat(); }
1782
+ // Auto-send next queued message if any
1783
+ if (ss.queue.length > 0) {
1784
+ const next = ss.queue.shift();
1785
+ sendMessage(agentId, next, tk);
1786
+ }
1787
+ } else {
1788
+ // Regular non-streaming mode
1789
+ isThinking = true;
1790
+ renderChat();
1791
+ scrollChat();
1792
+
1793
+ try {
1794
+ const senderIdNs = tk.split('::')[1] || 'default';
1795
+ const res = await fetch(`/api/chat/${agentId}`, {
1796
+ method: 'POST',
1797
+ headers: { 'Content-Type': 'application/json' },
1798
+ body: JSON.stringify({ text, accountOverride: accountOverrides[agentId], senderId: senderIdNs }),
1799
+ });
1800
+ const data = await res.json();
1801
+ isThinking = false;
1802
+
1803
+ if (data.ok) {
1804
+ chatHistory[tk].push({ role: 'agent', text: data.response, time: new Date().toISOString() });
1805
+ } else {
1806
+ chatHistory[tk].push({ role: 'agent', text: `Error: ${data.error}`, time: new Date().toISOString() });
1807
+ }
1808
+ } catch (err) {
1809
+ isThinking = false;
1810
+ chatHistory[tk].push({ role: 'agent', text: `Connection error: ${err.message}`, time: new Date().toISOString() });
1811
+ }
1812
+ }
1813
+
1814
+ renderChat();
1815
+ scrollChat();
1816
+ fetchDashboard();
1817
+ }
1818
+
1819
+ // ─── Render ────────────────────────────────────────────────────
1820
+ function renderStatus(data) {
1821
+ const upMin = Math.floor(data.uptime / 60);
1822
+ const upH = Math.floor(upMin / 60);
1823
+ const upStr = upH > 0 ? `${upH}h ${upMin % 60}m` : `${upMin}m`;
1824
+ document.getElementById('statusText').textContent = `Running · ${upStr} · ${data.agents.length} agents`;
1825
+ document.getElementById('channelPills').innerHTML = data.channels
1826
+ .map(c => `<span class="channel-pill">${c}</span>`).join('');
1827
+ }
1828
+
1829
+ function renderAgentList() {
1830
+ const list = document.getElementById('agentList');
1831
+ const allSorted = getFilteredAgents();
1832
+
1833
+ // Detect active agents (currently streaming/thinking)
1834
+ const activeIds = new Set();
1835
+ for (const [id, ss] of Object.entries(streamState)) {
1836
+ if (ss.isThinking || ss.jobId) activeIds.add(id);
1837
+ }
1838
+
1839
+ const activeAgents = allSorted.filter(a => activeIds.has(a.id));
1840
+ const recentAgents = allSorted
1841
+ .filter(a => !activeIds.has(a.id) && a.lastMessage && a.lastMessage !== 'never')
1842
+ .slice(0, 10);
1843
+ const recentIds = new Set([...activeIds, ...recentAgents.map(a => a.id)]);
1844
+ const restAgents = allSorted.filter(a => !recentIds.has(a.id));
1845
+
1846
+ function compactCard(a, showPulse) {
1847
+ const initials = a.aliases[0]?.replace('@','').slice(0,2).toUpperCase() || a.id.slice(0,2).toUpperCase();
1848
+ const sel = selectedAgent === a.id ? ' selected' : '';
1849
+ const blur = isAgentOrgHidden(a) ? ' name-blurred' : '';
1850
+ const time = a.lastMessage && a.lastMessage !== 'never' ? formatRelativeTime(a.lastMessage) : '';
1851
+ const pulse = showPulse ? '<div class="thinking-pulse" title="Processing..."></div>' : '';
1852
+ return '<div class="agent-card-compact' + sel + '" onclick="selectAgent(\''+a.id+'\')"><div class="compact-avatar">'+initials+'</div><div class="compact-info"><div class="compact-name'+blur+'">'+a.name+'</div><div class="compact-time">'+((a.aliases||[])[0]||a.id)+(time?' · '+time:'')+'</div></div>'+pulse+'</div>';
1853
+ }
1854
+
1855
+ let html = '';
1856
+
1857
+ // Active section
1858
+ if (activeAgents.length) {
1859
+ html += '<div class="sidebar-section open"><div class="sidebar-section-header"><span style="color:var(--amber)">●</span> Active<span class="section-count">'+activeAgents.length+'</span></div><div class="section-body">';
1860
+ html += activeAgents.map(a => compactCard(a, true)).join('');
1861
+ html += '</div></div>';
1862
+ }
1863
+
1864
+ // Recent section
1865
+ if (recentAgents.length) {
1866
+ html += '<div class="sidebar-section open"><div class="sidebar-section-header">Recent<span class="section-count">'+recentAgents.length+'</span></div><div class="section-body">';
1867
+ html += recentAgents.map(a => compactCard(a, false)).join('');
1868
+ html += '</div></div>';
1869
+ }
1870
+
1871
+ // All agents section (collapsed)
1872
+ if (restAgents.length) {
1873
+ html += '<div class="sidebar-section" id="allAgentsSection"><div class="sidebar-section-header collapsible" onclick="this.parentElement.classList.toggle(\'open\')"><span class="section-arrow">▶</span> All Agents<span class="section-count">'+restAgents.length+'</span></div><div class="section-body">';
1874
+ html += restAgents.map(a => compactCard(a, false)).join('');
1875
+ html += '</div></div>';
1876
+ }
1877
+
1878
+ if (!html) html = '<div style="padding:20px;text-align:center;color:var(--text-muted);font-size:12px">No agents</div>';
1879
+ list.innerHTML = html;
1880
+ }
1881
+
1882
+ // Track which agent the frame was built for — avoid rebuilding header/input unnecessarily
1883
+ let renderedFrameAgent = null;
1884
+
1885
+ function renderChat() {
1886
+ if (!selectedAgent) return;
1887
+ const agent = agents.find(a => a.id === selectedAgent);
1888
+ if (!agent) return;
1889
+
1890
+ // Sync globals from selected agent's active tab stream state
1891
+ const ss = getStream(activeTabKey(selectedAgent));
1892
+ isThinking = ss.isThinking;
1893
+ streamingText = ss.text;
1894
+ streamingStatus = ss.status;
1895
+ streamingTools = ss.tools;
1896
+
1897
+ // Only rebuild the full frame (header + input) when switching agents
1898
+ if (renderedFrameAgent !== selectedAgent) {
1899
+ renderedFrameAgent = selectedAgent;
1900
+ const main = document.getElementById('mainArea');
1901
+ main.innerHTML = `
1902
+ <div class="chat-header">
1903
+ <div class="agent-avatar" style="width:40px;height:40px;font-size:14px">
1904
+ ${agent.aliases[0]?.replace('@','').slice(0,2).toUpperCase() || '??'}
1905
+ </div>
1906
+ <div class="chat-header-info">
1907
+ <div class="chat-header-name${isAgentOrgHidden(agent) ? ' name-blurred' : ''}">${agent.name}</div>
1908
+ <div class="chat-header-alias"><span class="${isAgentOrgHidden(agent) ? 'name-blurred' : ''}">${agent.aliases[0] || agent.id}</span> · ${agent.persistent?'persistent session':'single-shot'}</div>
1909
+ </div>
1910
+ <div class="chat-header-actions">
1911
+ ${availableAccounts.length > 0 ? `<select class="header-btn" style="padding:2px 6px;font-size:11px;cursor:pointer" onchange="accountOverrides['${agent.id}']=this.value||undefined" title="Claude Account override (in-memory only)">
1912
+ <option value="">${agent.claudeAccount || 'default'}</option>
1913
+ ${availableAccounts.filter(a => a !== agent.claudeAccount).map(a => `<option value="${a}" ${accountOverrides[agent.id]===a?'selected':''}>${a}</option>`).join('')}
1914
+ </select>` : ''}
1915
+ <button class="canvas-toggle-btn${Canvas.isOpen() ? ' active' : ''}" onclick="Canvas.toggle()" title="Toggle preview panel">&#x25A8; Canvas</button>
1916
+ ${agent.persistent ? `<button class="header-btn danger" onclick="confirmResetSession('${agent.id}')">&#x21BA; Reset session</button>` : ''}
1917
+ </div>
1918
+ </div>
1919
+ <div class="session-tabs" id="sessionTabBar"></div>
1920
+ <div class="chat-body-wrap${Canvas.isOpen() ? ' canvas-open' : ''}" id="chatBodyWrap">
1921
+ <div class="chat-messages" id="chatMessages"></div>
1922
+ ${Canvas.getPanelHtml()}
1923
+ </div>
1924
+ <div class="recent-files-bar" id="recentFilesBar"></div>
1925
+ <div class="chat-input-wrap">
1926
+ <div class="file-drop-zone hidden" id="fileDropZone"
1927
+ onclick="document.getElementById('fileInput').click()"
1928
+ ondragover="event.preventDefault();this.classList.add('dragover')"
1929
+ ondragleave="this.classList.remove('dragover')"
1930
+ ondrop="event.preventDefault();this.classList.remove('dragover');handleFileDrop(event)">
1931
+ <input type="file" id="fileInput" multiple onchange="handleFileSelect(event)">
1932
+ <div id="fileListArea">
1933
+ <span>Drop files here or click to browse</span>
1934
+ </div>
1935
+ </div>
1936
+ <div class="chat-input-box">
1937
+ <button class="clip-btn" id="clipBtn" onclick="toggleFileZone()" title="Attach files">📎</button>
1938
+ <button class="mic-btn" id="micBtn" onclick="toggleVoice()" title="Click to speak">🎤</button>
1939
+ <button class="mic-btn" id="voiceModeBtn" onclick="toggleVoiceMode()" title="Read responses aloud" style="font-size:14px">🔇</button>
1940
+ <button class="raw-logs-btn${rawLogsOpen ? ' active' : ''}" id="rawLogsBtn" onclick="toggleRawLogs()" title="Raw logs (live feed)">&#x2318;</button>
1941
+ <textarea class="chat-textarea" id="chatInput" placeholder="Message ${agent.aliases[0] || agent.name}..." rows="1"
1942
+ onkeydown="handleKey(event)" oninput="autoResize(this)"></textarea>
1943
+ ${(ss.jobId || ss.isThinking) ? `<button class="stop-btn" onclick="stopStreaming()" title="Stop (Esc)">&#x25A0;</button>` : ''}
1944
+ <button class="send-btn${ss.jobId ? ' queue-mode' : ''}" id="sendBtn" onclick="doSend()">${ss.jobId ? 'Queue' : '→'}</button>
1945
+ </div>
1946
+ <div id="queueContainer"></div>
1947
+ <div class="raw-logs-drawer${rawLogsOpen ? ' open' : ''}" id="rawLogsDrawer">
1948
+ <div class="raw-logs-header">
1949
+ <span style="font-size:10px;color:var(--text-muted);text-transform:uppercase;letter-spacing:.05em">Activity</span>
1950
+ <button class="raw-logs-mode-btn" id="rawModeBtn" onclick="toggleRawMode()">${rawMode ? '● Raw' : '○ Clean'}</button>
1951
+ </div>
1952
+ <pre id="rawLogsContent"></pre>
1953
+ </div>
1954
+ </div>
1955
+ `;
1956
+ setTimeout(() => {
1957
+ const inp = document.getElementById('chatInput');
1958
+ if (inp) inp.focus();
1959
+ renderQueueUI();
1960
+ renderTabBar(selectedAgent);
1961
+ }, 50);
1962
+ } else {
1963
+ // Even on message-only updates, refresh tab bar for activity dots
1964
+ renderTabBar(selectedAgent);
1965
+ }
1966
+
1967
+ // Update only the messages area — leaves header, input, and canvas untouched
1968
+ updateMessages();
1969
+ }
1970
+
1971
+ function updateMessages() {
1972
+ const chatEl = document.getElementById('chatMessages');
1973
+ if (!chatEl) return;
1974
+
1975
+ const agent = agents.find(a => a.id === selectedAgent);
1976
+ if (!agent) return;
1977
+
1978
+ const key = activeTabKey(selectedAgent);
1979
+ const messages = chatHistory[key] || [];
1980
+ const ss = getStream(key);
1981
+ isThinking = ss.isThinking;
1982
+ streamingText = ss.text;
1983
+ streamingStatus = ss.status;
1984
+ streamingTools = ss.tools;
1985
+
1986
+ // Build messages HTML
1987
+ let html = '';
1988
+ if (messages.length === 0 && !streamingText && !streamingTools.length && !isThinking) {
1989
+ html = `<div class="msg-system">Start a conversation with ${agent.aliases[0] || agent.name}</div>`;
1990
+ } else {
1991
+ html = messages.map(m => {
1992
+ const cls = m.role === 'user' ? 'msg-user' : 'msg-agent';
1993
+ const formatted = m.role === 'agent' ? formatMarkdown(m.text) : escapeHtml(m.text);
1994
+ const time = formatTime(m.time);
1995
+ const channelBadge = m.channel && m.channel !== 'web' && m.channel !== 'unknown' ? `<span class="channel-badge">${m.channel}</span>` : '';
1996
+ const toolHtml = (m.tools && m.tools.length) ? renderActivityFeed(m.tools, false) : '';
1997
+ return `${toolHtml}<div class="msg ${cls}">${formatted}<div class="msg-time">${channelBadge}${time}</div></div>`;
1998
+ }).join('');
1999
+ }
2000
+
2001
+ // Streaming state
2002
+ html += `<div id="streamingToolsWrap">${streamingTools.length ? renderActivityFeed(streamingTools, true) : ''}</div>`;
2003
+ if (streamingText) {
2004
+ html += `<div class="msg msg-agent" id="streamingMsg"><div id="streamingContent">${formatMarkdown(streamingText)}</div><div class="msg-time">streaming...</div></div>`;
2005
+ }
2006
+ html += `<div id="thinkingWrap">${isThinking && !streamingText && !streamingTools.length ? `<div class="thinking"><div class="thinking-dots"><span></span><span></span><span></span></div><span class="thinking-text">${streamingStatus || 'Thinking...'}</span></div>` : ''}</div>`;
2007
+
2008
+ chatEl.innerHTML = html;
2009
+
2010
+ // Update input buttons (stop/queue) without touching textarea
2011
+ updateInputButtons(ss);
2012
+ }
2013
+
2014
+ function scrollChat() {
2015
+ setTimeout(() => {
2016
+ const el = document.getElementById('chatMessages');
2017
+ if (el) el.scrollTop = el.scrollHeight;
2018
+ }, 60);
2019
+ }
2020
+
2021
+ // ─── Actions ───────────────────────────────────────────────────
2022
+ async function selectAgent(id) {
2023
+ selectedAgent = id;
2024
+ renderAgentList();
2025
+ await fetchAgentMessages(id);
2026
+ renderChat();
2027
+ scrollChat();
2028
+ }
2029
+
2030
+ async function doSend() {
2031
+ const input = document.getElementById('chatInput');
2032
+ if (!input || !selectedAgent) return;
2033
+ let text = input.value.trim();
2034
+ if (!text && pendingFiles.length === 0) return;
2035
+
2036
+ // Upload files first if any
2037
+ if (pendingFiles.length > 0) {
2038
+ const uploaded = await uploadFiles(selectedAgent);
2039
+ if (uploaded.length > 0) {
2040
+ const fileNotes = uploaded.map(f =>
2041
+ `[File ${f.mode === 'permanent' ? 'saved' : 'attached'}: ${f.name} at ${f.path}]`
2042
+ ).join('\n');
2043
+ text = text ? `${text}\n\n${fileNotes}` : fileNotes;
2044
+ }
2045
+ // Hide drop zone after upload
2046
+ const zone = document.getElementById('fileDropZone');
2047
+ if (zone) zone.classList.add('hidden');
2048
+ const btn = document.getElementById('clipBtn');
2049
+ if (btn) btn.classList.remove('has-files');
2050
+ }
2051
+
2052
+ input.value = '';
2053
+ autoResize(input);
2054
+
2055
+ // Handle /whoami command locally
2056
+ const _tk = activeTabKey(selectedAgent);
2057
+ if (text.trim().toLowerCase() === '/whoami') {
2058
+ if (!chatHistory[_tk]) chatHistory[_tk] = [];
2059
+ chatHistory[_tk].push({ role: 'user', text, time: new Date().toISOString() });
2060
+ updateMessages(); scrollChat();
2061
+ try {
2062
+ const r = await fetch('/api/whoami/' + encodeURIComponent(selectedAgent));
2063
+ const d = await r.json();
2064
+ const now = new Date().toLocaleTimeString();
2065
+ let info = '`claude auth status` ran at ' + now + '\n\n';
2066
+ if (d.email) {
2067
+ info += '| Field | Value |\n|-------|-------|\n'
2068
+ + '| **Logged In** | ' + (d.loggedIn ? 'Yes ✓' : 'No ✗') + ' |\n'
2069
+ + '| **Email** | ' + d.email + ' |\n'
2070
+ + '| **Org** | ' + (d.orgName || 'N/A') + ' |\n'
2071
+ + '| **Plan** | ' + (d.subscriptionType || 'N/A') + ' |\n'
2072
+ + '| **Auth Method** | ' + (d.authMethod || 'N/A') + ' |\n'
2073
+ + '| **Account Key** | ' + (d.accountName || 'default') + ' |\n'
2074
+ + '| **Config Dir** | `' + (d.configDir || 'N/A') + '` |\n'
2075
+ + '| **Agent** | ' + selectedAgent + ' |';
2076
+ } else if (d.loggedIn === false) {
2077
+ info += '**Not logged in.** This agent\'s configured account is not authenticated.\n\n'
2078
+ + '- Account key: ' + (d.accountName || 'default') + '\n'
2079
+ + '- Config dir: `' + (d.configDir || 'N/A') + '`\n'
2080
+ + '- Go to `/settings` to re-authorize.';
2081
+ } else {
2082
+ info += 'Unexpected response: ```\n' + JSON.stringify(d, null, 2) + '\n```';
2083
+ }
2084
+ chatHistory[_tk].push({ role: 'agent', text: info, time: new Date().toISOString() });
2085
+ } catch (e) {
2086
+ chatHistory[_tk].push({ role: 'agent', text: 'Error: ' + e.message, time: new Date().toISOString() });
2087
+ }
2088
+ updateMessages(); scrollChat();
2089
+ return;
2090
+ }
2091
+
2092
+ // If streaming is active, queue the message instead of sending
2093
+ const ss = getStream(_tk);
2094
+ if (ss.isThinking || ss.jobId || ss.text) {
2095
+ ss.queue.push(text);
2096
+ renderQueueUI();
2097
+ return;
2098
+ }
2099
+
2100
+ sendMessage(selectedAgent, text, _tk);
2101
+ }
2102
+
2103
+ function updateInputButtons(ss) {
2104
+ const box = document.querySelector('.chat-input-box');
2105
+ if (!box) return;
2106
+ const sendBtn = document.getElementById('sendBtn');
2107
+ // Add or remove stop button
2108
+ const existingStop = box.querySelector('.stop-btn');
2109
+ const showStop = ss.jobId || ss.isThinking;
2110
+ if (showStop && !existingStop) {
2111
+ const stopBtn = document.createElement('button');
2112
+ stopBtn.className = 'stop-btn';
2113
+ stopBtn.onclick = stopStreaming;
2114
+ stopBtn.title = 'Stop (Esc)';
2115
+ stopBtn.innerHTML = '\u25A0';
2116
+ if (sendBtn) box.insertBefore(stopBtn, sendBtn);
2117
+ } else if (!showStop && existingStop) {
2118
+ existingStop.remove();
2119
+ }
2120
+ // Update send button
2121
+ if (sendBtn) {
2122
+ if (ss.jobId) {
2123
+ sendBtn.classList.add('queue-mode');
2124
+ sendBtn.textContent = 'Queue';
2125
+ sendBtn.disabled = false;
2126
+ } else {
2127
+ sendBtn.classList.remove('queue-mode');
2128
+ sendBtn.innerHTML = '\u2192';
2129
+ sendBtn.disabled = false;
2130
+ }
2131
+ }
2132
+ }
2133
+
2134
+ function renderQueueUI() {
2135
+ const container = document.getElementById('queueContainer');
2136
+ if (!container || !selectedAgent) return;
2137
+ const ss = getStream(activeTabKey(selectedAgent));
2138
+ if (!ss.queue.length) {
2139
+ container.innerHTML = '';
2140
+ return;
2141
+ }
2142
+ container.innerHTML = `<div class="msg-queue">${ss.queue.map((msg, i) => {
2143
+ const truncated = msg.length > 60 ? msg.slice(0, 60) + '...' : msg;
2144
+ return `<div class="msg-queue-item">
2145
+ <span class="msg-queue-pos">${i + 1}</span>
2146
+ <span class="msg-queue-text" title="${msg.replace(/"/g, '&quot;')}">${truncated.replace(/</g, '&lt;')}</span>
2147
+ <div class="msg-queue-actions">
2148
+ <button class="msg-queue-btn" onclick="editQueueItem(${i})" title="Edit">&#x270E;</button>
2149
+ <button class="msg-queue-btn danger" onclick="deleteQueueItem(${i})" title="Delete">&#x2715;</button>
2150
+ </div>
2151
+ </div>`;
2152
+ }).join('')}</div>`;
2153
+ }
2154
+
2155
+ function editQueueItem(idx) {
2156
+ if (!selectedAgent) return;
2157
+ const ss = getStream(activeTabKey(selectedAgent));
2158
+ if (idx < 0 || idx >= ss.queue.length) return;
2159
+ const text = ss.queue.splice(idx, 1)[0];
2160
+ const input = document.getElementById('chatInput');
2161
+ if (input) {
2162
+ input.value = text;
2163
+ input.focus();
2164
+ autoResize(input);
2165
+ }
2166
+ renderQueueUI();
2167
+ }
2168
+
2169
+ function deleteQueueItem(idx) {
2170
+ if (!selectedAgent) return;
2171
+ const ss = getStream(activeTabKey(selectedAgent));
2172
+ if (idx < 0 || idx >= ss.queue.length) return;
2173
+ ss.queue.splice(idx, 1);
2174
+ renderQueueUI();
2175
+ }
2176
+
2177
+ async function stopStreaming() {
2178
+ if (!selectedAgent) return;
2179
+ const agentId = selectedAgent;
2180
+ const tk = activeTabKey(agentId);
2181
+ const ss = getStream(tk);
2182
+ if (ss.jobId) {
2183
+ try {
2184
+ await fetch(`/api/chat/jobs/${ss.jobId}/stop`, { method: 'POST' });
2185
+ } catch (e) {
2186
+ console.warn('Stop request failed:', e);
2187
+ }
2188
+ }
2189
+ // Force-clear UI state even if jobId is already null (desynced state)
2190
+ if (ss.isThinking) {
2191
+ ss.isThinking = false;
2192
+ ss.jobId = null;
2193
+ if (ss.text) {
2194
+ chatHistory[tk].push({ role: 'agent', text: ss.text, time: new Date().toISOString(), tools: ss.tools.length ? [...ss.tools] : undefined });
2195
+ }
2196
+ ss.text = '';
2197
+ ss.status = '';
2198
+ ss.tools = [];
2199
+ if (selectedAgent === agentId) { renderChat(); scrollChat(); }
2200
+ }
2201
+ // Drain queue: auto-send next queued message after stop
2202
+ if (ss.queue.length > 0) {
2203
+ const next = ss.queue.shift();
2204
+ renderQueueUI();
2205
+ sendMessage(agentId, next, tk);
2206
+ }
2207
+ }
2208
+
2209
+ function handleKey(e) {
2210
+ if (e.key === 'Enter' && !e.shiftKey) {
2211
+ e.preventDefault();
2212
+ doSend();
2213
+ }
2214
+ // Escape key stops streaming
2215
+ if (e.key === 'Escape') {
2216
+ stopStreaming();
2217
+ }
2218
+ }
2219
+
2220
+ function autoResize(el) {
2221
+ el.style.height = 'auto';
2222
+ el.style.height = Math.min(el.scrollHeight, 140) + 'px';
2223
+ }
2224
+
2225
+ // ─── Modal: New Agent ──────────────────────────────────────────
2226
+ let availableMcps = [];
2227
+ let routeCount = 0;
2228
+
2229
+ async function openNewAgentModal() {
2230
+ // Fetch available MCPs
2231
+ try {
2232
+ const res = await fetch('/api/mcps');
2233
+ const data = await res.json();
2234
+ availableMcps = data.mcps || [];
2235
+ } catch { availableMcps = []; }
2236
+
2237
+ // Render MCP checkboxes
2238
+ const mcpGroup = document.getElementById('f-mcps');
2239
+ const mcpHint = document.getElementById('f-mcps-hint');
2240
+ if (availableMcps.length > 0) {
2241
+ mcpGroup.innerHTML = availableMcps.map(m =>
2242
+ `<div class="checkbox-pill" data-val="${m}" onclick="togglePill(this)">${m}</div>`
2243
+ ).join('');
2244
+ mcpHint.textContent = 'Select which MCP servers this agent can use.';
2245
+ } else {
2246
+ mcpGroup.innerHTML = '';
2247
+ mcpHint.textContent = 'No MCPs registered. Add them in config.json → mcps.';
2248
+ }
2249
+
2250
+ // Clear form
2251
+ document.getElementById('f-agentId').value = '';
2252
+ document.getElementById('f-alias').value = '';
2253
+ document.getElementById('f-name').value = '';
2254
+ document.getElementById('f-desc').value = '';
2255
+ document.getElementById('f-workspace').value = '~';
2256
+ document.getElementById('f-persistent').classList.add('checked');
2257
+
2258
+ // Reset routes
2259
+ routeCount = 0;
2260
+ document.getElementById('routeEntries').innerHTML = '';
2261
+ addRouteEntry();
2262
+
2263
+ // Reset tools to all checked
2264
+ document.querySelectorAll('#f-tools .checkbox-pill').forEach(p => p.classList.add('checked'));
2265
+
2266
+ document.getElementById('newAgentModal').classList.add('show');
2267
+ }
2268
+
2269
+ function closeModal() {
2270
+ document.getElementById('newAgentModal').classList.remove('show');
2271
+ }
2272
+
2273
+ function togglePill(el) {
2274
+ el.classList.toggle('checked');
2275
+ }
2276
+
2277
+ function autoAlias() {
2278
+ const id = document.getElementById('f-agentId').value.trim();
2279
+ const aliasEl = document.getElementById('f-alias');
2280
+ if (!aliasEl.value || aliasEl.value === '@' + aliasEl.dataset.auto) {
2281
+ const clean = id.replace(/-/g, '');
2282
+ aliasEl.value = clean ? '@' + clean : '';
2283
+ aliasEl.dataset.auto = clean;
2284
+ }
2285
+ }
2286
+
2287
+ function addRouteEntry() {
2288
+ routeCount++;
2289
+ const container = document.getElementById('routeEntries');
2290
+ const channels = agents.length > 0
2291
+ ? [...new Set(agents.flatMap(a => a.routes.map(r => r.split(':')[0])))]
2292
+ : ['telegram', 'slack', 'imessage'];
2293
+
2294
+ const div = document.createElement('div');
2295
+ div.className = 'route-entry';
2296
+ div.id = `route-${routeCount}`;
2297
+ div.innerHTML = `
2298
+ <select>
2299
+ ${channels.map(c => `<option value="${c}">${c}</option>`).join('')}
2300
+ </select>
2301
+ <input placeholder="Chat/Channel ID" />
2302
+ <button class="route-remove" onclick="this.parentElement.remove()">×</button>
2303
+ `;
2304
+ container.appendChild(div);
2305
+ }
2306
+
2307
+ async function createAgent() {
2308
+ const btn = document.getElementById('createBtn');
2309
+ btn.disabled = true;
2310
+ btn.textContent = 'Creating...';
2311
+
2312
+ const agentId = document.getElementById('f-agentId').value.trim();
2313
+ const alias = document.getElementById('f-alias').value.trim();
2314
+ const name = document.getElementById('f-name').value.trim();
2315
+ const description = document.getElementById('f-desc').value.trim();
2316
+ const workspace = document.getElementById('f-workspace').value.trim() || '~';
2317
+ const persistent = document.getElementById('f-persistent').classList.contains('checked');
2318
+
2319
+ const tools = [...document.querySelectorAll('#f-tools .checkbox-pill.checked')]
2320
+ .map(el => el.dataset.val).filter(Boolean);
2321
+
2322
+ const mcps = [...document.querySelectorAll('#f-mcps .checkbox-pill.checked')]
2323
+ .map(el => el.dataset.val).filter(Boolean);
2324
+
2325
+ const routeEls = document.querySelectorAll('.route-entry');
2326
+ const routes = [...routeEls].map(el => {
2327
+ const channel = el.querySelector('select').value;
2328
+ const chatId = el.querySelector('input').value.trim();
2329
+ return chatId ? { channel, chatId, requireMention: true } : null;
2330
+ }).filter(Boolean);
2331
+
2332
+ if (!agentId || !name || !alias) {
2333
+ alert('Agent ID, Name, and Alias are required.');
2334
+ btn.disabled = false;
2335
+ btn.textContent = 'Create Agent';
2336
+ return;
2337
+ }
2338
+
2339
+ try {
2340
+ const res = await fetch('/api/agents', {
2341
+ method: 'POST',
2342
+ headers: { 'Content-Type': 'application/json' },
2343
+ body: JSON.stringify({ agentId, name, description, alias, workspace, persistent, tools, mcps, routes }),
2344
+ });
2345
+ const data = await res.json();
2346
+
2347
+ if (data.ok) {
2348
+ closeModal();
2349
+ await fetchDashboard();
2350
+ selectAgent(agentId);
2351
+ } else {
2352
+ alert('Error: ' + (data.error || 'Unknown error'));
2353
+ }
2354
+ } catch (err) {
2355
+ alert('Error: ' + err.message);
2356
+ }
2357
+
2358
+ btn.disabled = false;
2359
+ btn.textContent = 'Create Agent';
2360
+ }
2361
+
2362
+ // Close modal on overlay click
2363
+ document.getElementById('newAgentModal').addEventListener('click', function(e) {
2364
+ if (e.target === this) closeModal();
2365
+ });
2366
+
2367
+ // Close modal on Escape
2368
+ document.addEventListener('keydown', function(e) {
2369
+ if (e.key === 'Escape') closeModal();
2370
+ });
2371
+
2372
+ async function resetSession(agentId) {
2373
+ if (!confirm('Reset session? This clears conversation history. Use /opcompact first to save important context.')) return;
2374
+ await sendMessage(agentId, '/opreset');
2375
+ }
2376
+ function confirmResetSession(agentId) { resetSession(agentId); }
2377
+
2378
+ function viewConfig(agentId) {
2379
+ const agent = agents.find(a => a.id === agentId);
2380
+ if (!agent) return;
2381
+ const configStr = JSON.stringify({
2382
+ id: agent.id,
2383
+ name: agent.name,
2384
+ aliases: agent.aliases,
2385
+ workspace: agent.workspace,
2386
+ persistent: agent.persistent,
2387
+ mcps: agent.mcps,
2388
+ skills: agent.skills,
2389
+ tools: agent.tools,
2390
+ routes: agent.routes,
2391
+ }, null, 2);
2392
+
2393
+ const _vk = activeTabKey(agentId);
2394
+ if (!chatHistory[_vk]) chatHistory[_vk] = [];
2395
+ chatHistory[_vk].push({
2396
+ role: 'agent',
2397
+ text: '```json\n' + configStr + '\n```',
2398
+ time: new Date().toISOString()
2399
+ });
2400
+ renderChat();
2401
+ scrollChat();
2402
+ }
2403
+
2404
+ // ─── Formatting ────────────────────────────────────────────────
2405
+ function escapeHtml(s) {
2406
+ return s.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
2407
+ }
2408
+
2409
+ function formatMarkdown(text) {
2410
+ if (!text) return '';
2411
+ let html = escapeHtml(text);
2412
+ // Code blocks
2413
+ html = html.replace(/```(\w*)\n([\s\S]*?)```/g, '<pre><code>$2</code></pre>');
2414
+ // Inline code
2415
+ html = html.replace(/`([^`]+)`/g, '<code>$1</code>');
2416
+ // Bold
2417
+ html = html.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>');
2418
+ // Detect file paths and add download/preview buttons (via shared Canvas module)
2419
+ html = Canvas.injectFileButtons(html, escapeHtml);
2420
+ // Break run-on sentences: period followed immediately by capital letter
2421
+ html = html.replace(/\.([A-Z])/g, '.<br>$1');
2422
+ // Paragraph breaks (double newline) get spacing, single newlines get line breaks
2423
+ html = html.replace(/\n{2,}/g, '</p><p>');
2424
+ html = html.replace(/\n/g, '<br>');
2425
+ html = '<p>' + html + '</p>';
2426
+ return html;
2427
+ }
2428
+
2429
+ function toolDetail(t) {
2430
+ if (!t.input) return t.name || 'tool';
2431
+ if (t.name === 'Bash' && t.input.command) return `Bash: ${t.input.command.slice(0, 80)}`;
2432
+ if (t.name === 'Read' && t.input.file_path) return `Read ${t.input.file_path.split('/').pop()}`;
2433
+ if (t.name === 'Write' && t.input.file_path) return `Write ${t.input.file_path.split('/').pop()}`;
2434
+ if (t.name === 'Edit' && t.input.file_path) return `Edit ${t.input.file_path.split('/').pop()}`;
2435
+ if (t.name === 'Grep' && t.input.pattern) return `Grep "${t.input.pattern}"`;
2436
+ if (t.name === 'Glob' && t.input.pattern) return `Glob ${t.input.pattern}`;
2437
+ if (t.name === 'WebSearch') return `WebSearch: ${(t.input.query || t.input.search_query || '').slice(0, 60)}`;
2438
+ if (t.name === 'WebFetch') return `WebFetch: ${(t.input.url || '').slice(0, 60)}`;
2439
+ return t.name || 'tool';
2440
+ }
2441
+
2442
+ function renderActivityFeed(tools, isLive) {
2443
+ if (!tools || !tools.length) return '';
2444
+ const collapsed = !isLive; // auto-collapse when done
2445
+ const lines = tools.map((t, i) => {
2446
+ const isLast = i === tools.length - 1;
2447
+ const dotClass = (isLive && isLast) ? 'active' : 'done';
2448
+ return `<div class="activity-line"><span class="activity-dot ${dotClass}"></span><span class="activity-name">${escapeHtml(toolDetail(t))}</span></div>`;
2449
+ });
2450
+ const uid = 'af-' + Math.random().toString(36).slice(2, 8);
2451
+ const headerText = isLive ? 'Working...' : `${tools.length} steps`;
2452
+ const arrow = collapsed ? '&#x25B8;' : '&#x25BE;';
2453
+ return `<div class="activity-feed ${collapsed ? 'collapsed' : ''}" id="${uid}">` +
2454
+ `<div class="activity-header" onclick="this.parentElement.classList.toggle('collapsed')" style="cursor:pointer">${arrow} ${headerText}</div>` +
2455
+ `<div class="activity-lines">${lines.join('')}</div></div>`;
2456
+ }
2457
+
2458
+ function downloadFile(encodedPath) {
2459
+ if (!selectedAgent) return;
2460
+ const path = decodeURIComponent(encodedPath);
2461
+ const a = document.createElement('a');
2462
+ a.href = `/api/agents/${selectedAgent}/download?path=${encodeURIComponent(path)}`;
2463
+ a.download = path.split('/').pop() || 'download';
2464
+ document.body.appendChild(a);
2465
+ a.click();
2466
+ document.body.removeChild(a);
2467
+ }
2468
+
2469
+ function formatTime(ts) {
2470
+ if (!ts) return '';
2471
+ try {
2472
+ const d = new Date(ts);
2473
+ const now = new Date();
2474
+ const isToday = d.toDateString() === now.toDateString();
2475
+ if (isToday) return d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
2476
+ return d.toLocaleDateString([], { month: 'short', day: 'numeric' }) + ' ' +
2477
+ d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
2478
+ } catch { return ts; }
2479
+ }
2480
+
2481
+ function formatRelativeTime(ts) {
2482
+ if (!ts) return '';
2483
+ try {
2484
+ const d = new Date(ts);
2485
+ const now = new Date();
2486
+ const diff = now - d;
2487
+ const mins = Math.floor(diff / 60000);
2488
+ if (mins < 1) return 'just now';
2489
+ if (mins < 60) return mins + 'm ago';
2490
+ const hours = Math.floor(mins / 60);
2491
+ if (hours < 24) return hours + 'h ago';
2492
+ const days = Math.floor(hours / 24);
2493
+ if (days === 1) return 'yesterday';
2494
+ if (days < 7) return days + 'd ago';
2495
+ return d.toLocaleDateString([], { month: 'short', day: 'numeric' });
2496
+ } catch { return ''; }
2497
+ }
2498
+
2499
+ // ─── File Upload ───────────────────────────────────────────────
2500
+ let pendingFiles = []; // {file: File, mode: 'temp'|'permanent'}
2501
+
2502
+ function toggleFileZone() {
2503
+ const zone = document.getElementById('fileDropZone');
2504
+ if (!zone) return;
2505
+ zone.classList.toggle('hidden');
2506
+ const btn = document.getElementById('clipBtn');
2507
+ if (btn) btn.classList.toggle('has-files', !zone.classList.contains('hidden'));
2508
+ }
2509
+
2510
+ function handleFileDrop(e) {
2511
+ const files = e.dataTransfer?.files;
2512
+ if (files) addFiles(files);
2513
+ }
2514
+
2515
+ function handleFileSelect(e) {
2516
+ const files = e.target?.files;
2517
+ if (files) addFiles(files);
2518
+ e.target.value = ''; // reset so same file can be re-selected
2519
+ }
2520
+
2521
+ // Handle Ctrl+V paste of images
2522
+ document.addEventListener('paste', function(e) {
2523
+ if (!selectedAgent) return;
2524
+ const items = e.clipboardData?.items;
2525
+ if (!items) return;
2526
+ const imageFiles = [];
2527
+ for (const item of items) {
2528
+ if (item.type.startsWith('image/')) {
2529
+ const file = item.getAsFile();
2530
+ if (file) imageFiles.push(file);
2531
+ }
2532
+ }
2533
+ if (imageFiles.length > 0) {
2534
+ e.preventDefault();
2535
+ addFiles(imageFiles);
2536
+ // Show the drop zone if hidden
2537
+ const zone = document.getElementById('fileDropZone');
2538
+ if (zone) zone.classList.remove('hidden');
2539
+ const btn = document.getElementById('clipBtn');
2540
+ if (btn) btn.classList.add('has-files');
2541
+ }
2542
+ });
2543
+
2544
+ function addFiles(fileList) {
2545
+ for (const f of fileList) {
2546
+ if (!pendingFiles.find(p => p.file.name === f.name && p.file.size === f.size)) {
2547
+ pendingFiles.push({ file: f, mode: 'temp' });
2548
+ }
2549
+ }
2550
+ renderFileList();
2551
+ }
2552
+
2553
+ function removeFile(idx) {
2554
+ pendingFiles.splice(idx, 1);
2555
+ renderFileList();
2556
+ if (pendingFiles.length === 0) {
2557
+ const btn = document.getElementById('clipBtn');
2558
+ if (btn) btn.classList.remove('has-files');
2559
+ }
2560
+ }
2561
+
2562
+ function toggleFileMode(idx) {
2563
+ pendingFiles[idx].mode = pendingFiles[idx].mode === 'temp' ? 'permanent' : 'temp';
2564
+ renderFileList();
2565
+ }
2566
+
2567
+ function renderFileList() {
2568
+ const area = document.getElementById('fileListArea');
2569
+ if (!area) return;
2570
+ if (pendingFiles.length === 0) {
2571
+ area.innerHTML = '<span>Drop files here or click to browse</span>';
2572
+ return;
2573
+ }
2574
+ area.innerHTML = '<div class="file-list">' + pendingFiles.map((p, i) => {
2575
+ const size = p.file.size > 1024*1024
2576
+ ? (p.file.size/(1024*1024)).toFixed(1) + ' MB'
2577
+ : (p.file.size/1024).toFixed(0) + ' KB';
2578
+ const storeIcon = p.mode === 'permanent' ? '💾' : '⏳';
2579
+ const storeTitle = p.mode === 'permanent' ? 'Permanent (click for temp)' : 'Temp (click to save permanently)';
2580
+ return `<div class="file-pill" onclick="event.stopPropagation()">
2581
+ <span class="file-pill-name">${escapeHtml(p.file.name)}</span>
2582
+ <span class="file-pill-size">${size}</span>
2583
+ <span class="file-pill-store ${p.mode==='permanent'?'active':''}" onclick="toggleFileMode(${i})" title="${storeTitle}">${storeIcon}</span>
2584
+ <span class="file-pill-remove" onclick="removeFile(${i})">×</span>
2585
+ </div>`;
2586
+ }).join('') + '</div>';
2587
+ }
2588
+
2589
+ async function uploadFiles(agentId) {
2590
+ const uploaded = [];
2591
+ for (const p of pendingFiles) {
2592
+ try {
2593
+ const formData = new FormData();
2594
+ formData.append('file', p.file);
2595
+ formData.append('mode', p.mode);
2596
+
2597
+ const resp = await fetch(`/api/upload/${agentId}`, {
2598
+ method: 'POST',
2599
+ body: formData,
2600
+ });
2601
+ const data = await resp.json();
2602
+ if (data.ok) {
2603
+ uploaded.push({ name: data.fileName, path: data.path, mode: data.mode });
2604
+ }
2605
+ } catch (err) {
2606
+ console.warn('Upload failed:', p.file.name, err);
2607
+ }
2608
+ }
2609
+ pendingFiles = [];
2610
+ renderFileList();
2611
+ return uploaded;
2612
+ }
2613
+
2614
+ // ─── Voice Input ───────────────────────────────────────────────
2615
+ let recognition = null;
2616
+ let isRecording = false;
2617
+
2618
+ function toggleVoice() {
2619
+ if (!('webkitSpeechRecognition' in window) && !('SpeechRecognition' in window)) {
2620
+ alert('Speech recognition not supported in this browser. Use Chrome or Edge.');
2621
+ return;
2622
+ }
2623
+
2624
+ if (isRecording) {
2625
+ stopVoice();
2626
+ } else {
2627
+ startVoice();
2628
+ }
2629
+ }
2630
+
2631
+ function startVoice() {
2632
+ const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
2633
+ recognition = new SpeechRecognition();
2634
+ recognition.continuous = true;
2635
+ recognition.interimResults = true;
2636
+ recognition.lang = 'en-US';
2637
+
2638
+ let finalTranscript = '';
2639
+ const input = document.getElementById('chatInput');
2640
+ const existingText = input ? input.value : '';
2641
+
2642
+ recognition.onresult = (event) => {
2643
+ let interim = '';
2644
+ for (let i = event.resultIndex; i < event.results.length; i++) {
2645
+ if (event.results[i].isFinal) {
2646
+ finalTranscript += event.results[i][0].transcript + ' ';
2647
+ } else {
2648
+ interim += event.results[i][0].transcript;
2649
+ }
2650
+ }
2651
+ if (input) {
2652
+ input.value = existingText + (existingText ? ' ' : '') + finalTranscript + interim;
2653
+ autoResize(input);
2654
+ }
2655
+ };
2656
+
2657
+ recognition.onerror = (event) => {
2658
+ console.warn('Speech recognition error:', event.error);
2659
+ stopVoice();
2660
+ };
2661
+
2662
+ recognition.onend = () => {
2663
+ // If still in recording mode, it ended unexpectedly — clean up
2664
+ if (isRecording) stopVoice();
2665
+ };
2666
+
2667
+ recognition.start();
2668
+ isRecording = true;
2669
+
2670
+ const btn = document.getElementById('micBtn');
2671
+ if (btn) {
2672
+ btn.classList.add('recording');
2673
+ btn.title = 'Click to stop';
2674
+ }
2675
+ }
2676
+
2677
+ function stopVoice() {
2678
+ if (recognition) {
2679
+ recognition.stop();
2680
+ recognition = null;
2681
+ }
2682
+ isRecording = false;
2683
+
2684
+ const btn = document.getElementById('micBtn');
2685
+ if (btn) {
2686
+ btn.classList.remove('recording');
2687
+ btn.title = 'Click to speak';
2688
+ }
2689
+
2690
+ // Focus the input so user can edit or send
2691
+ const input = document.getElementById('chatInput');
2692
+ if (input) input.focus();
2693
+ }
2694
+
2695
+ // ─── Canvas / Preview Panel ────────────────────────────────────
2696
+ function getFileIcon(ext) {
2697
+ const icons = {
2698
+ html:'&#x1F310;', htm:'&#x1F310;',
2699
+ csv:'&#x1F4CA;', tsv:'&#x1F4CA;', xlsx:'&#x1F4CA;', xls:'&#x1F4CA;',
2700
+ md:'&#x1F4DD;', markdown:'&#x1F4DD;',
2701
+ png:'&#x1F5BC;', jpg:'&#x1F5BC;', jpeg:'&#x1F5BC;', gif:'&#x1F5BC;', svg:'&#x1F5BC;',
2702
+ pdf:'&#x1F4C4;',
2703
+ docx:'&#x1F4C4;', doc:'&#x1F4C4;',
2704
+ pptx:'&#x1F4CA;', ppt:'&#x1F4CA;',
2705
+ json:'&#x007B;&#x007D;',
2706
+ js:'JS', ts:'TS', py:'PY', rb:'RB', go:'GO', rs:'RS', java:'JV',
2707
+ xml:'&#x2329;&#x232A;', yaml:'&#x2699;', yml:'&#x2699;',
2708
+ txt:'&#x1F4C3;',
2709
+ };
2710
+ return icons[ext] || '&#x1F4C1;';
2711
+ }
2712
+
2713
+ // Canvas functions provided by shared canvas.js module (getFileType, toggleCanvas, openInCanvas, etc.)
2714
+ // Backwards-compat globals: openInCanvas, closeCanvas, toggleCanvas, navigatePptxSlide, sortCsvTable, renderXlsxSheet
2715
+
2716
+ // (removed: inline canvas functions — now in canvas.js)
2717
+ // ─── Close docs dropdown on outside click ──────────────────────
2718
+ document.addEventListener('click', (e) => {
2719
+ const dd = document.getElementById('docsDropdown');
2720
+ if (dd && !dd.contains(e.target)) {
2721
+ document.getElementById('docsMenu')?.classList.remove('open');
2722
+ }
2723
+ });
2724
+
2725
+ // ─── Init ──────────────────────────────────────────────────────
2726
+ fetchDashboard().then(() => {
2727
+ // Check URL hash for deep-link to agent (from org page "Chat" or activity page)
2728
+ // Format: #agentId or #agentId?ts=2026-03-25T12:00:00Z
2729
+ const rawHash = window.location.hash.slice(1);
2730
+ const [hashId, hashParams] = rawHash.split('?');
2731
+ const tsParam = hashParams ? new URLSearchParams(hashParams).get('ts') : null;
2732
+ const msgParam = hashParams ? new URLSearchParams(hashParams).get('msg') : null;
2733
+
2734
+ if (hashId && agents.find(a => a.id === hashId)) {
2735
+ // Auto-filter to this agent's org/dept
2736
+ const agent = agents.find(a => a.id === hashId);
2737
+ selectAgent(hashId).then(() => {
2738
+ // Scroll to specific message if ts param provided
2739
+ if (tsParam) {
2740
+ setTimeout(() => scrollToMessage(decodeURIComponent(tsParam)), 300);
2741
+ }
2742
+ // Pre-fill and auto-send message if msg param provided
2743
+ if (msgParam) {
2744
+ setTimeout(() => {
2745
+ const inp = document.getElementById('chatInput');
2746
+ if (inp) { inp.value = decodeURIComponent(msgParam); doSend(); }
2747
+ }, 300);
2748
+ }
2749
+ });
2750
+ window.location.hash = ''; // clean up URL
2751
+ }
2752
+ });
2753
+ setInterval(fetchDashboard, 30000);
2754
+
2755
+ // ─── Scroll to specific message by timestamp ───────────
2756
+ function scrollToMessage(ts) {
2757
+ if (!selectedAgent) return;
2758
+ const messages = chatHistory[activeTabKey(selectedAgent)] || [];
2759
+ // Find the message index matching this timestamp
2760
+ const idx = messages.findIndex(m => m.time === ts);
2761
+ if (idx < 0) return;
2762
+
2763
+ const chatEl = document.getElementById('chatMessages');
2764
+ if (!chatEl) return;
2765
+ const msgEls = chatEl.querySelectorAll('.msg');
2766
+ if (idx >= msgEls.length) return;
2767
+
2768
+ const targetEl = msgEls[idx];
2769
+ targetEl.scrollIntoView({ behavior: 'smooth', block: 'center' });
2770
+ // Flash highlight
2771
+ targetEl.style.transition = 'box-shadow 0.3s';
2772
+ targetEl.style.boxShadow = '0 0 0 2px var(--accent), 0 0 20px rgba(34,211,238,0.3)';
2773
+ setTimeout(() => { targetEl.style.boxShadow = ''; }, 3000);
2774
+ }
2775
+
2776
+ // ─── Sidebar Toggle ────────────────────────────────────
2777
+ function toggleSidebar() {
2778
+ const sidebar = document.querySelector('.sidebar');
2779
+ const expandBtn = document.getElementById('sidebarExpandBtn');
2780
+ sidebar.classList.toggle('collapsed');
2781
+ const isCollapsed = sidebar.classList.contains('collapsed');
2782
+ if (expandBtn) expandBtn.classList.toggle('visible', isCollapsed);
2783
+ localStorage.setItem('sidebarCollapsed', isCollapsed);
2784
+ }
2785
+ // Restore sidebar state
2786
+ (function() {
2787
+ if (localStorage.getItem('sidebarCollapsed') === 'true') {
2788
+ document.querySelector('.sidebar')?.classList.add('collapsed');
2789
+ document.getElementById('sidebarExpandBtn')?.classList.add('visible');
2790
+ }
2791
+ })();
2792
+ </script>
2793
+
2794
+ <!-- Session Picker Modal -->
2795
+ <div class="modal-overlay" id="sessionPickerModal" onclick="if(event.target===this)closeSessionPicker()">
2796
+ <div class="modal" style="width:420px">
2797
+ <div class="modal-header">
2798
+ <span class="modal-title">Sessions</span>
2799
+ <button class="modal-close" onclick="closeSessionPicker()">×</button>
2800
+ </div>
2801
+ <div id="sessionPickerList"></div>
2802
+ <div class="picker-footer">
2803
+ <button class="picker-new-btn" onclick="createNewSession()">+ New Session</button>
2804
+ </div>
2805
+ </div>
2806
+ </div>
2807
+ <script>(function(){fetch("/api/config/service").then(function(r){return r.json()}).then(function(d){if(d.gymEnabled)document.querySelectorAll(".gym-tab-link").forEach(function(el){el.style.display=""});if(d.gymOnlyMode){var keep=["/gym","/org","/admin"];document.querySelectorAll(".tab-group .tab-btn").forEach(function(el){var h=el.getAttribute("href");if(h&&keep.indexOf(h)===-1&&!el.classList.contains("gym-tab-link"))el.style.display="none"});document.querySelectorAll(".topbar-right .gear-btn").forEach(function(el){var h=el.getAttribute("href");if(h&&["/marketplace","/monitor","/user-guide"].indexOf(h)!==-1)el.style.display="none"})}}).catch(function(){})})()</script>
2808
+ </body>
2809
+ </html>