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,1930 @@
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
+ :root{
16
+ --bg-deep:#060a13;
17
+ --bg-surface:rgba(12,18,33,0.92);
18
+ --bg-card:rgba(16,22,40,0.85);
19
+ --bg-input:rgba(0,0,0,0.35);
20
+ --border-dim:rgba(56,189,248,0.08);
21
+ --border-glow:rgba(56,189,248,0.18);
22
+ --border-active:rgba(56,189,248,0.45);
23
+ --text-primary:rgba(255,255,255,0.92);
24
+ --text-secondary:rgba(255,255,255,0.68);
25
+ --text-muted:rgba(148,163,184,0.55);
26
+ --accent:#22d3ee;
27
+ --accent-soft:#38bdf8;
28
+ --accent-bg:rgba(6,182,212,0.15);
29
+ --purple:rgba(139,92,246,0.7);
30
+ --purple-bg:rgba(139,92,246,0.12);
31
+ --green:#4ade80;
32
+ --green-bg:rgba(74,222,128,0.1);
33
+ --amber:#fbbf24;
34
+ --shadow:0 2px 12px rgba(0,0,0,0.3);
35
+ --radius:12px;
36
+ --font-sans:'DM Sans',system-ui,-apple-system,sans-serif;
37
+ --font-mono:'IBM Plex Mono',monospace;
38
+ --font-display:'Syne',sans-serif;
39
+ }
40
+ [data-theme="light"]{
41
+ --bg-deep:#f4f6f9;
42
+ --bg-surface:rgba(255,255,255,0.95);
43
+ --bg-card:rgba(255,255,255,0.9);
44
+ --bg-input:rgba(0,0,0,0.04);
45
+ --border-dim:rgba(0,0,0,0.08);
46
+ --border-glow:rgba(14,116,144,0.18);
47
+ --border-active:rgba(14,116,144,0.45);
48
+ --text-primary:rgba(15,23,42,0.92);
49
+ --text-secondary:rgba(51,65,85,0.8);
50
+ --text-muted:rgba(100,116,139,0.6);
51
+ --accent:#0891b2;
52
+ --accent-soft:#0e7490;
53
+ --accent-bg:rgba(14,116,144,0.08);
54
+ --purple:rgba(109,40,217,0.75);
55
+ --purple-bg:rgba(139,92,246,0.08);
56
+ --green:#16a34a;
57
+ --green-bg:rgba(22,163,74,0.08);
58
+ --amber:#d97706;
59
+ --shadow:0 1px 8px rgba(0,0,0,0.06);
60
+ }
61
+ html,body{width:100%;height:100%;overflow:hidden;background:var(--bg-deep);font-family:var(--font-sans);color:var(--text-primary);transition:background .3s,color .3s}
62
+
63
+ /* ─── Layout ──────────────────────────────────────────── */
64
+ .home{display:flex;flex-direction:column;height:100vh;position:relative}
65
+
66
+ /* ─── Landing State (centered) ────────────────────────── */
67
+ .landing{
68
+ flex:1;display:flex;flex-direction:column;align-items:center;justify-content:center;
69
+ transition:all 0.5s ease;padding:20px;
70
+ }
71
+ .landing.hidden{opacity:0;pointer-events:none;position:absolute;top:0;left:0;right:0;bottom:0}
72
+
73
+ .landing-logo{font-family:var(--font-display);font-size:28px;font-weight:800;color:var(--accent);margin-bottom:8px}
74
+ .landing-sub{font-size:14px;color:var(--text-muted);margin-bottom:32px}
75
+ .landing-input-wrap{width:100%;max-width:600px}
76
+ .landing-input-box{
77
+ display:flex;align-items:center;gap:10px;
78
+ background:var(--bg-surface);border:1px solid var(--border-glow);
79
+ border-radius:16px;padding:8px 12px;
80
+ box-shadow:0 4px 24px rgba(0,0,0,0.3),0 0 40px rgba(34,211,238,0.06);
81
+ transition:border-color .3s;
82
+ }
83
+ .landing-input-box:focus-within{border-color:var(--border-active)}
84
+ .landing-textarea{
85
+ flex:1;background:transparent;border:none;outline:none;
86
+ color:var(--text-primary);font-family:var(--font-sans);font-size:15px;
87
+ resize:none;max-height:120px;line-height:1.5;padding:8px 4px;
88
+ }
89
+ .landing-textarea::placeholder{color:var(--text-muted)}
90
+ .landing-send{
91
+ width:44px;height:44px;border-radius:12px;border:none;
92
+ background:var(--accent);color:#000;cursor:pointer;
93
+ font-size:18px;font-weight:700;display:flex;align-items:center;justify-content:center;
94
+ transition:all .2s;flex-shrink:0;
95
+ }
96
+ .landing-send:hover{filter:brightness(1.1);transform:scale(1.05)}
97
+ .agent-count{font-size:12px;color:var(--text-muted);margin-top:16px}
98
+
99
+ /* ─── Agent Tiles ─────────────────────────────────────── */
100
+ .tiles-wrap{margin-top:16px;display:flex;align-items:center;gap:8px;max-width:600px;flex-wrap:wrap;justify-content:center}
101
+ .org-select{
102
+ font-family:var(--font-mono);font-size:11px;padding:4px 10px;border-radius:8px;
103
+ border:1px solid var(--border-glow);background:var(--bg-input);
104
+ color:var(--text-primary);outline:none;cursor:pointer;
105
+ }
106
+ .agent-tiles{
107
+ display:flex;gap:8px;flex-wrap:wrap;align-items:center;
108
+ }
109
+ .agent-tile{
110
+ display:flex;align-items:center;gap:6px;
111
+ padding:6px 12px;border-radius:10px;
112
+ background:var(--bg-card);border:1px solid var(--border-dim);
113
+ cursor:pointer;transition:all .2s;font-size:12px;
114
+ }
115
+ .agent-tile:hover{border-color:var(--border-glow);background:var(--bg-surface)}
116
+ .tile-avatar{
117
+ width:24px;height:24px;border-radius:6px;
118
+ background:var(--accent-bg);border:1px solid rgba(34,211,238,0.3);
119
+ display:flex;align-items:center;justify-content:center;
120
+ font-family:var(--font-mono);font-size:9px;font-weight:600;color:var(--accent);
121
+ }
122
+ .tile-name{color:var(--text-secondary);font-weight:500}
123
+ .tile-time{color:var(--text-muted);font-size:10px}
124
+
125
+ /* ─── Chat State ──────────────────────────────────────── */
126
+ .chat-view{
127
+ flex:1;display:flex;flex-direction:column;
128
+ opacity:0;pointer-events:none;position:absolute;top:0;left:0;right:0;bottom:0;
129
+ transition:all 0.5s ease;overflow:hidden;
130
+ }
131
+ .chat-view.active{opacity:1;pointer-events:all;position:relative}
132
+
133
+ /* ─── Topbar (matches /ui) ────────────────────────────── */
134
+ .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);flex-shrink:0;overflow:hidden;max-width:100vw}
135
+ .tab-group{display:flex;gap:0;flex-shrink:1;min-width:0}
136
+ .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}
137
+ .tab-btn:hover{color:var(--text-secondary)}
138
+ .tab-btn.active{color:var(--accent)}
139
+ .tab-btn.active::after{content:'';position:absolute;bottom:0;left:10px;right:10px;height:2px;background:var(--accent);border-radius:1px}
140
+ .topbar-right{margin-left:auto;display:flex;align-items:center;gap:8px;flex-shrink:0}
141
+ .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}
142
+ .gym-nav-btn:hover{opacity:.88;transform:translateY(-1px)}
143
+ .logo-mark{width:28px;height:28px;border-radius:8px;object-fit:contain}
144
+ .logo-text{font-family:var(--font-display);font-size:15px;font-weight:700;color:var(--accent)}
145
+ /* Second-level bar for hub actions */
146
+ .hub-actions-bar{
147
+ padding:6px 20px;border-bottom:1px solid var(--border-dim);
148
+ display:flex;align-items:center;gap:8px;
149
+ background:var(--bg-surface);flex-shrink:0;
150
+ }
151
+ .hub-actions-bar .hub-label{font-family:var(--font-display);font-size:12px;font-weight:700;color:var(--accent);margin-right:8px}
152
+ .header-btn{font-family:var(--font-sans);font-size:11px;font-weight:600;padding:4px 10px;border-radius:6px;border:1px solid var(--border-dim);background:transparent;color:var(--text-muted);cursor:pointer;transition:all .2s;text-decoration:none}
153
+ .header-btn:hover{border-color:var(--border-glow);color:var(--text-secondary)}
154
+ .header-btn.danger{color:var(--text-muted);border-color:rgba(248,113,113,0.2)}
155
+ .header-btn.danger:hover{color:#f87171;border-color:rgba(248,113,113,0.4);background:rgba(248,113,113,0.08)}
156
+ .docs-dropdown{position:relative;display:inline-block}
157
+ .docs-btn{display:flex;align-items:center;justify-content:center;width:28px;height:28px;border-radius:6px;border:1px solid var(--border-dim);background:transparent;color:var(--text-muted);cursor:pointer;font-size:13px;transition:all .2s}
158
+ .docs-btn:hover{border-color:var(--border-glow);color:var(--text-secondary)}
159
+ .docs-menu{display:none;position:absolute;top:34px;right:0;min-width:130px;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)}
160
+ .docs-menu.open{display:block}
161
+ .docs-menu a{display:flex;align-items:center;gap:8px;padding:6px 10px;font-size:11px;font-weight:500;color:var(--text-secondary);text-decoration:none;border-radius:6px;transition:all .15s}
162
+ .docs-menu a:hover{background:var(--accent-bg);color:var(--accent)}
163
+ .docs-menu a .dm-icon{font-size:13px;width:16px;text-align:center}
164
+ .theme-toggle,.gear-btn{
165
+ background:none;border:1px solid var(--border-dim);border-radius:6px;
166
+ padding:4px 8px;cursor:pointer;font-size:14px;color:var(--text-muted);transition:all .2s;
167
+ text-decoration:none;}
168
+ .theme-toggle:hover,.gear-btn:hover{border-color:var(--border-glow);color:var(--text-secondary)}
169
+
170
+ /* Landing needs padding below topbar */
171
+ .landing{padding-top:48px}
172
+ .landing-header .logo{font-family:var(--font-display);font-size:16px;font-weight:700;color:var(--accent)}
173
+
174
+ .chat-messages-home{
175
+ flex:1;overflow-y:auto;padding:20px;min-height:0;
176
+ display:flex;flex-direction:column;gap:12px;
177
+ }
178
+ .chat-body-wrap.canvas-open .chat-messages-home{flex:0 0 55%}
179
+
180
+ /* ─── Recent Files Bar ─────────────────────────────────────── */
181
+ .recent-files-bar{
182
+ display:flex;align-items:center;gap:6px;padding:4px 12px;
183
+ border-top:1px solid var(--border-dim);background:var(--bg-surface);
184
+ overflow-x:auto;flex-shrink:0;
185
+ }
186
+ .recent-files-bar:empty{display:none}
187
+ .recent-files-label{
188
+ font-family:var(--font-mono);font-size:9px;font-weight:600;
189
+ color:var(--text-muted);text-transform:uppercase;letter-spacing:.04em;
190
+ white-space:nowrap;flex-shrink:0;
191
+ }
192
+ .recent-file-chip{
193
+ display:inline-flex;align-items:center;gap:4px;
194
+ padding:2px 8px;border-radius:5px;
195
+ border:1px solid var(--border-dim);background:var(--bg-input);
196
+ font-family:var(--font-mono);font-size:10px;color:var(--text-secondary);
197
+ cursor:pointer;transition:all .15s;white-space:nowrap;flex-shrink:0;
198
+ }
199
+ .recent-file-chip:hover{border-color:var(--border-glow);color:var(--text-primary);background:var(--accent-bg)}
200
+ .recent-file-chip .chip-icon{font-size:12px}
201
+ .recent-file-chip .chip-folder{font-size:10px;opacity:.5;margin-left:2px;cursor:pointer}
202
+ .recent-file-chip .chip-folder:hover{opacity:1}
203
+ .chat-messages-home::-webkit-scrollbar{width:4px}
204
+ .chat-messages-home::-webkit-scrollbar-thumb{background:var(--border-glow);border-radius:2px}
205
+
206
+ .msg{padding:12px 16px;border-radius:14px;max-width:75%;line-height:1.6;font-size:14px;word-wrap:break-word;position:relative}
207
+ .msg-user{background:rgba(56,189,248,0.08);align-self:flex-end;border-bottom-right-radius:4px}
208
+ .msg-agent{background:rgba(139,92,246,0.06);align-self:flex-start;border-bottom-left-radius:4px}
209
+ .msg-time{font-size:10px;color:var(--text-muted);margin-top:6px;display:flex;align-items:center;gap:6px}
210
+ .msg code{background:rgba(0,0,0,0.3);padding:1px 5px;border-radius:4px;font-family:var(--font-mono);font-size:12px}
211
+ .msg pre{background:rgba(0,0,0,0.3);padding:12px;border-radius:8px;overflow-x:auto;margin:8px 0}
212
+ .msg pre code{background:none;padding:0}
213
+
214
+ .via-badge{
215
+ display:inline-flex;align-items:center;gap:3px;
216
+ padding:1px 8px;border-radius:4px;
217
+ background:var(--purple-bg);color:var(--purple);
218
+ font-family:var(--font-mono);font-size:10px;font-weight:500;
219
+ }
220
+
221
+ .thinking{display:flex;align-items:center;gap:10px;padding:12px 16px;color:var(--text-muted);font-size:13px}
222
+ .thinking-dots span{display:inline-block;width:6px;height:6px;border-radius:50%;background:var(--accent);margin:0 2px;animation:bounce .6s infinite alternate}
223
+ .thinking-dots span:nth-child(2){animation-delay:.2s}
224
+ .thinking-dots span:nth-child(3){animation-delay:.4s}
225
+ @keyframes bounce{to{opacity:.3;transform:translateY(-4px)}}
226
+
227
+ .chat-input-home{
228
+ padding:12px 20px 16px;border-top:1px solid var(--border-dim);
229
+ background:var(--bg-surface);flex-shrink:0;
230
+ }
231
+ .chat-input-home-box{
232
+ display:flex;align-items:center;gap:10px;max-width:800px;margin:0 auto;
233
+ }
234
+ .chat-input-home-box textarea{
235
+ flex:1;background:var(--bg-input);border:1px solid var(--border-dim);
236
+ border-radius:12px;padding:10px 14px;
237
+ color:var(--text-primary);font-family:var(--font-sans);font-size:14px;
238
+ resize:none;max-height:120px;line-height:1.5;outline:none;
239
+ transition:border-color .2s;
240
+ }
241
+ .chat-input-home-box textarea:focus{border-color:var(--border-active)}
242
+ .chat-input-home-box textarea::placeholder{color:var(--text-muted)}
243
+ .chat-input-home-box > .home-send-btn{
244
+ width:40px;height:40px;border-radius:10px;border:none;
245
+ background:var(--accent);color:#000;cursor:pointer;
246
+ font-size:16px;font-weight:700;flex-shrink:0;transition:all .2s;
247
+ }
248
+ .chat-input-home-box > .home-send-btn:hover{filter:brightness(1.1);transform:scale(1.05)}
249
+
250
+ /* Stop & Queue */
251
+ .home-stop-btn{
252
+ width:40px;height:40px;border-radius:10px;border:none;
253
+ background:rgba(239,68,68,0.85);color:#fff;cursor:pointer;
254
+ display:flex;align-items:center;justify-content:center;
255
+ font-size:16px;transition:all .2s;flex-shrink:0;
256
+ }
257
+ .home-stop-btn:hover{filter:brightness(1.1);transform:scale(1.05);background:#ef4444}
258
+ .home-send-btn.queue-mode{background:var(--amber);color:#fff;font-size:11px;font-weight:700;font-family:var(--font-mono);width:auto;padding:0 14px;letter-spacing:.02em}
259
+ .home-send-btn.queue-mode:hover{filter:brightness(1.1);transform:scale(1.05)}
260
+ .home-queue{
261
+ max-width:800px;margin:4px auto 0;display:flex;flex-direction:column;gap:4px;
262
+ }
263
+ .home-queue-item{
264
+ display:flex;align-items:center;gap:8px;padding:4px 10px;
265
+ background:var(--bg-card);border:1px solid var(--border-dim);border-radius:8px;
266
+ font-size:11px;font-family:var(--font-mono);color:var(--text-secondary);
267
+ }
268
+ .home-queue-item .q-pos{color:var(--accent);font-weight:600;flex-shrink:0}
269
+ .home-queue-item .q-text{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
270
+ .home-queue-item .q-btn{
271
+ background:none;border:1px solid var(--border-dim);color:var(--text-muted);
272
+ width:22px;height:22px;border-radius:4px;cursor:pointer;font-size:10px;
273
+ display:flex;align-items:center;justify-content:center;
274
+ }
275
+ .home-queue-item .q-btn:hover{border-color:var(--border-glow);color:var(--text-secondary)}
276
+ .home-queue-item .q-btn.danger:hover{border-color:rgba(239,68,68,.3);color:#ef4444}
277
+
278
+ /* ─── Session Tabs ──────────────────────────────────────────── */
279
+ .session-tabs{
280
+ display:flex;align-items:center;gap:0;
281
+ padding:0 16px;
282
+ border-bottom:1px solid var(--border-dim);
283
+ background:var(--bg-surface);
284
+ min-height:32px;
285
+ overflow-x:auto;
286
+ scrollbar-width:none;
287
+ flex-shrink:0;
288
+ }
289
+ .session-tabs::-webkit-scrollbar{display:none}
290
+ .session-tab{
291
+ display:flex;align-items:center;gap:4px;
292
+ padding:6px 10px;
293
+ font-family:var(--font-mono);font-size:11px;
294
+ color:var(--text-muted);cursor:pointer;
295
+ border-bottom:2px solid transparent;
296
+ white-space:nowrap;transition:all .15s;
297
+ position:relative;
298
+ }
299
+ .session-tab:hover{color:var(--text-secondary);background:rgba(255,255,255,.03)}
300
+ .session-tab.active{
301
+ color:var(--accent);border-bottom-color:var(--accent);
302
+ background:rgba(34,211,238,.05);
303
+ }
304
+ .session-tab .tab-label{max-width:120px;overflow:hidden;text-overflow:ellipsis}
305
+ .session-tab .tab-close{
306
+ display:none;font-size:13px;line-height:1;
307
+ padding:0 2px;border-radius:3px;color:var(--text-muted);
308
+ cursor:pointer;margin-left:2px;
309
+ }
310
+ .session-tab:hover .tab-close{display:inline}
311
+ .session-tab .tab-close:hover{color:#ef4444;background:rgba(239,68,68,.15)}
312
+ .session-tab .tab-dot{
313
+ width:5px;height:5px;border-radius:50%;
314
+ background:var(--accent);display:none;
315
+ }
316
+ .session-tab.has-activity .tab-dot{display:inline-block}
317
+ .session-tab-add{
318
+ padding:4px 8px;font-size:14px;line-height:1;
319
+ color:var(--text-muted);cursor:pointer;
320
+ border-radius:4px;transition:all .15s;
321
+ flex-shrink:0;
322
+ }
323
+ .session-tab-add:hover{color:var(--accent);background:rgba(255,255,255,.05)}
324
+ .tab-rename-input{
325
+ font-family:var(--font-mono);font-size:11px;
326
+ background:transparent;border:1px solid var(--accent);
327
+ color:var(--text-primary);padding:1px 4px;border-radius:3px;
328
+ outline:none;width:90px;
329
+ }
330
+
331
+ /* Bottom agent bar in chat mode */
332
+ .chat-agent-bar{
333
+ display:flex;align-items:center;gap:8px;max-width:800px;margin:8px auto 0;flex-wrap:wrap;
334
+ }
335
+ .chat-agent-bar .org-select{flex-shrink:0}
336
+ .chat-tiles{
337
+ display:flex;gap:6px;flex-wrap:wrap;align-items:center;
338
+ }
339
+ .chat-tiles .agent-tile{padding:4px 10px;font-size:11px}
340
+ .chat-tiles .tile-avatar{width:20px;height:20px;font-size:8px;border-radius:5px}
341
+
342
+ /* @mention autocomplete */
343
+ .autocomplete{
344
+ position:absolute;bottom:100%;left:60px;right:60px;
345
+ max-height:240px;overflow-y:auto;
346
+ background:var(--bg-surface);border:1px solid var(--border-glow);
347
+ border-radius:10px;box-shadow:0 -4px 20px rgba(0,0,0,0.3);
348
+ display:none;z-index:20;
349
+ }
350
+ .autocomplete.visible{display:block}
351
+ .autocomplete-item{
352
+ display:flex;align-items:center;gap:10px;
353
+ padding:8px 14px;cursor:pointer;transition:background .15s;
354
+ }
355
+ .autocomplete-item:hover,.autocomplete-item.selected{background:var(--accent-bg)}
356
+ .autocomplete-item-avatar{
357
+ width:26px;height:26px;border-radius:6px;flex-shrink:0;
358
+ background:var(--accent-bg);border:1px solid rgba(34,211,238,0.3);
359
+ display:flex;align-items:center;justify-content:center;
360
+ font-family:var(--font-mono);font-size:9px;font-weight:600;color:var(--accent);
361
+ }
362
+ .autocomplete-item-info{flex:1;min-width:0}
363
+ .autocomplete-item-alias{font-family:var(--font-mono);font-size:12px;color:var(--accent);font-weight:500}
364
+ .autocomplete-item-name{font-size:11px;color:var(--text-muted)}
365
+
366
+ /* Input action buttons */
367
+ .input-action{
368
+ width:36px;height:36px;border-radius:10px;border:1px solid var(--border-dim);
369
+ background:transparent;color:var(--text-muted);cursor:pointer;
370
+ display:flex;align-items:center;justify-content:center;
371
+ font-size:16px;transition:all .2s;flex-shrink:0;
372
+ }
373
+ .input-action:hover{border-color:var(--border-glow);color:var(--text-secondary)}
374
+ .input-action.recording{border-color:#ef4444;color:#ef4444;animation:pulse-rec 1.5s infinite}
375
+ .input-action.has-files{border-color:var(--accent);color:var(--accent)}
376
+ .home-file-drop-zone{
377
+ display:none;padding:10px 14px;margin-bottom:6px;
378
+ border:2px dashed var(--border-glow);border-radius:10px;
379
+ background:var(--bg-input);font-size:12px;color:var(--text-muted);cursor:pointer;
380
+ transition:all .2s;min-height:52px;
381
+ flex-direction:column;align-items:center;justify-content:center;gap:6px;
382
+ }
383
+ .home-file-drop-zone.visible{display:flex}
384
+ .home-file-drop-zone.dragover{border-color:var(--accent);background:var(--accent-bg);color:var(--accent)}
385
+ .home-file-drop-zone input[type=file]{display:none}
386
+ .home-file-list{display:flex;flex-wrap:wrap;gap:6px;width:100%;justify-content:flex-start}
387
+ .home-file-pill{
388
+ display:inline-flex;align-items:center;gap:6px;padding:4px 9px;border-radius:7px;
389
+ background:var(--bg-surface);border:1px solid var(--border-dim);
390
+ font-family:var(--font-mono);font-size:10px;color:var(--text-primary);max-width:220px;
391
+ }
392
+ .home-file-pill-name{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1}
393
+ .home-file-pill-size{color:var(--text-muted);flex-shrink:0}
394
+ .home-file-pill-remove{cursor:pointer;color:var(--text-muted);font-size:13px;flex-shrink:0}
395
+ .home-file-pill-remove:hover{color:#ef4444}
396
+ @keyframes pulse-rec{0%,100%{opacity:1}50%{opacity:.5}}
397
+
398
+ /* Drawer overlay */
399
+ .drawer-overlay{
400
+ position:fixed;top:0;left:0;right:0;bottom:0;
401
+ background:rgba(0,0,0,0.5);backdrop-filter:blur(4px);
402
+ z-index:100;opacity:0;pointer-events:none;transition:opacity .3s;
403
+ }
404
+ .drawer-overlay.open{opacity:1;pointer-events:all}
405
+
406
+ /* Drawer panel */
407
+ .drawer{
408
+ position:fixed;top:0;left:0;bottom:0;width:320px;
409
+ background:var(--bg-surface);border-right:1px solid var(--border-dim);
410
+ z-index:101;transform:translateX(-100%);transition:transform .3s ease;
411
+ display:flex;flex-direction:column;
412
+ }
413
+ .drawer-overlay.open .drawer{transform:translateX(0)}
414
+ .drawer-header{
415
+ padding:16px 20px;border-bottom:1px solid var(--border-dim);
416
+ display:flex;align-items:center;justify-content:space-between;
417
+ }
418
+ .drawer-header-title{font-family:var(--font-display);font-size:15px;font-weight:700;color:var(--accent)}
419
+ .drawer-close{background:none;border:none;color:var(--text-muted);font-size:20px;cursor:pointer}
420
+ .drawer-close:hover{color:var(--text-primary)}
421
+ .drawer-list{flex:1;overflow-y:auto;padding:12px}
422
+ .drawer-agent{
423
+ display:flex;align-items:center;gap:10px;
424
+ padding:10px 12px;border-radius:10px;cursor:pointer;
425
+ transition:all .2s;margin-bottom:4px;
426
+ }
427
+ .drawer-agent:hover{background:var(--bg-card)}
428
+ .drawer-agent-avatar{
429
+ width:32px;height:32px;border-radius:8px;flex-shrink:0;
430
+ background:var(--accent-bg);border:1px solid rgba(34,211,238,0.3);
431
+ display:flex;align-items:center;justify-content:center;
432
+ font-family:var(--font-mono);font-size:11px;font-weight:600;color:var(--accent);
433
+ }
434
+ .drawer-agent-info{flex:1;min-width:0}
435
+ .drawer-agent-name{font-size:13px;font-weight:500;color:var(--text-primary);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
436
+ .drawer-agent-alias{font-size:11px;color:var(--text-muted);font-family:var(--font-mono)}
437
+ .drawer-agent-time{font-size:10px;color:var(--text-muted)}
438
+
439
+ /* Logs drawer (inline bottom panel — pushes chat up) */
440
+ .logs-drawer{
441
+ height:0;overflow:hidden;
442
+ background:var(--bg-surface);border-top:1px solid var(--border-dim);
443
+ transition:height .3s ease;
444
+ display:flex;flex-direction:column;flex-shrink:0;
445
+ }
446
+ .logs-drawer.open{height:240px}
447
+ .logs-header{
448
+ padding:8px 16px;border-bottom:1px solid var(--border-dim);
449
+ display:flex;align-items:center;justify-content:space-between;flex-shrink:0;
450
+ }
451
+ .logs-content{
452
+ flex:1;overflow-y:auto;padding:8px 16px;
453
+ font-family:var(--font-mono);font-size:11px;line-height:1.6;color:var(--text-secondary);
454
+ white-space:pre-wrap;word-break:break-all;
455
+ }
456
+ .logs-content::-webkit-scrollbar{width:4px}
457
+ .logs-content::-webkit-scrollbar-thumb{background:var(--border-glow);border-radius:2px}
458
+ .log-entry{padding:2px 0;border-bottom:1px solid rgba(255,255,255,0.03)}
459
+ .log-entry .log-time{color:var(--text-muted);margin-right:8px}
460
+ .log-entry .log-agent{color:var(--accent);font-weight:500;margin-right:6px}
461
+ .log-entry .log-channel{color:var(--purple);margin-right:6px}
462
+ .log-line{opacity:0.7}
463
+ .log-line:last-child{opacity:1}
464
+ .log-stderr{color:#ef4444}
465
+ .log-dim{opacity:0.4}
466
+ </style>
467
+ </head>
468
+ <body>
469
+ <div class="home">
470
+ <!-- Topbar (same as /ui) -->
471
+ <div class="topbar" id="mainTopbar">
472
+ <a href="/" style="display:flex;align-items:center;gap:10px;margin-right:28px;text-decoration:none">
473
+ <img class="logo-mark" src="/MyAIforOne-logomark-transparent.svg" alt="MyAIforOne">
474
+ <span class="logo-text">MyAIforOne</span>
475
+ </a>
476
+ <nav class="tab-group">
477
+ <a class="tab-btn active" href="/">Home</a>
478
+ <a class="tab-btn" href="/org">Agents</a>
479
+ <a class="tab-btn" href="/ui">Chat</a>
480
+ <a class="tab-btn" href="/library">Library</a>
481
+ <a class="tab-btn" href="/lab">Lab</a>
482
+ </nav>
483
+ <div class="topbar-right">
484
+ <a class="gym-nav-btn gym-tab-link" href="/gym" style="display:none">Gym</a>
485
+ <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>
486
+ <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>
487
+ <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>
488
+ <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>
489
+ <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)'">&#x22A1;</button>
490
+ <button class="theme-toggle" onclick="toggleTheme()" title="Toggle light/dark mode" id="themeBtn">&#x1F319;</button>
491
+ </div>
492
+ </div>
493
+
494
+ <!-- Landing state -->
495
+ <div class="landing" id="landing">
496
+ <div class="landing-logo">MyAIforOne</div>
497
+ <div class="landing-sub">Ask me anything</div>
498
+ <div class="landing-input-wrap">
499
+ <div class="landing-input-box">
500
+ <textarea class="landing-textarea" id="landingInput" placeholder="What do you need?" rows="1"
501
+ onkeydown="if(event.key==='Enter'&&!event.shiftKey){event.preventDefault();sendFirst()}"
502
+ oninput="autoResize(this)"></textarea>
503
+ <button class="landing-send" onclick="sendFirst()">→</button>
504
+ </div>
505
+ <div class="agent-count" id="agentCount"></div>
506
+ </div>
507
+ <div class="tiles-wrap" id="tilesWrap">
508
+ <select class="org-select" id="orgFilters" onchange="setOrgFilter(this.value)"></select>
509
+ <div class="agent-tiles" id="agentTiles"></div>
510
+ </div>
511
+ </div>
512
+
513
+ <!-- Chat state -->
514
+ <div class="chat-view" id="chatView">
515
+ <div class="hub-actions-bar">
516
+ <span class="hub-label">@hub</span>
517
+ <select class="header-btn" id="accountSelect" style="padding:2px 6px;cursor:pointer;display:none" onchange="setAccountOverride(this.value)" title="Claude Account override"></select>
518
+ <button class="header-btn danger" onclick="confirmResetSession()" title="Reset hub session">&#x21BA; Reset session</button>
519
+ <button class="header-btn" id="configureHubLink" onclick="openConfigOverlay()" title="Configure hub agent">&#x2699; Configure</button>
520
+ <button class="canvas-toggle-btn" onclick="Canvas.toggle()" title="Toggle preview panel">&#x25A8; Canvas</button>
521
+ </div>
522
+ <div class="session-tabs" id="homeSessionTabBar"></div>
523
+ <div class="chat-body-wrap" id="chatBodyWrap">
524
+ <div class="chat-messages-home chat-messages" id="chatMessages"></div>
525
+ <div class="canvas-panel" id="canvasPanel">
526
+ <div class="canvas-resize-handle" id="canvasResizeHandle"></div>
527
+ <div class="canvas-header">
528
+ <span class="canvas-file-icon">&#x25A8;</span>
529
+ <span class="canvas-filename" style="color:var(--text-muted)">No file open</span>
530
+ <button class="canvas-header-btn" onclick="Canvas.close()" title="Close">&#x2715;</button>
531
+ </div>
532
+ <div class="canvas-content" style="display:flex;align-items:center;justify-content:center;padding:40px;color:var(--text-muted);font-size:13px">
533
+ <div style="text-align:center">
534
+ <div style="font-size:32px;opacity:.3;margin-bottom:8px">&#x25A8;</div>
535
+ <div>Click "Preview" on a file path to open it here</div>
536
+ </div>
537
+ </div>
538
+ </div>
539
+ </div>
540
+ <div class="recent-files-bar" id="recentFilesBar"></div>
541
+ <div class="logs-drawer" id="logsDrawer">
542
+ <div class="logs-header">
543
+ <span class="drawer-header-title">Raw Logs</span>
544
+ <div style="display:flex;gap:8px;align-items:center">
545
+ <button class="input-action" style="width:28px;height:28px;font-size:12px" onclick="clearLogs()" title="Clear">&#x2715;</button>
546
+ <button class="input-action" style="width:28px;height:28px;font-size:12px" onclick="toggleLogs()" title="Close">&#x25BC;</button>
547
+ </div>
548
+ </div>
549
+ <div class="logs-content" id="logsContent"></div>
550
+ </div>
551
+ <div class="chat-input-home" style="position:relative">
552
+ <div class="autocomplete" id="autocomplete"></div>
553
+ <div class="home-file-drop-zone" id="homeFileDropZone"
554
+ onclick="document.getElementById('homeFileInput').click()"
555
+ ondragover="event.preventDefault();this.classList.add('dragover')"
556
+ ondragleave="this.classList.remove('dragover')"
557
+ ondrop="event.preventDefault();this.classList.remove('dragover');handleHomeFileDrop(event)">
558
+ <input type="file" id="homeFileInput" multiple onchange="handleHomeFiles(event)" style="display:none">
559
+ <div id="homeFileListArea"><span>Drop files here or click to browse</span></div>
560
+ </div>
561
+ <div class="chat-input-home-box">
562
+ <button class="input-action" onclick="toggleDrawer()" title="Agents">☰</button>
563
+ <button class="input-action" onclick="toggleLogs()" title="Raw Logs (live claude -p output)">⌘</button>
564
+ <button class="input-action" id="homeClipBtn" onclick="toggleHomeFileZone()" title="Attach file">📎</button>
565
+ <button class="input-action" id="homeMicBtn" onclick="toggleHomeVoice()" title="Voice input">🎤</button>
566
+ <button class="input-action" id="voiceModeBtn" onclick="toggleVoiceMode()" title="Read responses aloud" style="font-size:14px">🔇</button>
567
+ <textarea id="chatInput" placeholder="Ask anything... (type @ to mention an agent)" rows="1"
568
+ onkeydown="handleChatKeydown(event)"
569
+ oninput="autoResize(this);checkAutocomplete(this)"></textarea>
570
+ <button class="home-send-btn" id="homeSendBtn" onclick="sendMessage()">→</button>
571
+ </div>
572
+ <div id="homeQueueContainer"></div>
573
+ <div class="chat-agent-bar">
574
+ <select class="org-select" id="chatOrgFilters" onchange="setOrgFilter(this.value)"></select>
575
+ <div class="chat-tiles" id="chatTiles"></div>
576
+ </div>
577
+ </div>
578
+ </div>
579
+
580
+ <!-- Agent drawer -->
581
+ <div class="drawer-overlay" id="drawerOverlay" onclick="toggleDrawer()">
582
+ <div class="drawer" onclick="event.stopPropagation()">
583
+ <div class="drawer-header">
584
+ <span class="drawer-header-title">Agents</span>
585
+ <button class="drawer-close" onclick="toggleDrawer()">&times;</button>
586
+ </div>
587
+ <div class="drawer-list" id="drawerList"></div>
588
+ </div>
589
+ </div>
590
+
591
+ </div>
592
+
593
+ <script>
594
+ let agents = [];
595
+ let messages = [];
596
+ let hubAgentId = null;
597
+
598
+ // Initialize Canvas module — uses hubAgentId for file downloads
599
+ Canvas.init({ getAgentId: () => hubAgentId, escapeHtml: s => (s||'').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;') });
600
+
601
+ // ─── Recent Files Tracking ─────────────────────────────────────
602
+ let recentFiles = [];
603
+ const FILE_TOOLS = new Set(['Write', 'Edit', 'NotebookEdit']);
604
+
605
+ function trackFileFromTool(tool) {
606
+ if (!tool || !tool.input) return;
607
+ let filePath = null;
608
+ if (FILE_TOOLS.has(tool.name) && tool.input.file_path) {
609
+ filePath = tool.input.file_path;
610
+ } else if (tool.name === 'Bash' && tool.input.command) {
611
+ const m = tool.input.command.match(/>\s*["']?([^\s"'|&;]+\.\w+)/);
612
+ if (m) filePath = m[1];
613
+ }
614
+ if (!filePath) return;
615
+ const fileName = filePath.split('/').pop() || filePath;
616
+ const idx = recentFiles.findIndex(f => f.path === filePath);
617
+ if (idx >= 0) { recentFiles[idx].ts = Date.now(); }
618
+ else { recentFiles.unshift({ name: fileName, path: filePath, ts: Date.now() }); if (recentFiles.length > 20) recentFiles.pop(); }
619
+ renderRecentFiles();
620
+ }
621
+
622
+ function renderRecentFiles() {
623
+ const bar = document.getElementById('recentFilesBar');
624
+ if (!bar || recentFiles.length === 0) { if (bar) bar.innerHTML = ''; return; }
625
+ const icons = {csv:'📊',json:'📋',md:'📝',txt:'📝',html:'🌐',js:'⚡',ts:'⚡',py:'🐍',css:'🎨',pdf:'📄',png:'🖼️',jpg:'🖼️',svg:'🖼️',xlsx:'📊',docx:'📃'};
626
+ let html = '<span class="recent-files-label">Recent Files</span>';
627
+ for (const f of recentFiles.slice(0, 10)) {
628
+ const ext = (f.name.split('.').pop() || '').toLowerCase();
629
+ const icon = icons[ext] || '📁';
630
+ const enc = encodeURIComponent(f.path);
631
+ html += `<span class="recent-file-chip" title="${escapeHtml(f.path)}"><span class="chip-icon" onclick="Canvas.openFile('${enc}')">${icon}</span><span onclick="Canvas.openFile('${enc}')">${escapeHtml(f.name)}</span><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></span>`;
632
+ }
633
+ bar.innerHTML = html;
634
+ }
635
+ let isThinking = false;
636
+ let streamingText = '';
637
+ let voiceModeOn = false;
638
+ let homeQueue = [];
639
+ let currentJobId = null;
640
+ let availableAccounts = [];
641
+ let accountOverride = undefined;
642
+
643
+ // ─── Session Tabs ──────────────────────────────────────────────
644
+ // Home page tabs — each tab has its own message list, streaming state, and queue.
645
+ let homeTabs = []; // [{id, label, createdAt}]
646
+ let homeActiveTabId = null;
647
+ let tabMessages = {}; // tabId -> messages[]
648
+ let tabStreaming = {}; // tabId -> {text, isThinking, jobId, queue}
649
+
650
+ function loadHomeTabs() {
651
+ try {
652
+ const t = localStorage.getItem('homeTabs');
653
+ const a = localStorage.getItem('homeActiveTab');
654
+ if (t) homeTabs = JSON.parse(t);
655
+ if (a) homeActiveTabId = JSON.parse(a);
656
+ } catch {}
657
+ if (!homeTabs.length) {
658
+ homeTabs = [{ id: 'default', label: 'Session 1', createdAt: Date.now() }];
659
+ homeActiveTabId = 'default';
660
+ }
661
+ if (!homeActiveTabId) homeActiveTabId = homeTabs[0]?.id;
662
+ }
663
+ function saveHomeTabs() {
664
+ try {
665
+ localStorage.setItem('homeTabs', JSON.stringify(homeTabs));
666
+ localStorage.setItem('homeActiveTab', JSON.stringify(homeActiveTabId));
667
+ } catch {}
668
+ }
669
+ loadHomeTabs();
670
+
671
+ function getTabState(tabId) {
672
+ if (!tabStreaming[tabId]) tabStreaming[tabId] = { text: '', isThinking: false, jobId: null, queue: [] };
673
+ return tabStreaming[tabId];
674
+ }
675
+
676
+ function syncFromActiveTab() {
677
+ const ts = getTabState(homeActiveTabId);
678
+ messages = tabMessages[homeActiveTabId] || [];
679
+ isThinking = ts.isThinking;
680
+ streamingText = ts.text;
681
+ currentJobId = ts.jobId;
682
+ homeQueue = ts.queue;
683
+ }
684
+ function syncToActiveTab() {
685
+ tabMessages[homeActiveTabId] = messages;
686
+ const ts = getTabState(homeActiveTabId);
687
+ ts.isThinking = isThinking;
688
+ ts.text = streamingText;
689
+ ts.jobId = currentJobId;
690
+ ts.queue = homeQueue;
691
+ }
692
+
693
+ function createHomeTab() {
694
+ syncToActiveTab();
695
+ const num = homeTabs.length + 1;
696
+ const id = 'htab-' + Date.now().toString(36);
697
+ homeTabs.push({ id, label: 'Session ' + num, createdAt: Date.now() });
698
+ homeActiveTabId = id;
699
+ tabMessages[id] = [];
700
+ tabStreaming[id] = { text: '', isThinking: false, jobId: null, queue: [] };
701
+ saveHomeTabs();
702
+ syncFromActiveTab();
703
+ renderMessages();
704
+ scrollChat();
705
+ renderHomeTabBar();
706
+ updateHomeInputButtons();
707
+ }
708
+
709
+ function closeHomeTab(tabId) {
710
+ if (homeTabs.length <= 1) return;
711
+ const ts = getTabState(tabId);
712
+ if (ts.isThinking || ts.jobId) {
713
+ if (!confirm('This session has an active stream. Close anyway?')) return;
714
+ }
715
+ const idx = homeTabs.findIndex(t => t.id === tabId);
716
+ if (idx < 0) return;
717
+ homeTabs.splice(idx, 1);
718
+ delete tabMessages[tabId];
719
+ delete tabStreaming[tabId];
720
+ if (homeActiveTabId === tabId) {
721
+ homeActiveTabId = homeTabs[Math.min(idx, homeTabs.length - 1)].id;
722
+ }
723
+ saveHomeTabs();
724
+ syncFromActiveTab();
725
+ renderMessages();
726
+ scrollChat();
727
+ renderHomeTabBar();
728
+ updateHomeInputButtons();
729
+ }
730
+
731
+ function switchHomeTab(tabId) {
732
+ if (homeActiveTabId === tabId) return;
733
+ syncToActiveTab();
734
+ homeActiveTabId = tabId;
735
+ saveHomeTabs();
736
+ syncFromActiveTab();
737
+ renderMessages();
738
+ scrollChat();
739
+ renderHomeTabBar();
740
+ updateHomeInputButtons();
741
+ renderHomeQueue();
742
+ const inp = document.getElementById('chatInput');
743
+ if (inp) inp.focus();
744
+ }
745
+
746
+ function startRenameHomeTab(tabId) {
747
+ const el = document.querySelector(`.session-tab[data-tab-id="${tabId}"] .tab-label`);
748
+ if (!el) return;
749
+ const tab = homeTabs.find(t => t.id === tabId);
750
+ if (!tab) return;
751
+ const input = document.createElement('input');
752
+ input.className = 'tab-rename-input';
753
+ input.value = tab.label;
754
+ input.onblur = () => finishRenameHomeTab(tabId, input.value);
755
+ input.onkeydown = (e) => { if (e.key === 'Enter') input.blur(); if (e.key === 'Escape') { input.value = tab.label; input.blur(); } };
756
+ el.replaceWith(input);
757
+ input.focus();
758
+ input.select();
759
+ }
760
+
761
+ function finishRenameHomeTab(tabId, newLabel) {
762
+ const tab = homeTabs.find(t => t.id === tabId);
763
+ if (tab && newLabel.trim()) {
764
+ tab.label = newLabel.trim();
765
+ saveHomeTabs();
766
+ }
767
+ renderHomeTabBar();
768
+ }
769
+
770
+ function renderHomeTabBar() {
771
+ const container = document.getElementById('homeSessionTabBar');
772
+ if (!container) return;
773
+ let html = homeTabs.map(t => {
774
+ const isActive = t.id === homeActiveTabId;
775
+ const ts = tabStreaming[t.id];
776
+ const hasActivity = !isActive && ts && (ts.isThinking || ts.jobId);
777
+ return `<div class="session-tab${isActive ? ' active' : ''}${hasActivity ? ' has-activity' : ''}" data-tab-id="${t.id}"
778
+ onclick="switchHomeTab('${t.id}')"
779
+ ondblclick="startRenameHomeTab('${t.id}')">
780
+ <span class="tab-dot"></span>
781
+ <span class="tab-label">${escapeHtml(t.label)}</span>
782
+ ${homeTabs.length > 1 ? `<span class="tab-close" onclick="event.stopPropagation();closeHomeTab('${t.id}')">&times;</span>` : ''}
783
+ </div>`;
784
+ }).join('');
785
+ html += `<div class="session-tab-add" onclick="createHomeTab()" title="New session">+</div>`;
786
+ container.innerHTML = html;
787
+ }
788
+
789
+ function toggleVoiceMode() {
790
+ voiceModeOn = !voiceModeOn;
791
+ const btn = document.getElementById('voiceModeBtn');
792
+ if (btn) {
793
+ btn.textContent = voiceModeOn ? '🔊' : '🔇';
794
+ btn.style.color = voiceModeOn ? 'var(--accent)' : '';
795
+ btn.title = voiceModeOn ? 'Voice mode on — click to mute' : 'Read responses aloud';
796
+ }
797
+ if (!voiceModeOn) window.speechSynthesis?.cancel();
798
+ }
799
+
800
+ function speakText(text) {
801
+ if (!voiceModeOn || !window.speechSynthesis) return;
802
+ // Strip markdown symbols and [via:...] tags before speaking
803
+ const clean = text
804
+ .replace(/\[via:[^\]]+\]/g, '')
805
+ .replace(/[*_`#>~]/g, '')
806
+ .replace(/\n+/g, ' ')
807
+ .trim();
808
+ if (!clean) return;
809
+ window.speechSynthesis.cancel();
810
+ const utterance = new SpeechSynthesisUtterance(clean);
811
+ utterance.rate = 1.05;
812
+ window.speechSynthesis.speak(utterance);
813
+ }
814
+
815
+ // ─── Init ──────────────────────────────────────────────
816
+ async function init() {
817
+ try {
818
+ const res = await fetch('/api/dashboard');
819
+ const data = await res.json();
820
+ agents = data.agents || [];
821
+ availableAccounts = data.claudeAccounts || [];
822
+
823
+ // Use the default group agent from config, or find first group agent
824
+ hubAgentId = data.defaultGroupAgent
825
+ || agents.find(a => a.subAgents)?.id
826
+ || agents[0]?.id;
827
+
828
+ // Update configure link to point to hub agent dashboard
829
+ const configLink = document.getElementById('configureHubLink');
830
+ // configureHubLink is now a button; hubAgentId used by openConfigOverlay()
831
+
832
+ // Populate account dropdown
833
+ if (availableAccounts.length > 0 && hubAgentId) {
834
+ const sel = document.getElementById('accountSelect');
835
+ const hubAgent = agents.find(a => a.id === hubAgentId);
836
+ const defaultAcct = hubAgent?.claudeAccount || 'default';
837
+ sel.innerHTML = `<option value="">${defaultAcct}</option>` +
838
+ availableAccounts.filter(a => a !== hubAgent?.claudeAccount).map(a =>
839
+ `<option value="${a}">${a}</option>`
840
+ ).join('');
841
+ sel.style.display = '';
842
+ }
843
+
844
+ // ─── Fresh install detection ─────────────────────────
845
+ const personalAgents = agents.filter(a => a.agentClass !== 'platform' && !a.subAgents);
846
+ const channelList = data.channels || [];
847
+ const hasChannels = channelList.length > 0;
848
+ const hasPersonalAgents = personalAgents.length > 0;
849
+ const onboardingDismissed = localStorage.getItem('onboardingDismissed');
850
+
851
+ const isSetupMode = new URLSearchParams(window.location.search).get('setup') === 'true';
852
+
853
+ if (!hasPersonalAgents && !onboardingDismissed && !isSetupMode) {
854
+ // Fresh install — redirect to /monitor which has setup checklist
855
+ window.location.href = '/monitor';
856
+ return;
857
+ }
858
+
859
+ // If coming from monitor "Start Setup" button, go straight to hub chat with onboarding prompt
860
+ if (isSetupMode) {
861
+ history.replaceState(null, '', '/'); // clean URL
862
+ transitionToChat();
863
+ const onboardingPrompt = "I just set up MyAIforOne for the first time. Please use the /onboarding skill to walk me through connecting a messaging channel and creating my first personal agent.";
864
+ addMessage('user', onboardingPrompt);
865
+ syncToActiveTab();
866
+ executeHub(onboardingPrompt, homeActiveTabId);
867
+ return;
868
+ }
869
+
870
+ // Agent count
871
+ document.getElementById('agentCount').textContent =
872
+ `Powered by ${agents.length} agents · ${data.mcpCount || 0} integrations`;
873
+
874
+ // Render recent agent tiles
875
+ renderTiles();
876
+
877
+ // Load previous messages from hub's conversation log into the default tab
878
+ if (hubAgentId) {
879
+ try {
880
+ const msgRes = await fetch(`/api/agents/${hubAgentId}`);
881
+ const msgData = await msgRes.json();
882
+ if (msgData.recentMessages && msgData.recentMessages.length > 0) {
883
+ const loaded = msgData.recentMessages.map(m => ([
884
+ { role: 'user', text: m.text, time: m.ts },
885
+ { role: 'agent', text: m.response, time: m.ts },
886
+ ])).flat().filter(m => m.text);
887
+ // Put into default tab if it has no messages yet
888
+ const defId = homeTabs[0]?.id || 'default';
889
+ if (!tabMessages[defId] || !tabMessages[defId].length) {
890
+ tabMessages[defId] = loaded;
891
+ }
892
+ syncFromActiveTab();
893
+ // Returning user — skip landing, go straight to chat
894
+ transitionToChat();
895
+ renderMessages();
896
+ scrollChat();
897
+ }
898
+ } catch { /* ignore */ }
899
+ }
900
+ renderHomeTabBar();
901
+ } catch { /* ignore */ }
902
+
903
+ // Check for ?agent= param (e.g. from Apps registry "@ Dev" button)
904
+ const agentParam = new URLSearchParams(window.location.search).get('agent');
905
+ if(agentParam) {
906
+ const found = agents.find(a => a.id === agentParam);
907
+ const alias = found?.aliases?.[0] || ('@' + agentParam);
908
+ // Clean URL without reloading
909
+ history.replaceState(null, '', '/');
910
+ mentionAgent(alias);
911
+ }
912
+ }
913
+
914
+ let activeOrgFilter = 'recent'; // 'recent' | org name
915
+
916
+ function renderTiles() {
917
+ // Build org list from all agents
918
+ const orgs = [...new Set(
919
+ agents.flatMap(a => (a.org || []).map(o => o.organization).filter(Boolean))
920
+ )].sort();
921
+
922
+ // Render filter dropdowns (landing + chat)
923
+ const optionsHtml = [
924
+ `<option value="recent"${activeOrgFilter==='recent'?' selected':''}>Recent</option>`,
925
+ ...orgs.map(org => `<option value="${org.replace(/"/g,'&quot;')}"${activeOrgFilter===org?' selected':''}>${org}</option>`)
926
+ ].join('');
927
+ const f1 = document.getElementById('orgFilters');
928
+ const f2 = document.getElementById('chatOrgFilters');
929
+ if (f1) f1.innerHTML = optionsHtml;
930
+ if (f2) f2.innerHTML = optionsHtml;
931
+
932
+ // Filter agents
933
+ let filtered;
934
+ if (activeOrgFilter === 'recent') {
935
+ filtered = agents
936
+ .filter(a => a.lastMessage && a.lastMessage !== 'never' && !a.subAgents)
937
+ .sort((a, b) => new Date(b.lastMessage) - new Date(a.lastMessage))
938
+ .slice(0, 7);
939
+ } else {
940
+ filtered = agents
941
+ .filter(a => !a.subAgents && (a.org || []).some(o => o.organization === activeOrgFilter))
942
+ .sort((a, b) => (a.name || '').localeCompare(b.name || ''));
943
+ }
944
+
945
+ const html = filtered.map(a => {
946
+ const initials = (a.aliases?.[0] || a.id).replace('@','').slice(0,2).toUpperCase();
947
+ const alias = a.aliases?.[0] || ('@' + a.id);
948
+ const time = activeOrgFilter === 'recent' ? `<span class="tile-time">${formatRelativeTime(a.lastMessage)}</span>` : '';
949
+ return `<div class="agent-tile" onclick="mentionAgent('${alias}')">
950
+ <div class="tile-avatar">${initials}</div>
951
+ <span class="tile-name">${a.name}</span>
952
+ ${time}
953
+ </div>`;
954
+ }).join('') || `<span style="font-size:12px;color:var(--text-muted)">No agents in this org</span>`;
955
+
956
+ const t1 = document.getElementById('agentTiles');
957
+ const t2 = document.getElementById('chatTiles');
958
+ if (t1) t1.innerHTML = html;
959
+ if (t2) t2.innerHTML = html;
960
+ }
961
+
962
+ function setOrgFilter(org) {
963
+ activeOrgFilter = org;
964
+ renderTiles();
965
+ }
966
+
967
+ // ─── Mention agent from tile ───────────────────────────
968
+ function mentionAgent(alias) {
969
+ if (!document.getElementById('chatView').classList.contains('active')) {
970
+ transitionToChat();
971
+ }
972
+ const input = document.getElementById('chatInput');
973
+ const val = input.value;
974
+ // If input already ends with the alias, just focus
975
+ if (val.trimEnd().endsWith(alias)) {
976
+ input.focus();
977
+ input.setSelectionRange(input.value.length, input.value.length);
978
+ return;
979
+ }
980
+ // Prepend alias (clear any existing @-start or append)
981
+ input.value = alias + ' ' + val.trimStart();
982
+ input.focus();
983
+ input.setSelectionRange(input.value.length, input.value.length);
984
+ autoResize(input);
985
+ }
986
+
987
+ // ─── Transition ────────────────────────────────────────
988
+ function transitionToChat() {
989
+ document.getElementById('landing').classList.add('hidden');
990
+ document.getElementById('chatView').classList.add('active');
991
+ setTimeout(() => {
992
+ document.getElementById('chatInput').focus();
993
+ scrollChat();
994
+ renderHomeTabBar();
995
+ }, 100);
996
+ }
997
+
998
+ // ─── @alias parsing ────────────────────────────────────
999
+ function parseDirectAgent(text) {
1000
+ const match = text.match(/^@(\S+)\s+([\s\S]+)/);
1001
+ if (!match) return null;
1002
+ const alias = '@' + match[1];
1003
+ const agent = agents.find(a => (a.aliases || []).some(al => al.toLowerCase() === alias.toLowerCase()));
1004
+ if (!agent) return null;
1005
+ return { agentId: agent.id, message: match[2].trim(), agent };
1006
+ }
1007
+
1008
+ // ─── Send first message (from landing) ─────────────────
1009
+ function sendFirst() {
1010
+ const input = document.getElementById('landingInput');
1011
+ const text = input.value.trim();
1012
+ if (!text || !hubAgentId) return;
1013
+ input.value = '';
1014
+ transitionToChat();
1015
+ addMessage('user', text);
1016
+ syncToActiveTab();
1017
+ const direct = parseDirectAgent(text);
1018
+ if (direct) {
1019
+ executeDirectAgent(direct.agentId, direct.message, homeActiveTabId);
1020
+ } else {
1021
+ executeHub(text, homeActiveTabId);
1022
+ }
1023
+ }
1024
+
1025
+ // ─── Send message (from chat) ──────────────────────────
1026
+ async function sendMessage(queuedText) {
1027
+ const input = document.getElementById('chatInput');
1028
+ let text = queuedText || input.value.trim();
1029
+ if ((!text && !homeFiles.length) || !hubAgentId) return;
1030
+ const tabId = homeActiveTabId;
1031
+ const ts = getTabState(tabId);
1032
+
1033
+ // Queue if already streaming or job active
1034
+ if ((ts.isThinking || ts.jobId) && !queuedText) {
1035
+ ts.queue.push(text);
1036
+ homeQueue = ts.queue;
1037
+ input.value = '';
1038
+ autoResize(input);
1039
+ hideAutocomplete();
1040
+ renderHomeQueue();
1041
+ return;
1042
+ }
1043
+
1044
+ if (!queuedText) { input.value = ''; autoResize(input); hideAutocomplete(); }
1045
+
1046
+ // Upload any attached files and append file notes to text
1047
+ if (homeFiles.length > 0) {
1048
+ const uploaded = await uploadHomeFiles(hubAgentId);
1049
+ if (uploaded.length > 0) {
1050
+ const fileNotes = uploaded.map(f => `[File attached: ${f.name} at ${f.path}]`).join('\n');
1051
+ text = text ? `${text}\n\n${fileNotes}` : fileNotes;
1052
+ }
1053
+ homeFiles = [];
1054
+ renderHomeFiles();
1055
+ const zone = document.getElementById('homeFileDropZone');
1056
+ if (zone) zone.classList.remove('visible');
1057
+ }
1058
+
1059
+ addMessage('user', text, tabId);
1060
+ syncToActiveTab();
1061
+ updateHomeInputButtons();
1062
+ const direct = parseDirectAgent(text);
1063
+ if (direct) {
1064
+ executeDirectAgent(direct.agentId, direct.message, tabId);
1065
+ } else {
1066
+ executeHub(text, tabId);
1067
+ }
1068
+ }
1069
+
1070
+ async function uploadHomeFiles(agentId) {
1071
+ const uploaded = [];
1072
+ for (const f of homeFiles) {
1073
+ try {
1074
+ const formData = new FormData();
1075
+ formData.append('file', f);
1076
+ formData.append('mode', 'temporary');
1077
+ const resp = await fetch(`/api/upload/${agentId}`, { method: 'POST', body: formData });
1078
+ const data = await resp.json();
1079
+ if (data.ok) uploaded.push({ name: data.fileName, path: data.path });
1080
+ } catch (err) {
1081
+ console.warn('Upload failed:', f.name, err);
1082
+ }
1083
+ }
1084
+ return uploaded;
1085
+ }
1086
+
1087
+ // ─── Direct agent execution (bypasses hub) ─────────────
1088
+ async function executeDirectAgent(agentId, text, tabId) {
1089
+ const tid = tabId || homeActiveTabId;
1090
+ const ts = getTabState(tid);
1091
+ ts.isThinking = true;
1092
+ if (tid === homeActiveTabId) { isThinking = true; }
1093
+ renderMessages();
1094
+ scrollChat();
1095
+
1096
+ try {
1097
+ const res = await fetch('/api/delegate', {
1098
+ method: 'POST',
1099
+ headers: { 'Content-Type': 'application/json' },
1100
+ body: JSON.stringify({ agentId, text }),
1101
+ });
1102
+ const data = await res.json();
1103
+ ts.isThinking = false;
1104
+ if (tid === homeActiveTabId) isThinking = false;
1105
+ if (data.ok) {
1106
+ addMessage('agent', data.response + ` [via:${agentId}]`, tid);
1107
+ } else {
1108
+ addMessage('agent', 'Error: ' + (data.error || 'Unknown error'), tid);
1109
+ }
1110
+ } catch (err) {
1111
+ ts.isThinking = false;
1112
+ if (tid === homeActiveTabId) isThinking = false;
1113
+ addMessage('agent', 'Connection error: ' + err.message, tid);
1114
+ }
1115
+ renderMessages();
1116
+ scrollChat();
1117
+ renderHomeTabBar();
1118
+ updateHomeInputButtons();
1119
+ // Drain queue: auto-send next queued message
1120
+ if (ts.queue.length > 0) {
1121
+ const next = ts.queue.shift();
1122
+ if (tid === homeActiveTabId) { homeQueue = ts.queue; }
1123
+ renderHomeQueue();
1124
+ sendMessage(next);
1125
+ }
1126
+ }
1127
+
1128
+ // ─── @mention autocomplete ─────────────────────────────
1129
+ let acSelectedIdx = -1;
1130
+ let acFiltered = [];
1131
+
1132
+ function checkAutocomplete(el) {
1133
+ const text = el.value;
1134
+ const cursorPos = el.selectionStart;
1135
+ // Find @mention being typed at cursor position
1136
+ const before = text.slice(0, cursorPos);
1137
+ const atMatch = before.match(/@(\w*)$/);
1138
+
1139
+ if (!atMatch) { hideAutocomplete(); return; }
1140
+
1141
+ const query = atMatch[1].toLowerCase();
1142
+ acFiltered = agents
1143
+ .filter(a => !a.subAgents) // exclude group agents
1144
+ .filter(a => {
1145
+ const alias = (a.aliases?.[0] || '').toLowerCase();
1146
+ const name = a.name.toLowerCase();
1147
+ return alias.includes(query) || name.includes(query) || a.id.includes(query);
1148
+ })
1149
+ .slice(0, 8);
1150
+
1151
+ if (acFiltered.length === 0) { hideAutocomplete(); return; }
1152
+
1153
+ acSelectedIdx = 0;
1154
+ renderAutocomplete();
1155
+ }
1156
+
1157
+ function renderAutocomplete() {
1158
+ const el = document.getElementById('autocomplete');
1159
+ el.innerHTML = acFiltered.map((a, i) => {
1160
+ const initials = (a.aliases?.[0] || a.id).replace('@','').slice(0,2).toUpperCase();
1161
+ const alias = a.aliases?.[0] || a.id;
1162
+ return `<div class="autocomplete-item${i === acSelectedIdx ? ' selected' : ''}" onmousedown="selectAutocomplete(${i})">
1163
+ <div class="autocomplete-item-avatar">${initials}</div>
1164
+ <div class="autocomplete-item-info">
1165
+ <div class="autocomplete-item-alias">${alias}</div>
1166
+ <div class="autocomplete-item-name">${escapeHtml(a.name)}</div>
1167
+ </div>
1168
+ </div>`;
1169
+ }).join('');
1170
+ el.classList.add('visible');
1171
+ }
1172
+
1173
+ function hideAutocomplete() {
1174
+ document.getElementById('autocomplete')?.classList.remove('visible');
1175
+ acFiltered = [];
1176
+ acSelectedIdx = -1;
1177
+ }
1178
+
1179
+ function selectAutocomplete(idx) {
1180
+ const agent = acFiltered[idx];
1181
+ if (!agent) return;
1182
+ const alias = agent.aliases?.[0] || '@' + agent.id;
1183
+ const input = document.getElementById('chatInput') || document.getElementById('landingInput');
1184
+ if (!input) return;
1185
+
1186
+ // Replace the @partial with the full alias
1187
+ const before = input.value.slice(0, input.selectionStart);
1188
+ const after = input.value.slice(input.selectionStart);
1189
+ const atIdx = before.lastIndexOf('@');
1190
+ input.value = before.slice(0, atIdx) + alias + ' ' + after;
1191
+ input.focus();
1192
+ const newPos = atIdx + alias.length + 1;
1193
+ input.setSelectionRange(newPos, newPos);
1194
+ hideAutocomplete();
1195
+ }
1196
+
1197
+ function handleChatKeydown(event) {
1198
+ const ac = document.getElementById('autocomplete');
1199
+ if (ac?.classList.contains('visible')) {
1200
+ if (event.key === 'ArrowDown') {
1201
+ event.preventDefault();
1202
+ acSelectedIdx = Math.min(acSelectedIdx + 1, acFiltered.length - 1);
1203
+ renderAutocomplete();
1204
+ return;
1205
+ }
1206
+ if (event.key === 'ArrowUp') {
1207
+ event.preventDefault();
1208
+ acSelectedIdx = Math.max(acSelectedIdx - 1, 0);
1209
+ renderAutocomplete();
1210
+ return;
1211
+ }
1212
+ if (event.key === 'Tab' || event.key === 'Enter') {
1213
+ if (acSelectedIdx >= 0) {
1214
+ event.preventDefault();
1215
+ selectAutocomplete(acSelectedIdx);
1216
+ return;
1217
+ }
1218
+ }
1219
+ if (event.key === 'Escape') {
1220
+ hideAutocomplete();
1221
+ return;
1222
+ }
1223
+ }
1224
+ if (event.key === 'Enter' && !event.shiftKey) {
1225
+ event.preventDefault();
1226
+ sendMessage();
1227
+ }
1228
+ if (event.key === 'Escape' && isThinking) {
1229
+ stopHomeStreaming();
1230
+ }
1231
+ }
1232
+
1233
+ // ─── Execute via hub agent (streaming) ─────────────────
1234
+ async function executeHub(text, tabId) {
1235
+ const tid = tabId || homeActiveTabId;
1236
+ const ts = getTabState(tid);
1237
+ const isActive = () => tid === homeActiveTabId;
1238
+
1239
+ // Safety: save any leftover streaming text from a prior run
1240
+ if (ts.text) {
1241
+ addMessage('agent', ts.text, tid);
1242
+ ts.text = '';
1243
+ if (isActive()) streamingText = '';
1244
+ }
1245
+ ts.isThinking = true;
1246
+ if (isActive()) isThinking = true;
1247
+ renderMessages();
1248
+ scrollChat();
1249
+ renderHomeTabBar();
1250
+
1251
+ try {
1252
+ const startRes = await fetch(`/api/chat/${hubAgentId}/stream`, {
1253
+ method: 'POST',
1254
+ headers: { 'Content-Type': 'application/json' },
1255
+ body: JSON.stringify({ text, accountOverride }),
1256
+ });
1257
+ const { jobId } = await startRes.json();
1258
+ if (!jobId) throw new Error('No jobId');
1259
+
1260
+ // Track job for stop + raw logs
1261
+ ts.jobId = jobId;
1262
+ if (isActive()) { currentJobId = jobId; }
1263
+ currentRawJobId = jobId;
1264
+ updateHomeInputButtons();
1265
+ if (logsOpen) streamRawLogs(jobId);
1266
+
1267
+ // Stream events using fetch+reader with reconnect (no EventSource)
1268
+ ts.text = '';
1269
+ if (isActive()) streamingText = '';
1270
+ let lastEventId = 0;
1271
+ let isDone = false;
1272
+ let retries = 0;
1273
+ const MAX_RETRIES = 10;
1274
+
1275
+ while (!isDone && retries < MAX_RETRIES) {
1276
+ try {
1277
+ const streamRes = await fetch(`/api/chat/jobs/${jobId}/stream?after=${lastEventId}`);
1278
+ if (!streamRes.ok) {
1279
+ if (streamRes.status === 404) {
1280
+ try {
1281
+ await fetch(`/api/agents/${hubAgentId}/recover`, {
1282
+ method: 'POST',
1283
+ headers: { 'Content-Type': 'application/json' },
1284
+ body: JSON.stringify({ userText: text, response: ts.text }),
1285
+ });
1286
+ } catch { /* best effort */ }
1287
+ if (ts.text) { addMessage('agent', ts.text, tid); ts.text = ''; if (isActive()) streamingText = ''; }
1288
+ isDone = true;
1289
+ break;
1290
+ }
1291
+ retries++;
1292
+ await new Promise(r => setTimeout(r, 1000));
1293
+ continue;
1294
+ }
1295
+ const reader = streamRes.body.getReader();
1296
+ const decoder = new TextDecoder();
1297
+ let buffer = '';
1298
+ retries = 0;
1299
+
1300
+ while (true) {
1301
+ const { done, value } = await reader.read();
1302
+ if (done) break;
1303
+
1304
+ buffer += decoder.decode(value, { stream: true });
1305
+ const lines = buffer.split('\n');
1306
+ buffer = lines.pop() || '';
1307
+
1308
+ for (const line of lines) {
1309
+ if (line.startsWith('id: ')) {
1310
+ lastEventId = parseInt(line.slice(4)) + 1;
1311
+ continue;
1312
+ }
1313
+ if (!line.startsWith('data: ')) continue;
1314
+ const data = line.slice(6);
1315
+ if (data === '[DONE]') { isDone = true; continue; }
1316
+
1317
+ try {
1318
+ const event = JSON.parse(data);
1319
+ if (event.type === 'text') {
1320
+ ts.text += event.data;
1321
+ ts.isThinking = false;
1322
+ if (isActive()) { streamingText = ts.text; isThinking = false; }
1323
+ renderMessages();
1324
+ scrollChat();
1325
+ } else if (event.type === 'status' || event.type === 'tool') {
1326
+ if (event.type === 'tool' && event.tool && event.tool.input && Object.keys(event.tool.input).length > 0) {
1327
+ trackFileFromTool(event.tool);
1328
+ }
1329
+ renderMessages();
1330
+ } else if (event.type === 'error') {
1331
+ ts.isThinking = false;
1332
+ if (isActive()) isThinking = false;
1333
+ addMessage('agent', 'Error: ' + event.data, tid);
1334
+ ts.text = '';
1335
+ if (isActive()) streamingText = '';
1336
+ renderMessages();
1337
+ isDone = true;
1338
+ }
1339
+ } catch { /* skip */ }
1340
+ }
1341
+ }
1342
+
1343
+ if (!isDone) {
1344
+ retries++;
1345
+ await new Promise(r => setTimeout(r, 1000));
1346
+ }
1347
+ } catch {
1348
+ retries++;
1349
+ if (retries < MAX_RETRIES) {
1350
+ await new Promise(r => setTimeout(r, 1000 * retries));
1351
+ }
1352
+ }
1353
+ }
1354
+
1355
+ // Finalize
1356
+ ts.isThinking = false;
1357
+ ts.jobId = null;
1358
+ if (isActive()) { isThinking = false; currentJobId = null; }
1359
+ updateHomeInputButtons();
1360
+ renderHomeTabBar();
1361
+ if (ts.text) {
1362
+ speakText(ts.text);
1363
+ addMessage('agent', ts.text, tid);
1364
+ ts.text = '';
1365
+ if (isActive()) streamingText = '';
1366
+ } else if (retries >= MAX_RETRIES) {
1367
+ addMessage('agent', 'Connection lost after multiple retries.', tid);
1368
+ }
1369
+ } catch (err) {
1370
+ ts.isThinking = false;
1371
+ ts.jobId = null;
1372
+ if (isActive()) { isThinking = false; currentJobId = null; }
1373
+ updateHomeInputButtons();
1374
+ renderHomeTabBar();
1375
+ if (ts.text) {
1376
+ addMessage('agent', ts.text, tid);
1377
+ ts.text = '';
1378
+ if (isActive()) streamingText = '';
1379
+ } else {
1380
+ addMessage('agent', 'Connection error: ' + err.message, tid);
1381
+ }
1382
+ }
1383
+ renderMessages();
1384
+ scrollChat();
1385
+ // Drain queue: auto-send next queued message
1386
+ if (ts.queue.length > 0) {
1387
+ const next = ts.queue.shift();
1388
+ if (isActive()) homeQueue = ts.queue;
1389
+ renderHomeQueue();
1390
+ sendMessage(next);
1391
+ }
1392
+ }
1393
+
1394
+ // ─── Messages ──────────────────────────────────────────
1395
+ function addMessage(role, text, tabId) {
1396
+ const tid = tabId || homeActiveTabId;
1397
+ if (!tabMessages[tid]) tabMessages[tid] = [];
1398
+ tabMessages[tid].push({ role, text, time: new Date().toISOString() });
1399
+ if (tid === homeActiveTabId) {
1400
+ messages = tabMessages[tid];
1401
+ }
1402
+ renderMessages();
1403
+ scrollChat();
1404
+ }
1405
+
1406
+ function renderMessages() {
1407
+ const el = document.getElementById('chatMessages');
1408
+ let html = messages.map(m => {
1409
+ const cls = m.role === 'user' ? 'msg-user' : 'msg-agent';
1410
+ const formatted = m.role === 'agent' ? formatMarkdown(m.text) : escapeHtml(m.text);
1411
+ const time = formatTime(m.time);
1412
+ // Parse via attribution: [via:agentId] at the end
1413
+ let viaBadge = '';
1414
+ const viaMatch = m.text?.match(/\[via:(\w[\w-]*)\]/);
1415
+ if (viaMatch && m.role === 'agent') {
1416
+ const viaAgent = agents.find(a => a.id === viaMatch[1]);
1417
+ viaBadge = `<span class="via-badge">via ${viaAgent?.aliases?.[0] || '@'+viaMatch[1]}</span>`;
1418
+ }
1419
+ return `<div class="msg ${cls}">${formatted}<div class="msg-time">${viaBadge}${time}</div></div>`;
1420
+ }).join('');
1421
+
1422
+ // Streaming message
1423
+ if (streamingText) {
1424
+ const formatted = formatMarkdown(streamingText);
1425
+ let viaBadge = '';
1426
+ const viaMatch = streamingText.match(/\[via:(\w[\w-]*)\]/);
1427
+ if (viaMatch) {
1428
+ const viaAgent = agents.find(a => a.id === viaMatch[1]);
1429
+ viaBadge = `<span class="via-badge">via ${viaAgent?.aliases?.[0] || '@'+viaMatch[1]}</span>`;
1430
+ }
1431
+ html += `<div class="msg msg-agent">${formatted}<div class="msg-time">${viaBadge}streaming...</div></div>`;
1432
+ }
1433
+
1434
+ // Thinking indicator
1435
+ if (isThinking && !streamingText) {
1436
+ html += `<div class="thinking"><div class="thinking-dots"><span></span><span></span><span></span></div>Thinking...</div>`;
1437
+ }
1438
+
1439
+ el.innerHTML = html;
1440
+ }
1441
+
1442
+ function scrollChat() {
1443
+ setTimeout(() => {
1444
+ const el = document.getElementById('chatMessages');
1445
+ if (el) el.scrollTop = el.scrollHeight;
1446
+ }, 60);
1447
+ }
1448
+
1449
+ // ─── Formatting ────────────────────────────────────────
1450
+ function escapeHtml(s) {
1451
+ return (s||'').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
1452
+ }
1453
+
1454
+ function formatMarkdown(text) {
1455
+ if (!text) return '';
1456
+ // Remove [via:xxx] markers from display text
1457
+ let html = escapeHtml(text.replace(/\[via:\w[\w-]*\]/g, '').trim());
1458
+ html = html.replace(/```(\w*)\n([\s\S]*?)```/g, '<pre><code>$2</code></pre>');
1459
+ html = html.replace(/`([^`]+)`/g, '<code>$1</code>');
1460
+ html = html.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>');
1461
+ // Detect file paths and add download/preview buttons (via Canvas module)
1462
+ html = Canvas.injectFileButtons(html, escapeHtml);
1463
+ html = html.replace(/\n/g, '<br>');
1464
+ return html;
1465
+ }
1466
+
1467
+ function formatTime(ts) {
1468
+ if (!ts) return '';
1469
+ try {
1470
+ const d = new Date(ts);
1471
+ return d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
1472
+ } catch { return ''; }
1473
+ }
1474
+
1475
+ function formatRelativeTime(ts) {
1476
+ if (!ts) return '';
1477
+ try {
1478
+ const d = new Date(ts);
1479
+ const diff = Date.now() - d;
1480
+ const mins = Math.floor(diff / 60000);
1481
+ if (mins < 1) return 'now';
1482
+ if (mins < 60) return mins + 'm';
1483
+ const hours = Math.floor(mins / 60);
1484
+ if (hours < 24) return hours + 'h';
1485
+ const days = Math.floor(hours / 24);
1486
+ if (days < 7) return days + 'd';
1487
+ return d.toLocaleDateString([], { month: 'short', day: 'numeric' });
1488
+ } catch { return ''; }
1489
+ }
1490
+
1491
+ function autoResize(el) {
1492
+ el.style.height = 'auto';
1493
+ el.style.height = Math.min(el.scrollHeight, 120) + 'px';
1494
+ }
1495
+
1496
+ // ─── Account Override ────────────────────────────────────
1497
+ function setAccountOverride(val) {
1498
+ accountOverride = val || undefined;
1499
+ }
1500
+
1501
+ // ─── Reset Session ──────────────────────────────────────
1502
+ async function confirmResetSession() {
1503
+ if (!hubAgentId) return;
1504
+ if (!confirm('Reset hub session? This clears conversation history. Use /opcompact first to save important context.')) return;
1505
+ try {
1506
+ await fetch(`/api/agents/${hubAgentId}/session`, { method: 'DELETE' });
1507
+ messages = [];
1508
+ tabMessages[homeActiveTabId] = [];
1509
+ const ts = getTabState(homeActiveTabId);
1510
+ ts.text = '';
1511
+ ts.isThinking = false;
1512
+ ts.jobId = null;
1513
+ ts.queue = [];
1514
+ streamingText = '';
1515
+ isThinking = false;
1516
+ currentJobId = null;
1517
+ homeQueue = [];
1518
+ renderMessages();
1519
+ updateHomeInputButtons();
1520
+ } catch (err) {
1521
+ alert('Failed to reset session: ' + err.message);
1522
+ }
1523
+ }
1524
+
1525
+ // ─── Close docs dropdown on outside click ──────────────
1526
+ document.addEventListener('click', (e) => {
1527
+ document.querySelectorAll('.docs-dropdown').forEach(dd => {
1528
+ if (!dd.contains(e.target)) dd.querySelector('.docs-menu')?.classList.remove('open');
1529
+ });
1530
+ });
1531
+
1532
+ // ─── Init ──────────────────────────────────────────────
1533
+ // ─── Theme Toggle ──────────────────────────────────────
1534
+ function toggleTheme() {
1535
+ const html = document.documentElement;
1536
+ const isLight = html.getAttribute('data-theme') === 'light';
1537
+ html.setAttribute('data-theme', isLight ? '' : 'light');
1538
+ const icon = isLight ? '🌙' : '☀️';
1539
+ const btn1 = document.getElementById('themeBtn');
1540
+ if (btn1) btn1.textContent = icon;
1541
+ localStorage.setItem('theme', isLight ? 'dark' : 'light');
1542
+ }
1543
+
1544
+ // Load saved theme
1545
+ (function() {
1546
+ const saved = localStorage.getItem('theme');
1547
+ if (saved === 'light') {
1548
+ document.documentElement.setAttribute('data-theme', 'light');
1549
+ setTimeout(() => {
1550
+ const btn1 = document.getElementById('themeBtn');
1551
+ if (btn1) btn1.textContent = '☀️';
1552
+ if (btn2) btn2.textContent = '☀️';
1553
+ }, 50);
1554
+ }
1555
+ })();
1556
+
1557
+ // ─── Drawer ────────────────────────────────────────────
1558
+ function toggleDrawer() {
1559
+ const overlay = document.getElementById('drawerOverlay');
1560
+ overlay.classList.toggle('open');
1561
+ if (overlay.classList.contains('open')) renderDrawer();
1562
+ }
1563
+
1564
+ function renderDrawer() {
1565
+ const list = document.getElementById('drawerList');
1566
+ const sorted = agents
1567
+ .filter(a => !a.subAgents)
1568
+ .sort((a, b) => {
1569
+ const aT = a.lastMessage && a.lastMessage !== 'never' ? new Date(a.lastMessage).getTime() : 0;
1570
+ const bT = b.lastMessage && b.lastMessage !== 'never' ? new Date(b.lastMessage).getTime() : 0;
1571
+ return bT - aT;
1572
+ });
1573
+
1574
+ list.innerHTML = sorted.map(a => {
1575
+ const initials = (a.aliases?.[0] || a.id).replace('@','').slice(0,2).toUpperCase();
1576
+ const alias = a.aliases?.[0] || ('@' + a.id);
1577
+ const time = a.lastMessage && a.lastMessage !== 'never' ? formatRelativeTime(a.lastMessage) : '';
1578
+ return `<div class="drawer-agent" onclick="toggleDrawer();mentionAgent('${alias}')">
1579
+ <div class="drawer-agent-avatar">${initials}</div>
1580
+ <div class="drawer-agent-info">
1581
+ <div class="drawer-agent-name">${a.name}</div>
1582
+ <div class="drawer-agent-alias">${alias}</div>
1583
+ </div>
1584
+ <span class="drawer-agent-time">${time}</span>
1585
+ </div>`;
1586
+ }).join('');
1587
+ }
1588
+
1589
+ // ─── File Attachment ───────────────────────────────────
1590
+ let homeFiles = [];
1591
+
1592
+ function toggleHomeFileZone() {
1593
+ const zone = document.getElementById('homeFileDropZone');
1594
+ if (!zone) return;
1595
+ zone.classList.toggle('visible');
1596
+ const btn = document.getElementById('homeClipBtn');
1597
+ if (btn) btn.classList.toggle('has-files', zone.classList.contains('visible') || homeFiles.length > 0);
1598
+ }
1599
+
1600
+ function handleHomeFileDrop(e) {
1601
+ const files = e.dataTransfer?.files;
1602
+ if (files) { for (const f of files) homeFiles.push(f); renderHomeFiles(); }
1603
+ }
1604
+
1605
+ function handleHomeFiles(e) {
1606
+ const files = e.target.files;
1607
+ if (!files) return;
1608
+ for (const f of files) homeFiles.push(f);
1609
+ renderHomeFiles();
1610
+ e.target.value = '';
1611
+ }
1612
+
1613
+ function renderHomeFiles() {
1614
+ const area = document.getElementById('homeFileListArea');
1615
+ const btn = document.getElementById('homeClipBtn');
1616
+ if (!area) return;
1617
+ if (!homeFiles.length) {
1618
+ area.innerHTML = '<span>Drop files here or click to browse</span>';
1619
+ if (btn) btn.classList.remove('has-files');
1620
+ return;
1621
+ }
1622
+ if (btn) btn.classList.add('has-files');
1623
+ area.innerHTML = '<div class="home-file-list">' + homeFiles.map((f, i) => {
1624
+ const size = f.size > 1024*1024 ? (f.size/(1024*1024)).toFixed(1)+'MB' : (f.size/1024).toFixed(0)+'KB';
1625
+ return `<div class="home-file-pill" onclick="event.stopPropagation()">
1626
+ <span class="home-file-pill-name">${escapeHtml(f.name)}</span>
1627
+ <span class="home-file-pill-size">${size}</span>
1628
+ <span class="home-file-pill-remove" onclick="homeFiles.splice(${i},1);renderHomeFiles()">×</span>
1629
+ </div>`;
1630
+ }).join('') + '</div>';
1631
+ }
1632
+
1633
+ // ─── Voice Input ───────────────────────────────────────
1634
+ let homeRecognition = null;
1635
+ let homeIsRecording = false;
1636
+
1637
+ function toggleHomeVoice() {
1638
+ if (!('webkitSpeechRecognition' in window) && !('SpeechRecognition' in window)) {
1639
+ alert('Speech recognition not supported. Use Chrome or Edge.');
1640
+ return;
1641
+ }
1642
+ homeIsRecording ? stopHomeVoice() : startHomeVoice();
1643
+ }
1644
+
1645
+ function startHomeVoice() {
1646
+ const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
1647
+ homeRecognition = new SpeechRecognition();
1648
+ homeRecognition.continuous = true;
1649
+ homeRecognition.interimResults = true;
1650
+ homeRecognition.lang = 'en-US';
1651
+
1652
+ let finalTranscript = '';
1653
+ const input = document.getElementById('chatInput') || document.getElementById('landingInput');
1654
+ const existing = input ? input.value : '';
1655
+
1656
+ homeRecognition.onresult = (event) => {
1657
+ let interim = '';
1658
+ for (let i = event.resultIndex; i < event.results.length; i++) {
1659
+ if (event.results[i].isFinal) finalTranscript += event.results[i][0].transcript + ' ';
1660
+ else interim += event.results[i][0].transcript;
1661
+ }
1662
+ if (input) { input.value = existing + (existing ? ' ' : '') + finalTranscript + interim; autoResize(input); }
1663
+ };
1664
+
1665
+ homeRecognition.onerror = () => stopHomeVoice();
1666
+ homeRecognition.onend = () => { if (homeIsRecording) stopHomeVoice(); };
1667
+
1668
+ homeRecognition.start();
1669
+ homeIsRecording = true;
1670
+ const btn = document.getElementById('homeMicBtn');
1671
+ if (btn) btn.classList.add('recording');
1672
+ }
1673
+
1674
+ function stopHomeVoice() {
1675
+ if (homeRecognition) { homeRecognition.stop(); homeRecognition = null; }
1676
+ homeIsRecording = false;
1677
+ const btn = document.getElementById('homeMicBtn');
1678
+ if (btn) btn.classList.remove('recording');
1679
+ }
1680
+
1681
+ // ─── Stop & Queue ─────────────────────────────────────
1682
+ async function stopHomeStreaming() {
1683
+ const ts = getTabState(homeActiveTabId);
1684
+ if (ts.jobId) {
1685
+ try { await fetch(`/api/chat/jobs/${ts.jobId}/stop`, { method: 'POST' }); } catch {}
1686
+ }
1687
+ ts.isThinking = false;
1688
+ ts.jobId = null;
1689
+ isThinking = false;
1690
+ currentJobId = null;
1691
+ if (ts.text) {
1692
+ addMessage('agent', ts.text);
1693
+ ts.text = '';
1694
+ streamingText = '';
1695
+ }
1696
+ renderMessages();
1697
+ scrollChat();
1698
+ updateHomeInputButtons();
1699
+ renderHomeTabBar();
1700
+ // Drain queue
1701
+ if (ts.queue.length > 0) {
1702
+ const next = ts.queue.shift();
1703
+ homeQueue = ts.queue;
1704
+ renderHomeQueue();
1705
+ sendMessage(next);
1706
+ }
1707
+ }
1708
+
1709
+ function updateHomeInputButtons() {
1710
+ const box = document.querySelector('.chat-input-home-box');
1711
+ if (!box) return;
1712
+ const sendBtn = document.getElementById('homeSendBtn');
1713
+ const existingStop = box.querySelector('.home-stop-btn');
1714
+ const ts = getTabState(homeActiveTabId);
1715
+
1716
+ if (ts.isThinking || ts.jobId) {
1717
+ // Show stop button
1718
+ if (!existingStop) {
1719
+ const stopBtn = document.createElement('button');
1720
+ stopBtn.className = 'home-stop-btn';
1721
+ stopBtn.onclick = stopHomeStreaming;
1722
+ stopBtn.title = 'Stop (Esc)';
1723
+ stopBtn.innerHTML = '\u25A0';
1724
+ if (sendBtn) box.insertBefore(stopBtn, sendBtn);
1725
+ }
1726
+ // Switch send to queue mode
1727
+ if (sendBtn) { sendBtn.classList.add('queue-mode'); sendBtn.textContent = 'Queue'; }
1728
+ } else {
1729
+ // Remove stop button
1730
+ if (existingStop) existingStop.remove();
1731
+ // Restore send button
1732
+ if (sendBtn) { sendBtn.classList.remove('queue-mode'); sendBtn.textContent = '\u2192'; }
1733
+ }
1734
+ }
1735
+
1736
+ function renderHomeQueue() {
1737
+ const container = document.getElementById('homeQueueContainer');
1738
+ if (!container) return;
1739
+ if (!homeQueue.length) { container.innerHTML = ''; return; }
1740
+ container.innerHTML = `<div class="home-queue">${homeQueue.map((msg, i) => {
1741
+ const truncated = msg.length > 60 ? msg.slice(0, 60) + '...' : msg;
1742
+ return `<div class="home-queue-item">
1743
+ <span class="q-pos">${i + 1}</span>
1744
+ <span class="q-text" title="${msg.replace(/"/g, '&quot;')}">${truncated.replace(/</g, '&lt;')}</span>
1745
+ <button class="q-btn" onclick="editHomeQueueItem(${i})" title="Edit">&#x270E;</button>
1746
+ <button class="q-btn danger" onclick="deleteHomeQueueItem(${i})" title="Delete">&#x2715;</button>
1747
+ </div>`;
1748
+ }).join('')}</div>`;
1749
+ }
1750
+
1751
+ function editHomeQueueItem(idx) {
1752
+ if (idx < 0 || idx >= homeQueue.length) return;
1753
+ const text = homeQueue.splice(idx, 1)[0];
1754
+ const input = document.getElementById('chatInput');
1755
+ if (input) { input.value = text; input.focus(); autoResize(input); }
1756
+ renderHomeQueue();
1757
+ }
1758
+
1759
+ function deleteHomeQueueItem(idx) {
1760
+ if (idx < 0 || idx >= homeQueue.length) return;
1761
+ homeQueue.splice(idx, 1);
1762
+ renderHomeQueue();
1763
+ }
1764
+
1765
+ // ─── Raw Logs Drawer (claude -p stdout) ───────────────
1766
+ let logsOpen = false;
1767
+ let rawLogsReader = null;
1768
+ let currentRawJobId = null;
1769
+
1770
+ function toggleLogs() {
1771
+ const drawer = document.getElementById('logsDrawer');
1772
+ logsOpen = !logsOpen;
1773
+ drawer.classList.toggle('open', logsOpen);
1774
+ if (logsOpen) connectRawLogs();
1775
+ }
1776
+
1777
+ function clearLogs() {
1778
+ document.getElementById('logsContent').textContent = '';
1779
+ }
1780
+
1781
+ function formatLogLine(data) {
1782
+ if (data.startsWith('[stderr]')) return { icon: '\u26A0', text: data.slice(9), cls: 'log-stderr' };
1783
+ try {
1784
+ let evt = JSON.parse(data);
1785
+ if (evt.type === 'stream_event' && evt.event) evt = evt.event;
1786
+ if (evt.type === 'content_block_delta' && evt.delta?.type === 'text_delta')
1787
+ return { icon: '\uD83D\uDCAC', text: evt.delta.text };
1788
+ if (evt.type === 'content_block_delta' && evt.delta?.type === 'thinking_delta')
1789
+ return { icon: '\uD83E\uDDE0', text: evt.delta.thinking };
1790
+ if (evt.type === 'content_block_start' && evt.content_block?.type === 'tool_use')
1791
+ return { icon: '\uD83D\uDD27', text: evt.content_block.name };
1792
+ if (evt.type === 'assistant' && evt.message?.content) {
1793
+ const tools = evt.message.content.filter(b => b.type === 'tool_use');
1794
+ if (tools.length) {
1795
+ return { icon: '\uD83D\uDCD6', text: tools.map(t => {
1796
+ const path = t.input?.file_path || t.input?.command?.slice(0, 80) || t.input?.pattern || '';
1797
+ return t.name + (path ? ' ' + path : '');
1798
+ }).join(', ') };
1799
+ }
1800
+ const texts = evt.message.content.filter(b => b.type === 'text' && b.text);
1801
+ if (texts.length) return { icon: '\uD83D\uDCAC', text: texts.map(t => t.text).join(' ') };
1802
+ }
1803
+ if (evt.type === 'user' && evt.message?.content?.[0]?.type === 'tool_result')
1804
+ return { icon: '\u2713', text: 'tool_result', cls: 'log-dim' };
1805
+ if (evt.type === 'content_block_delta' && evt.delta?.type === 'input_json_delta') return null;
1806
+ if (evt.type === 'content_block_stop' || evt.type === 'message_stop' || evt.type === 'message_delta') return null;
1807
+ if (evt.type === 'rate_limit_event') return null;
1808
+ if (evt.type === 'content_block_delta' && evt.delta?.type === 'signature_delta') return null;
1809
+ if (evt.type === 'system' && evt.subtype === 'init')
1810
+ return { icon: '\u25CF', text: 'Session started \u2014 ' + (evt.model || '') + ' | ' + (evt.mcp_servers?.length || 0) + ' MCPs' };
1811
+ if (evt.type === 'system') return { icon: '\u2699', text: evt.subtype || 'system' };
1812
+ if (evt.type === 'result')
1813
+ return { icon: '\uD83C\uDFC1', text: 'Done \u2014 ' + (evt.duration_ms ? Math.round(evt.duration_ms/1000) + 's' : '') };
1814
+ if (evt.type === 'message_start') return null;
1815
+ return { icon: '\u00B7', text: evt.type || data.slice(0, 80), cls: 'log-dim' };
1816
+ } catch {
1817
+ return { icon: '\u00B7', text: data.slice(0, 120) };
1818
+ }
1819
+ }
1820
+
1821
+ function connectRawLogs() {
1822
+ if (!currentRawJobId) {
1823
+ document.getElementById('logsContent').textContent = 'No active job. Send a message to start streaming logs.\n';
1824
+ return;
1825
+ }
1826
+ streamRawLogs(currentRawJobId);
1827
+ }
1828
+
1829
+ async function streamRawLogs(jobId) {
1830
+ const el = document.getElementById('logsContent');
1831
+ if (!el) return;
1832
+ el.textContent = '';
1833
+
1834
+ try {
1835
+ if (rawLogsReader) { try { rawLogsReader.cancel(); } catch {} }
1836
+ const res = await fetch(`/api/chat/jobs/${jobId}/raw?after=0`);
1837
+ const reader = res.body.getReader();
1838
+ rawLogsReader = reader;
1839
+ const decoder = new TextDecoder();
1840
+ let buffer = '';
1841
+
1842
+ while (true) {
1843
+ const { done, value } = await reader.read();
1844
+ if (done) break;
1845
+
1846
+ buffer += decoder.decode(value, { stream: true });
1847
+ const lines = buffer.split('\n');
1848
+ buffer = lines.pop() || '';
1849
+
1850
+ for (const line of lines) {
1851
+ if (!line.startsWith('data: ')) continue;
1852
+ const data = line.slice(6);
1853
+ if (data === '[DONE]') {
1854
+ el.textContent += '\n--- Job complete ---\n';
1855
+ break;
1856
+ }
1857
+ const fmt = formatLogLine(data);
1858
+ if (fmt) {
1859
+ const span = document.createElement('span');
1860
+ span.className = 'log-line' + (fmt.cls ? ' ' + fmt.cls : '');
1861
+ span.textContent = fmt.icon + ' ' + fmt.text + '\n';
1862
+ el.appendChild(span);
1863
+ }
1864
+ // Auto-scroll
1865
+ const drawer = document.getElementById('logsDrawer');
1866
+ if (drawer) drawer.scrollTop = drawer.scrollHeight;
1867
+ }
1868
+ }
1869
+ } catch { /* connection closed */ }
1870
+ }
1871
+
1872
+ // ─── Config Overlay (iframe) ─────────────────────────────
1873
+ function openConfigOverlay() {
1874
+ if (!hubAgentId) return;
1875
+ const overlay = document.getElementById('configOverlay');
1876
+ const iframe = document.getElementById('configIframe');
1877
+ iframe.src = `/org?edit=${encodeURIComponent(hubAgentId)}`;
1878
+ overlay.classList.add('show');
1879
+ }
1880
+ function closeConfigOverlay() {
1881
+ const overlay = document.getElementById('configOverlay');
1882
+ const iframe = document.getElementById('configIframe');
1883
+ overlay.classList.remove('show');
1884
+ setTimeout(() => { iframe.src = 'about:blank'; }, 300);
1885
+ }
1886
+ // Listen for modal close from iframe
1887
+ window.addEventListener('message', (e) => {
1888
+ if (e.data?.type === 'configModalClosed') closeConfigOverlay();
1889
+ });
1890
+
1891
+ init();
1892
+ setTimeout(() => document.getElementById('landingInput')?.focus(), 100);
1893
+ </script>
1894
+
1895
+ <!-- Config Overlay -->
1896
+ <div class="config-overlay" id="configOverlay" onclick="closeConfigOverlay()">
1897
+ <div class="config-overlay-inner" onclick="event.stopPropagation()">
1898
+ <button class="config-overlay-close" onclick="closeConfigOverlay()">&#10005;</button>
1899
+ <iframe id="configIframe" src="about:blank"></iframe>
1900
+ </div>
1901
+ </div>
1902
+ <style>
1903
+ .config-overlay{
1904
+ position:fixed;top:0;left:0;right:0;bottom:0;z-index:9999;
1905
+ background:rgba(0,0,0,0.7);backdrop-filter:blur(4px);
1906
+ display:none;align-items:center;justify-content:center;
1907
+ opacity:0;transition:opacity .25s ease;
1908
+ }
1909
+ .config-overlay.show{display:flex;opacity:1}
1910
+ .config-overlay-inner{
1911
+ position:relative;width:90vw;max-width:680px;height:85vh;
1912
+ border-radius:16px;overflow:hidden;
1913
+ border:1px solid var(--border-glow);
1914
+ box-shadow:0 8px 40px rgba(0,0,0,0.5);
1915
+ }
1916
+ .config-overlay-inner iframe{
1917
+ width:100%;height:100%;border:none;background:var(--bg-deep);
1918
+ }
1919
+ .config-overlay-close{
1920
+ position:absolute;top:10px;right:14px;z-index:10;
1921
+ width:32px;height:32px;border-radius:8px;border:1px solid var(--border-dim);
1922
+ background:var(--bg-surface);color:var(--text-secondary);cursor:pointer;
1923
+ font-size:16px;display:flex;align-items:center;justify-content:center;
1924
+ transition:all .2s;
1925
+ }
1926
+ .config-overlay-close:hover{border-color:var(--border-glow);color:var(--text-primary)}
1927
+ </style>
1928
+ <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>
1929
+ </body>
1930
+ </html>