agent-orcha 0.0.5 → 0.0.7

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 (264) hide show
  1. package/README.md +194 -1277
  2. package/dist/lib/agents/agent-executor.d.ts +4 -2
  3. package/dist/lib/agents/agent-executor.d.ts.map +1 -1
  4. package/dist/lib/agents/agent-executor.js +68 -52
  5. package/dist/lib/agents/agent-executor.js.map +1 -1
  6. package/dist/lib/agents/agent-loader.d.ts +3 -0
  7. package/dist/lib/agents/agent-loader.d.ts.map +1 -1
  8. package/dist/lib/agents/agent-loader.js +10 -1
  9. package/dist/lib/agents/agent-loader.js.map +1 -1
  10. package/dist/lib/agents/react-loop.d.ts.map +1 -1
  11. package/dist/lib/agents/react-loop.js +180 -142
  12. package/dist/lib/agents/react-loop.js.map +1 -1
  13. package/dist/lib/agents/types.d.ts +181 -18
  14. package/dist/lib/agents/types.d.ts.map +1 -1
  15. package/dist/lib/agents/types.js +18 -2
  16. package/dist/lib/agents/types.js.map +1 -1
  17. package/dist/lib/functions/function-loader.d.ts +2 -0
  18. package/dist/lib/functions/function-loader.d.ts.map +1 -1
  19. package/dist/lib/functions/function-loader.js +10 -0
  20. package/dist/lib/functions/function-loader.js.map +1 -1
  21. package/dist/lib/integrations/email.d.ts +38 -0
  22. package/dist/lib/integrations/email.d.ts.map +1 -0
  23. package/dist/lib/integrations/email.js +249 -0
  24. package/dist/lib/integrations/email.js.map +1 -0
  25. package/dist/lib/integrations/integration-manager.d.ts +5 -0
  26. package/dist/lib/integrations/integration-manager.d.ts.map +1 -1
  27. package/dist/lib/integrations/integration-manager.js +53 -3
  28. package/dist/lib/integrations/integration-manager.js.map +1 -1
  29. package/dist/lib/integrations/types.d.ts +187 -4
  30. package/dist/lib/integrations/types.d.ts.map +1 -1
  31. package/dist/lib/integrations/types.js +24 -1
  32. package/dist/lib/integrations/types.js.map +1 -1
  33. package/dist/lib/knowledge/knowledge-store.d.ts +6 -0
  34. package/dist/lib/knowledge/knowledge-store.d.ts.map +1 -1
  35. package/dist/lib/knowledge/knowledge-store.js +71 -4
  36. package/dist/lib/knowledge/knowledge-store.js.map +1 -1
  37. package/dist/lib/knowledge/loaders/file-loaders.d.ts +8 -2
  38. package/dist/lib/knowledge/loaders/file-loaders.d.ts.map +1 -1
  39. package/dist/lib/knowledge/loaders/file-loaders.js +89 -60
  40. package/dist/lib/knowledge/loaders/file-loaders.js.map +1 -1
  41. package/dist/lib/knowledge/loaders/web-loader.d.ts +12 -3
  42. package/dist/lib/knowledge/loaders/web-loader.d.ts.map +1 -1
  43. package/dist/lib/knowledge/loaders/web-loader.js +56 -22
  44. package/dist/lib/knowledge/loaders/web-loader.js.map +1 -1
  45. package/dist/lib/knowledge/types.d.ts +56 -20
  46. package/dist/lib/knowledge/types.d.ts.map +1 -1
  47. package/dist/lib/knowledge/types.js +18 -3
  48. package/dist/lib/knowledge/types.js.map +1 -1
  49. package/dist/lib/llm/llm-call-logger.d.ts +3 -1
  50. package/dist/lib/llm/llm-call-logger.d.ts.map +1 -1
  51. package/dist/lib/llm/llm-call-logger.js +31 -26
  52. package/dist/lib/llm/llm-call-logger.js.map +1 -1
  53. package/dist/lib/llm/llm-config.d.ts +8 -0
  54. package/dist/lib/llm/llm-config.d.ts.map +1 -1
  55. package/dist/lib/llm/llm-config.js +3 -1
  56. package/dist/lib/llm/llm-config.js.map +1 -1
  57. package/dist/lib/llm/llm-factory.d.ts.map +1 -1
  58. package/dist/lib/llm/llm-factory.js +3 -0
  59. package/dist/lib/llm/llm-factory.js.map +1 -1
  60. package/dist/lib/llm/providers/anthropic-chat-model.d.ts +5 -1
  61. package/dist/lib/llm/providers/anthropic-chat-model.d.ts.map +1 -1
  62. package/dist/lib/llm/providers/anthropic-chat-model.js +118 -42
  63. package/dist/lib/llm/providers/anthropic-chat-model.js.map +1 -1
  64. package/dist/lib/llm/providers/gemini-chat-model.d.ts +3 -2
  65. package/dist/lib/llm/providers/gemini-chat-model.d.ts.map +1 -1
  66. package/dist/lib/llm/providers/gemini-chat-model.js +83 -24
  67. package/dist/lib/llm/providers/gemini-chat-model.js.map +1 -1
  68. package/dist/lib/llm/providers/openai-chat-model.d.ts +10 -1
  69. package/dist/lib/llm/providers/openai-chat-model.d.ts.map +1 -1
  70. package/dist/lib/llm/providers/openai-chat-model.js +233 -32
  71. package/dist/lib/llm/providers/openai-chat-model.js.map +1 -1
  72. package/dist/lib/logger.d.ts.map +1 -1
  73. package/dist/lib/logger.js +0 -1
  74. package/dist/lib/logger.js.map +1 -1
  75. package/dist/lib/mcp/mcp-client.d.ts.map +1 -1
  76. package/dist/lib/mcp/mcp-client.js +5 -3
  77. package/dist/lib/mcp/mcp-client.js.map +1 -1
  78. package/dist/lib/mcp/types.d.ts +0 -9
  79. package/dist/lib/mcp/types.d.ts.map +1 -1
  80. package/dist/lib/mcp/types.js +1 -2
  81. package/dist/lib/mcp/types.js.map +1 -1
  82. package/dist/lib/memory/memory-manager.d.ts +1 -0
  83. package/dist/lib/memory/memory-manager.d.ts.map +1 -1
  84. package/dist/lib/memory/memory-manager.js +9 -0
  85. package/dist/lib/memory/memory-manager.js.map +1 -1
  86. package/dist/lib/orchestrator.d.ts +2 -8
  87. package/dist/lib/orchestrator.d.ts.map +1 -1
  88. package/dist/lib/orchestrator.js +96 -3
  89. package/dist/lib/orchestrator.js.map +1 -1
  90. package/dist/lib/sandbox/cdp-client.d.ts +14 -0
  91. package/dist/lib/sandbox/cdp-client.d.ts.map +1 -0
  92. package/dist/lib/sandbox/cdp-client.js +113 -0
  93. package/dist/lib/sandbox/cdp-client.js.map +1 -0
  94. package/dist/lib/sandbox/html-to-markdown.d.ts +9 -1
  95. package/dist/lib/sandbox/html-to-markdown.d.ts.map +1 -1
  96. package/dist/lib/sandbox/html-to-markdown.js +67 -10
  97. package/dist/lib/sandbox/html-to-markdown.js.map +1 -1
  98. package/dist/lib/sandbox/index.d.ts +5 -0
  99. package/dist/lib/sandbox/index.d.ts.map +1 -1
  100. package/dist/lib/sandbox/index.js +4 -0
  101. package/dist/lib/sandbox/index.js.map +1 -1
  102. package/dist/lib/sandbox/page-readiness.d.ts +37 -0
  103. package/dist/lib/sandbox/page-readiness.d.ts.map +1 -0
  104. package/dist/lib/sandbox/page-readiness.js +235 -0
  105. package/dist/lib/sandbox/page-readiness.js.map +1 -0
  106. package/dist/lib/sandbox/sandbox-browser.d.ts +4 -0
  107. package/dist/lib/sandbox/sandbox-browser.d.ts.map +1 -0
  108. package/dist/lib/sandbox/sandbox-browser.js +303 -0
  109. package/dist/lib/sandbox/sandbox-browser.js.map +1 -0
  110. package/dist/lib/sandbox/sandbox-file.d.ts +4 -0
  111. package/dist/lib/sandbox/sandbox-file.d.ts.map +1 -0
  112. package/dist/lib/sandbox/sandbox-file.js +168 -0
  113. package/dist/lib/sandbox/sandbox-file.js.map +1 -0
  114. package/dist/lib/sandbox/sandbox-shell.d.ts +4 -0
  115. package/dist/lib/sandbox/sandbox-shell.d.ts.map +1 -0
  116. package/dist/lib/sandbox/sandbox-shell.js +93 -0
  117. package/dist/lib/sandbox/sandbox-shell.js.map +1 -0
  118. package/dist/lib/sandbox/sandbox-web.d.ts.map +1 -1
  119. package/dist/lib/sandbox/sandbox-web.js +37 -22
  120. package/dist/lib/sandbox/sandbox-web.js.map +1 -1
  121. package/dist/lib/sandbox/types.d.ts +9 -0
  122. package/dist/lib/sandbox/types.d.ts.map +1 -1
  123. package/dist/lib/sandbox/types.js +1 -0
  124. package/dist/lib/sandbox/types.js.map +1 -1
  125. package/dist/lib/sandbox/vision-browser.d.ts +4 -0
  126. package/dist/lib/sandbox/vision-browser.d.ts.map +1 -0
  127. package/dist/lib/sandbox/vision-browser.js +289 -0
  128. package/dist/lib/sandbox/vision-browser.js.map +1 -0
  129. package/dist/lib/skills/skill-loader.d.ts +2 -0
  130. package/dist/lib/skills/skill-loader.d.ts.map +1 -1
  131. package/dist/lib/skills/skill-loader.js +12 -1
  132. package/dist/lib/skills/skill-loader.js.map +1 -1
  133. package/dist/lib/tasks/task-manager.d.ts +3 -1
  134. package/dist/lib/tasks/task-manager.d.ts.map +1 -1
  135. package/dist/lib/tasks/task-manager.js +11 -0
  136. package/dist/lib/tasks/task-manager.js.map +1 -1
  137. package/dist/lib/tasks/task-store.d.ts +1 -1
  138. package/dist/lib/tasks/task-store.d.ts.map +1 -1
  139. package/dist/lib/tasks/task-store.js.map +1 -1
  140. package/dist/lib/tasks/types.d.ts +18 -0
  141. package/dist/lib/tasks/types.d.ts.map +1 -1
  142. package/dist/lib/tools/built-in/integration-tools.d.ts +4 -0
  143. package/dist/lib/tools/built-in/integration-tools.d.ts.map +1 -0
  144. package/dist/lib/tools/built-in/integration-tools.js +47 -0
  145. package/dist/lib/tools/built-in/integration-tools.js.map +1 -0
  146. package/dist/lib/tools/built-in/knowledge-entity-lookup.tool.d.ts.map +1 -1
  147. package/dist/lib/tools/built-in/knowledge-entity-lookup.tool.js +12 -6
  148. package/dist/lib/tools/built-in/knowledge-entity-lookup.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 +4 -3
  151. package/dist/lib/tools/built-in/knowledge-sql.tool.js.map +1 -1
  152. package/dist/lib/tools/built-in/query-validators.d.ts.map +1 -1
  153. package/dist/lib/tools/built-in/query-validators.js +4 -0
  154. package/dist/lib/tools/built-in/query-validators.js.map +1 -1
  155. package/dist/lib/tools/workspace/workspace-tools.d.ts +1 -0
  156. package/dist/lib/tools/workspace/workspace-tools.d.ts.map +1 -1
  157. package/dist/lib/tools/workspace/workspace-tools.js +39 -0
  158. package/dist/lib/tools/workspace/workspace-tools.js.map +1 -1
  159. package/dist/lib/triggers/cron-trigger.d.ts +1 -1
  160. package/dist/lib/triggers/cron-trigger.d.ts.map +1 -1
  161. package/dist/lib/triggers/cron-trigger.js.map +1 -1
  162. package/dist/lib/triggers/trigger-manager.d.ts +1 -0
  163. package/dist/lib/triggers/trigger-manager.d.ts.map +1 -1
  164. package/dist/lib/triggers/trigger-manager.js +26 -0
  165. package/dist/lib/triggers/trigger-manager.js.map +1 -1
  166. package/dist/lib/triggers/webhook-trigger.d.ts +1 -1
  167. package/dist/lib/triggers/webhook-trigger.d.ts.map +1 -1
  168. package/dist/lib/triggers/webhook-trigger.js.map +1 -1
  169. package/dist/lib/types/llm-types.d.ts +22 -4
  170. package/dist/lib/types/llm-types.d.ts.map +1 -1
  171. package/dist/lib/types/llm-types.js +50 -0
  172. package/dist/lib/types/llm-types.js.map +1 -1
  173. package/dist/lib/types/tool-factory.d.ts +2 -2
  174. package/dist/lib/types/tool-factory.d.ts.map +1 -1
  175. package/dist/lib/types/tool-factory.js.map +1 -1
  176. package/dist/lib/utils/env-substitution.d.ts +6 -0
  177. package/dist/lib/utils/env-substitution.d.ts.map +1 -0
  178. package/dist/lib/utils/env-substitution.js +15 -0
  179. package/dist/lib/utils/env-substitution.js.map +1 -0
  180. package/dist/lib/workflows/react-workflow-executor.js +3 -3
  181. package/dist/lib/workflows/react-workflow-executor.js.map +1 -1
  182. package/dist/lib/workflows/types.d.ts +10 -10
  183. package/dist/lib/workflows/workflow-loader.d.ts +3 -0
  184. package/dist/lib/workflows/workflow-loader.d.ts.map +1 -1
  185. package/dist/lib/workflows/workflow-loader.js +10 -1
  186. package/dist/lib/workflows/workflow-loader.js.map +1 -1
  187. package/dist/public/chat.html +114 -0
  188. package/dist/public/index.html +157 -0
  189. package/dist/public/src/components/AgentComposer.js +807 -0
  190. package/dist/public/src/components/AgentsView.js +740 -317
  191. package/dist/public/src/components/AppRoot.js +30 -5
  192. package/dist/public/src/components/GraphView.js +372 -288
  193. package/dist/public/src/components/IdeView.js +163 -7
  194. package/dist/public/src/components/MonitorView.js +139 -1
  195. package/dist/public/src/components/StandaloneChat.js +889 -0
  196. package/dist/public/src/components/WorkflowsView.js +180 -28
  197. package/dist/public/src/services/ApiService.js +7 -2
  198. package/dist/public/src/services/SessionStore.js +83 -0
  199. package/dist/public/src/store.js +0 -2
  200. package/dist/public/src/utils/markdown.js +13 -0
  201. package/dist/src/cli/index.js +7 -4
  202. package/dist/src/cli/index.js.map +1 -1
  203. package/dist/src/middleware/auth.d.ts.map +1 -1
  204. package/dist/src/middleware/auth.js +28 -6
  205. package/dist/src/middleware/auth.js.map +1 -1
  206. package/dist/src/middleware/rate-limit.d.ts +8 -0
  207. package/dist/src/middleware/rate-limit.d.ts.map +1 -0
  208. package/dist/src/middleware/rate-limit.js +21 -0
  209. package/dist/src/middleware/rate-limit.js.map +1 -0
  210. package/dist/src/routes/agents.route.d.ts.map +1 -1
  211. package/dist/src/routes/agents.route.js +136 -10
  212. package/dist/src/routes/agents.route.js.map +1 -1
  213. package/dist/src/routes/chat.route.d.ts +3 -0
  214. package/dist/src/routes/chat.route.d.ts.map +1 -0
  215. package/dist/src/routes/chat.route.js +155 -0
  216. package/dist/src/routes/chat.route.js.map +1 -0
  217. package/dist/src/routes/files.route.d.ts.map +1 -1
  218. package/dist/src/routes/files.route.js +37 -2
  219. package/dist/src/routes/files.route.js.map +1 -1
  220. package/dist/src/routes/llm.route.d.ts.map +1 -1
  221. package/dist/src/routes/llm.route.js +40 -5
  222. package/dist/src/routes/llm.route.js.map +1 -1
  223. package/dist/src/routes/tasks.route.d.ts.map +1 -1
  224. package/dist/src/routes/tasks.route.js +15 -1
  225. package/dist/src/routes/tasks.route.js.map +1 -1
  226. package/dist/src/routes/vnc.route.d.ts +3 -0
  227. package/dist/src/routes/vnc.route.d.ts.map +1 -0
  228. package/dist/src/routes/vnc.route.js +49 -0
  229. package/dist/src/routes/vnc.route.js.map +1 -0
  230. package/dist/src/server.d.ts.map +1 -1
  231. package/dist/src/server.js +5 -1
  232. package/dist/src/server.js.map +1 -1
  233. package/dist/templates/Demo.md +152 -0
  234. package/dist/templates/README.md +12 -3
  235. package/dist/templates/agents/architect.agent.yaml +20 -12
  236. package/dist/templates/agents/chatbot.agent.yaml +23 -26
  237. package/dist/templates/agents/corporate.agent.yaml +65 -0
  238. package/dist/templates/agents/investment-analyst.agent.yaml +80 -0
  239. package/dist/templates/agents/music-librarian.agent.yaml +70 -0
  240. package/dist/templates/agents/network-security.agent.yaml +82 -0
  241. package/dist/templates/agents/transport-security.agent.yaml +70 -0
  242. package/dist/templates/agents/web-engineer.agent.yaml +99 -0
  243. package/dist/templates/agents/web-pilot.agent.yaml +58 -0
  244. package/dist/templates/knowledge/music-store/LICENSE.md +11 -0
  245. package/dist/templates/knowledge/music-store/musicstore.sqlite +0 -0
  246. package/dist/templates/knowledge/music-store/tables.png +0 -0
  247. package/dist/templates/knowledge/music-store.knowledge.yaml +138 -0
  248. package/dist/templates/knowledge/org-chart/personnel.csv +21 -21
  249. package/dist/templates/knowledge/org-chart.knowledge.yaml +4 -0
  250. package/dist/templates/knowledge/pet-store.knowledge.yaml +3 -0
  251. package/dist/templates/knowledge/security-incidents/incidents.json +55935 -0
  252. package/dist/templates/knowledge/security-incidents.knowledge.yaml +46 -0
  253. package/dist/templates/knowledge/{example.knowledge.yaml → transcripts.knowledge.yaml} +9 -5
  254. package/dist/templates/knowledge/transport-ot/systems.csv +117 -0
  255. package/dist/templates/knowledge/transport-ot.knowledge.yaml +55 -0
  256. package/dist/templates/llm.json +7 -30
  257. package/dist/templates/mcp.json +7 -4
  258. package/dist/templates/skills/orcha-builder/SKILL.md +106 -226
  259. package/dist/templates/skills/pii-guard/SKILL.md +22 -0
  260. package/dist/templates/skills/sandbox/SKILL.md +25 -48
  261. package/dist/templates/skills/web-pilot/SKILL.md +51 -0
  262. package/package.json +8 -3
  263. package/dist/templates/agents/knowledge-broker.agent.yaml +0 -39
  264. package/dist/templates/agents/sandbox.agent.yaml +0 -56
