agent-orcha 0.0.7 → 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 (259) hide show
  1. package/README.md +86 -28
  2. package/dist/lib/agents/agent-executor.d.ts.map +1 -1
  3. package/dist/lib/agents/agent-executor.js +23 -7
  4. package/dist/lib/agents/agent-executor.js.map +1 -1
  5. package/dist/lib/agents/react-loop.d.ts.map +1 -1
  6. package/dist/lib/agents/react-loop.js +27 -0
  7. package/dist/lib/agents/react-loop.js.map +1 -1
  8. package/dist/lib/functions/simple-function-wrapper.js +3 -3
  9. package/dist/lib/functions/simple-function-wrapper.js.map +1 -1
  10. package/dist/lib/knowledge/knowledge-store.d.ts +1 -1
  11. package/dist/lib/knowledge/knowledge-store.d.ts.map +1 -1
  12. package/dist/lib/knowledge/knowledge-store.js +25 -4
  13. package/dist/lib/knowledge/knowledge-store.js.map +1 -1
  14. package/dist/lib/knowledge/loaders/file-loaders.d.ts +0 -1
  15. package/dist/lib/knowledge/loaders/file-loaders.d.ts.map +1 -1
  16. package/dist/lib/knowledge/loaders/file-loaders.js +7 -15
  17. package/dist/lib/knowledge/loaders/file-loaders.js.map +1 -1
  18. package/dist/lib/knowledge/sqlite-store.d.ts.map +1 -1
  19. package/dist/lib/knowledge/sqlite-store.js +19 -10
  20. package/dist/lib/knowledge/sqlite-store.js.map +1 -1
  21. package/dist/lib/knowledge/types.d.ts +13 -13
  22. package/dist/lib/llm/index.d.ts +1 -1
  23. package/dist/lib/llm/index.d.ts.map +1 -1
  24. package/dist/lib/llm/index.js +1 -1
  25. package/dist/lib/llm/index.js.map +1 -1
  26. package/dist/lib/llm/llm-config.d.ts +51 -8
  27. package/dist/lib/llm/llm-config.d.ts.map +1 -1
  28. package/dist/lib/llm/llm-config.js +161 -17
  29. package/dist/lib/llm/llm-config.js.map +1 -1
  30. package/dist/lib/llm/llm-factory.d.ts +1 -2
  31. package/dist/lib/llm/llm-factory.d.ts.map +1 -1
  32. package/dist/lib/llm/llm-factory.js +41 -8
  33. package/dist/lib/llm/llm-factory.js.map +1 -1
  34. package/dist/lib/llm/providers/openai-chat-model.d.ts +10 -0
  35. package/dist/lib/llm/providers/openai-chat-model.d.ts.map +1 -1
  36. package/dist/lib/llm/providers/openai-chat-model.js +37 -5
  37. package/dist/lib/llm/providers/openai-chat-model.js.map +1 -1
  38. package/dist/lib/llm/providers/openai-embeddings.d.ts.map +1 -1
  39. package/dist/lib/llm/providers/openai-embeddings.js +41 -10
  40. package/dist/lib/llm/providers/openai-embeddings.js.map +1 -1
  41. package/dist/lib/local-llm/binary-manager.d.ts +66 -0
  42. package/dist/lib/local-llm/binary-manager.d.ts.map +1 -0
  43. package/dist/lib/local-llm/binary-manager.js +441 -0
  44. package/dist/lib/local-llm/binary-manager.js.map +1 -0
  45. package/dist/lib/local-llm/engine-interface.d.ts +47 -0
  46. package/dist/lib/local-llm/engine-interface.d.ts.map +1 -0
  47. package/dist/lib/local-llm/engine-interface.js +2 -0
  48. package/dist/lib/local-llm/engine-interface.js.map +1 -0
  49. package/dist/lib/local-llm/engine-registry.d.ts +20 -0
  50. package/dist/lib/local-llm/engine-registry.d.ts.map +1 -0
  51. package/dist/lib/local-llm/engine-registry.js +56 -0
  52. package/dist/lib/local-llm/engine-registry.js.map +1 -0
  53. package/dist/lib/local-llm/engines/llama-cpp-engine.d.ts +31 -0
  54. package/dist/lib/local-llm/engines/llama-cpp-engine.d.ts.map +1 -0
  55. package/dist/lib/local-llm/engines/llama-cpp-engine.js +164 -0
  56. package/dist/lib/local-llm/engines/llama-cpp-engine.js.map +1 -0
  57. package/dist/lib/local-llm/engines/mlx-serve-engine.d.ts +31 -0
  58. package/dist/lib/local-llm/engines/mlx-serve-engine.d.ts.map +1 -0
  59. package/dist/lib/local-llm/engines/mlx-serve-engine.js +161 -0
  60. package/dist/lib/local-llm/engines/mlx-serve-engine.js.map +1 -0
  61. package/dist/lib/local-llm/gguf-reader.d.ts +20 -0
  62. package/dist/lib/local-llm/gguf-reader.d.ts.map +1 -0
  63. package/dist/lib/local-llm/gguf-reader.js +190 -0
  64. package/dist/lib/local-llm/gguf-reader.js.map +1 -0
  65. package/dist/lib/local-llm/index.d.ts +9 -0
  66. package/dist/lib/local-llm/index.d.ts.map +1 -0
  67. package/dist/lib/local-llm/index.js +6 -0
  68. package/dist/lib/local-llm/index.js.map +1 -0
  69. package/dist/lib/local-llm/llama-server-process.d.ts +42 -0
  70. package/dist/lib/local-llm/llama-server-process.d.ts.map +1 -0
  71. package/dist/lib/local-llm/llama-server-process.js +237 -0
  72. package/dist/lib/local-llm/llama-server-process.js.map +1 -0
  73. package/dist/lib/local-llm/mlx-binary-manager.d.ts +33 -0
  74. package/dist/lib/local-llm/mlx-binary-manager.d.ts.map +1 -0
  75. package/dist/lib/local-llm/mlx-binary-manager.js +211 -0
  76. package/dist/lib/local-llm/mlx-binary-manager.js.map +1 -0
  77. package/dist/lib/local-llm/mlx-server-process.d.ts +26 -0
  78. package/dist/lib/local-llm/mlx-server-process.d.ts.map +1 -0
  79. package/dist/lib/local-llm/mlx-server-process.js +210 -0
  80. package/dist/lib/local-llm/mlx-server-process.js.map +1 -0
  81. package/dist/lib/local-llm/model-manager.d.ts +33 -0
  82. package/dist/lib/local-llm/model-manager.d.ts.map +1 -0
  83. package/dist/lib/local-llm/model-manager.js +591 -0
  84. package/dist/lib/local-llm/model-manager.js.map +1 -0
  85. package/dist/lib/local-llm/types.d.ts +51 -0
  86. package/dist/lib/local-llm/types.d.ts.map +1 -0
  87. package/dist/lib/local-llm/types.js +2 -0
  88. package/dist/lib/local-llm/types.js.map +1 -0
  89. package/dist/lib/logger.d.ts +2 -0
  90. package/dist/lib/logger.d.ts.map +1 -1
  91. package/dist/lib/logger.js +68 -5
  92. package/dist/lib/logger.js.map +1 -1
  93. package/dist/lib/orchestrator.d.ts +9 -0
  94. package/dist/lib/orchestrator.d.ts.map +1 -1
  95. package/dist/lib/orchestrator.js +151 -3
  96. package/dist/lib/orchestrator.js.map +1 -1
  97. package/dist/lib/sandbox/cdp-client.d.ts +2 -1
  98. package/dist/lib/sandbox/cdp-client.d.ts.map +1 -1
  99. package/dist/lib/sandbox/cdp-client.js +33 -7
  100. package/dist/lib/sandbox/cdp-client.js.map +1 -1
  101. package/dist/lib/sandbox/index.d.ts +1 -0
  102. package/dist/lib/sandbox/index.d.ts.map +1 -1
  103. package/dist/lib/sandbox/index.js +1 -0
  104. package/dist/lib/sandbox/index.js.map +1 -1
  105. package/dist/lib/sandbox/page-readiness.d.ts.map +1 -1
  106. package/dist/lib/sandbox/page-readiness.js +33 -0
  107. package/dist/lib/sandbox/page-readiness.js.map +1 -1
  108. package/dist/lib/sandbox/sandbox-browser.d.ts.map +1 -1
  109. package/dist/lib/sandbox/sandbox-browser.js +14 -1
  110. package/dist/lib/sandbox/sandbox-browser.js.map +1 -1
  111. package/dist/lib/sandbox/sandbox-container.d.ts +39 -0
  112. package/dist/lib/sandbox/sandbox-container.d.ts.map +1 -0
  113. package/dist/lib/sandbox/sandbox-container.js +176 -0
  114. package/dist/lib/sandbox/sandbox-container.js.map +1 -0
  115. package/dist/lib/sandbox/sandbox-file.d.ts.map +1 -1
  116. package/dist/lib/sandbox/sandbox-file.js +5 -4
  117. package/dist/lib/sandbox/sandbox-file.js.map +1 -1
  118. package/dist/lib/sandbox/sandbox-shell.d.ts +2 -1
  119. package/dist/lib/sandbox/sandbox-shell.d.ts.map +1 -1
  120. package/dist/lib/sandbox/sandbox-shell.js +42 -24
  121. package/dist/lib/sandbox/sandbox-shell.js.map +1 -1
  122. package/dist/lib/sandbox/sandbox-web.d.ts.map +1 -1
  123. package/dist/lib/sandbox/sandbox-web.js +27 -2
  124. package/dist/lib/sandbox/sandbox-web.js.map +1 -1
  125. package/dist/lib/sandbox/vision-browser.d.ts.map +1 -1
  126. package/dist/lib/sandbox/vision-browser.js +9 -0
  127. package/dist/lib/sandbox/vision-browser.js.map +1 -1
  128. package/dist/lib/sea/app-window.d.ts +7 -0
  129. package/dist/lib/sea/app-window.d.ts.map +1 -0
  130. package/dist/lib/sea/app-window.js +95 -0
  131. package/dist/lib/sea/app-window.js.map +1 -0
  132. package/dist/lib/sea/bootstrap.d.ts +18 -0
  133. package/dist/lib/sea/bootstrap.d.ts.map +1 -0
  134. package/dist/lib/sea/bootstrap.js +103 -0
  135. package/dist/lib/sea/bootstrap.js.map +1 -0
  136. package/dist/lib/sea/sqlite-vec-shim.d.ts +3 -0
  137. package/dist/lib/sea/sqlite-vec-shim.d.ts.map +1 -0
  138. package/dist/lib/sea/sqlite-vec-shim.js +10 -0
  139. package/dist/lib/sea/sqlite-vec-shim.js.map +1 -0
  140. package/dist/lib/tools/built-in/knowledge-entity-lookup.tool.d.ts +1 -2
  141. package/dist/lib/tools/built-in/knowledge-entity-lookup.tool.d.ts.map +1 -1
  142. package/dist/lib/tools/built-in/knowledge-entity-lookup.tool.js +7 -13
  143. package/dist/lib/tools/built-in/knowledge-entity-lookup.tool.js.map +1 -1
  144. package/dist/lib/tools/built-in/knowledge-graph-schema.tool.d.ts.map +1 -1
  145. package/dist/lib/tools/built-in/knowledge-graph-schema.tool.js +2 -4
  146. package/dist/lib/tools/built-in/knowledge-graph-schema.tool.js.map +1 -1
  147. package/dist/lib/tools/built-in/knowledge-search.tool.js +4 -4
  148. package/dist/lib/tools/built-in/knowledge-search.tool.js.map +1 -1
  149. package/dist/lib/tools/built-in/knowledge-sql.tool.d.ts.map +1 -1
  150. package/dist/lib/tools/built-in/knowledge-sql.tool.js +70 -37
  151. package/dist/lib/tools/built-in/knowledge-sql.tool.js.map +1 -1
  152. package/dist/lib/tools/built-in/knowledge-tools-factory.js +2 -2
  153. package/dist/lib/tools/built-in/knowledge-tools-factory.js.map +1 -1
  154. package/dist/lib/tools/built-in/knowledge-traverse.tool.d.ts +1 -2
  155. package/dist/lib/tools/built-in/knowledge-traverse.tool.d.ts.map +1 -1
  156. package/dist/lib/tools/built-in/knowledge-traverse.tool.js +5 -11
  157. package/dist/lib/tools/built-in/knowledge-traverse.tool.js.map +1 -1
  158. package/dist/lib/tools/workspace/workspace-tools.d.ts.map +1 -1
  159. package/dist/lib/tools/workspace/workspace-tools.js +5 -4
  160. package/dist/lib/tools/workspace/workspace-tools.js.map +1 -1
  161. package/dist/lib/types/tool-factory.d.ts.map +1 -1
  162. package/dist/lib/types/tool-factory.js +9 -2
  163. package/dist/lib/types/tool-factory.js.map +1 -1
  164. package/dist/lib/utils/document-extract.d.ts +10 -0
  165. package/dist/lib/utils/document-extract.d.ts.map +1 -0
  166. package/dist/lib/utils/document-extract.js +149 -0
  167. package/dist/lib/utils/document-extract.js.map +1 -0
  168. package/dist/lib/workflows/react-workflow-executor.d.ts.map +1 -1
  169. package/dist/lib/workflows/react-workflow-executor.js +20 -14
  170. package/dist/lib/workflows/react-workflow-executor.js.map +1 -1
  171. package/dist/lib/workflows/types.d.ts +71 -45
  172. package/dist/lib/workflows/types.d.ts.map +1 -1
  173. package/dist/lib/workflows/types.js +10 -0
  174. package/dist/lib/workflows/types.js.map +1 -1
  175. package/dist/public/assets/logo.png +0 -0
  176. package/dist/public/chat.html +3 -78
  177. package/dist/public/index.html +3 -330
  178. package/dist/public/src/components/AgentComposer.js +132 -132
  179. package/dist/public/src/components/AgentsView.js +1231 -350
  180. package/dist/public/src/components/AppRoot.js +101 -39
  181. package/dist/public/src/components/GraphView.js +11 -13
  182. package/dist/public/src/components/IdeView.js +133 -98
  183. package/dist/public/src/components/KnowledgeView.js +94 -130
  184. package/dist/public/src/components/LlmView.js +15 -19
  185. package/dist/public/src/components/LocalLlmView.js +2440 -0
  186. package/dist/public/src/components/LogViewer.js +155 -0
  187. package/dist/public/src/components/McpView.js +41 -49
  188. package/dist/public/src/components/MonitorView.js +79 -126
  189. package/dist/public/src/components/NavBar.js +16 -26
  190. package/dist/public/src/components/StandaloneChat.js +136 -150
  191. package/dist/public/src/services/ApiService.js +196 -2
  192. package/dist/public/src/services/SessionStore.js +6 -3
  193. package/dist/public/src/services/StreamManager.js +183 -0
  194. package/dist/public/src/store.js +1 -1
  195. package/dist/public/src/utils/card.js +21 -0
  196. package/dist/public/src/utils/markdown.js +1 -7
  197. package/dist/public/styles.css +2777 -0
  198. package/dist/src/cli/commands/init.d.ts.map +1 -1
  199. package/dist/src/cli/commands/init.js +7 -1
  200. package/dist/src/cli/commands/init.js.map +1 -1
  201. package/dist/src/cli/commands/start.d.ts.map +1 -1
  202. package/dist/src/cli/commands/start.js +28 -5
  203. package/dist/src/cli/commands/start.js.map +1 -1
  204. package/dist/src/cli/index.js +13 -2
  205. package/dist/src/cli/index.js.map +1 -1
  206. package/dist/src/index.js +7 -1
  207. package/dist/src/index.js.map +1 -1
  208. package/dist/src/routes/agents.route.d.ts.map +1 -1
  209. package/dist/src/routes/agents.route.js +2 -0
  210. package/dist/src/routes/agents.route.js.map +1 -1
  211. package/dist/src/routes/chat.route.d.ts.map +1 -1
  212. package/dist/src/routes/chat.route.js +3 -2
  213. package/dist/src/routes/chat.route.js.map +1 -1
  214. package/dist/src/routes/llm.route.d.ts.map +1 -1
  215. package/dist/src/routes/llm.route.js +227 -7
  216. package/dist/src/routes/llm.route.js.map +1 -1
  217. package/dist/src/routes/local-llm.route.d.ts +3 -0
  218. package/dist/src/routes/local-llm.route.d.ts.map +1 -0
  219. package/dist/src/routes/local-llm.route.js +688 -0
  220. package/dist/src/routes/local-llm.route.js.map +1 -0
  221. package/dist/src/routes/logs.route.d.ts +3 -0
  222. package/dist/src/routes/logs.route.d.ts.map +1 -0
  223. package/dist/src/routes/logs.route.js +24 -0
  224. package/dist/src/routes/logs.route.js.map +1 -0
  225. package/dist/src/routes/vnc.route.d.ts +10 -1
  226. package/dist/src/routes/vnc.route.d.ts.map +1 -1
  227. package/dist/src/routes/vnc.route.js +37 -12
  228. package/dist/src/routes/vnc.route.js.map +1 -1
  229. package/dist/src/routes/workflows.route.d.ts.map +1 -1
  230. package/dist/src/routes/workflows.route.js +24 -0
  231. package/dist/src/routes/workflows.route.js.map +1 -1
  232. package/dist/src/server.d.ts.map +1 -1
  233. package/dist/src/server.js +24 -2
  234. package/dist/src/server.js.map +1 -1
  235. package/dist/templates/agents/actor.agent.yaml +34 -0
  236. package/dist/templates/agents/architect.agent.yaml +0 -1
  237. package/dist/templates/agents/chatbot.agent.yaml +0 -1
  238. package/dist/templates/agents/corporate.agent.yaml +0 -1
  239. package/dist/templates/agents/functions.agent.yaml +29 -0
  240. package/dist/templates/agents/investment-analyst.agent.yaml +0 -1
  241. package/dist/templates/agents/music-librarian.agent.yaml +3 -27
  242. package/dist/templates/agents/network-security.agent.yaml +0 -1
  243. package/dist/templates/agents/transport-security.agent.yaml +0 -1
  244. package/dist/templates/agents/web-engineer.agent.yaml +3 -4
  245. package/dist/templates/agents/web-pilot.agent.yaml +0 -1
  246. package/dist/templates/knowledge/patient-records.knowledge.yaml +20 -0
  247. package/dist/templates/knowledge/pdf-patients/PDF_Deid_Deidentification_0.pdf +0 -0
  248. package/dist/templates/knowledge/pdf-patients/PDF_Deid_Deidentification_1.pdf +0 -0
  249. package/dist/templates/knowledge/pdf-patients/PDF_Deid_Deidentification_10.pdf +0 -0
  250. package/dist/templates/knowledge/pdf-patients/PDF_Deid_Deidentification_11.pdf +0 -0
  251. package/dist/templates/knowledge/web-docs.knowledge.yaml +1 -1
  252. package/dist/templates/llm.json +73 -10
  253. package/dist/templates/skills/orcha-builder/SKILL.md +56 -3
  254. package/dist/templates/workflows/example.workflow.yaml +27 -35
  255. package/dist/templates/workflows/react-example.workflow.yaml +14 -19
  256. package/dist/templates/workflows/team-chat.workflow.yaml +47 -0
  257. package/package.json +14 -6
  258. package/dist/public/src/components/SkillsView.js +0 -137
  259. package/dist/public/src/components/WorkflowsView.js +0 -568
