agent-orcha 0.0.5 → 0.0.8

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 (404) hide show
  1. package/README.md +250 -1275
  2. package/dist/lib/agents/agent-executor.d.ts +4 -2
  3. package/dist/lib/agents/agent-executor.d.ts.map +1 -1
  4. package/dist/lib/agents/agent-executor.js +85 -53
  5. package/dist/lib/agents/agent-executor.js.map +1 -1
  6. package/dist/lib/agents/agent-loader.d.ts +3 -0
  7. package/dist/lib/agents/agent-loader.d.ts.map +1 -1
  8. package/dist/lib/agents/agent-loader.js +10 -1
  9. package/dist/lib/agents/agent-loader.js.map +1 -1
  10. package/dist/lib/agents/react-loop.d.ts.map +1 -1
  11. package/dist/lib/agents/react-loop.js +207 -142
  12. package/dist/lib/agents/react-loop.js.map +1 -1
  13. package/dist/lib/agents/types.d.ts +181 -18
  14. package/dist/lib/agents/types.d.ts.map +1 -1
  15. package/dist/lib/agents/types.js +18 -2
  16. package/dist/lib/agents/types.js.map +1 -1
  17. package/dist/lib/functions/function-loader.d.ts +2 -0
  18. package/dist/lib/functions/function-loader.d.ts.map +1 -1
  19. package/dist/lib/functions/function-loader.js +10 -0
  20. package/dist/lib/functions/function-loader.js.map +1 -1
  21. package/dist/lib/functions/simple-function-wrapper.js +3 -3
  22. package/dist/lib/functions/simple-function-wrapper.js.map +1 -1
  23. package/dist/lib/integrations/email.d.ts +38 -0
  24. package/dist/lib/integrations/email.d.ts.map +1 -0
  25. package/dist/lib/integrations/email.js +249 -0
  26. package/dist/lib/integrations/email.js.map +1 -0
  27. package/dist/lib/integrations/integration-manager.d.ts +5 -0
  28. package/dist/lib/integrations/integration-manager.d.ts.map +1 -1
  29. package/dist/lib/integrations/integration-manager.js +53 -3
  30. package/dist/lib/integrations/integration-manager.js.map +1 -1
  31. package/dist/lib/integrations/types.d.ts +187 -4
  32. package/dist/lib/integrations/types.d.ts.map +1 -1
  33. package/dist/lib/integrations/types.js +24 -1
  34. package/dist/lib/integrations/types.js.map +1 -1
  35. package/dist/lib/knowledge/knowledge-store.d.ts +7 -1
  36. package/dist/lib/knowledge/knowledge-store.d.ts.map +1 -1
  37. package/dist/lib/knowledge/knowledge-store.js +96 -8
  38. package/dist/lib/knowledge/knowledge-store.js.map +1 -1
  39. package/dist/lib/knowledge/loaders/file-loaders.d.ts +8 -3
  40. package/dist/lib/knowledge/loaders/file-loaders.d.ts.map +1 -1
  41. package/dist/lib/knowledge/loaders/file-loaders.js +96 -75
  42. package/dist/lib/knowledge/loaders/file-loaders.js.map +1 -1
  43. package/dist/lib/knowledge/loaders/web-loader.d.ts +12 -3
  44. package/dist/lib/knowledge/loaders/web-loader.d.ts.map +1 -1
  45. package/dist/lib/knowledge/loaders/web-loader.js +56 -22
  46. package/dist/lib/knowledge/loaders/web-loader.js.map +1 -1
  47. package/dist/lib/knowledge/sqlite-store.d.ts.map +1 -1
  48. package/dist/lib/knowledge/sqlite-store.js +19 -10
  49. package/dist/lib/knowledge/sqlite-store.js.map +1 -1
  50. package/dist/lib/knowledge/types.d.ts +69 -33
  51. package/dist/lib/knowledge/types.d.ts.map +1 -1
  52. package/dist/lib/knowledge/types.js +18 -3
  53. package/dist/lib/knowledge/types.js.map +1 -1
  54. package/dist/lib/llm/index.d.ts +1 -1
  55. package/dist/lib/llm/index.d.ts.map +1 -1
  56. package/dist/lib/llm/index.js +1 -1
  57. package/dist/lib/llm/index.js.map +1 -1
  58. package/dist/lib/llm/llm-call-logger.d.ts +3 -1
  59. package/dist/lib/llm/llm-call-logger.d.ts.map +1 -1
  60. package/dist/lib/llm/llm-call-logger.js +31 -26
  61. package/dist/lib/llm/llm-call-logger.js.map +1 -1
  62. package/dist/lib/llm/llm-config.d.ts +59 -8
  63. package/dist/lib/llm/llm-config.d.ts.map +1 -1
  64. package/dist/lib/llm/llm-config.js +163 -17
  65. package/dist/lib/llm/llm-config.js.map +1 -1
  66. package/dist/lib/llm/llm-factory.d.ts +1 -2
  67. package/dist/lib/llm/llm-factory.d.ts.map +1 -1
  68. package/dist/lib/llm/llm-factory.js +44 -8
  69. package/dist/lib/llm/llm-factory.js.map +1 -1
  70. package/dist/lib/llm/providers/anthropic-chat-model.d.ts +5 -1
  71. package/dist/lib/llm/providers/anthropic-chat-model.d.ts.map +1 -1
  72. package/dist/lib/llm/providers/anthropic-chat-model.js +118 -42
  73. package/dist/lib/llm/providers/anthropic-chat-model.js.map +1 -1
  74. package/dist/lib/llm/providers/gemini-chat-model.d.ts +3 -2
  75. package/dist/lib/llm/providers/gemini-chat-model.d.ts.map +1 -1
  76. package/dist/lib/llm/providers/gemini-chat-model.js +83 -24
  77. package/dist/lib/llm/providers/gemini-chat-model.js.map +1 -1
  78. package/dist/lib/llm/providers/openai-chat-model.d.ts +20 -1
  79. package/dist/lib/llm/providers/openai-chat-model.d.ts.map +1 -1
  80. package/dist/lib/llm/providers/openai-chat-model.js +265 -32
  81. package/dist/lib/llm/providers/openai-chat-model.js.map +1 -1
  82. package/dist/lib/llm/providers/openai-embeddings.d.ts.map +1 -1
  83. package/dist/lib/llm/providers/openai-embeddings.js +41 -10
  84. package/dist/lib/llm/providers/openai-embeddings.js.map +1 -1
  85. package/dist/lib/local-llm/binary-manager.d.ts +66 -0
  86. package/dist/lib/local-llm/binary-manager.d.ts.map +1 -0
  87. package/dist/lib/local-llm/binary-manager.js +441 -0
  88. package/dist/lib/local-llm/binary-manager.js.map +1 -0
  89. package/dist/lib/local-llm/engine-interface.d.ts +47 -0
  90. package/dist/lib/local-llm/engine-interface.d.ts.map +1 -0
  91. package/dist/lib/local-llm/engine-interface.js +2 -0
  92. package/dist/lib/local-llm/engine-interface.js.map +1 -0
  93. package/dist/lib/local-llm/engine-registry.d.ts +20 -0
  94. package/dist/lib/local-llm/engine-registry.d.ts.map +1 -0
  95. package/dist/lib/local-llm/engine-registry.js +56 -0
  96. package/dist/lib/local-llm/engine-registry.js.map +1 -0
  97. package/dist/lib/local-llm/engines/llama-cpp-engine.d.ts +31 -0
  98. package/dist/lib/local-llm/engines/llama-cpp-engine.d.ts.map +1 -0
  99. package/dist/lib/local-llm/engines/llama-cpp-engine.js +164 -0
  100. package/dist/lib/local-llm/engines/llama-cpp-engine.js.map +1 -0
  101. package/dist/lib/local-llm/engines/mlx-serve-engine.d.ts +31 -0
  102. package/dist/lib/local-llm/engines/mlx-serve-engine.d.ts.map +1 -0
  103. package/dist/lib/local-llm/engines/mlx-serve-engine.js +161 -0
  104. package/dist/lib/local-llm/engines/mlx-serve-engine.js.map +1 -0
  105. package/dist/lib/local-llm/gguf-reader.d.ts +20 -0
  106. package/dist/lib/local-llm/gguf-reader.d.ts.map +1 -0
  107. package/dist/lib/local-llm/gguf-reader.js +190 -0
  108. package/dist/lib/local-llm/gguf-reader.js.map +1 -0
  109. package/dist/lib/local-llm/index.d.ts +9 -0
  110. package/dist/lib/local-llm/index.d.ts.map +1 -0
  111. package/dist/lib/local-llm/index.js +6 -0
  112. package/dist/lib/local-llm/index.js.map +1 -0
  113. package/dist/lib/local-llm/llama-server-process.d.ts +42 -0
  114. package/dist/lib/local-llm/llama-server-process.d.ts.map +1 -0
  115. package/dist/lib/local-llm/llama-server-process.js +237 -0
  116. package/dist/lib/local-llm/llama-server-process.js.map +1 -0
  117. package/dist/lib/local-llm/mlx-binary-manager.d.ts +33 -0
  118. package/dist/lib/local-llm/mlx-binary-manager.d.ts.map +1 -0
  119. package/dist/lib/local-llm/mlx-binary-manager.js +211 -0
  120. package/dist/lib/local-llm/mlx-binary-manager.js.map +1 -0
  121. package/dist/lib/local-llm/mlx-server-process.d.ts +26 -0
  122. package/dist/lib/local-llm/mlx-server-process.d.ts.map +1 -0
  123. package/dist/lib/local-llm/mlx-server-process.js +210 -0
  124. package/dist/lib/local-llm/mlx-server-process.js.map +1 -0
  125. package/dist/lib/local-llm/model-manager.d.ts +33 -0
  126. package/dist/lib/local-llm/model-manager.d.ts.map +1 -0
  127. package/dist/lib/local-llm/model-manager.js +591 -0
  128. package/dist/lib/local-llm/model-manager.js.map +1 -0
  129. package/dist/lib/local-llm/types.d.ts +51 -0
  130. package/dist/lib/local-llm/types.d.ts.map +1 -0
  131. package/dist/lib/local-llm/types.js +2 -0
  132. package/dist/lib/local-llm/types.js.map +1 -0
  133. package/dist/lib/logger.d.ts +2 -0
  134. package/dist/lib/logger.d.ts.map +1 -1
  135. package/dist/lib/logger.js +68 -6
  136. package/dist/lib/logger.js.map +1 -1
  137. package/dist/lib/mcp/mcp-client.d.ts.map +1 -1
  138. package/dist/lib/mcp/mcp-client.js +5 -3
  139. package/dist/lib/mcp/mcp-client.js.map +1 -1
  140. package/dist/lib/mcp/types.d.ts +0 -9
  141. package/dist/lib/mcp/types.d.ts.map +1 -1
  142. package/dist/lib/mcp/types.js +1 -2
  143. package/dist/lib/mcp/types.js.map +1 -1
  144. package/dist/lib/memory/memory-manager.d.ts +1 -0
  145. package/dist/lib/memory/memory-manager.d.ts.map +1 -1
  146. package/dist/lib/memory/memory-manager.js +9 -0
  147. package/dist/lib/memory/memory-manager.js.map +1 -1
  148. package/dist/lib/orchestrator.d.ts +11 -8
  149. package/dist/lib/orchestrator.d.ts.map +1 -1
  150. package/dist/lib/orchestrator.js +246 -5
  151. package/dist/lib/orchestrator.js.map +1 -1
  152. package/dist/lib/sandbox/cdp-client.d.ts +15 -0
  153. package/dist/lib/sandbox/cdp-client.d.ts.map +1 -0
  154. package/dist/lib/sandbox/cdp-client.js +139 -0
  155. package/dist/lib/sandbox/cdp-client.js.map +1 -0
  156. package/dist/lib/sandbox/html-to-markdown.d.ts +9 -1
  157. package/dist/lib/sandbox/html-to-markdown.d.ts.map +1 -1
  158. package/dist/lib/sandbox/html-to-markdown.js +67 -10
  159. package/dist/lib/sandbox/html-to-markdown.js.map +1 -1
  160. package/dist/lib/sandbox/index.d.ts +6 -0
  161. package/dist/lib/sandbox/index.d.ts.map +1 -1
  162. package/dist/lib/sandbox/index.js +5 -0
  163. package/dist/lib/sandbox/index.js.map +1 -1
  164. package/dist/lib/sandbox/page-readiness.d.ts +37 -0
  165. package/dist/lib/sandbox/page-readiness.d.ts.map +1 -0
  166. package/dist/lib/sandbox/page-readiness.js +268 -0
  167. package/dist/lib/sandbox/page-readiness.js.map +1 -0
  168. package/dist/lib/sandbox/sandbox-browser.d.ts +4 -0
  169. package/dist/lib/sandbox/sandbox-browser.d.ts.map +1 -0
  170. package/dist/lib/sandbox/sandbox-browser.js +316 -0
  171. package/dist/lib/sandbox/sandbox-browser.js.map +1 -0
  172. package/dist/lib/sandbox/sandbox-container.d.ts +39 -0
  173. package/dist/lib/sandbox/sandbox-container.d.ts.map +1 -0
  174. package/dist/lib/sandbox/sandbox-container.js +176 -0
  175. package/dist/lib/sandbox/sandbox-container.js.map +1 -0
  176. package/dist/lib/sandbox/sandbox-file.d.ts +4 -0
  177. package/dist/lib/sandbox/sandbox-file.d.ts.map +1 -0
  178. package/dist/lib/sandbox/sandbox-file.js +169 -0
  179. package/dist/lib/sandbox/sandbox-file.js.map +1 -0
  180. package/dist/lib/sandbox/sandbox-shell.d.ts +5 -0
  181. package/dist/lib/sandbox/sandbox-shell.d.ts.map +1 -0
  182. package/dist/lib/sandbox/sandbox-shell.js +111 -0
  183. package/dist/lib/sandbox/sandbox-shell.js.map +1 -0
  184. package/dist/lib/sandbox/sandbox-web.d.ts.map +1 -1
  185. package/dist/lib/sandbox/sandbox-web.js +64 -24
  186. package/dist/lib/sandbox/sandbox-web.js.map +1 -1
  187. package/dist/lib/sandbox/types.d.ts +9 -0
  188. package/dist/lib/sandbox/types.d.ts.map +1 -1
  189. package/dist/lib/sandbox/types.js +1 -0
  190. package/dist/lib/sandbox/types.js.map +1 -1
  191. package/dist/lib/sandbox/vision-browser.d.ts +4 -0
  192. package/dist/lib/sandbox/vision-browser.d.ts.map +1 -0
  193. package/dist/lib/sandbox/vision-browser.js +298 -0
  194. package/dist/lib/sandbox/vision-browser.js.map +1 -0
  195. package/dist/lib/sea/app-window.d.ts +7 -0
  196. package/dist/lib/sea/app-window.d.ts.map +1 -0
  197. package/dist/lib/sea/app-window.js +95 -0
  198. package/dist/lib/sea/app-window.js.map +1 -0
  199. package/dist/lib/sea/bootstrap.d.ts +18 -0
  200. package/dist/lib/sea/bootstrap.d.ts.map +1 -0
  201. package/dist/lib/sea/bootstrap.js +103 -0
  202. package/dist/lib/sea/bootstrap.js.map +1 -0
  203. package/dist/lib/sea/sqlite-vec-shim.d.ts +3 -0
  204. package/dist/lib/sea/sqlite-vec-shim.d.ts.map +1 -0
  205. package/dist/lib/sea/sqlite-vec-shim.js +10 -0
  206. package/dist/lib/sea/sqlite-vec-shim.js.map +1 -0
  207. package/dist/lib/skills/skill-loader.d.ts +2 -0
  208. package/dist/lib/skills/skill-loader.d.ts.map +1 -1
  209. package/dist/lib/skills/skill-loader.js +12 -1
  210. package/dist/lib/skills/skill-loader.js.map +1 -1
  211. package/dist/lib/tasks/task-manager.d.ts +3 -1
  212. package/dist/lib/tasks/task-manager.d.ts.map +1 -1
  213. package/dist/lib/tasks/task-manager.js +11 -0
  214. package/dist/lib/tasks/task-manager.js.map +1 -1
  215. package/dist/lib/tasks/task-store.d.ts +1 -1
  216. package/dist/lib/tasks/task-store.d.ts.map +1 -1
  217. package/dist/lib/tasks/task-store.js.map +1 -1
  218. package/dist/lib/tasks/types.d.ts +18 -0
  219. package/dist/lib/tasks/types.d.ts.map +1 -1
  220. package/dist/lib/tools/built-in/integration-tools.d.ts +4 -0
  221. package/dist/lib/tools/built-in/integration-tools.d.ts.map +1 -0
  222. package/dist/lib/tools/built-in/integration-tools.js +47 -0
  223. package/dist/lib/tools/built-in/integration-tools.js.map +1 -0
  224. package/dist/lib/tools/built-in/knowledge-entity-lookup.tool.d.ts +1 -2
  225. package/dist/lib/tools/built-in/knowledge-entity-lookup.tool.d.ts.map +1 -1
  226. package/dist/lib/tools/built-in/knowledge-entity-lookup.tool.js +17 -17
  227. package/dist/lib/tools/built-in/knowledge-entity-lookup.tool.js.map +1 -1
  228. package/dist/lib/tools/built-in/knowledge-graph-schema.tool.d.ts.map +1 -1
  229. package/dist/lib/tools/built-in/knowledge-graph-schema.tool.js +2 -4
  230. package/dist/lib/tools/built-in/knowledge-graph-schema.tool.js.map +1 -1
  231. package/dist/lib/tools/built-in/knowledge-search.tool.js +4 -4
  232. package/dist/lib/tools/built-in/knowledge-search.tool.js.map +1 -1
  233. package/dist/lib/tools/built-in/knowledge-sql.tool.d.ts.map +1 -1
  234. package/dist/lib/tools/built-in/knowledge-sql.tool.js +74 -40
  235. package/dist/lib/tools/built-in/knowledge-sql.tool.js.map +1 -1
  236. package/dist/lib/tools/built-in/knowledge-tools-factory.js +2 -2
  237. package/dist/lib/tools/built-in/knowledge-tools-factory.js.map +1 -1
  238. package/dist/lib/tools/built-in/knowledge-traverse.tool.d.ts +1 -2
  239. package/dist/lib/tools/built-in/knowledge-traverse.tool.d.ts.map +1 -1
  240. package/dist/lib/tools/built-in/knowledge-traverse.tool.js +5 -11
  241. package/dist/lib/tools/built-in/knowledge-traverse.tool.js.map +1 -1
  242. package/dist/lib/tools/built-in/query-validators.d.ts.map +1 -1
  243. package/dist/lib/tools/built-in/query-validators.js +4 -0
  244. package/dist/lib/tools/built-in/query-validators.js.map +1 -1
  245. package/dist/lib/tools/workspace/workspace-tools.d.ts +1 -0
  246. package/dist/lib/tools/workspace/workspace-tools.d.ts.map +1 -1
  247. package/dist/lib/tools/workspace/workspace-tools.js +44 -4
  248. package/dist/lib/tools/workspace/workspace-tools.js.map +1 -1
  249. package/dist/lib/triggers/cron-trigger.d.ts +1 -1
  250. package/dist/lib/triggers/cron-trigger.d.ts.map +1 -1
  251. package/dist/lib/triggers/cron-trigger.js.map +1 -1
  252. package/dist/lib/triggers/trigger-manager.d.ts +1 -0
  253. package/dist/lib/triggers/trigger-manager.d.ts.map +1 -1
  254. package/dist/lib/triggers/trigger-manager.js +26 -0
  255. package/dist/lib/triggers/trigger-manager.js.map +1 -1
  256. package/dist/lib/triggers/webhook-trigger.d.ts +1 -1
  257. package/dist/lib/triggers/webhook-trigger.d.ts.map +1 -1
  258. package/dist/lib/triggers/webhook-trigger.js.map +1 -1
  259. package/dist/lib/types/llm-types.d.ts +22 -4
  260. package/dist/lib/types/llm-types.d.ts.map +1 -1
  261. package/dist/lib/types/llm-types.js +50 -0
  262. package/dist/lib/types/llm-types.js.map +1 -1
  263. package/dist/lib/types/tool-factory.d.ts +2 -2
  264. package/dist/lib/types/tool-factory.d.ts.map +1 -1
  265. package/dist/lib/types/tool-factory.js +9 -2
  266. package/dist/lib/types/tool-factory.js.map +1 -1
  267. package/dist/lib/utils/document-extract.d.ts +10 -0
  268. package/dist/lib/utils/document-extract.d.ts.map +1 -0
  269. package/dist/lib/utils/document-extract.js +149 -0
  270. package/dist/lib/utils/document-extract.js.map +1 -0
  271. package/dist/lib/utils/env-substitution.d.ts +6 -0
  272. package/dist/lib/utils/env-substitution.d.ts.map +1 -0
  273. package/dist/lib/utils/env-substitution.js +15 -0
  274. package/dist/lib/utils/env-substitution.js.map +1 -0
  275. package/dist/lib/workflows/react-workflow-executor.d.ts.map +1 -1
  276. package/dist/lib/workflows/react-workflow-executor.js +23 -17
  277. package/dist/lib/workflows/react-workflow-executor.js.map +1 -1
  278. package/dist/lib/workflows/types.d.ts +81 -55
  279. package/dist/lib/workflows/types.d.ts.map +1 -1
  280. package/dist/lib/workflows/types.js +10 -0
  281. package/dist/lib/workflows/types.js.map +1 -1
  282. package/dist/lib/workflows/workflow-loader.d.ts +3 -0
  283. package/dist/lib/workflows/workflow-loader.d.ts.map +1 -1
  284. package/dist/lib/workflows/workflow-loader.js +10 -1
  285. package/dist/lib/workflows/workflow-loader.js.map +1 -1
  286. package/dist/public/assets/logo.png +0 -0
  287. package/dist/public/chat.html +39 -0
  288. package/dist/public/index.html +6 -176
  289. package/dist/public/src/components/AgentComposer.js +807 -0
  290. package/dist/public/src/components/AgentsView.js +1812 -508
  291. package/dist/public/src/components/AppRoot.js +125 -38
  292. package/dist/public/src/components/GraphView.js +382 -300
  293. package/dist/public/src/components/IdeView.js +277 -86
  294. package/dist/public/src/components/KnowledgeView.js +94 -130
  295. package/dist/public/src/components/LlmView.js +15 -19
  296. package/dist/public/src/components/LocalLlmView.js +2440 -0
  297. package/dist/public/src/components/LogViewer.js +155 -0
  298. package/dist/public/src/components/McpView.js +41 -49
  299. package/dist/public/src/components/MonitorView.js +174 -83
  300. package/dist/public/src/components/NavBar.js +16 -26
  301. package/dist/public/src/components/StandaloneChat.js +875 -0
  302. package/dist/public/src/services/ApiService.js +203 -4
  303. package/dist/public/src/services/SessionStore.js +86 -0
  304. package/dist/public/src/services/StreamManager.js +183 -0
  305. package/dist/public/src/store.js +1 -3
  306. package/dist/public/src/utils/card.js +21 -0
  307. package/dist/public/src/utils/markdown.js +7 -0
  308. package/dist/public/styles.css +2777 -0
  309. package/dist/src/cli/commands/init.d.ts.map +1 -1
  310. package/dist/src/cli/commands/init.js +7 -1
  311. package/dist/src/cli/commands/init.js.map +1 -1
  312. package/dist/src/cli/commands/start.d.ts.map +1 -1
  313. package/dist/src/cli/commands/start.js +28 -5
  314. package/dist/src/cli/commands/start.js.map +1 -1
  315. package/dist/src/cli/index.js +19 -5
  316. package/dist/src/cli/index.js.map +1 -1
  317. package/dist/src/index.js +7 -1
  318. package/dist/src/index.js.map +1 -1
  319. package/dist/src/middleware/auth.d.ts.map +1 -1
  320. package/dist/src/middleware/auth.js +28 -6
  321. package/dist/src/middleware/auth.js.map +1 -1
  322. package/dist/src/middleware/rate-limit.d.ts +8 -0
  323. package/dist/src/middleware/rate-limit.d.ts.map +1 -0
  324. package/dist/src/middleware/rate-limit.js +21 -0
  325. package/dist/src/middleware/rate-limit.js.map +1 -0
  326. package/dist/src/routes/agents.route.d.ts.map +1 -1
  327. package/dist/src/routes/agents.route.js +138 -10
  328. package/dist/src/routes/agents.route.js.map +1 -1
  329. package/dist/src/routes/chat.route.d.ts +3 -0
  330. package/dist/src/routes/chat.route.d.ts.map +1 -0
  331. package/dist/src/routes/chat.route.js +156 -0
  332. package/dist/src/routes/chat.route.js.map +1 -0
  333. package/dist/src/routes/files.route.d.ts.map +1 -1
  334. package/dist/src/routes/files.route.js +37 -2
  335. package/dist/src/routes/files.route.js.map +1 -1
  336. package/dist/src/routes/llm.route.d.ts.map +1 -1
  337. package/dist/src/routes/llm.route.js +263 -8
  338. package/dist/src/routes/llm.route.js.map +1 -1
  339. package/dist/src/routes/local-llm.route.d.ts +3 -0
  340. package/dist/src/routes/local-llm.route.d.ts.map +1 -0
  341. package/dist/src/routes/local-llm.route.js +688 -0
  342. package/dist/src/routes/local-llm.route.js.map +1 -0
  343. package/dist/src/routes/logs.route.d.ts +3 -0
  344. package/dist/src/routes/logs.route.d.ts.map +1 -0
  345. package/dist/src/routes/logs.route.js +24 -0
  346. package/dist/src/routes/logs.route.js.map +1 -0
  347. package/dist/src/routes/tasks.route.d.ts.map +1 -1
  348. package/dist/src/routes/tasks.route.js +15 -1
  349. package/dist/src/routes/tasks.route.js.map +1 -1
  350. package/dist/src/routes/vnc.route.d.ts +12 -0
  351. package/dist/src/routes/vnc.route.d.ts.map +1 -0
  352. package/dist/src/routes/vnc.route.js +74 -0
  353. package/dist/src/routes/vnc.route.js.map +1 -0
  354. package/dist/src/routes/workflows.route.d.ts.map +1 -1
  355. package/dist/src/routes/workflows.route.js +24 -0
  356. package/dist/src/routes/workflows.route.js.map +1 -1
  357. package/dist/src/server.d.ts.map +1 -1
  358. package/dist/src/server.js +29 -3
  359. package/dist/src/server.js.map +1 -1
  360. package/dist/templates/Demo.md +152 -0
  361. package/dist/templates/README.md +12 -3
  362. package/dist/templates/agents/actor.agent.yaml +34 -0
  363. package/dist/templates/agents/architect.agent.yaml +20 -13
  364. package/dist/templates/agents/chatbot.agent.yaml +23 -27
  365. package/dist/templates/agents/corporate.agent.yaml +64 -0
  366. package/dist/templates/agents/functions.agent.yaml +29 -0
  367. package/dist/templates/agents/investment-analyst.agent.yaml +79 -0
  368. package/dist/templates/agents/music-librarian.agent.yaml +46 -0
  369. package/dist/templates/agents/network-security.agent.yaml +81 -0
  370. package/dist/templates/agents/transport-security.agent.yaml +69 -0
  371. package/dist/templates/agents/web-engineer.agent.yaml +98 -0
  372. package/dist/templates/agents/web-pilot.agent.yaml +57 -0
  373. package/dist/templates/knowledge/music-store/LICENSE.md +11 -0
  374. package/dist/templates/knowledge/music-store/musicstore.sqlite +0 -0
  375. package/dist/templates/knowledge/music-store/tables.png +0 -0
  376. package/dist/templates/knowledge/music-store.knowledge.yaml +138 -0
  377. package/dist/templates/knowledge/org-chart/personnel.csv +21 -21
  378. package/dist/templates/knowledge/org-chart.knowledge.yaml +4 -0
  379. package/dist/templates/knowledge/patient-records.knowledge.yaml +20 -0
  380. package/dist/templates/knowledge/pdf-patients/PDF_Deid_Deidentification_0.pdf +0 -0
  381. package/dist/templates/knowledge/pdf-patients/PDF_Deid_Deidentification_1.pdf +0 -0
  382. package/dist/templates/knowledge/pdf-patients/PDF_Deid_Deidentification_10.pdf +0 -0
  383. package/dist/templates/knowledge/pdf-patients/PDF_Deid_Deidentification_11.pdf +0 -0
  384. package/dist/templates/knowledge/pet-store.knowledge.yaml +3 -0
  385. package/dist/templates/knowledge/security-incidents/incidents.json +55935 -0
  386. package/dist/templates/knowledge/security-incidents.knowledge.yaml +46 -0
  387. package/dist/templates/knowledge/{example.knowledge.yaml → transcripts.knowledge.yaml} +9 -5
  388. package/dist/templates/knowledge/transport-ot/systems.csv +117 -0
  389. package/dist/templates/knowledge/transport-ot.knowledge.yaml +55 -0
  390. package/dist/templates/knowledge/web-docs.knowledge.yaml +1 -1
  391. package/dist/templates/llm.json +62 -22
  392. package/dist/templates/mcp.json +7 -4
  393. package/dist/templates/skills/orcha-builder/SKILL.md +148 -215
  394. package/dist/templates/skills/pii-guard/SKILL.md +22 -0
  395. package/dist/templates/skills/sandbox/SKILL.md +25 -48
  396. package/dist/templates/skills/web-pilot/SKILL.md +51 -0
  397. package/dist/templates/workflows/example.workflow.yaml +27 -35
  398. package/dist/templates/workflows/react-example.workflow.yaml +14 -19
  399. package/dist/templates/workflows/team-chat.workflow.yaml +47 -0
  400. package/package.json +17 -4
  401. package/dist/public/src/components/SkillsView.js +0 -137
  402. package/dist/public/src/components/WorkflowsView.js +0 -416
  403. package/dist/templates/agents/knowledge-broker.agent.yaml +0 -39
  404. package/dist/templates/agents/sandbox.agent.yaml +0 -56