@@ -1,6 +1,7 @@
1
1
 
2
2
  import { Component } from '../utils/Component.js';
3
3
  import { api } from '../services/ApiService.js';
4
+ import { sessionStore } from '../services/SessionStore.js';
4
5
  import { store } from '../store.js';
5
6
  import { markdownRenderer } from '../utils/markdown.js';
6
7
 
@@ -12,11 +13,13 @@ export class AgentsView extends Component {
12
13
  this.streamStartTime = null;
13
14
  this.streamTimerInterval = null;
14
15
  this.streamUsageData = null;
16
+ this.pendingAttachments = [];
15
17
  }
16
18
 
17
19
  async connectedCallback() {
18
20
  super.connectedCallback();
19
21
  await Promise.all([this.loadAgents(), this.loadLLMs()]);
22
+ this.restoreActiveSession();
20
23
  }
21
24
 
22
25
  disconnectedCallback() {
@@ -116,15 +119,8 @@ export class AgentsView extends Component {
116
119
  async loadAgents() {
117
120
  try {
118
121
  const agents = await api.getAgents();
122
+ agents.sort((a, b) => (a.name || '').localeCompare(b.name || ''));
119
123
  store.set('agents', agents);
120
-
121
- if (agents.length > 0 && !store.get('selectedAgent') && !store.get('selectedLlm')) {
122
- store.set('selectedAgent', agents[agents.length - 1]);
123
- store.set('selectionType', 'agent');
124
- }
125
-
126
- this.renderAgentDropdown();
127
- this.updateSelectedAgentUI();
128
124
  } catch (e) {
129
125
  console.error('Failed to load agents', e);
130
126
  }
@@ -134,152 +130,491 @@ export class AgentsView extends Component {
134
130
  try {
135
131
  const llms = await api.getLLMs();
136
132
  store.set('llms', llms);
137
- this.renderAgentDropdown();
138
133
  } catch (e) {
139
134
  console.error('Failed to load LLMs', e);
140
135
  }
141
136
  }
142
137
 
143
- renderAgentDropdown() {
144
- const list = this.querySelector('#agentDropdownList');
138
+ // --- Sidebar toggle (mobile) ---
139
+
140
+ _isMobile() {
141
+ return !window.matchMedia('(min-width: 768px)').matches;
142
+ }
143
+
144
+ toggleSidebar(show) {
145
+ const sidebar = this.querySelector('#sidebar');
146
+ const backdrop = this.querySelector('#sidebarBackdrop');
147
+ if (!sidebar || !backdrop) return;
148
+
149
+ if (show) {
150
+ sidebar.classList.remove('hidden');
151
+ sidebar.classList.add('flex', 'sidebar-open');
152
+ backdrop.classList.remove('hidden');
153
+ } else {
154
+ sidebar.classList.add('hidden');
155
+ sidebar.classList.remove('flex', 'sidebar-open');
156
+ backdrop.classList.add('hidden');
157
+ }
158
+ }
159
+
160
+ // --- Session management ---
161
+
162
+ restoreActiveSession() {
163
+ const activeId = sessionStore.getActiveId();
164
+ if (activeId && sessionStore.get(activeId)) {
165
+ this.switchToSession(activeId);
166
+ } else {
167
+ this.showEmptyState();
168
+ }
169
+ this.renderSessionList();
170
+ }
171
+
172
+ renderSessionList() {
173
+ const list = this.querySelector('#sessionList');
174
+ if (!list) return;
175
+
176
+ const sessions = sessionStore.getAll();
177
+ const activeId = sessionStore.getActiveId();
178
+
179
+ if (sessions.length === 0) {
180
+ list.innerHTML = '<div class="text-gray-500 text-sm text-center py-8">No conversations yet</div>';
181
+ return;
182
+ }
183
+
184
+ list.innerHTML = sessions.map(s => {
185
+ 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';
193
+
194
+ 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}">
196
+ <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>
200
+ <span class="truncate">${this.escapeHtml(displayName)}</span>
201
+ </div>
202
+ </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">
204
+ <i class="fas fa-xmark text-xs"></i>
205
+ </button>
206
+ </div>
207
+ `;
208
+ }).join('');
209
+
210
+ // Event listeners
211
+ list.querySelectorAll('.session-item').forEach(item => {
212
+ item.addEventListener('click', (e) => {
213
+ if (e.target.closest('.session-delete-btn')) return;
214
+ this.switchToSession(item.dataset.sessionId);
215
+ });
216
+ });
217
+
218
+ list.querySelectorAll('.session-delete-btn').forEach(btn => {
219
+ btn.addEventListener('click', (e) => {
220
+ e.stopPropagation();
221
+ this.deleteSession(btn.dataset.deleteId);
222
+ });
223
+ });
224
+ }
225
+
226
+ switchToSession(sessionId) {
227
+ // Abort any running stream
228
+ if (this.currentAbortController) {
229
+ this.currentAbortController.abort();
230
+ this.currentAbortController = null;
231
+ }
232
+ this.isLoading = false;
233
+
234
+ const session = sessionStore.get(sessionId);
235
+ if (!session) return;
236
+
237
+ sessionStore.setActiveId(sessionId);
238
+
239
+ // Update store with this session's agent/LLM
145
240
  const agents = store.get('agents') || [];
146
241
  const llms = store.get('llms') || [];
147
- const selectionType = store.get('selectionType');
148
- const selectedAgent = store.get('selectedAgent');
149
- const selectedLlm = store.get('selectedLlm');
150
242
 
151
- if (!list) return;
243
+ if (session.agentType === 'agent') {
244
+ const agent = agents.find(a => a.name === session.agentName);
245
+ store.set('selectedAgent', agent || null);
246
+ store.set('selectedLlm', null);
247
+ store.set('selectionType', 'agent');
248
+ } else {
249
+ const llm = llms.find(l => l.name === session.llmName);
250
+ store.set('selectedLlm', llm || null);
251
+ store.set('selectedAgent', null);
252
+ store.set('selectionType', 'llm');
253
+ }
254
+
255
+ this.restoreMessages(session);
256
+ this.updateChatHeader(session);
257
+ this.renderSessionList();
258
+ this.updateUiState();
259
+
260
+ // Close sidebar on mobile after selecting
261
+ if (this._isMobile()) {
262
+ this.toggleSidebar(false);
263
+ }
152
264
 
153
- if (agents.length === 0 && llms.length === 0) {
154
- list.innerHTML = '<div class="text-gray-500 text-sm text-center py-4">No agents or LLMs available</div>';
265
+ const input = this.querySelector('#chatInput');
266
+ if (input) {
267
+ input.disabled = false;
268
+ input.readOnly = false;
269
+ input.classList.remove('cursor-pointer');
270
+ input.focus();
271
+ }
272
+ }
273
+
274
+ async restoreMessages(session) {
275
+ const container = this.querySelector('#chatMessages');
276
+ if (!container) return;
277
+ container.innerHTML = '';
278
+
279
+ if (session.messages.length === 0) {
280
+ this._appendWelcomeMessage(container);
155
281
  return;
156
282
  }
157
283
 
158
- let html = '';
284
+ for (const msg of session.messages) {
285
+ if (msg.role === 'user') {
286
+ this.appendMessage('user', msg.content);
287
+ } else {
288
+ this.appendRestoredAssistantMessage(msg.content);
289
+ }
290
+ }
291
+
292
+ // Check if server still has this session (survives restarts)
293
+ try {
294
+ const exists = await api.checkSession(session.id);
295
+ if (!exists) {
296
+ this._appendSessionResetBanner(container);
297
+ }
298
+ } catch {
299
+ // Server unreachable — skip banner
300
+ }
301
+ }
302
+
303
+ appendRestoredAssistantMessage(content) {
304
+ const container = this.querySelector('#chatMessages');
305
+ const div = document.createElement('div');
306
+ div.className = 'response-wrapper';
307
+
308
+ const bubble = document.createElement('div');
309
+ bubble.className = 'flex justify-start';
310
+ 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">
312
+ <div class="response-content markdown-content"></div>
313
+ </div>
314
+ `;
315
+
316
+ const contentDiv = bubble.querySelector('.response-content');
317
+ const rendered = markdownRenderer.render(content);
318
+ contentDiv.innerHTML = rendered;
319
+ markdownRenderer.highlightCode(contentDiv);
320
+
321
+ div.appendChild(bubble);
322
+ container.appendChild(div);
323
+ container.scrollTop = container.scrollHeight;
324
+ }
325
+
326
+ showNewSessionModal() {
327
+ // Remove existing modal if any
328
+ const existing = document.querySelector('#newSessionModal');
329
+ if (existing) existing.remove();
330
+
331
+ const agents = store.get('agents') || [];
332
+ const llms = store.get('llms') || [];
333
+
334
+ const overlay = document.createElement('div');
335
+ overlay.id = 'newSessionModal';
336
+ overlay.className = 'modal-backdrop fixed inset-0 z-50 flex items-center justify-center bg-black/70';
337
+
338
+ let itemsHtml = '';
159
339
 
160
- // Agents section
161
340
  if (agents.length > 0) {
162
- html += '<div class="px-4 py-2 text-xs font-semibold text-gray-400 uppercase tracking-wider bg-dark-bg/50">Agents</div>';
163
- html += agents.map(agent => {
164
- const isSelected = selectionType === 'agent' && selectedAgent?.name === agent.name;
165
- return `
166
- <div data-type="agent" data-name="${agent.name}" class="selection-item px-4 py-3 hover:bg-dark-hover cursor-pointer transition-colors border-b border-dark-border ${isSelected ? 'bg-dark-hover' : ''}">
167
- <div class="flex items-start justify-between">
168
- <div class="flex-1">
169
- <div class="font-medium text-gray-200 mb-0.5">${agent.name}</div>
170
- <div class="text-xs text-gray-500 line-clamp-2">${agent.description}</div>
171
- </div>
172
- ${isSelected ? '<i class="fas fa-check text-blue-400 ml-2 mt-1"></i>' : ''}
173
- </div>
341
+ itemsHtml += '<div class="px-4 py-2 text-xs font-semibold text-gray-400 uppercase tracking-wider">Agents</div>';
342
+ 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>
345
+ <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>
174
348
  </div>
175
- `;
176
- }).join('');
349
+ </button>
350
+ `).join('');
177
351
  }
178
352
 
179
- // LLMs section
180
353
  if (llms.length > 0) {
181
- html += '<div class="px-4 py-2 text-xs font-semibold text-gray-400 uppercase tracking-wider bg-dark-bg/50">LLMs</div>';
182
- html += llms.map(llm => {
183
- const isSelected = selectionType === 'llm' && selectedLlm?.name === llm.name;
184
- return `
185
- <div data-type="llm" data-name="${llm.name}" class="selection-item px-4 py-3 hover:bg-dark-hover cursor-pointer transition-colors border-b border-dark-border last:border-b-0 ${isSelected ? 'bg-dark-hover' : ''}">
186
- <div class="flex items-start justify-between">
187
- <div class="flex-1">
188
- <div class="font-medium text-gray-200 mb-0.5">${llm.name}</div>
189
- <div class="text-xs text-gray-500 line-clamp-2">${llm.model}</div>
190
- </div>
191
- ${isSelected ? '<i class="fas fa-check text-blue-400 ml-2 mt-1"></i>' : ''}
192
- </div>
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>
358
+ <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>
193
361
  </div>
194
- `;
195
- }).join('');
362
+ </button>
363
+ `).join('');
364
+ }
365
+
366
+ if (!itemsHtml) {
367
+ itemsHtml = '<div class="text-gray-500 text-sm text-center py-8">No agents or LLMs available</div>';
196
368
  }
197
369
 
198
- list.innerHTML = html;
370
+ 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">
375
+ <i class="fas fa-xmark"></i>
376
+ </button>
377
+ </div>
378
+ <div class="overflow-y-auto custom-scrollbar flex-1">
379
+ ${itemsHtml}
380
+ </div>
381
+ </div>
382
+ `;
383
+
384
+ document.body.appendChild(overlay);
385
+
386
+ // Close on backdrop click
387
+ overlay.addEventListener('click', (e) => {
388
+ if (e.target === overlay) overlay.remove();
389
+ });
199
390
 
200
- list.querySelectorAll('.selection-item').forEach(item => {
391
+ overlay.querySelector('#closeNewSessionModal').addEventListener('click', () => overlay.remove());
392
+
393
+ // Pick handler
394
+ overlay.querySelectorAll('.modal-pick-item').forEach(item => {
201
395
  item.addEventListener('click', () => {
202
396
  const type = item.dataset.type;
203
397
  const name = item.dataset.name;
204
398
 
205
- // Check if we're actually switching to a different agent/llm
206
- const currentType = store.get('selectionType');
207
- const currentAgent = store.get('selectedAgent');
208
- const currentLlm = store.get('selectedLlm');
209
- const currentName = currentType === 'agent' ? currentAgent?.name : currentLlm?.name;
210
-
211
- const isSwitching = !(type === currentType && name === currentName);
212
-
213
- if (type === 'agent') {
214
- const agent = agents.find(a => a.name === name);
215
- store.set('selectedAgent', agent);
216
- store.set('selectedLlm', null);
217
- store.set('selectionType', 'agent');
218
- } else if (type === 'llm') {
219
- const llm = llms.find(l => l.name === name);
220
- store.set('selectedLlm', llm);
221
- store.set('selectedAgent', null);
222
- store.set('selectionType', 'llm');
223
- }
224
-
225
- // Clear chat history when switching
226
- if (isSwitching) {
227
- this.clearChatHistory();
228
- }
399
+ const session = sessionStore.create({
400
+ agentName: type === 'agent' ? name : null,
401
+ agentType: type,
402
+ llmName: type === 'llm' ? name : null
403
+ });
229
404
 
230
- this.updateSelectedAgentUI();
231
- this.toggleDropdown(false);
232
- this.renderAgentDropdown(); // Re-render to update selection checkmark
405
+ overlay.remove();
406
+ this.switchToSession(session.id);
233
407
  });
234
408
  });
235
409
  }
236
410
 
237
- updateSelectedAgentUI() {
238
- const selectionType = store.get('selectionType');
239
- const agent = store.get('selectedAgent');
240
- const llm = store.get('selectedLlm');
241
- const nameEl = this.querySelector('#selectedAgentName');
411
+ deleteSession(sessionId) {
412
+ sessionStore.delete(sessionId);
413
+ const activeId = sessionStore.getActiveId();
414
+
415
+ if (!activeId || activeId === sessionId) {
416
+ // Switch to most recent remaining session
417
+ const sessions = sessionStore.getAll();
418
+ if (sessions.length > 0) {
419
+ this.switchToSession(sessions[0].id);
420
+ } else {
421
+ this.showEmptyState();
422
+ this.renderSessionList();
423
+ }
424
+ } else {
425
+ this.renderSessionList();
426
+ }
427
+ }
428
+
429
+ showEmptyState() {
430
+ const container = this.querySelector('#chatMessages');
431
+ if (container) {
432
+ container.innerHTML = `
433
+ <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>
436
+ <p class="text-lg">Start a new conversation</p>
437
+ <p class="text-sm mt-1">Click "New chat" to begin</p>
438
+ </div>
439
+ </div>
440
+ `;
441
+ }
442
+
443
+ this.updateChatHeader(null);
444
+
445
+ const input = this.querySelector('#chatInput');
446
+ if (input) {
447
+ input.disabled = false;
448
+ input.readOnly = true;
449
+ input.classList.add('cursor-pointer');
450
+ }
451
+
242
452
  const btn = this.querySelector('#sendMessageBtn');
453
+ if (btn) btn.disabled = true;
454
+ }
243
455
 
244
- const selected = selectionType === 'agent' ? agent : llm;
456
+ updateChatHeader(session) {
457
+ const header = this.querySelector('#chatHeader');
458
+ if (!header) return;
245
459
 
246
- if (nameEl && selected) {
247
- nameEl.textContent = selected.name;
248
- } else if (nameEl) {
249
- nameEl.textContent = 'Select Agent/LLM';
460
+ if (!session) {
461
+ header.innerHTML = '<span class="text-gray-500">No conversation selected</span>';
462
+ return;
250
463
  }
251
464
 
252
- if (btn) btn.disabled = !selected || this.isLoading;
465
+ 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
+
471
+ let extraBadges = '';
472
+ if (isAgent) {
473
+ const agents = store.get('agents') || [];
474
+ const agent = agents.find(a => a.name === session.agentName);
475
+ if (agent) {
476
+ if (agent.publish?.enabled) {
477
+ 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>`;
479
+ }
480
+
481
+ const hasMemory = agent.memory === true || (agent.memory && agent.memory.enabled);
482
+ 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>`;
484
+ }
485
+
486
+ if (agent.tools?.length) {
487
+ const toolNames = agent.tools.map(t => typeof t === 'string' ? t : t.name);
488
+ const toolListHtml = toolNames.map(t => `<div class="tools-popover-item">${this.escapeHtml(t)}</div>`).join('');
489
+ extraBadges += `
490
+ <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>
492
+ <div class="tools-popover">${toolListHtml}</div>
493
+ </span>`;
494
+ }
495
+ }
496
+ }
497
+
498
+ header.innerHTML = `
499
+ <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>
503
+ ${extraBadges}
504
+ </div>
505
+ `;
253
506
  }
254
507
 
255
- toggleDropdown(show) {
256
- const dropdown = this.querySelector('#agentDropdown');
257
- if (dropdown) {
258
- if (show === undefined) {
259
- dropdown.classList.toggle('hidden');
260
- } else if (show) {
261
- dropdown.classList.remove('hidden');
508
+ // --- File Attachments ---
509
+
510
+ handleFileSelect(e) {
511
+ const files = Array.from(e.target.files);
512
+ e.target.value = '';
513
+
514
+ const needsConversion = ['image/webp', 'image/bmp', 'image/tiff'];
515
+
516
+ for (const file of files) {
517
+ if (needsConversion.includes(file.type)) {
518
+ this.convertImageToJpeg(file);
262
519
  } else {
263
- dropdown.classList.add('hidden');
520
+ const reader = new FileReader();
521
+ reader.onload = () => {
522
+ const dataUrl = reader.result;
523
+ const commaIdx = dataUrl.indexOf(',');
524
+ const base64 = dataUrl.slice(commaIdx + 1);
525
+ const mediaType = file.type || 'application/octet-stream';
526
+
527
+ this.pendingAttachments.push({ data: base64, mediaType, name: file.name });
528
+ this.renderAttachmentPreview();
529
+ };
530
+ reader.readAsDataURL(file);
264
531
  }
265
532
  }
266
533
  }
267
534
 
535
+ convertImageToJpeg(file) {
536
+ const img = new Image();
537
+ const url = URL.createObjectURL(file);
538
+ img.onload = () => {
539
+ const canvas = document.createElement('canvas');
540
+ canvas.width = img.naturalWidth;
541
+ canvas.height = img.naturalHeight;
542
+ canvas.getContext('2d').drawImage(img, 0, 0);
543
+ URL.revokeObjectURL(url);
544
+
545
+ const dataUrl = canvas.toDataURL('image/jpeg', 0.92);
546
+ const base64 = dataUrl.split(',')[1];
547
+ this.pendingAttachments.push({ data: base64, mediaType: 'image/jpeg', name: file.name });
548
+ this.renderAttachmentPreview();
549
+ };
550
+ img.src = url;
551
+ }
552
+
553
+ renderAttachmentPreview() {
554
+ const preview = this.querySelector('#attachmentPreview');
555
+ if (!preview) return;
556
+
557
+ if (this.pendingAttachments.length === 0) {
558
+ preview.classList.add('hidden');
559
+ preview.innerHTML = '';
560
+ return;
561
+ }
562
+
563
+ preview.classList.remove('hidden');
564
+ preview.innerHTML = this.pendingAttachments.map((att, i) => {
565
+ const isImage = att.mediaType.startsWith('image/');
566
+ 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>`;
569
+ 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">
571
+ ${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}">
574
+ <i class="fas fa-xmark text-xs"></i>
575
+ </button>
576
+ </div>
577
+ `;
578
+ }).join('');
579
+
580
+ preview.querySelectorAll('.attachment-remove').forEach(btn => {
581
+ btn.addEventListener('click', (e) => {
582
+ const idx = parseInt(e.currentTarget.dataset.index, 10);
583
+ this.pendingAttachments.splice(idx, 1);
584
+ this.renderAttachmentPreview();
585
+ });
586
+ });
587
+ }
588
+
589
+ clearAttachments() {
590
+ this.pendingAttachments = [];
591
+ this.renderAttachmentPreview();
592
+ }
593
+
594
+ // --- Messaging ---
595
+
268
596
  async sendMessage() {
269
597
  const input = this.querySelector('#chatInput');
270
598
  const message = input.value.trim();
271
599
  const selectionType = store.get('selectionType');
272
600
  const agent = store.get('selectedAgent');
273
601
  const llm = store.get('selectedLlm');
602
+ const activeId = sessionStore.getActiveId();
274
603
 
275
604
  const selected = selectionType === 'agent' ? agent : llm;
605
+ const hasAttachments = this.pendingAttachments.length > 0;
276
606
 
277
- if (!message || !selected || this.isLoading) return;
607
+ if ((!message && !hasAttachments) || !selected || this.isLoading || !activeId) return;
278
608
 
279
- // Add user message
280
- this.appendMessage('user', message);
609
+ // Capture attachments before clearing
610
+ const attachments = hasAttachments ? [...this.pendingAttachments] : null;
611
+
612
+ // Add user message (with optional attachment thumbnails)
613
+ this.appendMessage('user', message || '(attached files)', { attachments });
614
+ sessionStore.addMessage(activeId, 'user', message || '(attached files)');
281
615
  input.value = '';
282
616
  input.style.height = 'auto';
617
+ this.clearAttachments();
283
618
 
284
619
  this.isLoading = true;
285
620
  this.updateUiState();
@@ -296,9 +631,9 @@ export class AgentsView extends Component {
296
631
 
297
632
  try {
298
633
  if (selectionType === 'agent') {
299
- finalContent = await this.sendAgentMessage(agent, message, responseId);
634
+ finalContent = await this.sendAgentMessage(agent, message, responseId, attachments);
300
635
  } else if (selectionType === 'llm') {
301
- finalContent = await this.sendLlmMessage(llm, message, responseId);
636
+ finalContent = await this.sendLlmMessage(llm, message, responseId, attachments);
302
637
  }
303
638
  } catch (e) {
304
639
  if (e.name === 'AbortError') {
@@ -313,14 +648,26 @@ export class AgentsView extends Component {
313
648
  this.updateUiState();
314
649
  input.focus();
315
650
  }
651
+
652
+ // Persist assistant response
653
+ if (finalContent) {
654
+ sessionStore.addMessage(activeId, 'assistant', finalContent);
655
+ }
656
+
657
+ // Re-render sidebar (title/ordering may have changed)
658
+ this.renderSessionList();
316
659
  }
317
660
 
318
- async sendAgentMessage(agent, message, responseId) {
661
+ async sendAgentMessage(agent, message, responseId, attachments) {
319
662
  const inputVars = agent.inputVariables || ['message'];
320
663
  const inputObj = {};
321
664
  inputObj[inputVars[0] || 'message'] = message;
665
+ if (attachments) {
666
+ inputObj.attachments = attachments;
667
+ }
322
668
 
323
- const res = await api.streamAgent(agent.name, inputObj, store.get('sessionId'), { signal: this.currentAbortController?.signal });
669
+ const activeId = sessionStore.getActiveId();
670
+ const res = await api.streamAgent(agent.name, inputObj, activeId, { signal: this.currentAbortController?.signal });
324
671
  const reader = res.body.getReader();
325
672
  const decoder = new TextDecoder();
326
673
 
@@ -377,6 +724,10 @@ export class AgentsView extends Component {
377
724
  }
378
725
  }
379
726
 
727
+ // Finalize any remaining thinking pill
728
+ const toolsDiv = bubble.querySelector('.tool-invocations');
729
+ this.finalizeThinkingPill(toolsDiv, thinkingState);
730
+
380
731
  // If tools were called but no text content was produced, clear loading state
381
732
  if (hasToolCalls && !currentContent.trim()) {
382
733
  const loadingDots = contentDiv.querySelector('.loading-dots');
@@ -392,8 +743,9 @@ export class AgentsView extends Component {
392
743
  return currentContent;
393
744
  }
394
745
 
395
- async sendLlmMessage(llm, message, responseId) {
396
- const res = await api.streamLLM(llm.name, message, { signal: this.currentAbortController?.signal });
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 });
397
749
  const reader = res.body.getReader();
398
750
  const decoder = new TextDecoder();
399
751
 
@@ -471,153 +823,82 @@ export class AgentsView extends Component {
471
823
  }
472
824
 
473
825
  renderLlmContentStreaming(contentDiv, fullContent, responseId, state) {
474
- // Parse content to find think sections and regular text
475
- const parts = [];
476
- let pos = 0;
477
- let thinkIndex = 0;
478
-
479
- while (pos < fullContent.length) {
480
- const thinkStart = fullContent.indexOf('[THINK]', pos);
481
-
482
- if (thinkStart === -1) {
483
- // No more think sections, add remaining text
484
- const text = fullContent.slice(pos).trim();
485
- if (text) {
486
- parts.push({ type: 'text', content: text });
487
- }
488
- break;
489
- }
490
-
491
- // Add text before [THINK]
492
- if (thinkStart > pos) {
493
- const text = fullContent.slice(pos, thinkStart).trim();
494
- if (text) {
495
- parts.push({ type: 'text', content: text });
496
- }
497
- }
498
-
499
- // Find the end of this think section
500
- const thinkContentStart = thinkStart + 7; // After [THINK]
501
- const thinkEnd = fullContent.indexOf('[/THINK]', thinkContentStart);
826
+ const existing = contentDiv.querySelector('.content-text');
827
+ if (existing) {
828
+ const renderedHtml = markdownRenderer.render(fullContent);
829
+ existing.innerHTML = renderedHtml;
830
+ markdownRenderer.highlightCode(existing);
831
+ } else {
832
+ const div = document.createElement('div');
833
+ div.className = 'content-text markdown-content';
834
+ div.innerHTML = markdownRenderer.render(fullContent);
835
+ markdownRenderer.highlightCode(div);
836
+ contentDiv.appendChild(div);
837
+ }
838
+ }
502
839
 
503
- if (thinkEnd === -1) {
504
- // Think section is still streaming
505
- const thinkContent = fullContent.slice(thinkContentStart).trim();
506
- parts.push({ type: 'think', content: thinkContent, complete: false, index: thinkIndex });
507
- thinkIndex++;
508
- break;
509
- } else {
510
- // Complete think section
511
- const thinkContent = fullContent.slice(thinkContentStart, thinkEnd).trim();
512
- parts.push({ type: 'think', content: thinkContent, complete: true, index: thinkIndex });
513
- thinkIndex++;
514
- pos = thinkEnd + 8; // After [/THINK]
515
- }
840
+ handleThinkingEvent(event, toolsDiv, thinkingState, container) {
841
+ if (!thinkingState.thinkingContent) {
842
+ thinkingState.thinkingContent = '';
843
+ }
844
+ thinkingState.thinkingContent += event.content;
845
+
846
+ // Create pill on first thinking chunk
847
+ if (!thinkingState.thinkingPill) {
848
+ 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';
850
+ pill.innerHTML = `
851
+ <i class="fas fa-brain animate-pulse text-[10px]"></i>
852
+ <span>Thinking...</span>
853
+ `;
854
+ toolsDiv.appendChild(pill);
855
+ thinkingState.thinkingPill = pill;
856
+ container.scrollTop = container.scrollHeight;
516
857
  }
858
+ }
517
859
 
518
- // Update DOM incrementally instead of rebuilding
519
- let currentChildIndex = 0;
520
-
521
- for (let i = 0; i < parts.length; i++) {
522
- const part = parts[i];
523
- const existingChild = contentDiv.children[currentChildIndex];
524
-
525
- if (part.type === 'text') {
526
- if (existingChild && existingChild.classList.contains('content-text')) {
527
- // Update existing text element with markdown
528
- const renderedHtml = markdownRenderer.render(part.content);
529
- existingChild.innerHTML = renderedHtml;
530
- markdownRenderer.highlightCode(existingChild);
531
- } else {
532
- // Create new text element
533
- const div = document.createElement('div');
534
- div.className = 'content-text markdown-content';
535
- const renderedHtml = markdownRenderer.render(part.content);
536
- div.innerHTML = renderedHtml;
537
- markdownRenderer.highlightCode(div);
538
- if (existingChild) {
539
- contentDiv.insertBefore(div, existingChild);
540
- } else {
541
- contentDiv.appendChild(div);
542
- }
543
- }
544
- currentChildIndex++;
545
- } else if (part.type === 'think') {
546
- const thinkId = `think-${responseId}-${part.index}`;
547
-
548
- if (existingChild && existingChild.classList.contains('think-section')) {
549
- // Update existing think section
550
- const label = existingChild.querySelector('.think-label');
551
- const content = existingChild.querySelector('.think-content');
552
- if (label) {
553
- label.textContent = part.complete ? 'Thinking' : 'Thinking...';
554
- }
555
- if (content) {
556
- const renderedHtml = markdownRenderer.render(part.content);
557
- content.innerHTML = renderedHtml;
558
- content.classList.add('markdown-content');
559
- markdownRenderer.highlightCode(content);
560
- }
561
- } else {
562
- // Create new think section with event listener
563
- const section = document.createElement('div');
564
- section.className = 'think-section mb-3 border-l-2 border-blue-500/40 pl-3 py-1';
565
- section.dataset.thinkIndex = part.index;
566
-
567
- const toggle = document.createElement('button');
568
- toggle.className = 'think-toggle flex items-center gap-1.5 text-xs text-blue-400 hover:text-blue-300 py-1 cursor-pointer';
569
- toggle.dataset.thinkId = thinkId;
570
-
571
- toggle.innerHTML = `
572
- <i class="fas fa-brain text-xs"></i>
573
- <span class="font-medium think-label">${part.complete ? 'Thinking' : 'Thinking...'}</span>
574
- <i class="fas fa-chevron-right text-[10px] transition-transform think-chevron"></i>
575
- `;
576
-
577
- const thinkContent = document.createElement('div');
578
- thinkContent.id = thinkId;
579
- thinkContent.className = 'think-content hidden text-sm text-gray-400 markdown-content mt-1 leading-relaxed';
580
- const renderedHtml = markdownRenderer.render(part.content);
581
- thinkContent.innerHTML = renderedHtml;
582
- markdownRenderer.highlightCode(thinkContent);
583
-
584
- // Add click handler ONCE when creating
585
- toggle.addEventListener('click', (e) => {
586
- e.preventDefault();
587
- e.stopPropagation();
588
- const content = section.querySelector('.think-content');
589
- const chevron = section.querySelector('.think-chevron');
590
-
591
- if (content && chevron) {
592
- if (content.classList.contains('hidden')) {
593
- content.classList.remove('hidden');
594
- chevron.classList.remove('fa-chevron-right');
595
- chevron.classList.add('fa-chevron-down');
596
- } else {
597
- content.classList.add('hidden');
598
- chevron.classList.remove('fa-chevron-down');
599
- chevron.classList.add('fa-chevron-right');
600
- }
601
- }
602
- });
860
+ finalizeThinkingPill(toolsDiv, thinkingState) {
861
+ const pill = thinkingState.thinkingPill;
862
+ if (!pill) return;
603
863
 
604
- section.appendChild(toggle);
605
- section.appendChild(thinkContent);
864
+ const content = thinkingState.thinkingContent || '';
865
+ thinkingState.thinkingPill = null;
866
+ thinkingState.thinkingContent = '';
606
867
 
607
- if (existingChild) {
608
- contentDiv.insertBefore(section, existingChild);
609
- } else {
610
- contentDiv.appendChild(section);
611
- }
612
- }
613
- currentChildIndex++;
614
- }
615
- }
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';
869
+ pill.innerHTML = '';
616
870
 
617
- // Remove any extra children
618
- while (contentDiv.children.length > currentChildIndex) {
619
- contentDiv.removeChild(contentDiv.lastChild);
620
- }
871
+ 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
+ `;
877
+ pill.appendChild(pillContent);
878
+
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'));
621
902
  }
622
903
 
623
904
  createResponseBubble(id) {
@@ -643,7 +924,6 @@ export class AgentsView extends Component {
643
924
 
644
925
  wrapper.appendChild(div);
645
926
 
646
- // Stream status bar (visible during streaming)
647
927
  const statusBar = document.createElement('div');
648
928
  statusBar.className = 'stream-status-bar flex items-center gap-2 mt-1.5 ml-1 text-xs text-gray-400';
649
929
  statusBar.innerHTML = `
@@ -656,10 +936,8 @@ export class AgentsView extends Component {
656
936
  `;
657
937
  wrapper.appendChild(statusBar);
658
938
 
659
- // Wire up cancel button
660
939
  statusBar.querySelector('.stream-cancel-btn').addEventListener('click', () => this.cancelCurrentStream());
661
940
 
662
- // Stats bar (visible after completion)
663
941
  const statsBar = document.createElement('div');
664
942
  statsBar.className = 'stream-stats-bar hidden flex items-center gap-3 mt-1.5 ml-1 text-xs text-gray-500';
665
943
  statsBar.innerHTML = `
@@ -698,7 +976,11 @@ export class AgentsView extends Component {
698
976
  const loadingDots = contentDiv.querySelector('.loading-dots');
699
977
  const container = this.querySelector('#chatMessages');
700
978
 
701
- if (event.type === 'content') {
979
+ if (event.type === 'thinking') {
980
+ this.handleThinkingEvent(event, toolsDiv, thinkingState, container);
981
+ } else if (event.type === 'content') {
982
+ // Finalize any in-progress thinking pill
983
+ this.finalizeThinkingPill(toolsDiv, thinkingState);
702
984
  if (loadingDots) {
703
985
  loadingDots.remove();
704
986
  bubble.querySelector('.response-bubble-inner').classList.remove('py-4');
@@ -709,6 +991,7 @@ export class AgentsView extends Component {
709
991
  this.renderLlmContentStreaming(contentDiv, currentContent, responseId, thinkingState);
710
992
  container.scrollTop = container.scrollHeight;
711
993
  } else if (event.type === 'tool_start') {
994
+ this.finalizeThinkingPill(toolsDiv, thinkingState);
712
995
  const toolId = `tool-${event.runId}`;
713
996
  const toolEl = document.createElement('div');
714
997
  toolEl.id = toolId;
@@ -731,7 +1014,6 @@ export class AgentsView extends Component {
731
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';
732
1015
  toolEl.innerHTML = '';
733
1016
 
734
- // Pill content (icon + name)
735
1017
  const pillContent = document.createElement('span');
736
1018
  pillContent.className = 'inline-flex items-center gap-1.5';
737
1019
  pillContent.innerHTML = `
@@ -740,11 +1022,9 @@ export class AgentsView extends Component {
740
1022
  `;
741
1023
  toolEl.appendChild(pillContent);
742
1024
 
743
- // Popover details panel (positioned below the pill)
744
1025
  const details = document.createElement('div');
745
- details.className = 'tool-invocation-details hidden absolute left-0 top-full mt-1 z-50 bg-dark-surface border border-dark-border rounded-lg shadow-xl w-[400px] max-w-[90vw]';
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]';
746
1027
 
747
- // Input section
748
1028
  if (toolInput) {
749
1029
  const inputSection = document.createElement('div');
750
1030
  inputSection.className = 'p-3 border-b border-dark-border/50';
@@ -756,7 +1036,6 @@ export class AgentsView extends Component {
756
1036
  details.appendChild(inputSection);
757
1037
  }
758
1038
 
759
- // Output section
760
1039
  const outputSection = document.createElement('div');
761
1040
  outputSection.className = 'p-3';
762
1041
  outputSection.innerHTML = `<div class="text-xs font-semibold text-gray-400 mb-1">Output</div>`;
@@ -768,18 +1047,29 @@ export class AgentsView extends Component {
768
1047
 
769
1048
  toolEl.appendChild(details);
770
1049
 
771
- // Toggle popover on click
772
1050
  toolEl.addEventListener('click', (e) => {
1051
+ if (details.contains(e.target)) return;
773
1052
  e.preventDefault();
774
1053
  e.stopPropagation();
775
- // Close any other open popovers
776
1054
  toolsDiv.querySelectorAll('.tool-invocation-details:not(.hidden)').forEach(d => {
777
1055
  if (d !== details) d.classList.add('hidden');
778
1056
  });
1057
+ const wasHidden = details.classList.contains('hidden');
779
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
+ }
780
1071
  });
781
1072
 
782
- // Close popover when clicking outside
783
1073
  const closeHandler = (e) => {
784
1074
  if (!toolEl.contains(e.target)) {
785
1075
  details.classList.add('hidden');
@@ -787,6 +1077,13 @@ export class AgentsView extends Component {
787
1077
  };
788
1078
  document.addEventListener('click', closeHandler, { capture: true });
789
1079
  container.scrollTop = container.scrollHeight;
1080
+
1081
+ if (event.tool === 'workspace_write' || event.tool === 'workspace_delete') {
1082
+ try {
1083
+ const result = JSON.parse(typeof event.output === 'string' ? event.output : JSON.stringify(event.output));
1084
+ if (result.success && (result.reloaded === 'agent' || result.unloaded === 'agent')) this.loadAgents();
1085
+ } catch { /* ignore parse errors */ }
1086
+ }
790
1087
  }
791
1088
  } else if (event.type === 'result') {
792
1089
  if (loadingDots) {
@@ -797,7 +1094,6 @@ export class AgentsView extends Component {
797
1094
  contentDiv.innerHTML = '';
798
1095
  }
799
1096
 
800
- // Display structured output as formatted JSON
801
1097
  const resultContainer = document.createElement('div');
802
1098
  resultContainer.className = 'bg-dark-bg/50 border border-dark-border rounded-lg p-4';
803
1099
 
@@ -808,7 +1104,6 @@ export class AgentsView extends Component {
808
1104
  resultContainer.appendChild(resultPre);
809
1105
  contentDiv.appendChild(resultContainer);
810
1106
 
811
- // Scroll to bottom
812
1107
  container.scrollTop = container.scrollHeight;
813
1108
  } else if (event.type === 'error') {
814
1109
  if (loadingDots) {
@@ -828,6 +1123,13 @@ export class AgentsView extends Component {
828
1123
  output_tokens: event.output_tokens || 0,
829
1124
  total_tokens: event.total_tokens || 0,
830
1125
  };
1126
+ } else if (event.type === 'react_iteration') {
1127
+ const wrapper = bubble.closest('.response-wrapper');
1128
+ const statusText = wrapper?.querySelector('.stream-status-text');
1129
+ if (statusText) {
1130
+ const contextKB = (event.contextChars / 1024).toFixed(1);
1131
+ statusText.textContent = `Iteration ${event.iteration} · ${contextKB} KB context`;
1132
+ }
831
1133
  }
832
1134
  }
833
1135
 
@@ -842,8 +1144,13 @@ export class AgentsView extends Component {
842
1144
  updateUiState() {
843
1145
  const btn = this.querySelector('#sendMessageBtn');
844
1146
  const input = this.querySelector('#chatInput');
845
- if (btn) btn.disabled = this.isLoading;
846
- if (input) input.disabled = this.isLoading;
1147
+ const hasActiveSession = !!sessionStore.getActiveId();
1148
+ if (btn) btn.disabled = this.isLoading || !hasActiveSession;
1149
+ if (input) {
1150
+ input.disabled = this.isLoading;
1151
+ input.readOnly = !hasActiveSession;
1152
+ input.classList.toggle('cursor-pointer', !hasActiveSession);
1153
+ }
847
1154
  }
848
1155
 
849
1156
  appendMessage(role, content, metadata = {}) {
@@ -857,8 +1164,24 @@ export class AgentsView extends Component {
857
1164
  const bubbleColor = isUser ? 'bg-dark-surface' : (hasError ? 'bg-red-900/20 border-red-900/30' : 'bg-dark-surface');
858
1165
  const textColor = hasError ? 'text-red-300' : 'text-gray-100';
859
1166
 
1167
+ // Build attachment thumbnails for user messages
1168
+ let attachmentHtml = '';
1169
+ if (isUser && metadata.attachments && metadata.attachments.length > 0) {
1170
+ const thumbs = metadata.attachments.map(att => {
1171
+ 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">`;
1173
+ }
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">
1175
+ <i class="fas fa-file"></i>
1176
+ <span class="max-w-[100px] truncate">${this.escapeHtml(att.name)}</span>
1177
+ </div>`;
1178
+ }).join('');
1179
+ attachmentHtml = `<div class="flex flex-wrap gap-2 mb-2">${thumbs}</div>`;
1180
+ }
1181
+
860
1182
  div.innerHTML = `
861
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">
1184
+ ${attachmentHtml}
862
1185
  <div class="whitespace-pre-wrap">${this.escapeHtml(content)}</div>
863
1186
  ${!isUser && !hasError ? `
864
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">
@@ -912,30 +1235,116 @@ export class AgentsView extends Component {
912
1235
  return div.innerHTML;
913
1236
  }
914
1237
 
915
- clearChatHistory() {
916
- const container = this.querySelector('#chatMessages');
917
- if (!container) return;
918
-
919
- // Clear all messages
920
- container.innerHTML = '';
1238
+ _getRandomWelcomeMessage() {
1239
+ const messages = [
1240
+ 'Awaiting your command, master.',
1241
+ 'The agents are restless. Give them purpose.',
1242
+ 'Ready when you are. No pressure... okay, maybe a little.',
1243
+ 'Spinning up neurons... just kidding, I was already ready.',
1244
+ 'All systems nominal. Your move, human.',
1245
+ 'The orchestrator awaits. What shall we build today?',
1246
+ 'Standing by. The agents are stretching their digital legs.',
1247
+ 'Another day, another chance to orchestrate greatness.',
1248
+ 'Agents assembled. Awaiting mission briefing.',
1249
+ 'The stage is set. You are the conductor.',
1250
+ ];
1251
+ return messages[Math.floor(Math.random() * messages.length)];
1252
+ }
921
1253
 
922
- // Add welcome message
1254
+ _appendWelcomeMessage(container) {
923
1255
  const div = document.createElement('div');
924
- div.className = 'flex justify-start';
1256
+ div.className = 'welcome-container';
925
1257
  div.innerHTML = `
926
- <div class="max-w-4xl bg-dark-surface border border-dark-border rounded-3xl px-5 py-3 text-gray-100 text-[15px] leading-relaxed">
927
- Welcome to Agent Orcha. Start chatting with your AI agents and LLMs.
928
- </div>
1258
+ <svg class="welcome-orca" viewBox="0 0 220 140" xmlns="http://www.w3.org/2000/svg">
1259
+ <!-- Main body -->
1260
+ <path class="orca-body" d="
1261
+ M 30,68
1262
+ C 28,58 38,42 58,38
1263
+ C 68,35 74,30 78,18
1264
+ C 80,12 84,12 85,18
1265
+ C 87,28 86,35 92,38
1266
+ C 112,34 148,40 172,54
1267
+ C 176,50 182,44 188,40
1268
+ C 192,38 194,42 190,46
1269
+ C 186,50 184,54 182,56
1270
+ C 186,60 188,66 184,68
1271
+ C 180,70 178,66 176,62
1272
+ C 168,72 142,78 112,76
1273
+ C 82,74 52,70 38,66
1274
+ C 34,64 30,68 30,68 Z
1275
+ "/>
1276
+ <!-- Dorsal fin accent line -->
1277
+ <path class="orca-detail" d="M 72,38 C 74,28 78,18 80,14"/>
1278
+ <!-- Belly line -->
1279
+ <path class="orca-detail" d="M 42,64 C 62,70 100,74 140,72 C 158,70 170,66 176,62"/>
1280
+ <!-- Saddle patch -->
1281
+ <path class="orca-patch" d="M 92,40 C 102,38 112,40 108,48 C 104,54 90,50 92,40 Z"/>
1282
+ <!-- Eye patch -->
1283
+ <path class="orca-patch" d="M 44,52 C 50,48 60,50 56,58 C 52,62 42,58 44,52 Z"/>
1284
+ <!-- Eye -->
1285
+ <circle class="orca-eye" cx="42" cy="54" r="2.5"/>
1286
+ <!-- Pectoral fin -->
1287
+ <path class="orca-body" d="M 65,66 C 70,74 64,82 58,76 C 54,72 60,66 65,66 Z"/>
1288
+ <!-- Tail detail -->
1289
+ <path class="orca-detail" d="M 172,54 C 176,52 180,48 184,44"/>
1290
+ <path class="orca-detail" d="M 176,62 C 180,64 184,66 186,64"/>
1291
+ </svg>
1292
+ <div class="welcome-text">${this._getRandomWelcomeMessage()}</div>
1293
+ ${this._renderSampleQuestionChips()}
929
1294
  `;
930
1295
  container.appendChild(div);
931
1296
 
932
- // Generate a new session ID for fresh conversation
933
- store.set('sessionId', 'session-' + Date.now() + '-' + Math.random().toString(36).substring(2, 9));
1297
+ div.querySelectorAll('.sample-question-chip').forEach(chip => {
1298
+ chip.addEventListener('click', () => {
1299
+ const input = this.querySelector('#chatInput');
1300
+ if (input) {
1301
+ input.value = chip.textContent;
1302
+ input.focus();
1303
+ }
1304
+ });
1305
+ });
1306
+ }
1307
+
1308
+ _renderSampleQuestionChips() {
1309
+ const agent = store.get('selectedAgent');
1310
+ const questions = agent?.sampleQuestions;
1311
+ if (!questions || questions.length === 0) return '';
1312
+
1313
+ 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>`
1315
+ ).join('');
1316
+
1317
+ return `
1318
+ <div class="flex flex-wrap justify-center gap-2 max-w-2xl mt-4">${chips}</div>
1319
+ `;
1320
+ }
1321
+
1322
+ _appendSessionResetBanner(container) {
1323
+ const div = document.createElement('div');
1324
+ div.className = 'session-reset-banner';
1325
+ div.innerHTML = `
1326
+ <div class="session-reset-line"></div>
1327
+ <span class="session-reset-label">
1328
+ <i class="fas fa-rotate-right"></i>
1329
+ Server restarted — new session
1330
+ </span>
1331
+ <div class="session-reset-line"></div>
1332
+ `;
1333
+ container.appendChild(div);
1334
+ container.scrollTop = container.scrollHeight;
934
1335
  }
935
1336
 
936
1337
  postRender() {
937
1338
  const input = this.querySelector('#chatInput');
938
1339
 
1340
+ // Open new conversation modal when clicking input with no active session
1341
+ input.addEventListener('mousedown', (e) => {
1342
+ if (!sessionStore.getActiveId()) {
1343
+ e.preventDefault();
1344
+ this.showNewSessionModal();
1345
+ }
1346
+ });
1347
+
939
1348
  // Auto-resize
940
1349
  input.addEventListener('input', () => {
941
1350
  input.style.height = 'auto';
@@ -952,60 +1361,74 @@ export class AgentsView extends Component {
952
1361
 
953
1362
  this.querySelector('#sendMessageBtn').addEventListener('click', () => this.sendMessage());
954
1363
 
955
- // Dropdown toggle
956
- const selectorBtn = this.querySelector('#agentSelectorBtn');
957
- const dropdown = this.querySelector('#agentDropdown');
1364
+ // Attach button + file input
1365
+ this.querySelector('#attachBtn').addEventListener('click', () => this.querySelector('#fileInput').click());
1366
+ this.querySelector('#fileInput').addEventListener('change', (e) => this.handleFileSelect(e));
958
1367
 
959
- selectorBtn.addEventListener('click', (e) => {
960
- e.stopPropagation();
961
- this.toggleDropdown();
962
- });
1368
+ // New chat button
1369
+ this.querySelector('#newChatBtn').addEventListener('click', () => this.showNewSessionModal());
963
1370
 
964
- document.addEventListener('click', (e) => {
965
- if (!selectorBtn.contains(e.target) && !dropdown.contains(e.target)) {
966
- this.toggleDropdown(false);
967
- }
968
- });
1371
+ // Mobile sidebar toggle
1372
+ this.querySelector('#sidebarToggleBtn').addEventListener('click', () => this.toggleSidebar(true));
1373
+ this.querySelector('#sidebarBackdrop').addEventListener('click', () => this.toggleSidebar(false));
969
1374
  }
970
1375
 
971
1376
  template() {
972
1377
  return `
973
- <div class="flex flex-col h-[calc(100vh-220px)]">
974
- <!-- Chat Messages -->
975
- <div id="chatMessages" class="flex-1 overflow-y-auto mb-6 space-y-4 pr-2 custom-scrollbar">
976
- <div class="flex justify-start">
977
- <div class="max-w-4xl bg-dark-surface border border-dark-border rounded-3xl px-5 py-3 text-gray-100 text-[15px] leading-relaxed">
978
- Welcome to Agent Orcha. Start chatting with your AI agents and LLMs.
979
- </div>
1378
+ <div class="flex h-full relative border border-dark-border rounded-xl overflow-hidden bg-dark-surface/30">
1379
+ <!-- Mobile sidebar backdrop -->
1380
+ <div id="sidebarBackdrop" class="hidden fixed inset-0 bg-black/50 z-30 md:hidden"></div>
1381
+
1382
+ <!-- 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">
1385
+ <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>
1388
+ <span>New chat</span>
1389
+ </button>
980
1390
  </div>
1391
+ <div id="sessionList" class="flex-1 overflow-y-auto custom-scrollbar px-2 pb-2"></div>
981
1392
  </div>
982
1393
 
983
- <!-- Input Area -->
984
- <div class="border-t border-dark-border pt-4">
985
- <div class="relative bg-dark-surface border border-dark-border rounded-2xl focus-within:border-gray-500 transition-colors">
986
- <textarea id="chatInput" rows="1"
987
- class="w-full bg-transparent px-4 py-3 pr-32 text-gray-100 placeholder-gray-500 resize-none focus:outline-none max-h-[200px]"
988
- placeholder="Reply..."></textarea>
989
-
990
- <div class="absolute bottom-2 right-2 flex items-center gap-2">
991
- <!-- Agent Selector -->
992
- <div class="relative">
993
- <button id="agentSelectorBtn" class="flex items-center gap-2 px-3 py-1.5 bg-dark-bg hover:bg-dark-hover rounded-lg text-sm font-medium text-gray-300 transition-colors">
994
- <span id="selectedAgentName">Select Agent/LLM</span>
995
- <i class="fas fa-chevron-down text-xs text-gray-400"></i>
996
- </button>
1394
+ <!-- Chat Area -->
1395
+ <div class="flex-1 flex flex-col min-w-0">
1396
+ <!-- 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">
1399
+ <i class="fas fa-bars"></i>
1400
+ </button>
1401
+ <div id="chatHeader" class="flex-1 min-w-0">
1402
+ <span class="text-gray-500">No conversation selected</span>
1403
+ </div>
1404
+ </div>
997
1405
 
998
- <div id="agentDropdown" class="hidden absolute bottom-full mb-2 right-0 w-80 bg-dark-surface border border-dark-border rounded-xl shadow-2xl overflow-hidden z-10 max-h-96 flex flex-col">
999
- <div id="agentDropdownList" class="overflow-y-auto custom-scrollbar">
1000
- <div class="text-gray-500 text-sm text-center py-4">Loading...</div>
1001
- </div>
1002
- </div>
1406
+ <!-- Chat Messages -->
1407
+ <div id="chatMessages" class="flex-1 overflow-y-auto space-y-4 p-4 pr-2 pb-6 custom-scrollbar"></div>
1408
+
1409
+ <!-- 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">
1414
+ <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
+ placeholder="Ask anything"></textarea>
1417
+
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">
1422
+ <i class="fas fa-plus text-sm"></i>
1423
+ </button>
1003
1424
  </div>
1004
1425
 
1005
- <button id="sendMessageBtn" disabled
1006
- class="bg-gradient-to-r from-blue-600 to-blue-700 hover:from-blue-500 hover:to-blue-600 disabled:opacity-50 disabled:cursor-not-allowed text-white p-2 rounded-lg transition-all shadow-lg shadow-blue-900/20">
1007
- <i class="fas fa-paper-plane text-sm"></i>
1008
- </button>
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">
1429
+ <i class="fas fa-paper-plane text-sm"></i>
1430
+ </button>
1431
+ </div>
1009
1432
  </div>
1010
1433
  </div>
1011
1434
  </div>