@@ -2,9 +2,14 @@
2
2
  import { Component } from '../utils/Component.js';
3
3
  import { api } from '../services/ApiService.js';
4
4
  import { sessionStore } from '../services/SessionStore.js';
5
+ import { streamManager } from '../services/StreamManager.js';
6
+ import { escapeHtml as sharedEscapeHtml } from '../utils/card.js';
5
7
  import { store } from '../store.js';
6
8
  import { markdownRenderer } from '../utils/markdown.js';
7
9
 
10
+ // Survives component remount (tab navigation) but not page refresh
11
+ const workflowTasks = new Map();
12
+
8
13
  export class AgentsView extends Component {
9
14
  constructor() {
10
15
  super();
@@ -14,20 +19,27 @@ export class AgentsView extends Component {
14
19
  this.streamTimerInterval = null;
15
20
  this.streamUsageData = null;
16
21
  this.pendingAttachments = [];
22
+ this._streamUnsubscribe = null;
17
23
  }
18
24
 
19
25
  async connectedCallback() {
20
26
  super.connectedCallback();
21
- await Promise.all([this.loadAgents(), this.loadLLMs()]);
27
+ await Promise.all([this.loadAgents(), this.loadLLMs(), this.loadWorkflows()]);
22
28
  this.restoreActiveSession();
23
29
  }
24
30
 
25
31
  disconnectedCallback() {
26
- this.stopStreamTimer();
27
- if (this.currentAbortController) {
28
- this.currentAbortController.abort();
29
- 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;
30
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)
31
43
  }
32
44
 
33
45
  formatElapsedTime(ms) {
@@ -44,13 +56,33 @@ export class AgentsView extends Component {
44
56
  }
45
57
 
46
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
+
47
71
  if (this.currentAbortController) {
48
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;
49
80
  }
81
+ streamManager.cancel(activeId);
50
82
  }
51
83
 
52
- startStreamTimer(responseId) {
53
- this.streamStartTime = Date.now();
84
+ startStreamTimer(responseId, startTime) {
85
+ this.streamStartTime = startTime || Date.now();
54
86
  this.streamTimerInterval = setInterval(() => {
55
87
  const elapsed = Date.now() - this.streamStartTime;
56
88
  const bubble = this.querySelector(`#${responseId}`);
@@ -78,7 +110,7 @@ export class AgentsView extends Component {
78
110
  const statusBar = wrapper.querySelector('.stream-status-bar');
79
111
  const statsBar = wrapper.querySelector('.stream-stats-bar');
80
112
 
81
- if (statusBar) statusBar.classList.add('hidden');
113
+ if (statusBar) statusBar.remove();
82
114
 
83
115
  if (statsBar) {
84
116
  const elapsedEl = statsBar.querySelector('.stats-elapsed');
@@ -107,12 +139,12 @@ export class AgentsView extends Component {
107
139
 
108
140
  if (wasCancelled) {
109
141
  const cancelBadge = document.createElement('span');
110
- cancelBadge.className = 'text-xs text-amber-400 font-medium ml-2';
142
+ cancelBadge.className = 'badge badge-amber';
111
143
  cancelBadge.textContent = 'Cancelled';
112
144
  statsBar.appendChild(cancelBadge);
113
145
  }
114
146
 
115
- statsBar.classList.remove('hidden');
147
+ statsBar.classList.add('visible');
116
148
  }
117
149
  }
118
150
 
@@ -128,13 +160,27 @@ export class AgentsView extends Component {
128
160
 
129
161
  async loadLLMs() {
130
162
  try {
131
- 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;
132
167
  store.set('llms', llms);
168
+ store.set('defaultLlmName', defaultLlmName);
133
169
  } catch (e) {
134
170
  console.error('Failed to load LLMs', e);
135
171
  }
136
172
  }
137
173
 
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
+
138
184
  // --- Sidebar toggle (mobile) ---
139
185
 
140
186
  _isMobile() {
@@ -147,13 +193,11 @@ export class AgentsView extends Component {
147
193
  if (!sidebar || !backdrop) return;
148
194
 
149
195
  if (show) {
150
- sidebar.classList.remove('hidden');
151
- sidebar.classList.add('flex', 'sidebar-open');
152
- backdrop.classList.remove('hidden');
196
+ sidebar.classList.add('open');
197
+ backdrop.classList.add('visible');
153
198
  } else {
154
- sidebar.classList.add('hidden');
155
- sidebar.classList.remove('flex', 'sidebar-open');
156
- backdrop.classList.add('hidden');
199
+ sidebar.classList.remove('open');
200
+ backdrop.classList.remove('visible');
157
201
  }
158
202
  }
159
203
 
@@ -177,30 +221,34 @@ export class AgentsView extends Component {
177
221
  const activeId = sessionStore.getActiveId();
178
222
 
179
223
  if (sessions.length === 0) {
180
- list.innerHTML = '<div class="text-gray-500 text-sm text-center py-8">No conversations yet</div>';
224
+ list.innerHTML = '<div class="text-muted text-sm text-center py-8">No conversations yet</div>';
181
225
  return;
182
226
  }
183
227
 
184
228
  list.innerHTML = sessions.map(s => {
185
229
  const isActive = s.id === activeId;
186
- const isAgent = s.agentType === 'agent';
187
- const displayName = isAgent ? (s.agentName || 'Agent') : (s.llmName || 'LLM');
188
- const icon = isAgent ? 'fa-robot' : 'fa-microchip';
189
-
190
- const activeClasses = isActive
191
- ? 'bg-dark-hover/80 border-l-2 border-l-blue-500'
192
- : 'hover:bg-dark-hover/40 border-l-2 border-l-transparent';
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
+ }
193
241
 
194
242
  return `
195
- <div data-session-id="${s.id}" class="session-item group flex items-start gap-2 px-3 py-2.5 cursor-pointer rounded-lg mb-0.5 transition-colors ${activeClasses}">
243
+ <div data-session-id="${s.id}" class="session-item${isActive ? ' active' : ''}">
196
244
  <div class="flex-1 min-w-0">
197
- <div class="text-sm ${isActive ? 'text-gray-100' : 'text-gray-300'} truncate">${this.escapeHtml(s.title)}</div>
198
- <div class="flex items-center gap-1.5 mt-0.5 text-xs text-gray-500">
199
- <i class="fas ${icon} text-[10px]"></i>
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>
200
248
  <span class="truncate">${this.escapeHtml(displayName)}</span>
201
249
  </div>
202
250
  </div>
203
- <button data-delete-id="${s.id}" class="session-delete-btn flex-shrink-0 text-gray-600 hover:text-red-400 opacity-0 group-hover:opacity-100 transition-opacity p-1 mt-0.5" title="Delete">
251
+ <button data-delete-id="${s.id}" class="session-delete-btn" title="Delete">
204
252
  <i class="fas fa-xmark text-xs"></i>
205
253
  </button>
206
254
  </div>
@@ -224,11 +272,17 @@ export class AgentsView extends Component {
224
272
  }
225
273
 
226
274
  switchToSession(sessionId) {
227
- // Abort any running stream
228
- if (this.currentAbortController) {
229
- this.currentAbortController.abort();
230
- this.currentAbortController = null;
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;
231
279
  }
280
+ if (this.streamTimerInterval) {
281
+ clearInterval(this.streamTimerInterval);
282
+ this.streamTimerInterval = null;
283
+ }
284
+ this.streamStartTime = null;
285
+ this.currentAbortController = null;
232
286
  this.isLoading = false;
233
287
 
234
288
  const session = sessionStore.get(sessionId);
@@ -240,21 +294,39 @@ export class AgentsView extends Component {
240
294
  const agents = store.get('agents') || [];
241
295
  const llms = store.get('llms') || [];
242
296
 
243
- if (session.agentType === 'agent') {
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') {
244
305
  const agent = agents.find(a => a.name === session.agentName);
245
306
  store.set('selectedAgent', agent || null);
246
307
  store.set('selectedLlm', null);
308
+ store.set('selectedWorkflow', null);
247
309
  store.set('selectionType', 'agent');
248
310
  } else {
249
311
  const llm = llms.find(l => l.name === session.llmName);
250
312
  store.set('selectedLlm', llm || null);
251
313
  store.set('selectedAgent', null);
314
+ store.set('selectedWorkflow', null);
252
315
  store.set('selectionType', 'llm');
253
316
  }
254
317
 
255
318
  this.restoreMessages(session);
256
319
  this.updateChatHeader(session);
257
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
+ }
329
+
258
330
  this.updateUiState();
259
331
 
260
332
  // Close sidebar on mobile after selecting
@@ -285,7 +357,7 @@ export class AgentsView extends Component {
285
357
  if (msg.role === 'user') {
286
358
  this.appendMessage('user', msg.content);
287
359
  } else {
288
- this.appendRestoredAssistantMessage(msg.content);
360
+ this.appendRestoredAssistantMessage(msg.content, msg.meta);
289
361
  }
290
362
  }
291
363
 
@@ -300,7 +372,7 @@ export class AgentsView extends Component {
300
372
  }
301
373
  }
302
374
 
303
- appendRestoredAssistantMessage(content) {
375
+ appendRestoredAssistantMessage(content, meta) {
304
376
  const container = this.querySelector('#chatMessages');
305
377
  const div = document.createElement('div');
306
378
  div.className = 'response-wrapper';
@@ -308,17 +380,83 @@ export class AgentsView extends Component {
308
380
  const bubble = document.createElement('div');
309
381
  bubble.className = 'flex justify-start';
310
382
  bubble.innerHTML = `
311
- <div class="response-bubble-inner max-w-4xl bg-dark-surface border border-dark-border rounded-3xl px-5 py-3 text-gray-100 text-[15px] leading-relaxed relative group">
383
+ <div class="response-bubble-inner group">
312
384
  <div class="response-content markdown-content"></div>
385
+ <div class="tool-invocations"></div>
313
386
  </div>
314
387
  `;
315
388
 
316
389
  const contentDiv = bubble.querySelector('.response-content');
317
- const rendered = markdownRenderer.render(content);
318
- contentDiv.innerHTML = rendered;
319
- markdownRenderer.highlightCode(contentDiv);
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
+ }
320
421
 
321
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
+
322
460
  container.appendChild(div);
323
461
  container.scrollTop = container.scrollHeight;
324
462
  }
@@ -329,49 +467,69 @@ export class AgentsView extends Component {
329
467
  if (existing) existing.remove();
330
468
 
331
469
  const agents = store.get('agents') || [];
470
+ const workflows = store.get('workflows') || [];
332
471
  const llms = store.get('llms') || [];
333
472
 
334
473
  const overlay = document.createElement('div');
335
474
  overlay.id = 'newSessionModal';
336
- overlay.className = 'modal-backdrop fixed inset-0 z-50 flex items-center justify-center bg-black/70';
475
+ overlay.className = 'modal-backdrop';
337
476
 
338
477
  let itemsHtml = '';
339
478
 
340
479
  if (agents.length > 0) {
341
- itemsHtml += '<div class="px-4 py-2 text-xs font-semibold text-gray-400 uppercase tracking-wider">Agents</div>';
480
+ itemsHtml += '<div class="modal-section-label">Agents</div>';
342
481
  itemsHtml += agents.map(a => `
343
- <button data-type="agent" data-name="${this.escapeHtml(a.name)}" class="modal-pick-item w-full text-left px-4 py-3 hover:bg-dark-hover cursor-pointer transition-colors border-b border-dark-border/50 flex items-center gap-3">
344
- <i class="fas fa-robot text-blue-400 text-sm"></i>
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>
345
484
  <div class="flex-1 min-w-0">
346
- <div class="text-sm font-medium text-gray-200">${this.escapeHtml(a.name)}</div>
347
- <div class="text-xs text-gray-500 truncate">${this.escapeHtml(a.description || '')}</div>
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>
348
487
  </div>
349
488
  </button>
350
489
  `).join('');
351
490
  }
352
491
 
353
- if (llms.length > 0) {
354
- itemsHtml += '<div class="px-4 py-2 text-xs font-semibold text-gray-400 uppercase tracking-wider">LLMs</div>';
355
- itemsHtml += llms.map(l => `
356
- <button data-type="llm" data-name="${this.escapeHtml(l.name)}" class="modal-pick-item w-full text-left px-4 py-3 hover:bg-dark-hover cursor-pointer transition-colors border-b border-dark-border/50 flex items-center gap-3">
357
- <i class="fas fa-microchip text-purple-400 text-sm"></i>
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>
358
497
  <div class="flex-1 min-w-0">
359
- <div class="text-sm font-medium text-gray-200">${this.escapeHtml(l.name)}</div>
360
- <div class="text-xs text-gray-500 truncate">${this.escapeHtml(l.model || '')}</div>
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>
361
500
  </div>
362
501
  </button>
363
502
  `).join('');
364
503
  }
365
504
 
505
+ if (llms.length > 0) {
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;
510
+ return `
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>' : ''}
517
+ </div>
518
+ <div class="text-xs text-muted truncate">${this.escapeHtml(l.model || '')}</div>
519
+ </div>
520
+ </button>
521
+ `}).join('');
522
+ }
523
+
366
524
  if (!itemsHtml) {
367
- itemsHtml = '<div class="text-gray-500 text-sm text-center py-8">No agents or LLMs available</div>';
525
+ itemsHtml = '<div class="text-muted text-sm text-center py-8">No agents, workflows or LLMs available</div>';
368
526
  }
369
527
 
370
528
  overlay.innerHTML = `
371
- <div class="modal-content bg-dark-surface border border-dark-border rounded-2xl shadow-2xl w-[420px] max-w-[90vw] max-h-[70vh] flex flex-col overflow-hidden">
372
- <div class="flex items-center justify-between px-5 py-4 border-b border-dark-border">
373
- <h3 class="text-lg font-semibold text-gray-100">New conversation</h3>
374
- <button id="closeNewSessionModal" class="text-gray-400 hover:text-gray-200 transition-colors p-1">
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">
375
533
  <i class="fas fa-xmark"></i>
376
534
  </button>
377
535
  </div>
@@ -399,7 +557,8 @@ export class AgentsView extends Component {
399
557
  const session = sessionStore.create({
400
558
  agentName: type === 'agent' ? name : null,
401
559
  agentType: type,
402
- llmName: type === 'llm' ? name : null
560
+ llmName: type === 'llm' ? name : null,
561
+ workflowName: type === 'workflow' ? name : null
403
562
  });
404
563
 
405
564
  overlay.remove();
@@ -408,6 +567,87 @@ export class AgentsView extends Component {
408
567
  });
409
568
  }
410
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');
646
+ }
647
+ }, 200);
648
+ });
649
+ }
650
+
411
651
  deleteSession(sessionId) {
412
652
  sessionStore.delete(sessionId);
413
653
  const activeId = sessionStore.getActiveId();
@@ -431,8 +671,8 @@ export class AgentsView extends Component {
431
671
  if (container) {
432
672
  container.innerHTML = `
433
673
  <div class="flex-1 flex items-center justify-center h-full">
434
- <div class="text-center text-gray-500">
435
- <i class="fas fa-comments text-4xl mb-4 text-gray-600"></i>
674
+ <div class="text-center text-muted">
675
+ <i class="fas fa-comments text-4xl mb-4 text-muted"></i>
436
676
  <p class="text-lg">Start a new conversation</p>
437
677
  <p class="text-sm mt-1">Click "New chat" to begin</p>
438
678
  </div>
@@ -458,15 +698,28 @@ export class AgentsView extends Component {
458
698
  if (!header) return;
459
699
 
460
700
  if (!session) {
461
- header.innerHTML = '<span class="text-gray-500">No conversation selected</span>';
701
+ header.innerHTML = '<span class="text-muted">No conversation selected</span>';
462
702
  return;
463
703
  }
464
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
+ }
465
722
  const isAgent = session.agentType === 'agent';
466
- const name = isAgent ? (session.agentName || 'Agent') : (session.llmName || 'LLM');
467
- const badgeText = isAgent ? 'Agent' : 'LLM';
468
- const badgeColor = isAgent ? 'bg-blue-500/20 text-blue-400' : 'bg-purple-500/20 text-purple-400';
469
- const icon = isAgent ? 'fa-robot' : 'fa-microchip';
470
723
 
471
724
  let extraBadges = '';
472
725
  if (isAgent) {
@@ -475,12 +728,12 @@ export class AgentsView extends Component {
475
728
  if (agent) {
476
729
  if (agent.publish?.enabled) {
477
730
  const chatUrl = `/chat/${encodeURIComponent(agent.name)}`;
478
- extraBadges += `<a href="${chatUrl}" target="_blank" class="text-xs px-2 py-0.5 rounded-full bg-green-500/20 text-green-400 hover:bg-green-500/30 transition-colors no-underline" title="Open published chat"><i class="fas fa-globe text-[10px]"></i> Published</a>`;
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>`;
479
732
  }
480
733
 
481
734
  const hasMemory = agent.memory === true || (agent.memory && agent.memory.enabled);
482
735
  if (hasMemory) {
483
- extraBadges += `<span class="text-xs px-2 py-0.5 rounded-full bg-amber-500/20 text-amber-400" title="Persistent memory enabled"><i class="fas fa-brain text-[10px]"></i> Memory</span>`;
736
+ extraBadges += `<span class="badge badge-pill badge-amber" title="Persistent memory enabled"><i class="fas fa-brain text-2xs"></i> Memory</span>`;
484
737
  }
485
738
 
486
739
  if (agent.tools?.length) {
@@ -488,7 +741,7 @@ export class AgentsView extends Component {
488
741
  const toolListHtml = toolNames.map(t => `<div class="tools-popover-item">${this.escapeHtml(t)}</div>`).join('');
489
742
  extraBadges += `
490
743
  <span class="tools-badge-wrapper">
491
- <span class="text-xs px-2 py-0.5 rounded-full bg-gray-500/20 text-gray-400 cursor-default"><i class="fas fa-wrench text-[10px]"></i> ${toolNames.length} tool${toolNames.length !== 1 ? 's' : ''}</span>
744
+ <span class="badge badge-pill badge-gray"><i class="fas fa-wrench text-2xs"></i> ${toolNames.length} tool${toolNames.length !== 1 ? 's' : ''}</span>
492
745
  <div class="tools-popover">${toolListHtml}</div>
493
746
  </span>`;
494
747
  }
@@ -497,9 +750,9 @@ export class AgentsView extends Component {
497
750
 
498
751
  header.innerHTML = `
499
752
  <div class="flex items-center gap-2 flex-wrap">
500
- <i class="fas ${icon} text-sm text-gray-400"></i>
501
- <span class="font-medium text-gray-200">${this.escapeHtml(name)}</span>
502
- <span class="text-xs px-2 py-0.5 rounded-full ${badgeColor}">${badgeText}</span>
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>
503
756
  ${extraBadges}
504
757
  </div>
505
758
  `;
@@ -555,22 +808,22 @@ export class AgentsView extends Component {
555
808
  if (!preview) return;
556
809
 
557
810
  if (this.pendingAttachments.length === 0) {
558
- preview.classList.add('hidden');
811
+ preview.classList.remove('visible');
559
812
  preview.innerHTML = '';
560
813
  return;
561
814
  }
562
815
 
563
- preview.classList.remove('hidden');
816
+ preview.classList.add('visible');
564
817
  preview.innerHTML = this.pendingAttachments.map((att, i) => {
565
818
  const isImage = att.mediaType.startsWith('image/');
566
819
  const thumb = isImage
567
- ? `<img src="data:${att.mediaType};base64,${att.data}" class="w-10 h-10 object-cover rounded">`
568
- : `<i class="fas fa-file text-gray-400 text-lg"></i>`;
820
+ ? `<img src="data:${att.mediaType};base64,${att.data}">`
821
+ : `<i class="fas fa-file text-secondary text-lg"></i>`;
569
822
  return `
570
- <div class="attachment-pill flex items-center gap-2 bg-dark-bg/60 border border-dark-border/50 rounded-lg px-2 py-1.5 text-xs text-gray-400">
823
+ <div class="attachment-pill">
571
824
  ${thumb}
572
- <span class="max-w-[120px] truncate">${this.escapeHtml(att.name)}</span>
573
- <button class="attachment-remove text-gray-500 hover:text-gray-300 ml-1" data-index="${i}">
825
+ <span class="truncate attachment-name">${this.escapeHtml(att.name)}</span>
826
+ <button class="attachment-remove" data-index="${i}">
574
827
  <i class="fas fa-xmark text-xs"></i>
575
828
  </button>
576
829
  </div>
@@ -601,15 +854,19 @@ export class AgentsView extends Component {
601
854
  const llm = store.get('selectedLlm');
602
855
  const activeId = sessionStore.getActiveId();
603
856
 
604
- const selected = selectionType === 'agent' ? agent : llm;
605
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
+ }
606
864
 
607
- if ((!message && !hasAttachments) || !selected || this.isLoading || !activeId) return;
865
+ const selected = selectionType === 'agent' ? agent : llm;
866
+ if (!selected) return;
608
867
 
609
- // Capture attachments before clearing
610
868
  const attachments = hasAttachments ? [...this.pendingAttachments] : null;
611
869
 
612
- // Add user message (with optional attachment thumbnails)
613
870
  this.appendMessage('user', message || '(attached files)', { attachments });
614
871
  sessionStore.addMessage(activeId, 'user', message || '(attached files)');
615
872
  input.value = '';
@@ -622,204 +879,864 @@ export class AgentsView extends Component {
622
879
  const responseId = 'response-' + Date.now();
623
880
  this.createResponseBubble(responseId);
624
881
 
625
- this.currentAbortController = new AbortController();
882
+ const abortController = new AbortController();
883
+ this.currentAbortController = abortController;
626
884
  this.streamUsageData = null;
627
885
  this.startStreamTimer(responseId);
628
886
 
629
- let finalContent = '';
630
- let wasCancelled = false;
631
-
632
887
  try {
888
+ let response;
633
889
  if (selectionType === 'agent') {
634
- finalContent = await this.sendAgentMessage(agent, message, responseId, attachments);
635
- } else if (selectionType === 'llm') {
636
- finalContent = await this.sendLlmMessage(llm, message, responseId, attachments);
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 });
637
897
  }
898
+
899
+ streamManager.start(activeId, {
900
+ response,
901
+ abortController,
902
+ streamType: selectionType === 'agent' ? 'agent' : 'llm',
903
+ inputMessage: message,
904
+ responseId,
905
+ });
906
+
907
+ this._attachToStream(activeId, responseId);
638
908
  } catch (e) {
639
- if (e.name === 'AbortError') {
640
- wasCancelled = true;
641
- } else {
909
+ const wasCancelled = e.name === 'AbortError';
910
+ if (!wasCancelled) {
642
911
  this.updateResponseError(responseId, `Error: ${e.message}`);
643
912
  }
644
- } finally {
645
- this.stopStreamTimer(responseId, message, finalContent, wasCancelled);
913
+ this.stopStreamTimer(responseId, message, '', wasCancelled);
646
914
  this.currentAbortController = null;
647
915
  this.isLoading = false;
648
916
  this.updateUiState();
649
- input.focus();
650
- }
651
-
652
- // Persist assistant response
653
- if (finalContent) {
654
- sessionStore.addMessage(activeId, 'assistant', finalContent);
655
917
  }
656
918
 
657
- // Re-render sidebar (title/ordering may have changed)
658
919
  this.renderSessionList();
659
920
  }
660
921
 
661
- async sendAgentMessage(agent, message, responseId, attachments) {
662
- const inputVars = agent.inputVariables || ['message'];
663
- const inputObj = {};
664
- inputObj[inputVars[0] || 'message'] = message;
665
- if (attachments) {
666
- inputObj.attachments = attachments;
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
+ }
667
950
  }
668
951
 
669
- const activeId = sessionStore.getActiveId();
670
- const res = await api.streamAgent(agent.name, inputObj, activeId, { signal: this.currentAbortController?.signal });
671
- const reader = res.body.getReader();
672
- const decoder = new TextDecoder();
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);
673
1051
 
674
- const bubble = this.querySelector(`#${responseId}`);
675
- const contentDiv = bubble.querySelector('.response-content');
676
- const container = this.querySelector('#chatMessages');
677
1052
  const thinkingState = {
678
1053
  inThinking: false,
679
1054
  thinkingSections: [],
680
- currentSection: null
1055
+ currentSection: null,
1056
+ thinkingContent: snapshotState.thinkingContent || '',
1057
+ thinkingPill: snapshotState.thinkingPill || null,
681
1058
  };
682
1059
 
683
- let currentContent = '';
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);
1222
+ } else {
1223
+ this.updateResponseError(responseId, `Error: ${e.message}`);
1224
+ this.stopStreamTimer(responseId, message, '', false);
1225
+ this.isLoading = false;
1226
+ this.updateUiState();
1227
+ }
1228
+ }
1229
+
1230
+ this.renderSessionList();
1231
+ }
1232
+
1233
+ async _respondToWorkflowInterrupt(sessionId, message) {
1234
+ const wfState = workflowTasks.get(sessionId);
1235
+ if (!wfState?.interruptState) return;
1236
+
1237
+ const { threadId, workflowName } = wfState.interruptState;
1238
+
1239
+ const input = this.querySelector('#chatInput');
1240
+ this.appendMessage('user', message);
1241
+ sessionStore.addMessage(sessionId, 'user', message);
1242
+ input.value = '';
1243
+ input.style.height = 'auto';
1244
+
1245
+ const responseId = 'response-' + Date.now();
1246
+ this.createResponseBubble(responseId);
1247
+
1248
+ const abortController = new AbortController();
1249
+ wfState.responseId = responseId;
1250
+ wfState.interruptState = null;
1251
+ wfState.status = 'streaming';
1252
+ wfState.abortController = abortController;
1253
+
1254
+ this.isLoading = true;
1255
+ this.updateUiState();
1256
+ this.startStreamTimer(responseId);
1257
+
1258
+ try {
1259
+ const response = await api.resumeWorkflowStream(workflowName, threadId, message, abortController.signal);
1260
+ await this._processWorkflowStream(response, sessionId, responseId);
1261
+ } catch (e) {
1262
+ if (e.name === 'AbortError') {
1263
+ this._finishWorkflowStream(sessionId, responseId, null, null, true);
1264
+ } else {
1265
+ this.updateResponseError(responseId, `Error: ${e.message}`);
1266
+ this.stopStreamTimer(responseId, message, '', false);
1267
+ this.isLoading = false;
1268
+ this.updateUiState();
1269
+ }
1270
+ }
1271
+
1272
+ this.renderSessionList();
1273
+ }
1274
+
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
+ }
1282
+
1283
+ const reader = response.body.getReader();
1284
+ const decoder = new TextDecoder();
684
1285
  let buffer = '';
685
- let hasToolCalls = false;
686
1286
 
687
1287
  while (true) {
688
1288
  const { done, value } = await reader.read();
689
1289
  if (done) break;
690
1290
 
691
- const chunk = decoder.decode(value, { stream: true });
692
- buffer += chunk;
693
-
1291
+ buffer += decoder.decode(value, { stream: true });
694
1292
  const lines = buffer.split('\n');
695
1293
  buffer = lines.pop() || '';
696
1294
 
697
1295
  for (const line of lines) {
698
- if (line.trim() === '') continue;
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);
1305
+ }
1306
+ }
1307
+ }
699
1308
 
700
- if (line.startsWith('data: ')) {
701
- const data = line.slice(6);
702
- if (data === '[DONE]') continue;
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 */ }
1317
+ }
1318
+ }
703
1319
 
704
- try {
705
- const event = JSON.parse(data);
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
+ }
1325
+ }
706
1326
 
707
- // Handle server-side errors
708
- if (event.error) {
709
- this.updateResponseError(responseId, `Error: ${event.error}`);
710
- return currentContent;
711
- }
1327
+ _handleWorkflowStreamEvent(update, sessionId, responseId) {
1328
+ const wfState = workflowTasks.get(sessionId);
1329
+ if (!wfState) return;
712
1330
 
713
- if (event.type === 'content') {
714
- currentContent += event.content;
715
- }
716
- if (event.type === 'tool_start' || event.type === 'tool_end') {
717
- hasToolCalls = true;
718
- }
719
- this.handleStreamEvent(event, responseId, currentContent, thinkingState);
720
- } catch (e) {
721
- console.error('Error parsing stream event', e, data);
1331
+ // Capture task ID from server
1332
+ if (update.type === 'task_id') {
1333
+ wfState.taskId = update.taskId;
1334
+ return;
1335
+ }
1336
+
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) {
1367
+ const contentDiv = bubble.querySelector('.response-content');
1368
+
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);
722
1449
  }