@@ -1,9 +1,15 @@
1
1
 
2
2
  import { Component } from '../utils/Component.js';
3
3
  import { api } from '../services/ApiService.js';
4
+ import { sessionStore } from '../services/SessionStore.js';
5
+ import { streamManager } from '../services/StreamManager.js';
6
+ import { escapeHtml as sharedEscapeHtml } from '../utils/card.js';
4
7
  import { store } from '../store.js';
5
8
  import { markdownRenderer } from '../utils/markdown.js';
6
9
 
10
+ // Survives component remount (tab navigation) but not page refresh
11
+ const workflowTasks = new Map();
12
+
7
13
  export class AgentsView extends Component {
8
14
  constructor() {
9
15
  super();
@@ -12,19 +18,28 @@ export class AgentsView extends Component {
12
18
  this.streamStartTime = null;
13
19
  this.streamTimerInterval = null;
14
20
  this.streamUsageData = null;
21
+ this.pendingAttachments = [];
22
+ this._streamUnsubscribe = null;
15
23
  }
16
24
 
17
25
  async connectedCallback() {
18
26
  super.connectedCallback();
19
- await Promise.all([this.loadAgents(), this.loadLLMs()]);
27
+ await Promise.all([this.loadAgents(), this.loadLLMs(), this.loadWorkflows()]);
28
+ this.restoreActiveSession();
20
29
  }
21
30
 
22
31
  disconnectedCallback() {
23
- this.stopStreamTimer();
24
- if (this.currentAbortController) {
25
- this.currentAbortController.abort();
26
- this.currentAbortController = null;
32
+ if (this._streamUnsubscribe) {
33
+ this._streamUnsubscribe();
34
+ this._streamUnsubscribe = null;
35
+ }
36
+ if (this.streamTimerInterval) {
37
+ clearInterval(this.streamTimerInterval);
38
+ this.streamTimerInterval = null;
27
39
  }
40
+ this.currentAbortController = null;
41
+ // Workflow streams continue via AbortController — no cleanup needed on tab switch
42
+ // (state is preserved in the module-level workflowTasks map)
28
43
  }
29
44
 
30
45
  formatElapsedTime(ms) {
@@ -41,13 +56,33 @@ export class AgentsView extends Component {
41
56
  }
42
57
 
43
58
  cancelCurrentStream() {
59
+ const activeId = sessionStore.getActiveId();
60
+
61
+ // Cancel via server tasks API if we have a task ID
62
+ if (activeId) {
63
+ const streamState = streamManager.getState(activeId);
64
+ const wfState = workflowTasks.get(activeId);
65
+ const taskId = streamState?.taskId || wfState?.taskId;
66
+ if (taskId) {
67
+ api.cancelTask(taskId).catch(() => {});
68
+ }
69
+ }
70
+
44
71
  if (this.currentAbortController) {
45
72
  this.currentAbortController.abort();
73
+ return;
74
+ }
75
+ if (!activeId) return;
76
+ const wfState = workflowTasks.get(activeId);
77
+ if (wfState?.abortController) {
78
+ wfState.abortController.abort();
79
+ return;
46
80
  }
81
+ streamManager.cancel(activeId);
47
82
  }
48
83
 
49
- startStreamTimer(responseId) {
50
- this.streamStartTime = Date.now();
84
+ startStreamTimer(responseId, startTime) {
85
+ this.streamStartTime = startTime || Date.now();
51
86
  this.streamTimerInterval = setInterval(() => {
52
87
  const elapsed = Date.now() - this.streamStartTime;
53
88
  const bubble = this.querySelector(`#${responseId}`);
@@ -75,7 +110,7 @@ export class AgentsView extends Component {
75
110
  const statusBar = wrapper.querySelector('.stream-status-bar');
76
111
  const statsBar = wrapper.querySelector('.stream-stats-bar');
77
112
 
78
- if (statusBar) statusBar.classList.add('hidden');
113
+ if (statusBar) statusBar.remove();
79
114
 
80
115
  if (statsBar) {
81
116
  const elapsedEl = statsBar.querySelector('.stats-elapsed');
@@ -104,27 +139,20 @@ export class AgentsView extends Component {
104
139
 
105
140
  if (wasCancelled) {
106
141
  const cancelBadge = document.createElement('span');
107
- cancelBadge.className = 'text-xs text-amber-400 font-medium ml-2';
142
+ cancelBadge.className = 'badge badge-amber';
108
143
  cancelBadge.textContent = 'Cancelled';
109
144
  statsBar.appendChild(cancelBadge);
110
145
  }
111
146
 
112
- statsBar.classList.remove('hidden');
147
+ statsBar.classList.add('visible');
113
148
  }
114
149
  }
115
150
 
116
151
  async loadAgents() {
117
152
  try {
118
153
  const agents = await api.getAgents();
154
+ agents.sort((a, b) => (a.name || '').localeCompare(b.name || ''));
119
155
  store.set('agents', agents);
120
-
121
- if (agents.length > 0 && !store.get('selectedAgent') && !store.get('selectedLlm')) {
122
- store.set('selectedAgent', agents[agents.length - 1]);
123
- store.set('selectionType', 'agent');
124
- }
125
-
126
- this.renderAgentDropdown();
127
- this.updateSelectedAgentUI();
128
156
  } catch (e) {
129
157
  console.error('Failed to load agents', e);
130
158
  }
@@ -132,494 +160,1653 @@ export class AgentsView extends Component {
132
160
 
133
161
  async loadLLMs() {
134
162
  try {
135
- const llms = await api.getLLMs();
163
+ const [llms, llmConfig] = await Promise.all([api.getLLMs(), api.getLlmConfig()]);
164
+ // Resolve the default model name from the config pointer
165
+ const defaultPointer = llmConfig?.models?.default;
166
+ const defaultLlmName = typeof defaultPointer === 'string' ? defaultPointer : null;
136
167
  store.set('llms', llms);
137
- this.renderAgentDropdown();
168
+ store.set('defaultLlmName', defaultLlmName);
138
169
  } catch (e) {
139
170
  console.error('Failed to load LLMs', e);
140
171
  }
141
172
  }
142
173
 
143
- renderAgentDropdown() {
144
- const list = this.querySelector('#agentDropdownList');
174
+ async loadWorkflows() {
175
+ try {
176
+ const workflows = await api.getWorkflows();
177
+ workflows.sort((a, b) => (a.name || '').localeCompare(b.name || ''));
178
+ store.set('workflows', workflows);
179
+ } catch (e) {
180
+ console.error('Failed to load workflows', e);
181
+ }
182
+ }
183
+
184
+ // --- Sidebar toggle (mobile) ---
185
+
186
+ _isMobile() {
187
+ return !window.matchMedia('(min-width: 768px)').matches;
188
+ }
189
+
190
+ toggleSidebar(show) {
191
+ const sidebar = this.querySelector('#sidebar');
192
+ const backdrop = this.querySelector('#sidebarBackdrop');
193
+ if (!sidebar || !backdrop) return;
194
+
195
+ if (show) {
196
+ sidebar.classList.add('open');
197
+ backdrop.classList.add('visible');
198
+ } else {
199
+ sidebar.classList.remove('open');
200
+ backdrop.classList.remove('visible');
201
+ }
202
+ }
203
+
204
+ // --- Session management ---
205
+
206
+ restoreActiveSession() {
207
+ const activeId = sessionStore.getActiveId();
208
+ if (activeId && sessionStore.get(activeId)) {
209
+ this.switchToSession(activeId);
210
+ } else {
211
+ this.showEmptyState();
212
+ }
213
+ this.renderSessionList();
214
+ }
215
+
216
+ renderSessionList() {
217
+ const list = this.querySelector('#sessionList');
218
+ if (!list) return;
219
+
220
+ const sessions = sessionStore.getAll();
221
+ const activeId = sessionStore.getActiveId();
222
+
223
+ if (sessions.length === 0) {
224
+ list.innerHTML = '<div class="text-muted text-sm text-center py-8">No conversations yet</div>';
225
+ return;
226
+ }
227
+
228
+ list.innerHTML = sessions.map(s => {
229
+ const isActive = s.id === activeId;
230
+ let displayName, icon;
231
+ if (s.agentType === 'workflow') {
232
+ displayName = s.workflowName || 'Workflow';
233
+ icon = 'fa-project-diagram';
234
+ } else if (s.agentType === 'agent') {
235
+ displayName = s.agentName || 'Agent';
236
+ icon = 'fa-robot';
237
+ } else {
238
+ displayName = s.llmName || 'LLM';
239
+ icon = 'fa-microchip';
240
+ }
241
+
242
+ return `
243
+ <div data-session-id="${s.id}" class="session-item${isActive ? ' active' : ''}">
244
+ <div class="flex-1 min-w-0">
245
+ <div class="text-sm text-primary truncate">${this.escapeHtml(s.title)}</div>
246
+ <div class="flex items-center gap-1 mt-1 text-xs text-muted">
247
+ <i class="fas ${icon} text-2xs"></i>
248
+ <span class="truncate">${this.escapeHtml(displayName)}</span>
249
+ </div>
250
+ </div>
251
+ <button data-delete-id="${s.id}" class="session-delete-btn" title="Delete">
252
+ <i class="fas fa-xmark text-xs"></i>
253
+ </button>
254
+ </div>
255
+ `;
256
+ }).join('');
257
+
258
+ // Event listeners
259
+ list.querySelectorAll('.session-item').forEach(item => {
260
+ item.addEventListener('click', (e) => {
261
+ if (e.target.closest('.session-delete-btn')) return;
262
+ this.switchToSession(item.dataset.sessionId);
263
+ });
264
+ });
265
+
266
+ list.querySelectorAll('.session-delete-btn').forEach(btn => {
267
+ btn.addEventListener('click', (e) => {
268
+ e.stopPropagation();
269
+ this.deleteSession(btn.dataset.deleteId);
270
+ });
271
+ });
272
+ }
273
+
274
+ switchToSession(sessionId) {
275
+ // Detach from current stream rendering (don't abort — let it continue in background)
276
+ if (this._streamUnsubscribe) {
277
+ this._streamUnsubscribe();
278
+ this._streamUnsubscribe = null;
279
+ }
280
+ if (this.streamTimerInterval) {
281
+ clearInterval(this.streamTimerInterval);
282
+ this.streamTimerInterval = null;
283
+ }
284
+ this.streamStartTime = null;
285
+ this.currentAbortController = null;
286
+ this.isLoading = false;
287
+
288
+ const session = sessionStore.get(sessionId);
289
+ if (!session) return;
290
+
291
+ sessionStore.setActiveId(sessionId);
292
+
293
+ // Update store with this session's agent/LLM
145
294
  const agents = store.get('agents') || [];
146
295
  const llms = store.get('llms') || [];
147
- const selectionType = store.get('selectionType');
148
- const selectedAgent = store.get('selectedAgent');
149
- const selectedLlm = store.get('selectedLlm');
150
296
 
151
- if (!list) return;
297
+ if (session.agentType === 'workflow') {
298
+ const workflows = store.get('workflows') || [];
299
+ const wf = workflows.find(w => w.name === session.workflowName);
300
+ store.set('selectedWorkflow', wf || null);
301
+ store.set('selectedAgent', null);
302
+ store.set('selectedLlm', null);
303
+ store.set('selectionType', 'workflow');
304
+ } else if (session.agentType === 'agent') {
305
+ const agent = agents.find(a => a.name === session.agentName);
306
+ store.set('selectedAgent', agent || null);
307
+ store.set('selectedLlm', null);
308
+ store.set('selectedWorkflow', null);
309
+ store.set('selectionType', 'agent');
310
+ } else {
311
+ const llm = llms.find(l => l.name === session.llmName);
312
+ store.set('selectedLlm', llm || null);
313
+ store.set('selectedAgent', null);
314
+ store.set('selectedWorkflow', null);
315
+ store.set('selectionType', 'llm');
316
+ }
317
+
318
+ this.restoreMessages(session);
319
+ this.updateChatHeader(session);
320
+ this.renderSessionList();
321
+
322
+ // Reconnect to active stream if one exists for this session
323
+ const wfState = workflowTasks.get(sessionId);
324
+ if (wfState && wfState.status !== 'done') {
325
+ this._reconnectWorkflowStream(sessionId);
326
+ } else if (streamManager.isActive(sessionId)) {
327
+ this._reconnectToStream(sessionId);
328
+ }
152
329
 
153
- if (agents.length === 0 && llms.length === 0) {
154
- list.innerHTML = '<div class="text-gray-500 text-sm text-center py-4">No agents or LLMs available</div>';
330
+ this.updateUiState();
331
+
332
+ // Close sidebar on mobile after selecting
333
+ if (this._isMobile()) {
334
+ this.toggleSidebar(false);
335
+ }
336
+
337
+ const input = this.querySelector('#chatInput');
338
+ if (input) {
339
+ input.disabled = false;
340
+ input.readOnly = false;
341
+ input.classList.remove('cursor-pointer');
342
+ input.focus();
343
+ }
344
+ }
345
+
346
+ async restoreMessages(session) {
347
+ const container = this.querySelector('#chatMessages');
348
+ if (!container) return;
349
+ container.innerHTML = '';
350
+
351
+ if (session.messages.length === 0) {
352
+ this._appendWelcomeMessage(container);
155
353
  return;
156
354
  }
157
355
 
158
- let html = '';
356
+ for (const msg of session.messages) {
357
+ if (msg.role === 'user') {
358
+ this.appendMessage('user', msg.content);
359
+ } else {
360
+ this.appendRestoredAssistantMessage(msg.content, msg.meta);
361
+ }
362
+ }
363
+
364
+ // Check if server still has this session (survives restarts)
365
+ try {
366
+ const exists = await api.checkSession(session.id);
367
+ if (!exists) {
368
+ this._appendSessionResetBanner(container);
369
+ }
370
+ } catch {
371
+ // Server unreachable — skip banner
372
+ }
373
+ }
374
+
375
+ appendRestoredAssistantMessage(content, meta) {
376
+ const container = this.querySelector('#chatMessages');
377
+ const div = document.createElement('div');
378
+ div.className = 'response-wrapper';
379
+
380
+ const bubble = document.createElement('div');
381
+ bubble.className = 'flex justify-start';
382
+ bubble.innerHTML = `
383
+ <div class="response-bubble-inner group">
384
+ <div class="response-content markdown-content"></div>
385
+ <div class="tool-invocations"></div>
386
+ </div>
387
+ `;
388
+
389
+ const contentDiv = bubble.querySelector('.response-content');
390
+ if (content) {
391
+ const rendered = markdownRenderer.render(content);
392
+ contentDiv.innerHTML = rendered;
393
+ markdownRenderer.highlightCode(contentDiv);
394
+ }
395
+
396
+ // Render persisted thinking/tool pills
397
+ if (meta) {
398
+ const toolsDiv = bubble.querySelector('.tool-invocations');
399
+
400
+ if (meta.thinking) {
401
+ for (const thinkingContent of meta.thinking) {
402
+ this._createThinkingPill(toolsDiv, thinkingContent);
403
+ }
404
+ }
405
+
406
+ if (meta.tools) {
407
+ for (const t of meta.tools) {
408
+ if (t.output !== undefined) {
409
+ this._createToolPill(toolsDiv, t.runId, t.tool, t.input, t.output);
410
+ }
411
+ }
412
+ }
413
+
414
+ // Hide empty tool-invocations div
415
+ if (!toolsDiv.children.length) {
416
+ toolsDiv.classList.add('hidden');
417
+ }
418
+ } else {
419
+ bubble.querySelector('.tool-invocations').classList.add('hidden');
420
+ }
421
+
422
+ div.appendChild(bubble);
423
+
424
+ if (meta?.stats) {
425
+ const s = meta.stats;
426
+ const prefix = s.estimated ? '~' : '';
427
+ const tps = s.elapsed > 0 ? (s.outputTokens / (s.elapsed / 1000)).toFixed(1) : '0';
428
+ const statsBar = document.createElement('div');
429
+ statsBar.className = 'stream-stats-bar visible';
430
+ statsBar.innerHTML = `
431
+ <span class="flex items-center gap-1">
432
+ <i class="far fa-clock"></i>
433
+ <span>${this.formatElapsedTime(s.elapsed)}</span>
434
+ </span>
435
+ <span class="divider">|</span>
436
+ <span class="flex items-center gap-1">
437
+ <i class="fas fa-arrow-up text-2xs"></i>
438
+ <span>${prefix}${s.inputTokens} input</span>
439
+ </span>
440
+ <span class="divider">|</span>
441
+ <span class="flex items-center gap-1">
442
+ <i class="fas fa-arrow-down text-2xs"></i>
443
+ <span>${prefix}${s.outputTokens} output</span>
444
+ </span>
445
+ <span class="divider">|</span>
446
+ <span class="flex items-center gap-1">
447
+ <i class="fas fa-bolt text-2xs"></i>
448
+ <span>${prefix}${tps} tok/s</span>
449
+ </span>
450
+ `;
451
+ if (s.cancelled) {
452
+ const badge = document.createElement('span');
453
+ badge.className = 'badge badge-amber';
454
+ badge.textContent = 'Cancelled';
455
+ statsBar.appendChild(badge);
456
+ }
457
+ div.appendChild(statsBar);
458
+ }
459
+
460
+ container.appendChild(div);
461
+ container.scrollTop = container.scrollHeight;
462
+ }
463
+
464
+ showNewSessionModal() {
465
+ // Remove existing modal if any
466
+ const existing = document.querySelector('#newSessionModal');
467
+ if (existing) existing.remove();
468
+
469
+ const agents = store.get('agents') || [];
470
+ const workflows = store.get('workflows') || [];
471
+ const llms = store.get('llms') || [];
472
+
473
+ const overlay = document.createElement('div');
474
+ overlay.id = 'newSessionModal';
475
+ overlay.className = 'modal-backdrop';
476
+
477
+ let itemsHtml = '';
159
478
 
160
- // Agents section
161
479
  if (agents.length > 0) {
162
- html += '<div class="px-4 py-2 text-xs font-semibold text-gray-400 uppercase tracking-wider bg-dark-bg/50">Agents</div>';
163
- html += agents.map(agent => {
164
- const isSelected = selectionType === 'agent' && selectedAgent?.name === agent.name;
165
- return `
166
- <div data-type="agent" data-name="${agent.name}" class="selection-item px-4 py-3 hover:bg-dark-hover cursor-pointer transition-colors border-b border-dark-border ${isSelected ? 'bg-dark-hover' : ''}">
167
- <div class="flex items-start justify-between">
168
- <div class="flex-1">
169
- <div class="font-medium text-gray-200 mb-0.5">${agent.name}</div>
170
- <div class="text-xs text-gray-500 line-clamp-2">${agent.description}</div>
171
- </div>
172
- ${isSelected ? '<i class="fas fa-check text-blue-400 ml-2 mt-1"></i>' : ''}
173
- </div>
480
+ itemsHtml += '<div class="modal-section-label">Agents</div>';
481
+ itemsHtml += agents.map(a => `
482
+ <button data-type="agent" data-name="${this.escapeHtml(a.name)}" class="modal-pick-item">
483
+ <i class="fas fa-robot text-blue text-sm"></i>
484
+ <div class="flex-1 min-w-0">
485
+ <div class="text-sm font-medium text-primary">${this.escapeHtml(a.name)}</div>
486
+ <div class="text-xs text-muted truncate">${this.escapeHtml(a.description || '')}</div>
174
487
  </div>
175
- `;
176
- }).join('');
488
+ </button>
489
+ `).join('');
490
+ }
491
+
492
+ if (workflows.length > 0) {
493
+ itemsHtml += '<div class="modal-section-label">Workflows</div>';
494
+ itemsHtml += workflows.map(w => `
495
+ <button data-type="workflow" data-name="${this.escapeHtml(w.name)}" class="modal-pick-item">
496
+ <i class="fas fa-project-diagram text-orange text-sm"></i>
497
+ <div class="flex-1 min-w-0">
498
+ <div class="text-sm font-medium text-primary">${this.escapeHtml(w.name)}</div>
499
+ <div class="text-xs text-muted truncate">${this.escapeHtml(w.description || '')}</div>
500
+ </div>
501
+ </button>
502
+ `).join('');
177
503
  }
178
504
 
179
- // LLMs section
180
505
  if (llms.length > 0) {
181
- html += '<div class="px-4 py-2 text-xs font-semibold text-gray-400 uppercase tracking-wider bg-dark-bg/50">LLMs</div>';
182
- html += llms.map(llm => {
183
- const isSelected = selectionType === 'llm' && selectedLlm?.name === llm.name;
506
+ const defaultLlmName = store.get('defaultLlmName');
507
+ itemsHtml += '<div class="modal-section-label">LLMs</div>';
508
+ itemsHtml += llms.map(l => {
509
+ const isDefault = l.name === defaultLlmName;
184
510
  return `
185
- <div data-type="llm" data-name="${llm.name}" class="selection-item px-4 py-3 hover:bg-dark-hover cursor-pointer transition-colors border-b border-dark-border last:border-b-0 ${isSelected ? 'bg-dark-hover' : ''}">
186
- <div class="flex items-start justify-between">
187
- <div class="flex-1">
188
- <div class="font-medium text-gray-200 mb-0.5">${llm.name}</div>
189
- <div class="text-xs text-gray-500 line-clamp-2">${llm.model}</div>
190
- </div>
191
- ${isSelected ? '<i class="fas fa-check text-blue-400 ml-2 mt-1"></i>' : ''}
511
+ <button data-type="llm" data-name="${this.escapeHtml(l.name)}" class="modal-pick-item">
512
+ <i class="fas fa-microchip text-purple text-sm"></i>
513
+ <div class="flex-1 min-w-0">
514
+ <div class="flex items-center gap-2">
515
+ <span class="text-sm font-medium text-primary">${this.escapeHtml(l.name)}</span>
516
+ ${isDefault ? '<span class="badge badge-green text-2xs">default</span>' : ''}
192
517
  </div>
518
+ <div class="text-xs text-muted truncate">${this.escapeHtml(l.model || '')}</div>
193
519
  </div>
194
- `;
195
- }).join('');
520
+ </button>
521
+ `}).join('');
196
522
  }
197
523
 
198
- list.innerHTML = html;
524
+ if (!itemsHtml) {
525
+ itemsHtml = '<div class="text-muted text-sm text-center py-8">No agents, workflows or LLMs available</div>';
526
+ }
199
527
 
200
- list.querySelectorAll('.selection-item').forEach(item => {
528
+ overlay.innerHTML = `
529
+ <div class="modal-content modal-content-sm">
530
+ <div class="modal-header">
531
+ <h3 class="text-lg font-semibold text-primary">New conversation</h3>
532
+ <button id="closeNewSessionModal" class="modal-close-btn">
533
+ <i class="fas fa-xmark"></i>
534
+ </button>
535
+ </div>
536
+ <div class="overflow-y-auto custom-scrollbar flex-1">
537
+ ${itemsHtml}
538
+ </div>
539
+ </div>
540
+ `;
541
+
542
+ document.body.appendChild(overlay);
543
+
544
+ // Close on backdrop click
545
+ overlay.addEventListener('click', (e) => {
546
+ if (e.target === overlay) overlay.remove();
547
+ });
548
+
549
+ overlay.querySelector('#closeNewSessionModal').addEventListener('click', () => overlay.remove());
550
+
551
+ // Pick handler
552
+ overlay.querySelectorAll('.modal-pick-item').forEach(item => {
201
553
  item.addEventListener('click', () => {
202
554
  const type = item.dataset.type;
203
555
  const name = item.dataset.name;
204
556
 
205
- // Check if we're actually switching to a different agent/llm
206
- const currentType = store.get('selectionType');
207
- const currentAgent = store.get('selectedAgent');
208
- const currentLlm = store.get('selectedLlm');
209
- const currentName = currentType === 'agent' ? currentAgent?.name : currentLlm?.name;
210
-
211
- const isSwitching = !(type === currentType && name === currentName);
212
-
213
- if (type === 'agent') {
214
- const agent = agents.find(a => a.name === name);
215
- store.set('selectedAgent', agent);
216
- store.set('selectedLlm', null);
217
- store.set('selectionType', 'agent');
218
- } else if (type === 'llm') {
219
- const llm = llms.find(l => l.name === name);
220
- store.set('selectedLlm', llm);
221
- store.set('selectedAgent', null);
222
- store.set('selectionType', 'llm');
557
+ const session = sessionStore.create({
558
+ agentName: type === 'agent' ? name : null,
559
+ agentType: type,
560
+ llmName: type === 'llm' ? name : null,
561
+ workflowName: type === 'workflow' ? name : null
562
+ });
563
+
564
+ overlay.remove();
565
+ this.switchToSession(session.id);
566
+ });
567
+ });
568
+ }
569
+
570
+ showNewAgentModal() {
571
+ const existing = document.querySelector('#newAgentModal');
572
+ if (existing) existing.remove();
573
+
574
+ const agents = store.get('agents') || [];
575
+ const hasArchitect = agents.some(a => a.name === 'architect');
576
+
577
+ const overlay = document.createElement('div');
578
+ overlay.id = 'newAgentModal';
579
+ overlay.className = 'modal-backdrop';
580
+
581
+ overlay.innerHTML = `
582
+ <div class="modal-content modal-content-sm">
583
+ <div class="modal-header">
584
+ <h3 class="text-lg font-semibold text-primary">Create a new agent</h3>
585
+ <button id="closeNewAgentModal" class="modal-close-btn">
586
+ <i class="fas fa-xmark"></i>
587
+ </button>
588
+ </div>
589
+ <div class="p-4 flex flex-col gap-3">
590
+ ${hasArchitect ? `
591
+ <button id="agentViaArchitect" class="new-agent-option">
592
+ <div class="new-agent-option-icon bg-blue">
593
+ <i class="fas fa-comments"></i>
594
+ </div>
595
+ <div class="flex-1 min-w-0">
596
+ <div class="text-sm font-medium text-primary">Chat with Architect</div>
597
+ <div class="text-xs text-muted">Describe what you need and the Architect agent will build it for you</div>
598
+ </div>
599
+ <i class="fas fa-chevron-right text-xs text-muted"></i>
600
+ </button>
601
+ ` : ''}
602
+ <button id="agentViaIde" class="new-agent-option">
603
+ <div class="new-agent-option-icon bg-green">
604
+ <i class="fas fa-code"></i>
605
+ </div>
606
+ <div class="flex-1 min-w-0">
607
+ <div class="text-sm font-medium text-primary">Create in IDE</div>
608
+ <div class="text-xs text-muted">Open the IDE editor with a blank agent template</div>
609
+ </div>
610
+ <i class="fas fa-chevron-right text-xs text-muted"></i>
611
+ </button>
612
+ </div>
613
+ </div>
614
+ `;
615
+
616
+ document.body.appendChild(overlay);
617
+
618
+ overlay.addEventListener('click', (e) => {
619
+ if (e.target === overlay) overlay.remove();
620
+ });
621
+
622
+ overlay.querySelector('#closeNewAgentModal').addEventListener('click', () => overlay.remove());
623
+
624
+ if (hasArchitect) {
625
+ overlay.querySelector('#agentViaArchitect').addEventListener('click', () => {
626
+ overlay.remove();
627
+ const session = sessionStore.create({
628
+ agentName: 'architect',
629
+ agentType: 'agent',
630
+ llmName: null,
631
+ workflowName: null,
632
+ });
633
+ this.switchToSession(session.id);
634
+ });
635
+ }
636
+
637
+ overlay.querySelector('#agentViaIde').addEventListener('click', () => {
638
+ overlay.remove();
639
+ store.set('activeTab', 'ide');
640
+ window.location.hash = 'ide';
641
+ // Wait for IDE to mount, then trigger the new agent dialog
642
+ setTimeout(() => {
643
+ const ide = document.querySelector('ide-view');
644
+ if (ide && ide._selectResourceType) {
645
+ ide._selectResourceType('agent');
223
646
  }
647
+ }, 200);
648
+ });
649
+ }
650
+
651
+ deleteSession(sessionId) {
652
+ sessionStore.delete(sessionId);
653
+ const activeId = sessionStore.getActiveId();
654
+
655
+ if (!activeId || activeId === sessionId) {
656
+ // Switch to most recent remaining session
657
+ const sessions = sessionStore.getAll();
658
+ if (sessions.length > 0) {
659
+ this.switchToSession(sessions[0].id);
660
+ } else {
661
+ this.showEmptyState();
662
+ this.renderSessionList();
663
+ }
664
+ } else {
665
+ this.renderSessionList();
666
+ }
667
+ }
668
+
669
+ showEmptyState() {
670
+ const container = this.querySelector('#chatMessages');
671
+ if (container) {
672
+ container.innerHTML = `
673
+ <div class="flex-1 flex items-center justify-center h-full">
674
+ <div class="text-center text-muted">
675
+ <i class="fas fa-comments text-4xl mb-4 text-muted"></i>
676
+ <p class="text-lg">Start a new conversation</p>
677
+ <p class="text-sm mt-1">Click "New chat" to begin</p>
678
+ </div>
679
+ </div>
680
+ `;
681
+ }
224
682
 
225
- // Clear chat history when switching
226
- if (isSwitching) {
227
- this.clearChatHistory();
683
+ this.updateChatHeader(null);
684
+
685
+ const input = this.querySelector('#chatInput');
686
+ if (input) {
687
+ input.disabled = false;
688
+ input.readOnly = true;
689
+ input.classList.add('cursor-pointer');
690
+ }
691
+
692
+ const btn = this.querySelector('#sendMessageBtn');
693
+ if (btn) btn.disabled = true;
694
+ }
695
+
696
+ updateChatHeader(session) {
697
+ const header = this.querySelector('#chatHeader');
698
+ if (!header) return;
699
+
700
+ if (!session) {
701
+ header.innerHTML = '<span class="text-muted">No conversation selected</span>';
702
+ return;
703
+ }
704
+
705
+ let name, badgeText, badgeVariant, icon;
706
+ if (session.agentType === 'workflow') {
707
+ name = session.workflowName || 'Workflow';
708
+ badgeText = 'Workflow';
709
+ badgeVariant = 'badge-orange';
710
+ icon = 'fa-project-diagram';
711
+ } else if (session.agentType === 'agent') {
712
+ name = session.agentName || 'Agent';
713
+ badgeText = 'Agent';
714
+ badgeVariant = 'badge-blue';
715
+ icon = 'fa-robot';
716
+ } else {
717
+ name = session.llmName || 'LLM';
718
+ badgeText = 'LLM';
719
+ badgeVariant = 'badge-purple';
720
+ icon = 'fa-microchip';
721
+ }
722
+ const isAgent = session.agentType === 'agent';
723
+
724
+ let extraBadges = '';
725
+ if (isAgent) {
726
+ const agents = store.get('agents') || [];
727
+ const agent = agents.find(a => a.name === session.agentName);
728
+ if (agent) {
729
+ if (agent.publish?.enabled) {
730
+ const chatUrl = `/chat/${encodeURIComponent(agent.name)}`;
731
+ extraBadges += `<a href="${chatUrl}" target="_blank" class="badge badge-pill badge-green no-underline" title="Open published chat"><i class="fas fa-globe text-2xs"></i> Published</a>`;
228
732
  }
229
733
 
230
- this.updateSelectedAgentUI();
231
- this.toggleDropdown(false);
232
- this.renderAgentDropdown(); // Re-render to update selection checkmark
734
+ const hasMemory = agent.memory === true || (agent.memory && agent.memory.enabled);
735
+ if (hasMemory) {
736
+ extraBadges += `<span class="badge badge-pill badge-amber" title="Persistent memory enabled"><i class="fas fa-brain text-2xs"></i> Memory</span>`;
737
+ }
738
+
739
+ if (agent.tools?.length) {
740
+ const toolNames = agent.tools.map(t => typeof t === 'string' ? t : t.name);
741
+ const toolListHtml = toolNames.map(t => `<div class="tools-popover-item">${this.escapeHtml(t)}</div>`).join('');
742
+ extraBadges += `
743
+ <span class="tools-badge-wrapper">
744
+ <span class="badge badge-pill badge-gray"><i class="fas fa-wrench text-2xs"></i> ${toolNames.length} tool${toolNames.length !== 1 ? 's' : ''}</span>
745
+ <div class="tools-popover">${toolListHtml}</div>
746
+ </span>`;
747
+ }
748
+ }
749
+ }
750
+
751
+ header.innerHTML = `
752
+ <div class="flex items-center gap-2 flex-wrap">
753
+ <i class="fas ${icon} text-sm text-secondary"></i>
754
+ <span class="font-medium text-primary">${this.escapeHtml(name)}</span>
755
+ <span class="badge badge-pill ${badgeVariant}">${badgeText}</span>
756
+ ${extraBadges}
757
+ </div>
758
+ `;
759
+ }
760
+
761
+ // --- File Attachments ---
762
+
763
+ handleFileSelect(e) {
764
+ const files = Array.from(e.target.files);
765
+ e.target.value = '';
766
+
767
+ const needsConversion = ['image/webp', 'image/bmp', 'image/tiff'];
768
+
769
+ for (const file of files) {
770
+ if (needsConversion.includes(file.type)) {
771
+ this.convertImageToJpeg(file);
772
+ } else {
773
+ const reader = new FileReader();
774
+ reader.onload = () => {
775
+ const dataUrl = reader.result;
776
+ const commaIdx = dataUrl.indexOf(',');
777
+ const base64 = dataUrl.slice(commaIdx + 1);
778
+ const mediaType = file.type || 'application/octet-stream';
779
+
780
+ this.pendingAttachments.push({ data: base64, mediaType, name: file.name });
781
+ this.renderAttachmentPreview();
782
+ };
783
+ reader.readAsDataURL(file);
784
+ }
785
+ }
786
+ }
787
+
788
+ convertImageToJpeg(file) {
789
+ const img = new Image();
790
+ const url = URL.createObjectURL(file);
791
+ img.onload = () => {
792
+ const canvas = document.createElement('canvas');
793
+ canvas.width = img.naturalWidth;
794
+ canvas.height = img.naturalHeight;
795
+ canvas.getContext('2d').drawImage(img, 0, 0);
796
+ URL.revokeObjectURL(url);
797
+
798
+ const dataUrl = canvas.toDataURL('image/jpeg', 0.92);
799
+ const base64 = dataUrl.split(',')[1];
800
+ this.pendingAttachments.push({ data: base64, mediaType: 'image/jpeg', name: file.name });
801
+ this.renderAttachmentPreview();
802
+ };
803
+ img.src = url;
804
+ }
805
+
806
+ renderAttachmentPreview() {
807
+ const preview = this.querySelector('#attachmentPreview');
808
+ if (!preview) return;
809
+
810
+ if (this.pendingAttachments.length === 0) {
811
+ preview.classList.remove('visible');
812
+ preview.innerHTML = '';
813
+ return;
814
+ }
815
+
816
+ preview.classList.add('visible');
817
+ preview.innerHTML = this.pendingAttachments.map((att, i) => {
818
+ const isImage = att.mediaType.startsWith('image/');
819
+ const thumb = isImage
820
+ ? `<img src="data:${att.mediaType};base64,${att.data}">`
821
+ : `<i class="fas fa-file text-secondary text-lg"></i>`;
822
+ return `
823
+ <div class="attachment-pill">
824
+ ${thumb}
825
+ <span class="truncate attachment-name">${this.escapeHtml(att.name)}</span>
826
+ <button class="attachment-remove" data-index="${i}">
827
+ <i class="fas fa-xmark text-xs"></i>
828
+ </button>
829
+ </div>
830
+ `;
831
+ }).join('');
832
+
833
+ preview.querySelectorAll('.attachment-remove').forEach(btn => {
834
+ btn.addEventListener('click', (e) => {
835
+ const idx = parseInt(e.currentTarget.dataset.index, 10);
836
+ this.pendingAttachments.splice(idx, 1);
837
+ this.renderAttachmentPreview();
233
838
  });
234
839
  });
235
840
  }
236
841
 
237
- updateSelectedAgentUI() {
842
+ clearAttachments() {
843
+ this.pendingAttachments = [];
844
+ this.renderAttachmentPreview();
845
+ }
846
+
847
+ // --- Messaging ---
848
+
849
+ async sendMessage() {
850
+ const input = this.querySelector('#chatInput');
851
+ const message = input.value.trim();
238
852
  const selectionType = store.get('selectionType');
239
853
  const agent = store.get('selectedAgent');
240
854
  const llm = store.get('selectedLlm');
241
- const nameEl = this.querySelector('#selectedAgentName');
242
- const btn = this.querySelector('#sendMessageBtn');
855
+ const activeId = sessionStore.getActiveId();
856
+
857
+ const hasAttachments = this.pendingAttachments.length > 0;
858
+ if ((!message && !hasAttachments) || this.isLoading || !activeId) return;
859
+
860
+ // Handle workflow messages (including interrupt responses)
861
+ if (selectionType === 'workflow') {
862
+ return this._sendWorkflowMessage(message);
863
+ }
243
864
 
244
865
  const selected = selectionType === 'agent' ? agent : llm;
866
+ if (!selected) return;
867
+
868
+ const attachments = hasAttachments ? [...this.pendingAttachments] : null;
869
+
870
+ this.appendMessage('user', message || '(attached files)', { attachments });
871
+ sessionStore.addMessage(activeId, 'user', message || '(attached files)');
872
+ input.value = '';
873
+ input.style.height = 'auto';
874
+ this.clearAttachments();
875
+
876
+ this.isLoading = true;
877
+ this.updateUiState();
878
+
879
+ const responseId = 'response-' + Date.now();
880
+ this.createResponseBubble(responseId);
881
+
882
+ const abortController = new AbortController();
883
+ this.currentAbortController = abortController;
884
+ this.streamUsageData = null;
885
+ this.startStreamTimer(responseId);
886
+
887
+ try {
888
+ let response;
889
+ if (selectionType === 'agent') {
890
+ const inputVars = agent.inputVariables || ['message'];
891
+ const inputObj = {};
892
+ inputObj[inputVars[0] || 'message'] = message;
893
+ if (attachments) inputObj.attachments = attachments;
894
+ response = await api.streamAgent(agent.name, inputObj, activeId, { signal: abortController.signal });
895
+ } else {
896
+ response = await api.streamLLM(llm.name, message, activeId, attachments, { signal: abortController.signal });
897
+ }
898
+
899
+ streamManager.start(activeId, {
900
+ response,
901
+ abortController,
902
+ streamType: selectionType === 'agent' ? 'agent' : 'llm',
903
+ inputMessage: message,
904
+ responseId,
905
+ });
245
906
 
246
- if (nameEl && selected) {
247
- nameEl.textContent = selected.name;
248
- } else if (nameEl) {
249
- nameEl.textContent = 'Select Agent/LLM';
907
+ this._attachToStream(activeId, responseId);
908
+ } catch (e) {
909
+ const wasCancelled = e.name === 'AbortError';
910
+ if (!wasCancelled) {
911
+ this.updateResponseError(responseId, `Error: ${e.message}`);
912
+ }
913
+ this.stopStreamTimer(responseId, message, '', wasCancelled);
914
+ this.currentAbortController = null;
915
+ this.isLoading = false;
916
+ this.updateUiState();
250
917
  }
251
918
 
252
- if (btn) btn.disabled = !selected || this.isLoading;
919
+ this.renderSessionList();
253
920
  }
254
921
 
255
- toggleDropdown(show) {
256
- const dropdown = this.querySelector('#agentDropdown');
257
- if (dropdown) {
258
- if (show === undefined) {
259
- dropdown.classList.toggle('hidden');
260
- } else if (show) {
261
- dropdown.classList.remove('hidden');
922
+ _attachToStream(sessionId, responseId, initialThinkingState) {
923
+ const state = streamManager.getState(sessionId);
924
+ if (!state) return;
925
+
926
+ const thinkingState = initialThinkingState || {
927
+ inThinking: false,
928
+ thinkingSections: [],
929
+ currentSection: null,
930
+ thinkingContent: '',
931
+ thinkingPill: null,
932
+ };
933
+
934
+ // For LLM streams, remove loading dots (skip if reconnecting with no content yet)
935
+ if (state.streamType === 'llm') {
936
+ const hasContent = state.content || state.events.length > 0;
937
+ if (hasContent || !initialThinkingState) {
938
+ const bubble = this.querySelector(`#${responseId}`);
939
+ if (bubble) {
940
+ const contentDiv = bubble.querySelector('.response-content');
941
+ const loadingDots = contentDiv?.querySelector('.loading-dots');
942
+ if (loadingDots) {
943
+ loadingDots.remove();
944
+ bubble.querySelector('.response-bubble-inner')?.classList.remove('loading');
945
+ contentDiv.classList.remove('flex', 'items-center', 'whitespace-pre-wrap');
946
+ contentDiv.innerHTML = '';
947
+ }
948
+ }
949
+ }
950
+ }
951
+
952
+ let hasToolCalls = false;
953
+
954
+ this._streamUnsubscribe = streamManager.subscribe(sessionId, (event) => {
955
+ if (event.type === '_stream_end') {
956
+ this.streamUsageData = state.usageData;
957
+ const wasCancelled = event.status === 'cancelled';
958
+
959
+ // Finalize any in-progress thinking pill
960
+ const bubble = this.querySelector(`#${responseId}`);
961
+ if (bubble) {
962
+ const toolsDiv = bubble.querySelector('.tool-invocations');
963
+ this.finalizeThinkingPill(toolsDiv, thinkingState);
964
+ }
965
+
966
+ if (state.streamType === 'agent') {
967
+ if (hasToolCalls && !state.content.trim()) {
968
+ const bubble = this.querySelector(`#${responseId}`);
969
+ if (bubble) {
970
+ const contentDiv = bubble.querySelector('.response-content');
971
+ const loadingDots = contentDiv?.querySelector('.loading-dots');
972
+ if (loadingDots) {
973
+ loadingDots.remove();
974
+ bubble.querySelector('.response-bubble-inner')?.classList.remove('loading');
975
+ contentDiv.classList.remove('flex', 'items-center', 'whitespace-pre-wrap');
976
+ contentDiv.innerHTML = '';
977
+ }
978
+ }
979
+ }
980
+ }
981
+
982
+ this.stopStreamTimer(responseId, state.inputMessage, state.content, wasCancelled);
983
+ this.currentAbortController = null;
984
+ this.isLoading = false;
985
+ this.updateUiState();
986
+ this.renderSessionList();
987
+ this._streamUnsubscribe = null;
988
+
989
+ const input = this.querySelector('#chatInput');
990
+ if (input) input.focus();
991
+ return;
992
+ }
993
+
994
+ if (state.streamType === 'agent') {
995
+ if (event.type === 'tool_start' || event.type === 'tool_end') hasToolCalls = true;
996
+ this.handleStreamEvent(event, responseId, state.content, thinkingState);
997
+ } else {
998
+ if (event.type === 'usage') {
999
+ this.streamUsageData = state.usageData;
1000
+ return;
1001
+ }
1002
+ if (event.error) {
1003
+ this.updateResponseError(responseId, `Error: ${event.error}`);
1004
+ return;
1005
+ }
1006
+ if (event.type === 'thinking') {
1007
+ const bubble = this.querySelector(`#${responseId}`);
1008
+ if (bubble) {
1009
+ const toolsDiv = bubble.querySelector('.tool-invocations');
1010
+ const container = this.querySelector('#chatMessages');
1011
+ const loadingDots = bubble.querySelector('.response-content .loading-dots');
1012
+ if (loadingDots) {
1013
+ loadingDots.remove();
1014
+ bubble.querySelector('.response-bubble-inner')?.classList.remove('loading');
1015
+ const contentDiv = bubble.querySelector('.response-content');
1016
+ contentDiv.classList.remove('flex', 'items-center', 'whitespace-pre-wrap');
1017
+ contentDiv.innerHTML = '';
1018
+ }
1019
+ this.handleThinkingEvent(event, toolsDiv, thinkingState, container);
1020
+ }
1021
+ return;
1022
+ }
1023
+ if (event.content) {
1024
+ const bubble = this.querySelector(`#${responseId}`);
1025
+ if (bubble) {
1026
+ const contentDiv = bubble.querySelector('.response-content');
1027
+ const toolsDiv = bubble.querySelector('.tool-invocations');
1028
+ const loadingDots = contentDiv?.querySelector('.loading-dots');
1029
+ if (loadingDots) {
1030
+ loadingDots.remove();
1031
+ bubble.querySelector('.response-bubble-inner')?.classList.remove('loading');
1032
+ contentDiv.classList.remove('flex', 'items-center', 'whitespace-pre-wrap');
1033
+ contentDiv.innerHTML = '';
1034
+ }
1035
+ this.finalizeThinkingPill(toolsDiv, thinkingState);
1036
+ const container = this.querySelector('#chatMessages');
1037
+ this.renderLlmContentStreaming(contentDiv, state.content, responseId, thinkingState);
1038
+ if (container) container.scrollTop = container.scrollHeight;
1039
+ }
1040
+ }
1041
+ }
1042
+ });
1043
+ }
1044
+
1045
+ _reconnectToStream(sessionId) {
1046
+ const state = streamManager.getState(sessionId);
1047
+ if (!state || state.status !== 'streaming') return false;
1048
+
1049
+ this.createResponseBubble(state.responseId);
1050
+ const snapshotState = this._renderStreamSnapshot(state.responseId, state);
1051
+
1052
+ const thinkingState = {
1053
+ inThinking: false,
1054
+ thinkingSections: [],
1055
+ currentSection: null,
1056
+ thinkingContent: snapshotState.thinkingContent || '',
1057
+ thinkingPill: snapshotState.thinkingPill || null,
1058
+ };
1059
+
1060
+ this.streamUsageData = state.usageData;
1061
+ this.startStreamTimer(state.responseId, state.startTime);
1062
+
1063
+ this.isLoading = true;
1064
+ this.currentAbortController = state.abortController;
1065
+ this.updateUiState();
1066
+
1067
+ this._attachToStream(sessionId, state.responseId, thinkingState);
1068
+ return true;
1069
+ }
1070
+
1071
+ _renderStreamSnapshot(responseId, state) {
1072
+ const bubble = this.querySelector(`#${responseId}`);
1073
+ if (!bubble) return {};
1074
+
1075
+ const contentDiv = bubble.querySelector('.response-content');
1076
+ const toolsDiv = bubble.querySelector('.tool-invocations');
1077
+
1078
+ const hasVisualContent = state.content ||
1079
+ state.events.some(e => e.type === 'thinking' || e.type === 'tool_start' || e.type === 'content');
1080
+ const loadingDots = contentDiv.querySelector('.loading-dots');
1081
+ if (loadingDots && hasVisualContent) {
1082
+ loadingDots.remove();
1083
+ bubble.querySelector('.response-bubble-inner').classList.remove('loading');
1084
+ contentDiv.classList.remove('flex', 'items-center', 'whitespace-pre-wrap');
1085
+ contentDiv.innerHTML = '';
1086
+ }
1087
+
1088
+ let activeThinkingPill = null;
1089
+ let activeThinkingContent = '';
1090
+
1091
+ if (state.streamType === 'agent') {
1092
+ const tools = new Map();
1093
+ const completedThinking = [];
1094
+ let currentThinking = '';
1095
+ let lastWasThinking = false;
1096
+ let lastReactIteration = null;
1097
+
1098
+ for (const event of state.events) {
1099
+ if (event.type === 'thinking') {
1100
+ currentThinking += event.content;
1101
+ lastWasThinking = true;
1102
+ } else {
1103
+ if (lastWasThinking && currentThinking) {
1104
+ completedThinking.push(currentThinking);
1105
+ currentThinking = '';
1106
+ }
1107
+ lastWasThinking = false;
1108
+ }
1109
+
1110
+ if (event.type === 'tool_start') {
1111
+ tools.set(event.runId, { tool: event.tool, input: event.input, done: false });
1112
+ }
1113
+ if (event.type === 'tool_end') {
1114
+ const t = tools.get(event.runId);
1115
+ if (t) { t.output = event.output; t.done = true; }
1116
+ }
1117
+ if (event.type === 'react_iteration') {
1118
+ lastReactIteration = event;
1119
+ }
1120
+ }
1121
+
1122
+ for (const content of completedThinking) {
1123
+ this._createThinkingPill(toolsDiv, content);
1124
+ }
1125
+
1126
+ if (lastWasThinking && currentThinking) {
1127
+ const pill = document.createElement('div');
1128
+ pill.className = 'tool-pill thinking';
1129
+ pill.innerHTML = '<i class="fas fa-brain animate-pulse text-2xs"></i><span>Thinking...</span>';
1130
+ toolsDiv.appendChild(pill);
1131
+ activeThinkingPill = pill;
1132
+ activeThinkingContent = currentThinking;
1133
+ }
1134
+
1135
+ for (const [runId, t] of tools) {
1136
+ if (t.done) {
1137
+ this._createToolPill(toolsDiv, runId, t.tool, t.input, t.output);
1138
+ } else {
1139
+ const toolEl = document.createElement('div');
1140
+ toolEl.id = `tool-${runId}`;
1141
+ toolEl.className = 'tool-pill';
1142
+ toolEl.dataset.toolInput = typeof t.input === 'string' ? t.input : JSON.stringify(t.input, null, 2);
1143
+ toolEl.innerHTML = `<i class="fas fa-circle-notch animate-spin text-blue text-2xs"></i><span>${this.escapeHtml(t.tool)}</span>`;
1144
+ toolsDiv.appendChild(toolEl);
1145
+ }
1146
+ }
1147
+
1148
+ if (lastReactIteration) {
1149
+ const wrapper = bubble.closest('.response-wrapper');
1150
+ const statusText = wrapper?.querySelector('.stream-status-text');
1151
+ if (statusText) {
1152
+ const contextKB = (lastReactIteration.contextChars / 1024).toFixed(1);
1153
+ statusText.textContent = `Iteration ${lastReactIteration.iteration} · ${contextKB} KB context`;
1154
+ }
1155
+ }
1156
+ }
1157
+
1158
+ if (state.content) {
1159
+ const div = document.createElement('div');
1160
+ div.className = 'content-text markdown-content';
1161
+ div.innerHTML = markdownRenderer.render(state.content);
1162
+ markdownRenderer.highlightCode(div);
1163
+ contentDiv.appendChild(div);
1164
+ }
1165
+
1166
+ const container = this.querySelector('#chatMessages');
1167
+ if (container) container.scrollTop = container.scrollHeight;
1168
+
1169
+ return { thinkingPill: activeThinkingPill, thinkingContent: activeThinkingContent };
1170
+ }
1171
+
1172
+ // --- Workflow chat integration ---
1173
+
1174
+ async _sendWorkflowMessage(message) {
1175
+ const activeId = sessionStore.getActiveId();
1176
+ const wfState = workflowTasks.get(activeId);
1177
+
1178
+ if (wfState?.interruptState) {
1179
+ return this._respondToWorkflowInterrupt(activeId, message);
1180
+ }
1181
+
1182
+ const workflow = store.get('selectedWorkflow');
1183
+ if (!workflow) return;
1184
+
1185
+ const schema = workflow.inputSchema || {};
1186
+ const firstField = Object.keys(schema)[0] || 'input';
1187
+ const inputObj = { [firstField]: message };
1188
+
1189
+ const input = this.querySelector('#chatInput');
1190
+ this.appendMessage('user', message);
1191
+ sessionStore.addMessage(activeId, 'user', message);
1192
+ input.value = '';
1193
+ input.style.height = 'auto';
1194
+ this.clearAttachments();
1195
+
1196
+ const responseId = 'response-' + Date.now();
1197
+ this.createResponseBubble(responseId);
1198
+ this.isLoading = true;
1199
+ this.updateUiState();
1200
+ this.startStreamTimer(responseId);
1201
+
1202
+ const abortController = new AbortController();
1203
+ workflowTasks.set(activeId, {
1204
+ responseId,
1205
+ startTime: Date.now(),
1206
+ chatOutputFormat: workflow.chatOutputFormat || 'json',
1207
+ workflowName: workflow.name,
1208
+ abortController,
1209
+ interruptState: null,
1210
+ status: 'streaming',
1211
+ events: [],
1212
+ inputMessage: message,
1213
+ taskId: null,
1214
+ });
1215
+
1216
+ try {
1217
+ const response = await api.startWorkflowStream(workflow.name, inputObj, abortController.signal);
1218
+ await this._processWorkflowStream(response, activeId, responseId);
1219
+ } catch (e) {
1220
+ if (e.name === 'AbortError') {
1221
+ this._finishWorkflowStream(activeId, responseId, null, null, true);
262
1222
  } else {
263
- dropdown.classList.add('hidden');
1223
+ this.updateResponseError(responseId, `Error: ${e.message}`);
1224
+ this.stopStreamTimer(responseId, message, '', false);
1225
+ this.isLoading = false;
1226
+ this.updateUiState();
264
1227
  }
265
1228
  }
266
- }
267
1229
 
268
- async sendMessage() {
269
- const input = this.querySelector('#chatInput');
270
- const message = input.value.trim();
271
- const selectionType = store.get('selectionType');
272
- const agent = store.get('selectedAgent');
273
- const llm = store.get('selectedLlm');
1230
+ this.renderSessionList();
1231
+ }
274
1232
 
275
- const selected = selectionType === 'agent' ? agent : llm;
1233
+ async _respondToWorkflowInterrupt(sessionId, message) {
1234
+ const wfState = workflowTasks.get(sessionId);
1235
+ if (!wfState?.interruptState) return;
276
1236
 
277
- if (!message || !selected || this.isLoading) return;
1237
+ const { threadId, workflowName } = wfState.interruptState;
278
1238
 
279
- // Add user message
1239
+ const input = this.querySelector('#chatInput');
280
1240
  this.appendMessage('user', message);
1241
+ sessionStore.addMessage(sessionId, 'user', message);
281
1242
  input.value = '';
282
1243
  input.style.height = 'auto';
283
1244
 
284
- this.isLoading = true;
285
- this.updateUiState();
286
-
287
1245
  const responseId = 'response-' + Date.now();
288
1246
  this.createResponseBubble(responseId);
289
1247
 
290
- this.currentAbortController = new AbortController();
291
- this.streamUsageData = null;
292
- this.startStreamTimer(responseId);
1248
+ const abortController = new AbortController();
1249
+ wfState.responseId = responseId;
1250
+ wfState.interruptState = null;
1251
+ wfState.status = 'streaming';
1252
+ wfState.abortController = abortController;
293
1253
 
294
- let finalContent = '';
295
- let wasCancelled = false;
1254
+ this.isLoading = true;
1255
+ this.updateUiState();
1256
+ this.startStreamTimer(responseId);
296
1257
 
297
1258
  try {
298
- if (selectionType === 'agent') {
299
- finalContent = await this.sendAgentMessage(agent, message, responseId);
300
- } else if (selectionType === 'llm') {
301
- finalContent = await this.sendLlmMessage(llm, message, responseId);
302
- }
1259
+ const response = await api.resumeWorkflowStream(workflowName, threadId, message, abortController.signal);
1260
+ await this._processWorkflowStream(response, sessionId, responseId);
303
1261
  } catch (e) {
304
1262
  if (e.name === 'AbortError') {
305
- wasCancelled = true;
1263
+ this._finishWorkflowStream(sessionId, responseId, null, null, true);
306
1264
  } else {
307
1265
  this.updateResponseError(responseId, `Error: ${e.message}`);
1266
+ this.stopStreamTimer(responseId, message, '', false);
1267
+ this.isLoading = false;
1268
+ this.updateUiState();
308
1269
  }
309
- } finally {
310
- this.stopStreamTimer(responseId, message, finalContent, wasCancelled);
311
- this.currentAbortController = null;
312
- this.isLoading = false;
313
- this.updateUiState();
314
- input.focus();
315
1270
  }
1271
+
1272
+ this.renderSessionList();
316
1273
  }
317
1274
 
318
- async sendAgentMessage(agent, message, responseId) {
319
- const inputVars = agent.inputVariables || ['message'];
320
- const inputObj = {};
321
- inputObj[inputVars[0] || 'message'] = message;
1275
+ async _processWorkflowStream(response, sessionId, responseId) {
1276
+ if (!response.ok) {
1277
+ const text = await response.text();
1278
+ let msg = `HTTP ${response.status}`;
1279
+ try { msg = JSON.parse(text).error || msg; } catch { /* use status */ }
1280
+ throw new Error(msg);
1281
+ }
322
1282
 
323
- const res = await api.streamAgent(agent.name, inputObj, store.get('sessionId'), { signal: this.currentAbortController?.signal });
324
- const reader = res.body.getReader();
1283
+ const reader = response.body.getReader();
325
1284
  const decoder = new TextDecoder();
326
-
327
- const bubble = this.querySelector(`#${responseId}`);
328
- const contentDiv = bubble.querySelector('.response-content');
329
- const container = this.querySelector('#chatMessages');
330
- const thinkingState = {
331
- inThinking: false,
332
- thinkingSections: [],
333
- currentSection: null
334
- };
335
-
336
- let currentContent = '';
337
1285
  let buffer = '';
338
- let hasToolCalls = false;
339
1286
 
340
1287
  while (true) {
341
1288
  const { done, value } = await reader.read();
342
1289
  if (done) break;
343
1290
 
344
- const chunk = decoder.decode(value, { stream: true });
345
- buffer += chunk;
346
-
1291
+ buffer += decoder.decode(value, { stream: true });
347
1292
  const lines = buffer.split('\n');
348
1293
  buffer = lines.pop() || '';
349
1294
 
350
1295
  for (const line of lines) {
351
- if (line.trim() === '') continue;
352
-
353
- if (line.startsWith('data: ')) {
354
- const data = line.slice(6);
355
- if (data === '[DONE]') continue;
356
-
357
- try {
358
- const event = JSON.parse(data);
359
-
360
- // Handle server-side errors
361
- if (event.error) {
362
- this.updateResponseError(responseId, `Error: ${event.error}`);
363
- return currentContent;
364
- }
365
-
366
- if (event.type === 'content') {
367
- currentContent += event.content;
368
- }
369
- if (event.type === 'tool_start' || event.type === 'tool_end') {
370
- hasToolCalls = true;
371
- }
372
- this.handleStreamEvent(event, responseId, currentContent, thinkingState);
373
- } catch (e) {
374
- console.error('Error parsing stream event', e, data);
375
- }
1296
+ if (!line.startsWith('data: ')) continue;
1297
+ const payload = line.slice(6).trim();
1298
+ if (payload === '[DONE]') continue;
1299
+
1300
+ try {
1301
+ const update = JSON.parse(payload);
1302
+ this._handleWorkflowStreamEvent(update, sessionId, responseId);
1303
+ } catch (e) {
1304
+ console.error('Workflow stream parse error:', e);
376
1305
  }
377
1306
  }
378
1307
  }
379
1308
 
380
- // If tools were called but no text content was produced, clear loading state
381
- if (hasToolCalls && !currentContent.trim()) {
382
- const loadingDots = contentDiv.querySelector('.loading-dots');
383
- if (loadingDots) {
384
- loadingDots.remove();
385
- bubble.querySelector('.response-bubble-inner').classList.remove('py-4');
386
- bubble.querySelector('.response-bubble-inner').classList.add('py-3');
387
- contentDiv.classList.remove('flex', 'items-center', 'whitespace-pre-wrap');
388
- contentDiv.innerHTML = '';
1309
+ // Process any remaining buffer
1310
+ if (buffer.startsWith('data: ')) {
1311
+ const payload = buffer.slice(6).trim();
1312
+ if (payload && payload !== '[DONE]') {
1313
+ try {
1314
+ const update = JSON.parse(payload);
1315
+ this._handleWorkflowStreamEvent(update, sessionId, responseId);
1316
+ } catch (e) { /* ignore */ }
389
1317
  }
390
1318
  }
391
1319
 
392
- return currentContent;
1320
+ // If stream ended without a result/error event, treat as error
1321
+ const wfState = workflowTasks.get(sessionId);
1322
+ if (wfState && wfState.status === 'streaming') {
1323
+ this._finishWorkflowStream(sessionId, responseId, null, 'Stream ended unexpectedly', false);
1324
+ }
393
1325
  }
394
1326
 
395
- async sendLlmMessage(llm, message, responseId) {
396
- const res = await api.streamLLM(llm.name, message, { signal: this.currentAbortController?.signal });
397
- const reader = res.body.getReader();
398
- const decoder = new TextDecoder();
1327
+ _handleWorkflowStreamEvent(update, sessionId, responseId) {
1328
+ const wfState = workflowTasks.get(sessionId);
1329
+ if (!wfState) return;
1330
+
1331
+ // Capture task ID from server
1332
+ if (update.type === 'task_id') {
1333
+ wfState.taskId = update.taskId;
1334
+ return;
1335
+ }
399
1336
 
400
1337
  const bubble = this.querySelector(`#${responseId}`);
1338
+
1339
+ if (update.type === 'status' && bubble) {
1340
+ const event = update.data;
1341
+ wfState.events.push(event);
1342
+
1343
+ const toolsDiv = bubble.querySelector('.tool-invocations');
1344
+ this._renderWorkflowEvent(event, toolsDiv, bubble);
1345
+
1346
+ // Handle interrupt — the stream returns a result with interrupted output
1347
+ }
1348
+
1349
+ if (update.type === 'result') {
1350
+ const result = update.data;
1351
+ // Check if it's an interrupt
1352
+ if (result?.output?.interrupted && result?.output?.threadId) {
1353
+ this._handleWorkflowInterrupt(sessionId, responseId, result.output);
1354
+ } else if (result?.error) {
1355
+ this._finishWorkflowStream(sessionId, responseId, null, result.error, false);
1356
+ } else {
1357
+ this._finishWorkflowStream(sessionId, responseId, result, null, false);
1358
+ }
1359
+ }
1360
+
1361
+ if (update.type === 'error') {
1362
+ this._finishWorkflowStream(sessionId, responseId, null, update.error, false);
1363
+ }
1364
+ }
1365
+
1366
+ _renderWorkflowEvent(event, toolsDiv, bubble) {
401
1367
  const contentDiv = bubble.querySelector('.response-content');
402
- const loadingDots = contentDiv.querySelector('.loading-dots');
403
- const container = this.querySelector('#chatMessages');
404
1368
 
405
- if (loadingDots) {
1369
+ if (event.type === 'step_start') {
1370
+ const pill = document.createElement('div');
1371
+ pill.id = `wf-step-${event.stepId}`;
1372
+ pill.className = 'tool-pill';
1373
+ pill.innerHTML = `<i class="fas fa-circle-notch animate-spin text-blue text-2xs"></i><span>${this.escapeHtml(event.stepId)}</span>`;
1374
+ toolsDiv.appendChild(pill);
1375
+ }
1376
+
1377
+ if (event.type === 'step_complete') {
1378
+ const pill = toolsDiv.querySelector(`#wf-step-${event.stepId}`);
1379
+ if (pill) {
1380
+ pill.className = 'tool-pill done';
1381
+ pill.innerHTML = `<i class="fas fa-check text-green text-2xs"></i><span>${this.escapeHtml(event.stepId)}</span>`;
1382
+ }
1383
+ }
1384
+
1385
+ if (event.type === 'step_error') {
1386
+ const pill = toolsDiv.querySelector(`#wf-step-${event.stepId}`);
1387
+ if (pill) {
1388
+ pill.className = 'tool-pill done';
1389
+ pill.innerHTML = `<i class="fas fa-times text-red text-2xs"></i><span>${this.escapeHtml(event.stepId)}</span>`;
1390
+ }
1391
+ }
1392
+
1393
+ if (event.type === 'tool_call') {
1394
+ const toolName = event.message?.replace(/^Calling:?\s*/i, '').split(/\s/)[0] || 'tool';
1395
+ const pill = document.createElement('div');
1396
+ pill.className = 'tool-pill wf-tool-active';
1397
+
1398
+ const pillContent = document.createElement('span');
1399
+ pillContent.className = 'inline-flex items-center gap-1';
1400
+ pillContent.innerHTML = `<i class="fas fa-circle-notch animate-spin text-blue text-2xs"></i><span>${this.escapeHtml(toolName)}</span>`;
1401
+ pill.appendChild(pillContent);
1402
+
1403
+ // Create details panel (populated on tool_result)
1404
+ const details = document.createElement('div');
1405
+ details.className = 'tool-invocation-details';
1406
+ if (event.toolInput) {
1407
+ const inputSection = document.createElement('div');
1408
+ inputSection.className = 'tool-detail-section';
1409
+ inputSection.innerHTML = '<h4>Input</h4>';
1410
+ const inputPre = document.createElement('pre');
1411
+ inputPre.className = 'tool-detail-pre custom-scrollbar';
1412
+ inputPre.textContent = event.toolInput;
1413
+ inputSection.appendChild(inputPre);
1414
+ details.appendChild(inputSection);
1415
+ }
1416
+ pill.appendChild(details);
1417
+ pill.addEventListener('click', (e) => {
1418
+ if (details.contains(e.target)) return;
1419
+ e.preventDefault();
1420
+ e.stopPropagation();
1421
+ toolsDiv.querySelectorAll('.tool-invocation-details.visible').forEach(d => {
1422
+ if (d !== details) d.classList.remove('visible');
1423
+ });
1424
+ details.classList.toggle('visible');
1425
+ });
1426
+ toolsDiv.appendChild(pill);
1427
+ }
1428
+
1429
+ if (event.type === 'tool_result') {
1430
+ const activePill = toolsDiv.querySelector('.wf-tool-active');
1431
+ if (activePill) {
1432
+ activePill.classList.remove('wf-tool-active');
1433
+ activePill.classList.add('done');
1434
+ const icon = activePill.querySelector('i');
1435
+ if (icon) icon.className = 'fas fa-check text-green text-2xs';
1436
+
1437
+ // Append output to the details panel
1438
+ if (event.toolOutput) {
1439
+ const details = activePill.querySelector('.tool-invocation-details');
1440
+ if (details) {
1441
+ const outputSection = document.createElement('div');
1442
+ outputSection.className = 'tool-detail-section';
1443
+ outputSection.innerHTML = '<h4>Output</h4>';
1444
+ const outputPre = document.createElement('pre');
1445
+ outputPre.className = 'tool-detail-pre custom-scrollbar';
1446
+ outputPre.textContent = event.toolOutput;
1447
+ outputSection.appendChild(outputPre);
1448
+ details.appendChild(outputSection);
1449
+ }
1450
+ }
1451
+ }
1452
+ }
1453
+
1454
+ if (event.type === 'tool_discovery') {
1455
+ // Only show the final summary pill (e.g. "35 total tools ready"), skip intermediate progress
1456
+ if (event.message?.includes('total tools')) {
1457
+ const pill = document.createElement('div');
1458
+ pill.className = 'tool-pill done';
1459
+ pill.innerHTML = `<i class="fas fa-plug text-purple text-2xs"></i><span>${this.escapeHtml(event.message)}</span>`;
1460
+ toolsDiv.appendChild(pill);
1461
+ }
1462
+ const wrapper = bubble.closest('.response-wrapper');
1463
+ const statusText = wrapper?.querySelector('.stream-status-text');
1464
+ if (statusText) statusText.textContent = event.message || 'Discovering tools...';
1465
+ }
1466
+
1467
+ if (event.type === 'react_iteration' || event.type === 'workflow_start') {
1468
+ const wrapper = bubble.closest('.response-wrapper');
1469
+ const statusText = wrapper?.querySelector('.stream-status-text');
1470
+ if (statusText) statusText.textContent = event.message || 'Processing...';
1471
+ }
1472
+
1473
+ // Remove loading dots on first meaningful event
1474
+ const loadingDots = contentDiv?.querySelector('.loading-dots');
1475
+ if (loadingDots && (event.type === 'step_start' || event.type === 'workflow_start' || event.type === 'tool_call' || event.type === 'tool_discovery' || event.type === 'react_iteration')) {
406
1476
  loadingDots.remove();
407
- bubble.querySelector('.response-bubble-inner').classList.remove('py-4');
408
- bubble.querySelector('.response-bubble-inner').classList.add('py-3');
1477
+ bubble.querySelector('.response-bubble-inner')?.classList.remove('loading');
409
1478
  contentDiv.classList.remove('flex', 'items-center', 'whitespace-pre-wrap');
410
1479
  contentDiv.innerHTML = '';
411
1480
  }
412
1481
 
413
- let buffer = '';
414
- let fullContent = '';
415
- const thinkingState = {
416
- inThinking: false,
417
- thinkingSections: [],
418
- currentSection: null
419
- };
1482
+ const container = this.querySelector('#chatMessages');
1483
+ if (container) container.scrollTop = container.scrollHeight;
1484
+ }
420
1485
 
421
- while (true) {
422
- const { done, value } = await reader.read();
423
- if (done) break;
1486
+ _handleWorkflowInterrupt(sessionId, responseId, interruptData) {
1487
+ const wfState = workflowTasks.get(sessionId);
1488
+ if (!wfState) return;
424
1489
 
425
- const chunk = decoder.decode(value, { stream: true });
426
- buffer += chunk;
1490
+ wfState.status = 'interrupted';
1491
+ const question = interruptData?.question || 'Input required';
1492
+ wfState.interruptState = {
1493
+ question,
1494
+ threadId: interruptData?.threadId,
1495
+ workflowName: wfState.workflowName,
1496
+ };
427
1497
 
428
- const lines = buffer.split('\n');
429
- buffer = lines.pop() || '';
1498
+ const bubble = this.querySelector(`#${responseId}`);
1499
+ if (bubble) {
1500
+ const contentDiv = bubble.querySelector('.response-content');
1501
+ const loadingDots = contentDiv?.querySelector('.loading-dots');
1502
+ if (loadingDots) {
1503
+ loadingDots.remove();
1504
+ bubble.querySelector('.response-bubble-inner')?.classList.remove('loading');
1505
+ contentDiv.classList.remove('flex', 'items-center', 'whitespace-pre-wrap');
1506
+ contentDiv.innerHTML = '';
1507
+ }
430
1508
 
431
- for (const line of lines) {
432
- if (line.trim() === '') continue;
1509
+ const div = document.createElement('div');
1510
+ div.className = 'content-text markdown-content';
1511
+ div.innerHTML = markdownRenderer.render(question);
1512
+ markdownRenderer.highlightCode(div);
1513
+ contentDiv.appendChild(div);
1514
+
1515
+ const wrapper = bubble.closest('.response-wrapper');
1516
+ const statusBar = wrapper?.querySelector('.stream-status-bar');
1517
+ if (statusBar) {
1518
+ const statusText = statusBar.querySelector('.stream-status-text');
1519
+ if (statusText) statusText.textContent = 'Waiting for input...';
1520
+ }
1521
+ }
433
1522
 
434
- if (line.startsWith('data: ')) {
435
- const data = line.slice(6);
1523
+ sessionStore.addMessage(sessionId, 'assistant', question);
436
1524
 
437
- if (data === '[DONE]') continue;
1525
+ if (this.streamTimerInterval) {
1526
+ clearInterval(this.streamTimerInterval);
1527
+ this.streamTimerInterval = null;
1528
+ }
1529
+ this.isLoading = false;
1530
+ this.updateUiState();
438
1531
 
439
- try {
440
- const parsed = JSON.parse(data);
1532
+ const input = this.querySelector('#chatInput');
1533
+ if (input) input.focus();
1534
+ }
441
1535
 
442
- if (parsed.error) {
443
- this.updateResponseError(responseId, `Error: ${parsed.error}`);
444
- return fullContent;
445
- }
1536
+ _finishWorkflowStream(sessionId, responseId, result, error, wasCancelled) {
1537
+ const wfState = workflowTasks.get(sessionId);
1538
+ if (!wfState) return;
446
1539
 
447
- if (parsed.type === 'usage') {
448
- this.streamUsageData = {
449
- input_tokens: parsed.input_tokens || 0,
450
- output_tokens: parsed.output_tokens || 0,
451
- total_tokens: parsed.total_tokens || 0,
452
- };
453
- continue;
454
- }
1540
+ wfState.status = 'done';
1541
+ wfState.abortController = null;
1542
+
1543
+ const bubble = this.querySelector(`#${responseId}`);
1544
+ if (bubble) {
1545
+ const contentDiv = bubble.querySelector('.response-content');
1546
+ const loadingDots = contentDiv?.querySelector('.loading-dots');
1547
+ if (loadingDots) {
1548
+ loadingDots.remove();
1549
+ bubble.querySelector('.response-bubble-inner')?.classList.remove('loading');
1550
+ contentDiv.classList.remove('flex', 'items-center', 'whitespace-pre-wrap');
1551
+ contentDiv.innerHTML = '';
1552
+ }
455
1553
 
456
- const text = parsed.content || '';
1554
+ if (error) {
1555
+ const errorDiv = document.createElement('div');
1556
+ errorDiv.className = 'text-red text-sm';
1557
+ errorDiv.textContent = `Error: ${error}`;
1558
+ contentDiv.appendChild(errorDiv);
1559
+ } else if (wasCancelled) {
1560
+ // No output content for cancelled
1561
+ } else if (result?.output) {
1562
+ this._renderWorkflowOutput(contentDiv, result.output, wfState.chatOutputFormat);
1563
+ }
1564
+ }
457
1565
 
458
- if (text) {
459
- fullContent += text;
460
- this.renderLlmContentStreaming(contentDiv, fullContent, responseId, thinkingState);
461
- container.scrollTop = container.scrollHeight;
462
- }
463
- } catch (e) {
464
- console.error('Error parsing stream chunk:', e, data);
465
- }
466
- }
1566
+ let content = '';
1567
+ if (error) {
1568
+ content = `Error: ${error}`;
1569
+ } else if (result?.output) {
1570
+ if (wfState.chatOutputFormat === 'text') {
1571
+ content = Object.values(result.output).join('\n\n');
1572
+ } else {
1573
+ content = '```json\n' + JSON.stringify(result.output, null, 2) + '\n```';
467
1574
  }
468
1575
  }
469
1576
 
470
- return fullContent;
1577
+ const elapsed = wfState.startTime ? Date.now() - wfState.startTime : 0;
1578
+ const meta = {
1579
+ thinking: [],
1580
+ tools: wfState.events
1581
+ .filter(e => e.type === 'step_complete')
1582
+ .map(e => ({ runId: e.stepId, tool: e.stepId, input: e.agent || '', output: e.message || 'Completed' })),
1583
+ stats: {
1584
+ elapsed,
1585
+ inputTokens: Math.round((wfState.inputMessage || '').length / 4),
1586
+ outputTokens: Math.round(content.length / 4),
1587
+ cancelled: wasCancelled,
1588
+ estimated: true,
1589
+ },
1590
+ };
1591
+ sessionStore.addMessage(sessionId, 'assistant', content, meta);
1592
+
1593
+ this.stopStreamTimer(responseId, wfState.inputMessage || '', content, wasCancelled);
1594
+ this.isLoading = false;
1595
+ this.updateUiState();
1596
+ this.renderSessionList();
1597
+
1598
+ setTimeout(() => workflowTasks.delete(sessionId), 10000);
1599
+
1600
+ const input = this.querySelector('#chatInput');
1601
+ if (input) input.focus();
471
1602
  }
472
1603
 
473
- renderLlmContentStreaming(contentDiv, fullContent, responseId, state) {
474
- // Parse content to find think sections and regular text
475
- const parts = [];
476
- let pos = 0;
477
- let thinkIndex = 0;
478
-
479
- while (pos < fullContent.length) {
480
- const thinkStart = fullContent.indexOf('[THINK]', pos);
481
-
482
- if (thinkStart === -1) {
483
- // No more think sections, add remaining text
484
- const text = fullContent.slice(pos).trim();
485
- if (text) {
486
- parts.push({ type: 'text', content: text });
487
- }
488
- break;
489
- }
1604
+ _renderWorkflowOutput(contentDiv, output, format) {
1605
+ const div = document.createElement('div');
1606
+ div.className = 'content-text markdown-content';
1607
+ if (format === 'text') {
1608
+ div.innerHTML = markdownRenderer.render(Object.values(output).join('\n\n'));
1609
+ } else {
1610
+ div.innerHTML = markdownRenderer.render('```json\n' + JSON.stringify(output, null, 2) + '\n```');
1611
+ }
1612
+ markdownRenderer.highlightCode(div);
1613
+ contentDiv.appendChild(div);
1614
+ }
490
1615
 
491
- // Add text before [THINK]
492
- if (thinkStart > pos) {
493
- const text = fullContent.slice(pos, thinkStart).trim();
494
- if (text) {
495
- parts.push({ type: 'text', content: text });
496
- }
497
- }
1616
+ _reconnectWorkflowStream(sessionId) {
1617
+ const wfState = workflowTasks.get(sessionId);
1618
+ if (!wfState || wfState.status === 'done') return false;
498
1619
 
499
- // Find the end of this think section
500
- const thinkContentStart = thinkStart + 7; // After [THINK]
501
- const thinkEnd = fullContent.indexOf('[/THINK]', thinkContentStart);
1620
+ if (wfState.interruptState) {
1621
+ this.isLoading = false;
1622
+ this.updateUiState();
1623
+ return true;
1624
+ }
502
1625
 
503
- if (thinkEnd === -1) {
504
- // Think section is still streaming
505
- const thinkContent = fullContent.slice(thinkContentStart).trim();
506
- parts.push({ type: 'think', content: thinkContent, complete: false, index: thinkIndex });
507
- thinkIndex++;
508
- break;
509
- } else {
510
- // Complete think section
511
- const thinkContent = fullContent.slice(thinkContentStart, thinkEnd).trim();
512
- parts.push({ type: 'think', content: thinkContent, complete: true, index: thinkIndex });
513
- thinkIndex++;
514
- pos = thinkEnd + 8; // After [/THINK]
1626
+ // Re-create the response bubble and replay cached events
1627
+ this.createResponseBubble(wfState.responseId);
1628
+ const bubble = this.querySelector(`#${wfState.responseId}`);
1629
+ if (bubble && wfState.events.length > 0) {
1630
+ const toolsDiv = bubble.querySelector('.tool-invocations');
1631
+ for (const event of wfState.events) {
1632
+ this._renderWorkflowEvent(event, toolsDiv, bubble);
515
1633
  }
516
1634
  }
517
1635
 
518
- // Update DOM incrementally instead of rebuilding
519
- let currentChildIndex = 0;
1636
+ this.startStreamTimer(wfState.responseId, wfState.startTime);
1637
+ this.isLoading = true;
1638
+ this.currentAbortController = null;
1639
+ this.updateUiState();
520
1640
 
521
- for (let i = 0; i < parts.length; i++) {
522
- const part = parts[i];
523
- const existingChild = contentDiv.children[currentChildIndex];
1641
+ return true;
1642
+ }
524
1643
 
525
- if (part.type === 'text') {
526
- if (existingChild && existingChild.classList.contains('content-text')) {
527
- // Update existing text element with markdown
528
- const renderedHtml = markdownRenderer.render(part.content);
529
- existingChild.innerHTML = renderedHtml;
530
- markdownRenderer.highlightCode(existingChild);
1644
+ _attachClickDetails(pillEl, detailsEl, toolsDiv, container) {
1645
+ pillEl.addEventListener('click', (e) => {
1646
+ if (detailsEl.contains(e.target)) return;
1647
+ e.preventDefault();
1648
+ e.stopPropagation();
1649
+ toolsDiv.querySelectorAll('.tool-invocation-details.visible').forEach(d => {
1650
+ if (d !== detailsEl) d.classList.remove('visible');
1651
+ });
1652
+ const wasHidden = !detailsEl.classList.contains('visible');
1653
+ detailsEl.classList.toggle('visible');
1654
+ if (wasHidden && container) {
1655
+ const pillRect = pillEl.getBoundingClientRect();
1656
+ const containerRect = container.getBoundingClientRect();
1657
+ const spaceRight = containerRect.right - pillRect.left;
1658
+ if (spaceRight < 420) {
1659
+ detailsEl.style.right = '0';
1660
+ detailsEl.style.left = 'auto';
531
1661
  } else {
532
- // Create new text element
533
- const div = document.createElement('div');
534
- div.className = 'content-text markdown-content';
535
- const renderedHtml = markdownRenderer.render(part.content);
536
- div.innerHTML = renderedHtml;
537
- markdownRenderer.highlightCode(div);
538
- if (existingChild) {
539
- contentDiv.insertBefore(div, existingChild);
540
- } else {
541
- contentDiv.appendChild(div);
542
- }
1662
+ detailsEl.style.left = '0';
1663
+ detailsEl.style.right = 'auto';
543
1664
  }
544
- currentChildIndex++;
545
- } else if (part.type === 'think') {
546
- const thinkId = `think-${responseId}-${part.index}`;
547
-
548
- if (existingChild && existingChild.classList.contains('think-section')) {
549
- // Update existing think section
550
- const label = existingChild.querySelector('.think-label');
551
- const content = existingChild.querySelector('.think-content');
552
- if (label) {
553
- label.textContent = part.complete ? 'Thinking' : 'Thinking...';
554
- }
555
- if (content) {
556
- const renderedHtml = markdownRenderer.render(part.content);
557
- content.innerHTML = renderedHtml;
558
- content.classList.add('markdown-content');
559
- markdownRenderer.highlightCode(content);
560
- }
561
- } else {
562
- // Create new think section with event listener
563
- const section = document.createElement('div');
564
- section.className = 'think-section mb-3 border-l-2 border-blue-500/40 pl-3 py-1';
565
- section.dataset.thinkIndex = part.index;
566
-
567
- const toggle = document.createElement('button');
568
- toggle.className = 'think-toggle flex items-center gap-1.5 text-xs text-blue-400 hover:text-blue-300 py-1 cursor-pointer';
569
- toggle.dataset.thinkId = thinkId;
570
-
571
- toggle.innerHTML = `
572
- <i class="fas fa-brain text-xs"></i>
573
- <span class="font-medium think-label">${part.complete ? 'Thinking' : 'Thinking...'}</span>
574
- <i class="fas fa-chevron-right text-[10px] transition-transform think-chevron"></i>
575
- `;
576
-
577
- const thinkContent = document.createElement('div');
578
- thinkContent.id = thinkId;
579
- thinkContent.className = 'think-content hidden text-sm text-gray-400 markdown-content mt-1 leading-relaxed';
580
- const renderedHtml = markdownRenderer.render(part.content);
581
- thinkContent.innerHTML = renderedHtml;
582
- markdownRenderer.highlightCode(thinkContent);
583
-
584
- // Add click handler ONCE when creating
585
- toggle.addEventListener('click', (e) => {
586
- e.preventDefault();
587
- e.stopPropagation();
588
- const content = section.querySelector('.think-content');
589
- const chevron = section.querySelector('.think-chevron');
590
-
591
- if (content && chevron) {
592
- if (content.classList.contains('hidden')) {
593
- content.classList.remove('hidden');
594
- chevron.classList.remove('fa-chevron-right');
595
- chevron.classList.add('fa-chevron-down');
596
- } else {
597
- content.classList.add('hidden');
598
- chevron.classList.remove('fa-chevron-down');
599
- chevron.classList.add('fa-chevron-right');
600
- }
601
- }
602
- });
1665
+ }
1666
+ });
1667
+ document.addEventListener('click', (e) => {
1668
+ if (!pillEl.contains(e.target)) detailsEl.classList.remove('visible');
1669
+ }, { capture: true });
1670
+ }
603
1671
 
604
- section.appendChild(toggle);
605
- section.appendChild(thinkContent);
1672
+ _createThinkingPill(toolsDiv, content) {
1673
+ const container = this.querySelector('#chatMessages');
1674
+ const pill = document.createElement('div');
1675
+ pill.className = 'tool-pill done thinking';
1676
+
1677
+ const pillContent = document.createElement('span');
1678
+ pillContent.className = 'inline-flex items-center gap-1';
1679
+ pillContent.innerHTML = '<i class="fas fa-brain text-purple text-2xs"></i><span>Thinking</span>';
1680
+ pill.appendChild(pillContent);
1681
+
1682
+ const details = document.createElement('div');
1683
+ details.className = 'tool-invocation-details';
1684
+
1685
+ const section = document.createElement('div');
1686
+ section.className = 'tool-detail-section';
1687
+ const pre = document.createElement('div');
1688
+ pre.className = 'tool-detail-pre markdown-content custom-scrollbar';
1689
+ pre.innerHTML = markdownRenderer.render(content);
1690
+ markdownRenderer.highlightCode(pre);
1691
+ section.appendChild(pre);
1692
+ details.appendChild(section);
1693
+ pill.appendChild(details);
1694
+
1695
+ this._attachClickDetails(pill, details, toolsDiv, container);
1696
+ toolsDiv.appendChild(pill);
1697
+ }
606
1698
 
607
- if (existingChild) {
608
- contentDiv.insertBefore(section, existingChild);
609
- } else {
610
- contentDiv.appendChild(section);
611
- }
612
- }
613
- currentChildIndex++;
614
- }
1699
+ _createToolPill(toolsDiv, runId, toolName, input, output) {
1700
+ const container = this.querySelector('#chatMessages');
1701
+ const toolInput = typeof input === 'string' ? input : JSON.stringify(input, null, 2);
1702
+ const toolOutput = typeof output === 'string' ? output : JSON.stringify(output, null, 2);
1703
+
1704
+ const toolEl = document.createElement('div');
1705
+ toolEl.id = `tool-${runId}`;
1706
+ toolEl.className = 'tool-pill done';
1707
+
1708
+ const pillContent = document.createElement('span');
1709
+ pillContent.className = 'inline-flex items-center gap-1';
1710
+ pillContent.innerHTML = `<i class="fas fa-check text-green text-2xs"></i><span>${this.escapeHtml(toolName)}</span>`;
1711
+ toolEl.appendChild(pillContent);
1712
+
1713
+ const details = document.createElement('div');
1714
+ details.className = 'tool-invocation-details';
1715
+
1716
+ if (toolInput) {
1717
+ const inputSection = document.createElement('div');
1718
+ inputSection.className = 'tool-detail-section';
1719
+ inputSection.innerHTML = '<h4>Input</h4>';
1720
+ const inputPre = document.createElement('pre');
1721
+ inputPre.className = 'tool-detail-pre custom-scrollbar';
1722
+ inputPre.textContent = toolInput;
1723
+ inputSection.appendChild(inputPre);
1724
+ details.appendChild(inputSection);
1725
+ }
1726
+
1727
+ const outputSection = document.createElement('div');
1728
+ outputSection.className = 'tool-detail-section';
1729
+ outputSection.innerHTML = '<h4>Output</h4>';
1730
+ const outputPre = document.createElement('pre');
1731
+ outputPre.className = 'tool-detail-pre custom-scrollbar';
1732
+ outputPre.textContent = toolOutput;
1733
+ outputSection.appendChild(outputPre);
1734
+ details.appendChild(outputSection);
1735
+
1736
+ toolEl.appendChild(details);
1737
+
1738
+ this._attachClickDetails(toolEl, details, toolsDiv, container);
1739
+ toolsDiv.appendChild(toolEl);
1740
+ }
1741
+
1742
+ renderLlmContentStreaming(contentDiv, fullContent, responseId, state) {
1743
+ const existing = contentDiv.querySelector('.content-text');
1744
+ if (existing) {
1745
+ const renderedHtml = markdownRenderer.render(fullContent);
1746
+ existing.innerHTML = renderedHtml;
1747
+ markdownRenderer.highlightCode(existing);
1748
+ } else {
1749
+ const div = document.createElement('div');
1750
+ div.className = 'content-text markdown-content';
1751
+ div.innerHTML = markdownRenderer.render(fullContent);
1752
+ markdownRenderer.highlightCode(div);
1753
+ contentDiv.appendChild(div);
615
1754
  }
1755
+ }
616
1756
 
617
- // Remove any extra children
618
- while (contentDiv.children.length > currentChildIndex) {
619
- contentDiv.removeChild(contentDiv.lastChild);
1757
+ handleThinkingEvent(event, toolsDiv, thinkingState, container) {
1758
+ if (!thinkingState.thinkingContent) {
1759
+ thinkingState.thinkingContent = '';
1760
+ }
1761
+ thinkingState.thinkingContent += event.content;
1762
+
1763
+ // Create pill on first thinking chunk
1764
+ if (!thinkingState.thinkingPill) {
1765
+ const pill = document.createElement('div');
1766
+ pill.className = 'tool-pill thinking';
1767
+ pill.innerHTML = `
1768
+ <i class="fas fa-brain animate-pulse text-2xs"></i>
1769
+ <span>Thinking...</span>
1770
+ `;
1771
+ toolsDiv.appendChild(pill);
1772
+ thinkingState.thinkingPill = pill;
1773
+ container.scrollTop = container.scrollHeight;
620
1774
  }
621
1775
  }
622
1776
 
1777
+ finalizeThinkingPill(toolsDiv, thinkingState) {
1778
+ const container = this.querySelector('#chatMessages');
1779
+ const pill = thinkingState.thinkingPill;
1780
+ if (!pill) return;
1781
+
1782
+ const content = thinkingState.thinkingContent || '';
1783
+ thinkingState.thinkingPill = null;
1784
+ thinkingState.thinkingContent = '';
1785
+
1786
+ pill.className = 'tool-pill done thinking';
1787
+ pill.innerHTML = '';
1788
+
1789
+ const pillContent = document.createElement('span');
1790
+ pillContent.className = 'inline-flex items-center gap-1';
1791
+ pillContent.innerHTML = '<i class="fas fa-brain text-purple text-2xs"></i><span>Thinking</span>';
1792
+ pill.appendChild(pillContent);
1793
+
1794
+ const details = document.createElement('div');
1795
+ details.className = 'tool-invocation-details';
1796
+
1797
+ const section = document.createElement('div');
1798
+ section.className = 'tool-detail-section';
1799
+ const pre = document.createElement('div');
1800
+ pre.className = 'tool-detail-pre markdown-content custom-scrollbar';
1801
+ pre.innerHTML = markdownRenderer.render(content);
1802
+ markdownRenderer.highlightCode(pre);
1803
+ section.appendChild(pre);
1804
+ details.appendChild(section);
1805
+ pill.appendChild(details);
1806
+
1807
+ this._attachClickDetails(pill, details, toolsDiv, container);
1808
+ }
1809
+
623
1810
  createResponseBubble(id) {
624
1811
  const container = this.querySelector('#chatMessages');
625
1812
  const wrapper = document.createElement('div');
@@ -629,57 +1816,52 @@ export class AgentsView extends Component {
629
1816
  div.id = id;
630
1817
  div.className = 'flex justify-start';
631
1818
  div.innerHTML = `
632
- <div class="response-bubble-inner max-w-4xl bg-dark-surface border border-dark-border rounded-3xl px-5 py-4 text-gray-100 text-[15px] leading-relaxed relative group">
1819
+ <div class="response-bubble-inner loading group">
633
1820
  <div class="response-content whitespace-pre-wrap flex items-center">
634
- <div class="loading-dots flex gap-1">
635
- <div class="w-2 h-2 bg-blue-500 rounded-full animate-bounce"></div>
636
- <div class="w-2 h-2 bg-blue-500 rounded-full animate-bounce animation-delay-200"></div>
637
- <div class="w-2 h-2 bg-blue-500 rounded-full animate-bounce animation-delay-400"></div>
1821
+ <div class="loading-dots">
1822
+ <div></div>
1823
+ <div></div>
1824
+ <div></div>
638
1825
  </div>
639
1826
  </div>
640
- <div class="tool-invocations flex flex-wrap gap-1.5 mt-2"></div>
1827
+ <div class="tool-invocations"></div>
641
1828
  </div>
642
1829
  `;
643
1830
 
644
1831
  wrapper.appendChild(div);
645
1832
 
646
- // Stream status bar (visible during streaming)
647
1833
  const statusBar = document.createElement('div');
648
- statusBar.className = 'stream-status-bar flex items-center gap-2 mt-1.5 ml-1 text-xs text-gray-400';
1834
+ statusBar.className = 'stream-status-bar';
649
1835
  statusBar.innerHTML = `
650
- <div class="w-2 h-2 bg-blue-500 rounded-full animate-pulse-dot"></div>
1836
+ <div class="status-dot-pulse"></div>
651
1837
  <span class="stream-status-text">Generating...</span>
652
- <span class="stream-elapsed text-gray-500">0.0s</span>
653
- <button class="stream-cancel-btn ml-auto text-gray-500 hover:text-gray-300 text-xs px-2 py-0.5 rounded border border-dark-border hover:border-gray-500 transition-colors">
654
- Stop
655
- </button>
1838
+ <span class="stream-elapsed text-muted">0.0s</span>
1839
+ <button class="stream-cancel-btn">Stop</button>
656
1840
  `;
657
1841
  wrapper.appendChild(statusBar);
658
1842
 
659
- // Wire up cancel button
660
1843
  statusBar.querySelector('.stream-cancel-btn').addEventListener('click', () => this.cancelCurrentStream());
661
1844
 
662
- // Stats bar (visible after completion)
663
1845
  const statsBar = document.createElement('div');
664
- statsBar.className = 'stream-stats-bar hidden flex items-center gap-3 mt-1.5 ml-1 text-xs text-gray-500';
1846
+ statsBar.className = 'stream-stats-bar';
665
1847
  statsBar.innerHTML = `
666
1848
  <span class="flex items-center gap-1">
667
1849
  <i class="far fa-clock"></i>
668
1850
  <span class="stats-elapsed"></span>
669
1851
  </span>
670
- <span class="text-dark-border">|</span>
1852
+ <span class="divider">|</span>
671
1853
  <span class="flex items-center gap-1">
672
- <i class="fas fa-arrow-up text-[9px]"></i>
1854
+ <i class="fas fa-arrow-up text-2xs"></i>
673
1855
  <span class="stats-input-tokens"></span>
674
1856
  </span>
675
- <span class="text-dark-border">|</span>
1857
+ <span class="divider">|</span>
676
1858
  <span class="flex items-center gap-1">
677
- <i class="fas fa-arrow-down text-[9px]"></i>
1859
+ <i class="fas fa-arrow-down text-2xs"></i>
678
1860
  <span class="stats-output-tokens"></span>
679
1861
  </span>
680
- <span class="text-dark-border">|</span>
1862
+ <span class="divider">|</span>
681
1863
  <span class="flex items-center gap-1">
682
- <i class="fas fa-bolt text-[9px]"></i>
1864
+ <i class="fas fa-bolt text-2xs"></i>
683
1865
  <span class="stats-tps"></span>
684
1866
  </span>
685
1867
  `;
@@ -698,24 +1880,28 @@ export class AgentsView extends Component {
698
1880
  const loadingDots = contentDiv.querySelector('.loading-dots');
699
1881
  const container = this.querySelector('#chatMessages');
700
1882
 
701
- if (event.type === 'content') {
1883
+ if (event.type === 'thinking') {
1884
+ this.handleThinkingEvent(event, toolsDiv, thinkingState, container);
1885
+ } else if (event.type === 'content') {
1886
+ // Finalize any in-progress thinking pill
1887
+ this.finalizeThinkingPill(toolsDiv, thinkingState);
702
1888
  if (loadingDots) {
703
1889
  loadingDots.remove();
704
- bubble.querySelector('.response-bubble-inner').classList.remove('py-4');
705
- bubble.querySelector('.response-bubble-inner').classList.add('py-3');
1890
+ bubble.querySelector('.response-bubble-inner').classList.remove('loading');
706
1891
  contentDiv.classList.remove('flex', 'items-center', 'whitespace-pre-wrap');
707
1892
  contentDiv.innerHTML = '';
708
1893
  }
709
1894
  this.renderLlmContentStreaming(contentDiv, currentContent, responseId, thinkingState);
710
1895
  container.scrollTop = container.scrollHeight;
711
1896
  } else if (event.type === 'tool_start') {
1897
+ this.finalizeThinkingPill(toolsDiv, thinkingState);
712
1898
  const toolId = `tool-${event.runId}`;
713
1899
  const toolEl = document.createElement('div');
714
1900
  toolEl.id = toolId;
715
- toolEl.className = 'tool-pill inline-flex items-center gap-1.5 bg-dark-bg/50 border border-dark-border/60 rounded-full px-2.5 py-1 text-xs text-gray-400 font-mono';
1901
+ toolEl.className = 'tool-pill';
716
1902
  toolEl.dataset.toolInput = typeof event.input === 'string' ? event.input : JSON.stringify(event.input, null, 2);
717
1903
  toolEl.innerHTML = `
718
- <i class="fas fa-circle-notch animate-spin text-blue-400 text-[10px]"></i>
1904
+ <i class="fas fa-circle-notch animate-spin text-blue text-2xs"></i>
719
1905
  <span>${this.escapeHtml(event.tool)}</span>
720
1906
  `;
721
1907
  toolsDiv.appendChild(toolEl);
@@ -728,106 +1914,101 @@ export class AgentsView extends Component {
728
1914
  const toolInput = toolEl.dataset.toolInput || '';
729
1915
  const toolOutput = typeof event.output === 'string' ? event.output : JSON.stringify(event.output, null, 2);
730
1916
 
731
- toolEl.className = 'tool-pill relative inline-flex items-center gap-1.5 bg-dark-bg/30 border border-dark-border/50 rounded-full px-2.5 py-1 text-xs text-gray-500 font-mono cursor-pointer hover:bg-dark-bg/60 hover:border-dark-border transition-colors';
1917
+ toolEl.className = 'tool-pill done';
732
1918
  toolEl.innerHTML = '';
733
1919
 
734
- // Pill content (icon + name)
735
1920
  const pillContent = document.createElement('span');
736
- pillContent.className = 'inline-flex items-center gap-1.5';
1921
+ pillContent.className = 'inline-flex items-center gap-1';
737
1922
  pillContent.innerHTML = `
738
- <i class="fas fa-check text-green-500 text-[10px]"></i>
1923
+ <i class="fas fa-check text-green text-2xs"></i>
739
1924
  <span>${this.escapeHtml(event.tool)}</span>
740
1925
  `;
741
1926
  toolEl.appendChild(pillContent);
742
1927
 
743
- // Popover details panel (positioned below the pill)
744
1928
  const details = document.createElement('div');
745
- details.className = 'tool-invocation-details hidden absolute left-0 top-full mt-1 z-50 bg-dark-surface border border-dark-border rounded-lg shadow-xl w-[400px] max-w-[90vw]';
1929
+ details.className = 'tool-invocation-details';
746
1930
 
747
- // Input section
748
1931
  if (toolInput) {
749
1932
  const inputSection = document.createElement('div');
750
- inputSection.className = 'p-3 border-b border-dark-border/50';
751
- inputSection.innerHTML = `<div class="text-xs font-semibold text-gray-400 mb-1">Input</div>`;
1933
+ inputSection.className = 'tool-detail-section';
1934
+ inputSection.innerHTML = '<h4>Input</h4>';
752
1935
  const inputPre = document.createElement('pre');
753
- inputPre.className = 'text-xs text-gray-400 bg-dark-bg/60 rounded-md p-2 overflow-x-auto max-h-48 overflow-y-auto whitespace-pre-wrap break-all custom-scrollbar';
1936
+ inputPre.className = 'tool-detail-pre custom-scrollbar';
754
1937
  inputPre.textContent = toolInput;
755
1938
  inputSection.appendChild(inputPre);
756
1939
  details.appendChild(inputSection);
757
1940
  }
758
1941
 
759
- // Output section
760
1942
  const outputSection = document.createElement('div');
761
- outputSection.className = 'p-3';
762
- outputSection.innerHTML = `<div class="text-xs font-semibold text-gray-400 mb-1">Output</div>`;
1943
+ outputSection.className = 'tool-detail-section';
1944
+ outputSection.innerHTML = '<h4>Output</h4>';
763
1945
  const outputPre = document.createElement('pre');
764
- outputPre.className = 'text-xs text-gray-400 bg-dark-bg/60 rounded-md p-2 overflow-x-auto max-h-48 overflow-y-auto whitespace-pre-wrap break-all custom-scrollbar';
1946
+ outputPre.className = 'tool-detail-pre custom-scrollbar';
765
1947
  outputPre.textContent = toolOutput;
766
1948
  outputSection.appendChild(outputPre);
767
1949
  details.appendChild(outputSection);
768
1950
 
769
1951
  toolEl.appendChild(details);
770
1952
 
771
- // Toggle popover on click
772
- toolEl.addEventListener('click', (e) => {
773
- e.preventDefault();
774
- e.stopPropagation();
775
- // Close any other open popovers
776
- toolsDiv.querySelectorAll('.tool-invocation-details:not(.hidden)').forEach(d => {
777
- if (d !== details) d.classList.add('hidden');
778
- });
779
- details.classList.toggle('hidden');
780
- });
781
-
782
- // Close popover when clicking outside
783
- const closeHandler = (e) => {
784
- if (!toolEl.contains(e.target)) {
785
- details.classList.add('hidden');
786
- }
787
- };
788
- document.addEventListener('click', closeHandler, { capture: true });
1953
+ this._attachClickDetails(toolEl, details, toolsDiv, container);
789
1954
  container.scrollTop = container.scrollHeight;
1955
+
1956
+ if (event.tool === 'workspace_write' || event.tool === 'workspace_delete') {
1957
+ try {
1958
+ const result = JSON.parse(typeof event.output === 'string' ? event.output : JSON.stringify(event.output));
1959
+ if (result.success && (result.reloaded === 'agent' || result.unloaded === 'agent')) this.loadAgents();
1960
+ } catch { /* ignore parse errors */ }
1961
+ }
790
1962
  }
791
1963
  } else if (event.type === 'result') {
792
1964
  if (loadingDots) {
793
1965
  loadingDots.remove();
794
- bubble.querySelector('.response-bubble-inner').classList.remove('py-4');
795
- bubble.querySelector('.response-bubble-inner').classList.add('py-3');
1966
+ bubble.querySelector('.response-bubble-inner').classList.remove('loading');
796
1967
  contentDiv.classList.remove('flex', 'items-center', 'whitespace-pre-wrap');
797
1968
  contentDiv.innerHTML = '';
798
1969
  }
799
1970
 
800
- // Display structured output as formatted JSON
801
1971
  const resultContainer = document.createElement('div');
802
- resultContainer.className = 'bg-dark-bg/50 border border-dark-border rounded-lg p-4';
1972
+ resultContainer.className = 'panel';
803
1973
 
804
1974
  const resultPre = document.createElement('pre');
805
- resultPre.className = 'text-sm text-gray-300 font-mono whitespace-pre-wrap overflow-x-auto';
1975
+ resultPre.className = 'text-sm text-primary font-mono whitespace-pre-wrap overflow-x-auto';
806
1976
  resultPre.textContent = JSON.stringify(event.output, null, 2);
807
1977
 
808
1978
  resultContainer.appendChild(resultPre);
809
1979
  contentDiv.appendChild(resultContainer);
810
1980
 
811
- // Scroll to bottom
812
1981
  container.scrollTop = container.scrollHeight;
813
1982
  } else if (event.type === 'error') {
814
1983
  if (loadingDots) {
815
1984
  loadingDots.remove();
816
- bubble.querySelector('.response-bubble-inner').classList.remove('py-4');
817
- bubble.querySelector('.response-bubble-inner').classList.add('py-3');
1985
+ bubble.querySelector('.response-bubble-inner').classList.remove('loading');
818
1986
  contentDiv.classList.remove('flex', 'items-center', 'whitespace-pre-wrap');
819
1987
  }
820
1988
  const errorDiv = document.createElement('div');
821
- errorDiv.className = 'text-red-400 text-sm mt-2';
1989
+ errorDiv.className = 'text-red text-sm';
822
1990
  errorDiv.textContent = `Error: ${event.error}`;
823
1991
  contentDiv.appendChild(errorDiv);
824
1992
  container.scrollTop = container.scrollHeight;
1993
+ } else if (event.type === 'warning') {
1994
+ const warningDiv = document.createElement('div');
1995
+ warningDiv.className = 'text-yellow text-sm';
1996
+ warningDiv.textContent = event.message;
1997
+ contentDiv.appendChild(warningDiv);
1998
+ container.scrollTop = container.scrollHeight;
825
1999
  } else if (event.type === 'usage') {
826
2000
  this.streamUsageData = {
827
2001
  input_tokens: event.input_tokens || 0,
828
2002
  output_tokens: event.output_tokens || 0,
829
2003
  total_tokens: event.total_tokens || 0,
830
2004
  };
2005
+ } else if (event.type === 'react_iteration') {
2006
+ const wrapper = bubble.closest('.response-wrapper');
2007
+ const statusText = wrapper?.querySelector('.stream-status-text');
2008
+ if (statusText) {
2009
+ const contextKB = (event.contextChars / 1024).toFixed(1);
2010
+ statusText.textContent = `Iteration ${event.iteration} · ${contextKB} KB context`;
2011
+ }
831
2012
  }
832
2013
  }
833
2014
 
@@ -835,15 +2016,20 @@ export class AgentsView extends Component {
835
2016
  const bubble = this.querySelector(`#${id}`);
836
2017
  if (bubble) {
837
2018
  const content = bubble.querySelector('.response-content');
838
- content.innerHTML = `<span class="text-red-400">${errorMsg}</span>`;
2019
+ content.innerHTML = `<span class="text-red">${errorMsg}</span>`;
839
2020
  }
840
2021
  }
841
2022
 
842
2023
  updateUiState() {
843
2024
  const btn = this.querySelector('#sendMessageBtn');
844
2025
  const input = this.querySelector('#chatInput');
845
- if (btn) btn.disabled = this.isLoading;
846
- if (input) input.disabled = this.isLoading;
2026
+ const hasActiveSession = !!sessionStore.getActiveId();
2027
+ if (btn) btn.disabled = this.isLoading || !hasActiveSession;
2028
+ if (input) {
2029
+ input.disabled = this.isLoading;
2030
+ input.readOnly = !hasActiveSession;
2031
+ input.classList.toggle('cursor-pointer', !hasActiveSession);
2032
+ }
847
2033
  }
848
2034
 
849
2035
  appendMessage(role, content, metadata = {}) {
@@ -854,14 +2040,29 @@ export class AgentsView extends Component {
854
2040
  const div = document.createElement('div');
855
2041
  div.className = isUser ? 'flex justify-end' : 'flex justify-start';
856
2042
 
857
- const bubbleColor = isUser ? 'bg-dark-surface' : (hasError ? 'bg-red-900/20 border-red-900/30' : 'bg-dark-surface');
858
- const textColor = hasError ? 'text-red-300' : 'text-gray-100';
2043
+ const bubbleClass = isUser ? 'user-bubble' : (hasError ? 'response-bubble-inner error' : 'response-bubble-inner');
2044
+
2045
+ // Build attachment thumbnails for user messages
2046
+ let attachmentHtml = '';
2047
+ if (isUser && metadata.attachments && metadata.attachments.length > 0) {
2048
+ const thumbs = metadata.attachments.map(att => {
2049
+ if (att.mediaType.startsWith('image/')) {
2050
+ return `<img src="data:${att.mediaType};base64,${att.data}" class="attachment-thumb">`;
2051
+ }
2052
+ return `<div class="attachment-pill">
2053
+ <i class="fas fa-file"></i>
2054
+ <span class="truncate attachment-name">${this.escapeHtml(att.name)}</span>
2055
+ </div>`;
2056
+ }).join('');
2057
+ attachmentHtml = `<div class="flex flex-wrap gap-2 mb-2">${thumbs}</div>`;
2058
+ }
859
2059
 
860
2060
  div.innerHTML = `
861
- <div class="max-w-4xl ${bubbleColor} border ${isUser ? 'border-transparent' : 'border-dark-border'} rounded-3xl px-5 py-3 ${textColor} text-[15px] leading-relaxed relative group">
2061
+ <div class="${bubbleClass} group">
2062
+ ${attachmentHtml}
862
2063
  <div class="whitespace-pre-wrap">${this.escapeHtml(content)}</div>
863
2064
  ${!isUser && !hasError ? `
864
- <button class="copy-btn absolute -bottom-6 left-0 text-gray-500 hover:text-gray-300 opacity-0 group-hover:opacity-100 transition-opacity p-1" title="Copy">
2065
+ <button class="copy-btn" title="Copy">
865
2066
  <i class="far fa-copy"></i>
866
2067
  </button>
867
2068
  ` : ''}
@@ -888,11 +2089,11 @@ export class AgentsView extends Component {
888
2089
  div.id = id;
889
2090
  div.className = 'flex justify-start';
890
2091
  div.innerHTML = `
891
- <div class="max-w-4xl bg-dark-surface border border-dark-border rounded-3xl px-5 py-4">
892
- <div class="flex gap-1">
893
- <div class="w-2 h-2 bg-blue-500 rounded-full animate-bounce"></div>
894
- <div class="w-2 h-2 bg-blue-500 rounded-full animate-bounce animation-delay-200"></div>
895
- <div class="w-2 h-2 bg-blue-500 rounded-full animate-bounce animation-delay-400"></div>
2092
+ <div class="response-bubble-inner loading">
2093
+ <div class="loading-dots">
2094
+ <div></div>
2095
+ <div></div>
2096
+ <div></div>
896
2097
  </div>
897
2098
  </div>
898
2099
  `;
@@ -907,35 +2108,120 @@ export class AgentsView extends Component {
907
2108
  }
908
2109
 
909
2110
  escapeHtml(text) {
2111
+ return sharedEscapeHtml(text);
2112
+ }
2113
+
2114
+ _getRandomWelcomeMessage() {
2115
+ const messages = [
2116
+ 'Awaiting your command, master.',
2117
+ 'The agents are restless. Give them purpose.',
2118
+ 'Ready when you are. No pressure... okay, maybe a little.',
2119
+ 'Spinning up neurons... just kidding, I was already ready.',
2120
+ 'All systems nominal. Your move, human.',
2121
+ 'The orchestrator awaits. What shall we build today?',
2122
+ 'Standing by. The agents are stretching their digital legs.',
2123
+ 'Another day, another chance to orchestrate greatness.',
2124
+ 'Agents assembled. Awaiting mission briefing.',
2125
+ 'The stage is set. You are the conductor.',
2126
+ ];
2127
+ return messages[Math.floor(Math.random() * messages.length)];
2128
+ }
2129
+
2130
+ _appendWelcomeMessage(container) {
910
2131
  const div = document.createElement('div');
911
- div.textContent = text;
912
- return div.innerHTML;
2132
+ div.className = 'welcome-container';
2133
+ div.innerHTML = `
2134
+ <svg class="welcome-orca" viewBox="0 0 220 140" xmlns="http://www.w3.org/2000/svg">
2135
+ <!-- Main body -->
2136
+ <path class="orca-body" d="
2137
+ M 30,68
2138
+ C 28,58 38,42 58,38
2139
+ C 68,35 74,30 78,18
2140
+ C 80,12 84,12 85,18
2141
+ C 87,28 86,35 92,38
2142
+ C 112,34 148,40 172,54
2143
+ C 176,50 182,44 188,40
2144
+ C 192,38 194,42 190,46
2145
+ C 186,50 184,54 182,56
2146
+ C 186,60 188,66 184,68
2147
+ C 180,70 178,66 176,62
2148
+ C 168,72 142,78 112,76
2149
+ C 82,74 52,70 38,66
2150
+ C 34,64 30,68 30,68 Z
2151
+ "/>
2152
+ <!-- Dorsal fin accent line -->
2153
+ <path class="orca-detail" d="M 72,38 C 74,28 78,18 80,14"/>
2154
+ <!-- Belly line -->
2155
+ <path class="orca-detail" d="M 42,64 C 62,70 100,74 140,72 C 158,70 170,66 176,62"/>
2156
+ <!-- Saddle patch -->
2157
+ <path class="orca-patch" d="M 92,40 C 102,38 112,40 108,48 C 104,54 90,50 92,40 Z"/>
2158
+ <!-- Eye patch -->
2159
+ <path class="orca-patch" d="M 44,52 C 50,48 60,50 56,58 C 52,62 42,58 44,52 Z"/>
2160
+ <!-- Eye -->
2161
+ <circle class="orca-eye" cx="42" cy="54" r="2.5"/>
2162
+ <!-- Pectoral fin -->
2163
+ <path class="orca-body" d="M 65,66 C 70,74 64,82 58,76 C 54,72 60,66 65,66 Z"/>
2164
+ <!-- Tail detail -->
2165
+ <path class="orca-detail" d="M 172,54 C 176,52 180,48 184,44"/>
2166
+ <path class="orca-detail" d="M 176,62 C 180,64 184,66 186,64"/>
2167
+ </svg>
2168
+ <div class="welcome-text">${this._getRandomWelcomeMessage()}</div>
2169
+ ${this._renderSampleQuestionChips()}
2170
+ `;
2171
+ container.appendChild(div);
2172
+
2173
+ div.querySelectorAll('.sample-question-chip').forEach(chip => {
2174
+ chip.addEventListener('click', () => {
2175
+ const input = this.querySelector('#chatInput');
2176
+ if (input) {
2177
+ input.value = chip.textContent;
2178
+ input.focus();
2179
+ }
2180
+ });
2181
+ });
913
2182
  }
914
2183
 
915
- clearChatHistory() {
916
- const container = this.querySelector('#chatMessages');
917
- if (!container) return;
2184
+ _renderSampleQuestionChips() {
2185
+ const agent = store.get('selectedAgent');
2186
+ const workflow = store.get('selectedWorkflow');
2187
+ const questions = agent?.sampleQuestions || workflow?.sampleQuestions;
2188
+ if (!questions || questions.length === 0) return '';
918
2189
 
919
- // Clear all messages
920
- container.innerHTML = '';
2190
+ const chips = questions.map(q =>
2191
+ `<button class="sample-question-chip">${this.escapeHtml(q)}</button>`
2192
+ ).join('');
2193
+
2194
+ return `
2195
+ <div class="sample-questions-wrap">${chips}</div>
2196
+ `;
2197
+ }
921
2198
 
922
- // Add welcome message
2199
+ _appendSessionResetBanner(container) {
923
2200
  const div = document.createElement('div');
924
- div.className = 'flex justify-start';
2201
+ div.className = 'session-reset-banner';
925
2202
  div.innerHTML = `
926
- <div class="max-w-4xl bg-dark-surface border border-dark-border rounded-3xl px-5 py-3 text-gray-100 text-[15px] leading-relaxed">
927
- Welcome to Agent Orcha. Start chatting with your AI agents and LLMs.
928
- </div>
2203
+ <div class="session-reset-line"></div>
2204
+ <span class="session-reset-label">
2205
+ <i class="fas fa-rotate-right"></i>
2206
+ Server restarted — new session
2207
+ </span>
2208
+ <div class="session-reset-line"></div>
929
2209
  `;
930
2210
  container.appendChild(div);
931
-
932
- // Generate a new session ID for fresh conversation
933
- store.set('sessionId', 'session-' + Date.now() + '-' + Math.random().toString(36).substring(2, 9));
2211
+ container.scrollTop = container.scrollHeight;
934
2212
  }
935
2213
 
936
2214
  postRender() {
937
2215
  const input = this.querySelector('#chatInput');
938
2216
 
2217
+ // Open new conversation modal when clicking input with no active session
2218
+ input.addEventListener('mousedown', (e) => {
2219
+ if (!sessionStore.getActiveId()) {
2220
+ e.preventDefault();
2221
+ this.showNewSessionModal();
2222
+ }
2223
+ });
2224
+
939
2225
  // Auto-resize
940
2226
  input.addEventListener('input', () => {
941
2227
  input.style.height = 'auto';
@@ -952,60 +2238,78 @@ export class AgentsView extends Component {
952
2238
 
953
2239
  this.querySelector('#sendMessageBtn').addEventListener('click', () => this.sendMessage());
954
2240
 
955
- // Dropdown toggle
956
- const selectorBtn = this.querySelector('#agentSelectorBtn');
957
- const dropdown = this.querySelector('#agentDropdown');
2241
+ // Attach button + file input
2242
+ this.querySelector('#attachBtn').addEventListener('click', () => this.querySelector('#fileInput').click());
2243
+ this.querySelector('#fileInput').addEventListener('change', (e) => this.handleFileSelect(e));
958
2244
 
959
- selectorBtn.addEventListener('click', (e) => {
960
- e.stopPropagation();
961
- this.toggleDropdown();
962
- });
2245
+ // New chat button
2246
+ this.querySelector('#newChatBtn').addEventListener('click', () => this.showNewSessionModal());
963
2247
 
964
- document.addEventListener('click', (e) => {
965
- if (!selectorBtn.contains(e.target) && !dropdown.contains(e.target)) {
966
- this.toggleDropdown(false);
967
- }
968
- });
2248
+ // New agent button
2249
+ this.querySelector('#newAgentBtn').addEventListener('click', () => this.showNewAgentModal());
2250
+
2251
+ // Mobile sidebar toggle
2252
+ this.querySelector('#sidebarToggleBtn').addEventListener('click', () => this.toggleSidebar(true));
2253
+ this.querySelector('#sidebarBackdrop').addEventListener('click', () => this.toggleSidebar(false));
969
2254
  }
970
2255
 
971
2256
  template() {
972
2257
  return `
973
- <div class="flex flex-col h-[calc(100vh-220px)]">
974
- <!-- Chat Messages -->
975
- <div id="chatMessages" class="flex-1 overflow-y-auto mb-6 space-y-4 pr-2 custom-scrollbar">
976
- <div class="flex justify-start">
977
- <div class="max-w-4xl bg-dark-surface border border-dark-border rounded-3xl px-5 py-3 text-gray-100 text-[15px] leading-relaxed">
978
- Welcome to Agent Orcha. Start chatting with your AI agents and LLMs.
979
- </div>
2258
+ <div class="agent-shell">
2259
+ <!-- Mobile sidebar backdrop -->
2260
+ <div id="sidebarBackdrop" class="sidebar-backdrop"></div>
2261
+
2262
+ <!-- Sidebar -->
2263
+ <div id="sidebar" class="agent-sidebar">
2264
+ <div class="p-3">
2265
+ <button id="newChatBtn" class="new-chat-btn">
2266
+ <i class="fas fa-plus text-xs text-accent"></i>
2267
+ <span>New chat</span>
2268
+ </button>
2269
+ </div>
2270
+ <div id="sessionList" class="flex-1 overflow-y-auto custom-scrollbar px-2 pb-2"></div>
2271
+ <div class="px-3 sidebar-bottom-action">
2272
+ <button id="newAgentBtn" class="sidebar-secondary-btn">
2273
+ <i class="fas fa-robot text-xs text-blue"></i>
2274
+ <span>New agent</span>
2275
+ </button>
980
2276
  </div>
981
2277
  </div>
982
2278
 
983
- <!-- Input Area -->
984
- <div class="border-t border-dark-border pt-4">
985
- <div class="relative bg-dark-surface border border-dark-border rounded-2xl focus-within:border-gray-500 transition-colors">
986
- <textarea id="chatInput" rows="1"
987
- class="w-full bg-transparent px-4 py-3 pr-32 text-gray-100 placeholder-gray-500 resize-none focus:outline-none max-h-[200px]"
988
- placeholder="Reply..."></textarea>
989
-
990
- <div class="absolute bottom-2 right-2 flex items-center gap-2">
991
- <!-- Agent Selector -->
992
- <div class="relative">
993
- <button id="agentSelectorBtn" class="flex items-center gap-2 px-3 py-1.5 bg-dark-bg hover:bg-dark-hover rounded-lg text-sm font-medium text-gray-300 transition-colors">
994
- <span id="selectedAgentName">Select Agent/LLM</span>
995
- <i class="fas fa-chevron-down text-xs text-gray-400"></i>
996
- </button>
2279
+ <!-- Chat Area -->
2280
+ <div class="chat-area">
2281
+ <!-- Chat Header -->
2282
+ <div class="chat-header">
2283
+ <button id="sidebarToggleBtn" class="sidebar-toggle-btn">
2284
+ <i class="fas fa-bars"></i>
2285
+ </button>
2286
+ <div id="chatHeader" class="flex-1 min-w-0">
2287
+ <span class="text-muted">No conversation selected</span>
2288
+ </div>
2289
+ </div>
2290
+
2291
+ <!-- Chat Messages -->
2292
+ <div id="chatMessages" class="chat-messages custom-scrollbar"></div>
997
2293
 
998
- <div id="agentDropdown" class="hidden absolute bottom-full mb-2 right-0 w-80 bg-dark-surface border border-dark-border rounded-xl shadow-2xl overflow-hidden z-10 max-h-96 flex flex-col">
999
- <div id="agentDropdownList" class="overflow-y-auto custom-scrollbar">
1000
- <div class="text-gray-500 text-sm text-center py-4">Loading...</div>
1001
- </div>
1002
- </div>
2294
+ <!-- Input Area -->
2295
+ <div class="chat-input-area">
2296
+ <div id="attachmentPreview" class="attachment-preview"></div>
2297
+ <div class="chat-input-wrap">
2298
+ <input type="file" id="fileInput" multiple accept="image/*,.pdf,.doc,.docx,.xls,.xlsx,.pptx,.txt,.md,.csv,.json,.yaml,.yml,.xml,.html,.css,.js,.ts,.py,.java,.c,.cpp,.go,.rs,.rb,.php,.sql,.sh,.log,.ini,.toml,.env" class="hidden">
2299
+ <textarea id="chatInput" rows="1" readonly
2300
+ placeholder="Ask anything"></textarea>
2301
+
2302
+ <div class="chat-input-actions left">
2303
+ <button id="attachBtn" type="button" class="attach-btn" title="Attach files">
2304
+ <i class="fas fa-plus text-sm"></i>
2305
+ </button>
1003
2306
  </div>
1004
2307
 
1005
- <button id="sendMessageBtn" disabled
1006
- class="bg-gradient-to-r from-blue-600 to-blue-700 hover:from-blue-500 hover:to-blue-600 disabled:opacity-50 disabled:cursor-not-allowed text-white p-2 rounded-lg transition-all shadow-lg shadow-blue-900/20">
1007
- <i class="fas fa-paper-plane text-sm"></i>
1008
- </button>
2308
+ <div class="chat-input-actions right">
2309
+ <button id="sendMessageBtn" disabled class="send-btn">
2310
+ <i class="fas fa-paper-plane text-sm"></i>
2311
+ </button>
2312
+ </div>
1009
2313
  </div>
1010
2314
  </div>
1011
2315
  </div>