723
1450
  }
724
1451
  }
725
1452
  }
726
1453
 
727
- // Finalize any remaining thinking pill
728
- const toolsDiv = bubble.querySelector('.tool-invocations');
729
- this.finalizeThinkingPill(toolsDiv, thinkingState);
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')) {
1476
+ loadingDots.remove();
1477
+ bubble.querySelector('.response-bubble-inner')?.classList.remove('loading');
1478
+ contentDiv.classList.remove('flex', 'items-center', 'whitespace-pre-wrap');
1479
+ contentDiv.innerHTML = '';
1480
+ }
1481
+
1482
+ const container = this.querySelector('#chatMessages');
1483
+ if (container) container.scrollTop = container.scrollHeight;
1484
+ }
1485
+
1486
+ _handleWorkflowInterrupt(sessionId, responseId, interruptData) {
1487
+ const wfState = workflowTasks.get(sessionId);
1488
+ if (!wfState) return;
730
1489
 
731
- // If tools were called but no text content was produced, clear loading state
732
- if (hasToolCalls && !currentContent.trim()) {
733
- const loadingDots = contentDiv.querySelector('.loading-dots');
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
+ };
1497
+
1498
+ const bubble = this.querySelector(`#${responseId}`);
1499
+ if (bubble) {
1500
+ const contentDiv = bubble.querySelector('.response-content');
1501
+ const loadingDots = contentDiv?.querySelector('.loading-dots');
734
1502
  if (loadingDots) {
735
1503
  loadingDots.remove();
736
- bubble.querySelector('.response-bubble-inner').classList.remove('py-4');
737
- bubble.querySelector('.response-bubble-inner').classList.add('py-3');
1504
+ bubble.querySelector('.response-bubble-inner')?.classList.remove('loading');
738
1505
  contentDiv.classList.remove('flex', 'items-center', 'whitespace-pre-wrap');
739
1506
  contentDiv.innerHTML = '';
740
1507
  }
1508
+
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
+ }
1522
+
1523
+ sessionStore.addMessage(sessionId, 'assistant', question);
1524
+
1525
+ if (this.streamTimerInterval) {
1526
+ clearInterval(this.streamTimerInterval);
1527
+ this.streamTimerInterval = null;
741
1528
  }
1529
+ this.isLoading = false;
1530
+ this.updateUiState();
742
1531
 
743
- return currentContent;
1532
+ const input = this.querySelector('#chatInput');
1533
+ if (input) input.focus();
744
1534
  }
745
1535
 
746
- async sendLlmMessage(llm, message, responseId, attachments) {
747
- const activeId = sessionStore.getActiveId();
748
- const res = await api.streamLLM(llm.name, message, activeId, attachments, { signal: this.currentAbortController?.signal });
749
- const reader = res.body.getReader();
750
- const decoder = new TextDecoder();
1536
+ _finishWorkflowStream(sessionId, responseId, result, error, wasCancelled) {
1537
+ const wfState = workflowTasks.get(sessionId);
1538
+ if (!wfState) return;
1539
+
1540
+ wfState.status = 'done';
1541
+ wfState.abortController = null;
751
1542
 
752
1543
  const bubble = this.querySelector(`#${responseId}`);
753
- const contentDiv = bubble.querySelector('.response-content');
754
- const loadingDots = contentDiv.querySelector('.loading-dots');
755
- const container = this.querySelector('#chatMessages');
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
+ }
756
1553
 
757
- if (loadingDots) {
758
- loadingDots.remove();
759
- bubble.querySelector('.response-bubble-inner').classList.remove('py-4');
760
- bubble.querySelector('.response-bubble-inner').classList.add('py-3');
761
- contentDiv.classList.remove('flex', 'items-center', 'whitespace-pre-wrap');
762
- contentDiv.innerHTML = '';
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
+ }
763
1564
  }
764
1565
 
765
- let buffer = '';
766
- let fullContent = '';
767
- const thinkingState = {
768
- inThinking: false,
769
- thinkingSections: [],
770
- currentSection: null
771
- };
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```';
1574
+ }
1575
+ }
772
1576
 
773
- while (true) {
774
- const { done, value } = await reader.read();
775
- if (done) break;
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);
776
1592
 
777
- const chunk = decoder.decode(value, { stream: true });
778
- buffer += chunk;
1593
+ this.stopStreamTimer(responseId, wfState.inputMessage || '', content, wasCancelled);
1594
+ this.isLoading = false;
1595
+ this.updateUiState();
1596
+ this.renderSessionList();
779
1597
 
780
- const lines = buffer.split('\n');
781
- buffer = lines.pop() || '';
1598
+ setTimeout(() => workflowTasks.delete(sessionId), 10000);
782
1599
 
783
- for (const line of lines) {
784
- if (line.trim() === '') continue;
1600
+ const input = this.querySelector('#chatInput');
1601
+ if (input) input.focus();
1602
+ }
785
1603
 
786
- if (line.startsWith('data: ')) {
787
- const data = line.slice(6);
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
+ }
788
1615
 
789
- if (data === '[DONE]') continue;
1616
+ _reconnectWorkflowStream(sessionId) {
1617
+ const wfState = workflowTasks.get(sessionId);
1618
+ if (!wfState || wfState.status === 'done') return false;
790
1619
 
791
- try {
792
- const parsed = JSON.parse(data);
1620
+ if (wfState.interruptState) {
1621
+ this.isLoading = false;
1622
+ this.updateUiState();
1623
+ return true;
1624
+ }
793
1625
 
794
- if (parsed.error) {
795
- this.updateResponseError(responseId, `Error: ${parsed.error}`);
796
- return fullContent;
797
- }
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);
1633
+ }
1634
+ }
798
1635
 
799
- if (parsed.type === 'usage') {
800
- this.streamUsageData = {
801
- input_tokens: parsed.input_tokens || 0,
802
- output_tokens: parsed.output_tokens || 0,
803
- total_tokens: parsed.total_tokens || 0,
804
- };
805
- continue;
806
- }
1636
+ this.startStreamTimer(wfState.responseId, wfState.startTime);
1637
+ this.isLoading = true;
1638
+ this.currentAbortController = null;
1639
+ this.updateUiState();
807
1640
 
808
- const text = parsed.content || '';
1641
+ return true;
1642
+ }
809
1643
 
810
- if (text) {
811
- fullContent += text;
812
- this.renderLlmContentStreaming(contentDiv, fullContent, responseId, thinkingState);
813
- container.scrollTop = container.scrollHeight;
814
- }
815
- } catch (e) {
816
- console.error('Error parsing stream chunk:', e, data);
817
- }
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';
1661
+ } else {
1662
+ detailsEl.style.left = '0';
1663
+ detailsEl.style.right = 'auto';
818
1664
  }
819
1665
  }
1666
+ });
1667
+ document.addEventListener('click', (e) => {
1668
+ if (!pillEl.contains(e.target)) detailsEl.classList.remove('visible');
1669
+ }, { capture: true });
1670
+ }
1671
+
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
+ }
1698
+
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);
820
1725
  }
821
1726
 
822
- return fullContent;
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);
823
1740
  }
824
1741
 
825
1742
  renderLlmContentStreaming(contentDiv, fullContent, responseId, state) {
@@ -846,9 +1763,9 @@ export class AgentsView extends Component {
846
1763
  // Create pill on first thinking chunk
847
1764
  if (!thinkingState.thinkingPill) {
848
1765
  const pill = document.createElement('div');
849
- pill.className = 'thinking-pill 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-purple-400 font-mono';
1766
+ pill.className = 'tool-pill thinking';
850
1767
  pill.innerHTML = `
851
- <i class="fas fa-brain animate-pulse text-[10px]"></i>
1768
+ <i class="fas fa-brain animate-pulse text-2xs"></i>
852
1769
  <span>Thinking...</span>
853
1770
  `;
854
1771
  toolsDiv.appendChild(pill);
@@ -858,6 +1775,7 @@ export class AgentsView extends Component {
858
1775
  }
859
1776
 
860
1777
  finalizeThinkingPill(toolsDiv, thinkingState) {
1778
+ const container = this.querySelector('#chatMessages');
861
1779
  const pill = thinkingState.thinkingPill;
862
1780
  if (!pill) return;
863
1781
 
@@ -865,40 +1783,28 @@ export class AgentsView extends Component {
865
1783
  thinkingState.thinkingPill = null;
866
1784
  thinkingState.thinkingContent = '';
867
1785
 
868
- pill.className = 'thinking-pill 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';
1786
+ pill.className = 'tool-pill done thinking';
869
1787
  pill.innerHTML = '';
870
1788
 
871
1789
  const pillContent = document.createElement('span');
872
- pillContent.className = 'inline-flex items-center gap-1.5';
873
- pillContent.innerHTML = `
874
- <i class="fas fa-brain text-purple-400 text-[10px]"></i>
875
- <span>Thinking</span>
876
- `;
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>';
877
1792
  pill.appendChild(pillContent);
878
1793
 
879
- const popover = document.createElement('div');
880
- popover.className = 'hidden fixed z-50 bg-dark-surface border border-dark-border rounded-lg shadow-xl w-[400px] max-w-[90vw] p-3';
881
-
882
- const popoverContent = document.createElement('div');
883
- popoverContent.className = 'text-xs text-gray-400 max-h-64 overflow-y-auto markdown-content custom-scrollbar';
884
- popoverContent.innerHTML = markdownRenderer.render(content);
885
- markdownRenderer.highlightCode(popoverContent);
886
- popover.appendChild(popoverContent);
887
- pill.appendChild(popover);
888
-
889
- pill.addEventListener('mouseenter', () => {
890
- popover.classList.remove('hidden');
891
- const pillRect = pill.getBoundingClientRect();
892
- popover.style.bottom = (window.innerHeight - pillRect.top + 4) + 'px';
893
- popover.style.top = 'auto';
894
- if (pillRect.left + 400 > window.innerWidth - 16) {
895
- popover.style.left = Math.max(8, pillRect.right - 400) + 'px';
896
- } else {
897
- popover.style.left = pillRect.left + 'px';
898
- }
899
- popover.style.right = 'auto';
900
- });
901
- pill.addEventListener('mouseleave', () => popover.classList.add('hidden'));
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);
902
1808
  }
903
1809
 
904
1810
  createResponseBubble(id) {
@@ -910,54 +1816,52 @@ export class AgentsView extends Component {
910
1816
  div.id = id;
911
1817
  div.className = 'flex justify-start';
912
1818
  div.innerHTML = `
913
- <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">
914
1820
  <div class="response-content whitespace-pre-wrap flex items-center">
915
- <div class="loading-dots flex gap-1">
916
- <div class="w-2 h-2 bg-blue-500 rounded-full animate-bounce"></div>
917
- <div class="w-2 h-2 bg-blue-500 rounded-full animate-bounce animation-delay-200"></div>
918
- <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>
919
1825
  </div>
920
1826
  </div>
921
- <div class="tool-invocations flex flex-wrap gap-1.5 mt-2"></div>
1827
+ <div class="tool-invocations"></div>
922
1828
  </div>
923
1829
  `;
924
1830
 
925
1831
  wrapper.appendChild(div);
926
1832
 
927
1833
  const statusBar = document.createElement('div');
928
- 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';
929
1835
  statusBar.innerHTML = `
930
- <div class="w-2 h-2 bg-blue-500 rounded-full animate-pulse-dot"></div>
1836
+ <div class="status-dot-pulse"></div>
931
1837
  <span class="stream-status-text">Generating...</span>
932
- <span class="stream-elapsed text-gray-500">0.0s</span>
933
- <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">
934
- Stop
935
- </button>
1838
+ <span class="stream-elapsed text-muted">0.0s</span>
1839
+ <button class="stream-cancel-btn">Stop</button>
936
1840
  `;
937
1841
  wrapper.appendChild(statusBar);
938
1842
 
939
1843
  statusBar.querySelector('.stream-cancel-btn').addEventListener('click', () => this.cancelCurrentStream());
940
1844
 
941
1845
  const statsBar = document.createElement('div');
942
- 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';
943
1847
  statsBar.innerHTML = `
944
1848
  <span class="flex items-center gap-1">
945
1849
  <i class="far fa-clock"></i>
946
1850
  <span class="stats-elapsed"></span>
947
1851
  </span>
948
- <span class="text-dark-border">|</span>
1852
+ <span class="divider">|</span>
949
1853
  <span class="flex items-center gap-1">
950
- <i class="fas fa-arrow-up text-[9px]"></i>
1854
+ <i class="fas fa-arrow-up text-2xs"></i>
951
1855
  <span class="stats-input-tokens"></span>
952
1856
  </span>
953
- <span class="text-dark-border">|</span>
1857
+ <span class="divider">|</span>
954
1858
  <span class="flex items-center gap-1">
955
- <i class="fas fa-arrow-down text-[9px]"></i>
1859
+ <i class="fas fa-arrow-down text-2xs"></i>
956
1860
  <span class="stats-output-tokens"></span>
957
1861
  </span>
958
- <span class="text-dark-border">|</span>
1862
+ <span class="divider">|</span>
959
1863
  <span class="flex items-center gap-1">
960
- <i class="fas fa-bolt text-[9px]"></i>
1864
+ <i class="fas fa-bolt text-2xs"></i>
961
1865
  <span class="stats-tps"></span>
962
1866
  </span>
963
1867
  `;
@@ -983,8 +1887,7 @@ export class AgentsView extends Component {
983
1887
  this.finalizeThinkingPill(toolsDiv, thinkingState);
984
1888
  if (loadingDots) {
985
1889
  loadingDots.remove();
986
- bubble.querySelector('.response-bubble-inner').classList.remove('py-4');
987
- bubble.querySelector('.response-bubble-inner').classList.add('py-3');
1890
+ bubble.querySelector('.response-bubble-inner').classList.remove('loading');
988
1891
  contentDiv.classList.remove('flex', 'items-center', 'whitespace-pre-wrap');
989
1892
  contentDiv.innerHTML = '';
990
1893
  }
@@ -995,10 +1898,10 @@ export class AgentsView extends Component {
995
1898
  const toolId = `tool-${event.runId}`;
996
1899
  const toolEl = document.createElement('div');
997
1900
  toolEl.id = toolId;
998
- 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';
999
1902
  toolEl.dataset.toolInput = typeof event.input === 'string' ? event.input : JSON.stringify(event.input, null, 2);
1000
1903
  toolEl.innerHTML = `
1001
- <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>
1002
1905
  <span>${this.escapeHtml(event.tool)}</span>
1003
1906
  `;
1004
1907
  toolsDiv.appendChild(toolEl);
@@ -1011,71 +1914,43 @@ export class AgentsView extends Component {
1011
1914
  const toolInput = toolEl.dataset.toolInput || '';
1012
1915
  const toolOutput = typeof event.output === 'string' ? event.output : JSON.stringify(event.output, null, 2);
1013
1916
 
1014
- 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';
1015
1918
  toolEl.innerHTML = '';
1016
1919
 
1017
1920
  const pillContent = document.createElement('span');
1018
- pillContent.className = 'inline-flex items-center gap-1.5';
1921
+ pillContent.className = 'inline-flex items-center gap-1';
1019
1922
  pillContent.innerHTML = `
1020
- <i class="fas fa-check text-green-500 text-[10px]"></i>
1923
+ <i class="fas fa-check text-green text-2xs"></i>
1021
1924
  <span>${this.escapeHtml(event.tool)}</span>
1022
1925
  `;
1023
1926
  toolEl.appendChild(pillContent);
1024
1927
 
1025
1928
  const details = document.createElement('div');
1026
- details.className = 'tool-invocation-details hidden absolute bottom-full mb-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';
1027
1930
 
1028
1931
  if (toolInput) {
1029
1932
  const inputSection = document.createElement('div');
1030
- inputSection.className = 'p-3 border-b border-dark-border/50';
1031
- 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>';
1032
1935
  const inputPre = document.createElement('pre');
1033
- 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';
1034
1937
  inputPre.textContent = toolInput;
1035
1938
  inputSection.appendChild(inputPre);
1036
1939
  details.appendChild(inputSection);
1037
1940
  }
1038
1941
 
1039
1942
  const outputSection = document.createElement('div');
1040
- outputSection.className = 'p-3';
1041
- 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>';
1042
1945
  const outputPre = document.createElement('pre');
1043
- 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';
1044
1947
  outputPre.textContent = toolOutput;
1045
1948
  outputSection.appendChild(outputPre);
1046
1949
  details.appendChild(outputSection);
1047
1950
 
1048
1951
  toolEl.appendChild(details);
1049
1952
 
1050
- toolEl.addEventListener('click', (e) => {
1051
- if (details.contains(e.target)) return;
1052
- e.preventDefault();
1053
- e.stopPropagation();
1054
- toolsDiv.querySelectorAll('.tool-invocation-details:not(.hidden)').forEach(d => {
1055
- if (d !== details) d.classList.add('hidden');
1056
- });
1057
- const wasHidden = details.classList.contains('hidden');
1058
- details.classList.toggle('hidden');
1059
- if (wasHidden) {
1060
- const pillRect = toolEl.getBoundingClientRect();
1061
- const containerRect = container.getBoundingClientRect();
1062
- const spaceRight = containerRect.right - pillRect.left;
1063
- if (spaceRight < 420) {
1064
- details.style.right = '0';
1065
- details.style.left = 'auto';
1066
- } else {
1067
- details.style.left = '0';
1068
- details.style.right = 'auto';
1069
- }
1070
- }
1071
- });
1072
-
1073
- const closeHandler = (e) => {
1074
- if (!toolEl.contains(e.target)) {
1075
- details.classList.add('hidden');
1076
- }
1077
- };
1078
- document.addEventListener('click', closeHandler, { capture: true });
1953
+ this._attachClickDetails(toolEl, details, toolsDiv, container);
1079
1954
  container.scrollTop = container.scrollHeight;
1080
1955
 
1081
1956
  if (event.tool === 'workspace_write' || event.tool === 'workspace_delete') {
@@ -1088,17 +1963,16 @@ export class AgentsView extends Component {
1088
1963
  } else if (event.type === 'result') {
1089
1964
  if (loadingDots) {
1090
1965
  loadingDots.remove();
1091
- bubble.querySelector('.response-bubble-inner').classList.remove('py-4');
1092
- bubble.querySelector('.response-bubble-inner').classList.add('py-3');
1966
+ bubble.querySelector('.response-bubble-inner').classList.remove('loading');
1093
1967
  contentDiv.classList.remove('flex', 'items-center', 'whitespace-pre-wrap');
1094
1968
  contentDiv.innerHTML = '';
1095
1969
  }
1096
1970
 
1097
1971
  const resultContainer = document.createElement('div');
1098
- resultContainer.className = 'bg-dark-bg/50 border border-dark-border rounded-lg p-4';
1972
+ resultContainer.className = 'panel';
1099
1973
 
1100
1974
  const resultPre = document.createElement('pre');
1101
- 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';
1102
1976
  resultPre.textContent = JSON.stringify(event.output, null, 2);
1103
1977
 
1104
1978
  resultContainer.appendChild(resultPre);
@@ -1108,15 +1982,20 @@ export class AgentsView extends Component {
1108
1982
  } else if (event.type === 'error') {
1109
1983
  if (loadingDots) {
1110
1984
  loadingDots.remove();
1111
- bubble.querySelector('.response-bubble-inner').classList.remove('py-4');
1112
- bubble.querySelector('.response-bubble-inner').classList.add('py-3');
1985
+ bubble.querySelector('.response-bubble-inner').classList.remove('loading');
1113
1986
  contentDiv.classList.remove('flex', 'items-center', 'whitespace-pre-wrap');
1114
1987
  }
1115
1988
  const errorDiv = document.createElement('div');
1116
- errorDiv.className = 'text-red-400 text-sm mt-2';
1989
+ errorDiv.className = 'text-red text-sm';
1117
1990
  errorDiv.textContent = `Error: ${event.error}`;
1118
1991
  contentDiv.appendChild(errorDiv);
1119
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;
1120
1999
  } else if (event.type === 'usage') {
1121
2000
  this.streamUsageData = {
1122
2001
  input_tokens: event.input_tokens || 0,
@@ -1137,7 +2016,7 @@ export class AgentsView extends Component {
1137
2016
  const bubble = this.querySelector(`#${id}`);
1138
2017
  if (bubble) {
1139
2018
  const content = bubble.querySelector('.response-content');
1140
- content.innerHTML = `<span class="text-red-400">${errorMsg}</span>`;
2019
+ content.innerHTML = `<span class="text-red">${errorMsg}</span>`;
1141
2020
  }
1142
2021
  }
1143
2022
 
@@ -1161,30 +2040,29 @@ export class AgentsView extends Component {
1161
2040
  const div = document.createElement('div');
1162
2041
  div.className = isUser ? 'flex justify-end' : 'flex justify-start';
1163
2042
 
1164
- const bubbleColor = isUser ? 'bg-dark-surface' : (hasError ? 'bg-red-900/20 border-red-900/30' : 'bg-dark-surface');
1165
- const textColor = hasError ? 'text-red-300' : 'text-gray-100';
2043
+ const bubbleClass = isUser ? 'user-bubble' : (hasError ? 'response-bubble-inner error' : 'response-bubble-inner');
1166
2044
 
1167
2045
  // Build attachment thumbnails for user messages
1168
2046
  let attachmentHtml = '';
1169
2047
  if (isUser && metadata.attachments && metadata.attachments.length > 0) {
1170
2048
  const thumbs = metadata.attachments.map(att => {
1171
2049
  if (att.mediaType.startsWith('image/')) {
1172
- return `<img src="data:${att.mediaType};base64,${att.data}" class="w-16 h-16 object-cover rounded-lg border border-dark-border/50">`;
2050
+ return `<img src="data:${att.mediaType};base64,${att.data}" class="attachment-thumb">`;
1173
2051
  }
1174
- return `<div class="flex items-center gap-1.5 bg-dark-bg/60 border border-dark-border/50 rounded-lg px-2 py-1.5 text-xs text-gray-400">
2052
+ return `<div class="attachment-pill">
1175
2053
  <i class="fas fa-file"></i>
1176
- <span class="max-w-[100px] truncate">${this.escapeHtml(att.name)}</span>
2054
+ <span class="truncate attachment-name">${this.escapeHtml(att.name)}</span>
1177
2055
  </div>`;
1178
2056
  }).join('');
1179
2057
  attachmentHtml = `<div class="flex flex-wrap gap-2 mb-2">${thumbs}</div>`;
1180
2058
  }
1181
2059
 
1182
2060
  div.innerHTML = `
1183
- <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">
1184
2062
  ${attachmentHtml}
1185
2063
  <div class="whitespace-pre-wrap">${this.escapeHtml(content)}</div>
1186
2064
  ${!isUser && !hasError ? `
1187
- <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">
1188
2066
  <i class="far fa-copy"></i>
1189
2067
  </button>
1190
2068
  ` : ''}
@@ -1211,11 +2089,11 @@ export class AgentsView extends Component {
1211
2089
  div.id = id;
1212
2090
  div.className = 'flex justify-start';
1213
2091
  div.innerHTML = `
1214
- <div class="max-w-4xl bg-dark-surface border border-dark-border rounded-3xl px-5 py-4">
1215
- <div class="flex gap-1">
1216
- <div class="w-2 h-2 bg-blue-500 rounded-full animate-bounce"></div>
1217
- <div class="w-2 h-2 bg-blue-500 rounded-full animate-bounce animation-delay-200"></div>
1218
- <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>
1219
2097
  </div>
1220
2098
  </div>
1221
2099
  `;
@@ -1230,9 +2108,7 @@ export class AgentsView extends Component {
1230
2108
  }
1231
2109
 
1232
2110
  escapeHtml(text) {
1233
- const div = document.createElement('div');
1234
- div.textContent = text;
1235
- return div.innerHTML;
2111
+ return sharedEscapeHtml(text);
1236
2112
  }
1237
2113
 
1238
2114
  _getRandomWelcomeMessage() {
@@ -1307,15 +2183,16 @@ export class AgentsView extends Component {
1307
2183
 
1308
2184
  _renderSampleQuestionChips() {
1309
2185
  const agent = store.get('selectedAgent');
1310
- const questions = agent?.sampleQuestions;
2186
+ const workflow = store.get('selectedWorkflow');
2187
+ const questions = agent?.sampleQuestions || workflow?.sampleQuestions;
1311
2188
  if (!questions || questions.length === 0) return '';
1312
2189
 
1313
2190
  const chips = questions.map(q =>
1314
- `<button class="sample-question-chip bg-dark-surface border border-dark-border/60 hover:border-gray-500 text-gray-300 text-sm px-4 py-2 rounded-2xl transition-colors text-left">${this.escapeHtml(q)}</button>`
2191
+ `<button class="sample-question-chip">${this.escapeHtml(q)}</button>`
1315
2192
  ).join('');
1316
2193
 
1317
2194
  return `
1318
- <div class="flex flex-wrap justify-center gap-2 max-w-2xl mt-4">${chips}</div>
2195
+ <div class="sample-questions-wrap">${chips}</div>
1319
2196
  `;
1320
2197
  }
1321
2198
 
@@ -1368,6 +2245,9 @@ export class AgentsView extends Component {
1368
2245
  // New chat button
1369
2246
  this.querySelector('#newChatBtn').addEventListener('click', () => this.showNewSessionModal());
1370
2247
 
2248
+ // New agent button
2249
+ this.querySelector('#newAgentBtn').addEventListener('click', () => this.showNewAgentModal());
2250
+
1371
2251
  // Mobile sidebar toggle
1372
2252
  this.querySelector('#sidebarToggleBtn').addEventListener('click', () => this.toggleSidebar(true));
1373
2253
  this.querySelector('#sidebarBackdrop').addEventListener('click', () => this.toggleSidebar(false));
@@ -1375,57 +2255,58 @@ export class AgentsView extends Component {
1375
2255
 
1376
2256
  template() {
1377
2257
  return `
1378
- <div class="flex h-full relative border border-dark-border rounded-xl overflow-hidden bg-dark-surface/30">
2258
+ <div class="agent-shell">
1379
2259
  <!-- Mobile sidebar backdrop -->
1380
- <div id="sidebarBackdrop" class="hidden fixed inset-0 bg-black/50 z-30 md:hidden"></div>
2260
+ <div id="sidebarBackdrop" class="sidebar-backdrop"></div>
1381
2261
 
1382
2262
  <!-- Sidebar -->
1383
- <div id="sidebar" class="hidden md:flex w-64 flex-shrink-0 bg-dark-bg md:bg-dark-bg/60 border-r border-dark-border/60 flex-col
1384
- fixed md:relative inset-y-0 left-0 z-40 md:z-auto">
2263
+ <div id="sidebar" class="agent-sidebar">
1385
2264
  <div class="p-3">
1386
- <button id="newChatBtn" class="w-full flex items-center justify-center gap-2 px-3 py-2.5 hover:bg-dark-hover rounded-lg text-sm font-medium text-gray-300 transition-colors">
1387
- <i class="fas fa-plus text-xs text-blue-400"></i>
2265
+ <button id="newChatBtn" class="new-chat-btn">
2266
+ <i class="fas fa-plus text-xs text-accent"></i>
1388
2267
  <span>New chat</span>
1389
2268
  </button>
1390
2269
  </div>
1391
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>
2276
+ </div>
1392
2277
  </div>
1393
2278
 
1394
2279
  <!-- Chat Area -->
1395
- <div class="flex-1 flex flex-col min-w-0">
2280
+ <div class="chat-area">
1396
2281
  <!-- Chat Header -->
1397
- <div class="flex-shrink-0 flex items-center gap-2 px-3 md:px-5 py-3 border-b border-dark-border/40 text-sm">
1398
- <button id="sidebarToggleBtn" class="md:hidden text-gray-400 hover:text-gray-200 p-1 -ml-1">
2282
+ <div class="chat-header">
2283
+ <button id="sidebarToggleBtn" class="sidebar-toggle-btn">
1399
2284
  <i class="fas fa-bars"></i>
1400
2285
  </button>
1401
2286
  <div id="chatHeader" class="flex-1 min-w-0">
1402
- <span class="text-gray-500">No conversation selected</span>
2287
+ <span class="text-muted">No conversation selected</span>
1403
2288
  </div>
1404
2289
  </div>
1405
2290
 
1406
2291
  <!-- Chat Messages -->
1407
- <div id="chatMessages" class="flex-1 overflow-y-auto space-y-4 p-4 pr-2 pb-6 custom-scrollbar"></div>
2292
+ <div id="chatMessages" class="chat-messages custom-scrollbar"></div>
1408
2293
 
1409
2294
  <!-- Input Area -->
1410
- <div class="p-3 pt-0">
1411
- <div id="attachmentPreview" class="hidden flex flex-wrap gap-2 px-2 pb-2"></div>
1412
- <div class="relative bg-dark-surface border border-dark-border/60 rounded-2xl focus-within:border-gray-500 transition-colors">
1413
- <input type="file" id="fileInput" multiple accept="image/*,.pdf" class="hidden">
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">
1414
2299
  <textarea id="chatInput" rows="1" readonly
1415
- class="w-full bg-transparent pl-11 pr-14 py-3 text-gray-100 placeholder-gray-500 resize-none focus:outline-none max-h-[200px] cursor-pointer"
1416
2300
  placeholder="Ask anything"></textarea>
1417
2301
 
1418
- <div class="absolute bottom-2 left-2 flex items-center">
1419
- <button id="attachBtn" type="button"
1420
- class="text-gray-500 hover:text-gray-300 p-1.5 rounded-lg hover:bg-dark-hover transition-colors"
1421
- title="Attach files">
2302
+ <div class="chat-input-actions left">
2303
+ <button id="attachBtn" type="button" class="attach-btn" title="Attach files">
1422
2304
  <i class="fas fa-plus text-sm"></i>
1423
2305
  </button>
1424
2306
  </div>
1425
2307
 
1426
- <div class="absolute bottom-2 right-2 flex items-center gap-2">
1427
- <button id="sendMessageBtn" disabled
1428
- 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">
2308
+ <div class="chat-input-actions right">
2309
+ <button id="sendMessageBtn" disabled class="send-btn">
1429
2310
  <i class="fas fa-paper-plane text-sm"></i>
1430
2311
  </button>
1431
2312
  </div>