@myrialabs/clopen 0.0.1

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 (456) hide show
  1. package/.env.example +6 -0
  2. package/.github/workflows/release.yml +60 -0
  3. package/.github/workflows/test.yml +40 -0
  4. package/CONTRIBUTING.md +499 -0
  5. package/LICENSE +21 -0
  6. package/README.md +209 -0
  7. package/backend/index.ts +156 -0
  8. package/backend/lib/chat/helpers.ts +42 -0
  9. package/backend/lib/chat/index.ts +2 -0
  10. package/backend/lib/chat/stream-manager.ts +1126 -0
  11. package/backend/lib/database/README.md +77 -0
  12. package/backend/lib/database/index.ts +119 -0
  13. package/backend/lib/database/migrations/001_create_projects_table.ts +31 -0
  14. package/backend/lib/database/migrations/002_create_chat_sessions_table.ts +33 -0
  15. package/backend/lib/database/migrations/003_create_messages_table.ts +32 -0
  16. package/backend/lib/database/migrations/004_create_prompt_templates_table.ts +34 -0
  17. package/backend/lib/database/migrations/005_create_settings_table.ts +24 -0
  18. package/backend/lib/database/migrations/006_add_user_to_messages.ts +58 -0
  19. package/backend/lib/database/migrations/007_create_stream_states_table.ts +41 -0
  20. package/backend/lib/database/migrations/008_create_message_snapshots_table.ts +62 -0
  21. package/backend/lib/database/migrations/009_add_delta_snapshot_fields.ts +41 -0
  22. package/backend/lib/database/migrations/010_add_soft_delete_and_branch_support.ts +70 -0
  23. package/backend/lib/database/migrations/011_git_like_commit_graph.ts +156 -0
  24. package/backend/lib/database/migrations/012_add_file_change_statistics.ts +41 -0
  25. package/backend/lib/database/migrations/013_checkpoint_tree_state.ts +118 -0
  26. package/backend/lib/database/migrations/014_add_engine_to_sessions.ts +18 -0
  27. package/backend/lib/database/migrations/015_add_model_to_sessions.ts +18 -0
  28. package/backend/lib/database/migrations/016_create_user_projects_table.ts +34 -0
  29. package/backend/lib/database/migrations/017_add_current_session_to_user_projects.ts +32 -0
  30. package/backend/lib/database/migrations/018_create_claude_accounts_table.ts +24 -0
  31. package/backend/lib/database/migrations/019_add_claude_account_to_sessions.ts +18 -0
  32. package/backend/lib/database/migrations/020_add_snapshot_tree_hash.ts +32 -0
  33. package/backend/lib/database/migrations/021_drop_prompt_templates_table.ts +33 -0
  34. package/backend/lib/database/migrations/index.ts +154 -0
  35. package/backend/lib/database/queries/checkpoint-queries.ts +87 -0
  36. package/backend/lib/database/queries/engine-queries.ts +75 -0
  37. package/backend/lib/database/queries/index.ts +9 -0
  38. package/backend/lib/database/queries/message-queries.ts +472 -0
  39. package/backend/lib/database/queries/project-queries.ts +117 -0
  40. package/backend/lib/database/queries/session-queries.ts +271 -0
  41. package/backend/lib/database/queries/settings-queries.ts +34 -0
  42. package/backend/lib/database/queries/snapshot-queries.ts +326 -0
  43. package/backend/lib/database/queries/utils-queries.ts +59 -0
  44. package/backend/lib/database/seeders/index.ts +13 -0
  45. package/backend/lib/database/seeders/settings_seeder.ts +84 -0
  46. package/backend/lib/database/utils/connection.ts +174 -0
  47. package/backend/lib/database/utils/index.ts +4 -0
  48. package/backend/lib/database/utils/migration-runner.ts +118 -0
  49. package/backend/lib/database/utils/seeder-runner.ts +121 -0
  50. package/backend/lib/engine/adapters/claude/environment.ts +164 -0
  51. package/backend/lib/engine/adapters/claude/error-handler.ts +60 -0
  52. package/backend/lib/engine/adapters/claude/index.ts +1 -0
  53. package/backend/lib/engine/adapters/claude/path-utils.ts +38 -0
  54. package/backend/lib/engine/adapters/claude/stream.ts +177 -0
  55. package/backend/lib/engine/adapters/opencode/index.ts +2 -0
  56. package/backend/lib/engine/adapters/opencode/message-converter.ts +862 -0
  57. package/backend/lib/engine/adapters/opencode/server.ts +104 -0
  58. package/backend/lib/engine/adapters/opencode/stream.ts +755 -0
  59. package/backend/lib/engine/index.ts +196 -0
  60. package/backend/lib/engine/types.ts +58 -0
  61. package/backend/lib/files/file-operations.ts +478 -0
  62. package/backend/lib/files/file-reading.ts +308 -0
  63. package/backend/lib/files/file-watcher.ts +383 -0
  64. package/backend/lib/files/path-browsing.ts +382 -0
  65. package/backend/lib/git/git-executor.ts +88 -0
  66. package/backend/lib/git/git-parser.ts +411 -0
  67. package/backend/lib/git/git-service.ts +505 -0
  68. package/backend/lib/mcp/README.md +1144 -0
  69. package/backend/lib/mcp/config.ts +316 -0
  70. package/backend/lib/mcp/index.ts +35 -0
  71. package/backend/lib/mcp/project-context.ts +236 -0
  72. package/backend/lib/mcp/servers/browser-automation/actions.ts +156 -0
  73. package/backend/lib/mcp/servers/browser-automation/browser.ts +419 -0
  74. package/backend/lib/mcp/servers/browser-automation/index.ts +791 -0
  75. package/backend/lib/mcp/servers/browser-automation/inspection.ts +501 -0
  76. package/backend/lib/mcp/servers/helper.ts +143 -0
  77. package/backend/lib/mcp/servers/index.ts +45 -0
  78. package/backend/lib/mcp/servers/weather/get-temperature.ts +56 -0
  79. package/backend/lib/mcp/servers/weather/index.ts +31 -0
  80. package/backend/lib/mcp/stdio-server.ts +103 -0
  81. package/backend/lib/mcp/types.ts +65 -0
  82. package/backend/lib/preview/browser/browser-audio-capture.ts +86 -0
  83. package/backend/lib/preview/browser/browser-console-manager.ts +263 -0
  84. package/backend/lib/preview/browser/browser-dialog-handler.ts +222 -0
  85. package/backend/lib/preview/browser/browser-interaction-handler.ts +421 -0
  86. package/backend/lib/preview/browser/browser-mcp-control.ts +415 -0
  87. package/backend/lib/preview/browser/browser-native-ui-handler.ts +512 -0
  88. package/backend/lib/preview/browser/browser-navigation-tracker.ts +104 -0
  89. package/backend/lib/preview/browser/browser-pool.ts +357 -0
  90. package/backend/lib/preview/browser/browser-preview-service.ts +882 -0
  91. package/backend/lib/preview/browser/browser-tab-manager.ts +935 -0
  92. package/backend/lib/preview/browser/browser-video-capture.ts +695 -0
  93. package/backend/lib/preview/browser/scripts/audio-stream.ts +292 -0
  94. package/backend/lib/preview/browser/scripts/cursor-tracking.ts +85 -0
  95. package/backend/lib/preview/browser/scripts/video-stream.ts +438 -0
  96. package/backend/lib/preview/browser/types.ts +359 -0
  97. package/backend/lib/preview/index.ts +24 -0
  98. package/backend/lib/project/index.ts +2 -0
  99. package/backend/lib/project/status-manager.ts +182 -0
  100. package/backend/lib/shared/index.ts +2 -0
  101. package/backend/lib/shared/port-utils.ts +25 -0
  102. package/backend/lib/shared/process-manager.ts +281 -0
  103. package/backend/lib/snapshot/blob-store.ts +227 -0
  104. package/backend/lib/snapshot/gitignore.ts +307 -0
  105. package/backend/lib/snapshot/helpers.ts +397 -0
  106. package/backend/lib/snapshot/snapshot-service.ts +483 -0
  107. package/backend/lib/terminal/helpers.ts +14 -0
  108. package/backend/lib/terminal/index.ts +8 -0
  109. package/backend/lib/terminal/pty-manager.ts +4 -0
  110. package/backend/lib/terminal/pty-session-manager.ts +387 -0
  111. package/backend/lib/terminal/shell-utils.ts +313 -0
  112. package/backend/lib/terminal/stream-manager.ts +293 -0
  113. package/backend/lib/tunnel/global-tunnel-manager.ts +243 -0
  114. package/backend/lib/tunnel/project-tunnel-manager.ts +311 -0
  115. package/backend/lib/user/helpers.ts +87 -0
  116. package/backend/lib/utils/ws.ts +944 -0
  117. package/backend/lib/vite-dev.ts +353 -0
  118. package/backend/middleware/cors.ts +15 -0
  119. package/backend/middleware/error-handler.ts +49 -0
  120. package/backend/middleware/logger.ts +9 -0
  121. package/backend/types/api.ts +24 -0
  122. package/backend/ws/README.md +1505 -0
  123. package/backend/ws/chat/background.ts +198 -0
  124. package/backend/ws/chat/index.ts +21 -0
  125. package/backend/ws/chat/stream.ts +707 -0
  126. package/backend/ws/engine/claude/accounts.ts +401 -0
  127. package/backend/ws/engine/claude/index.ts +13 -0
  128. package/backend/ws/engine/claude/status.ts +43 -0
  129. package/backend/ws/engine/index.ts +14 -0
  130. package/backend/ws/engine/opencode/index.ts +11 -0
  131. package/backend/ws/engine/opencode/status.ts +30 -0
  132. package/backend/ws/engine/utils.ts +36 -0
  133. package/backend/ws/files/index.ts +30 -0
  134. package/backend/ws/files/read.ts +189 -0
  135. package/backend/ws/files/search.ts +453 -0
  136. package/backend/ws/files/watch.ts +124 -0
  137. package/backend/ws/files/write.ts +143 -0
  138. package/backend/ws/git/branch.ts +106 -0
  139. package/backend/ws/git/commit.ts +39 -0
  140. package/backend/ws/git/conflict.ts +68 -0
  141. package/backend/ws/git/diff.ts +69 -0
  142. package/backend/ws/git/index.ts +24 -0
  143. package/backend/ws/git/log.ts +41 -0
  144. package/backend/ws/git/remote.ts +214 -0
  145. package/backend/ws/git/staging.ts +84 -0
  146. package/backend/ws/git/status.ts +90 -0
  147. package/backend/ws/index.ts +69 -0
  148. package/backend/ws/mcp/index.ts +61 -0
  149. package/backend/ws/messages/crud.ts +74 -0
  150. package/backend/ws/messages/index.ts +14 -0
  151. package/backend/ws/preview/browser/cleanup.ts +129 -0
  152. package/backend/ws/preview/browser/console.ts +114 -0
  153. package/backend/ws/preview/browser/interact.ts +513 -0
  154. package/backend/ws/preview/browser/mcp.ts +129 -0
  155. package/backend/ws/preview/browser/native-ui.ts +235 -0
  156. package/backend/ws/preview/browser/stats.ts +55 -0
  157. package/backend/ws/preview/browser/tab-info.ts +126 -0
  158. package/backend/ws/preview/browser/tab.ts +166 -0
  159. package/backend/ws/preview/browser/webcodecs.ts +293 -0
  160. package/backend/ws/preview/index.ts +146 -0
  161. package/backend/ws/projects/crud.ts +113 -0
  162. package/backend/ws/projects/index.ts +25 -0
  163. package/backend/ws/projects/presence.ts +46 -0
  164. package/backend/ws/projects/status.ts +116 -0
  165. package/backend/ws/sessions/crud.ts +327 -0
  166. package/backend/ws/sessions/index.ts +33 -0
  167. package/backend/ws/settings/crud.ts +112 -0
  168. package/backend/ws/settings/index.ts +14 -0
  169. package/backend/ws/snapshot/index.ts +17 -0
  170. package/backend/ws/snapshot/restore.ts +173 -0
  171. package/backend/ws/snapshot/timeline.ts +141 -0
  172. package/backend/ws/system/index.ts +14 -0
  173. package/backend/ws/system/operations.ts +49 -0
  174. package/backend/ws/terminal/index.ts +40 -0
  175. package/backend/ws/terminal/persistence.ts +153 -0
  176. package/backend/ws/terminal/session.ts +382 -0
  177. package/backend/ws/terminal/stream.ts +79 -0
  178. package/backend/ws/tunnel/index.ts +14 -0
  179. package/backend/ws/tunnel/operations.ts +91 -0
  180. package/backend/ws/types.ts +20 -0
  181. package/backend/ws/user/crud.ts +156 -0
  182. package/backend/ws/user/index.ts +14 -0
  183. package/bin/clopen.ts +307 -0
  184. package/bun.lock +1352 -0
  185. package/frontend/App.svelte +34 -0
  186. package/frontend/app.css +313 -0
  187. package/frontend/lib/app-environment.ts +10 -0
  188. package/frontend/lib/components/chat/ChatInterface.svelte +407 -0
  189. package/frontend/lib/components/chat/formatters/ErrorMessage.svelte +57 -0
  190. package/frontend/lib/components/chat/formatters/MessageFormatter.svelte +224 -0
  191. package/frontend/lib/components/chat/formatters/TextMessage.svelte +395 -0
  192. package/frontend/lib/components/chat/formatters/Tools.svelte +70 -0
  193. package/frontend/lib/components/chat/formatters/index.ts +3 -0
  194. package/frontend/lib/components/chat/input/ChatInput.svelte +421 -0
  195. package/frontend/lib/components/chat/input/components/ChatInputActions.svelte +78 -0
  196. package/frontend/lib/components/chat/input/components/DragDropOverlay.svelte +30 -0
  197. package/frontend/lib/components/chat/input/components/EditModeIndicator.svelte +33 -0
  198. package/frontend/lib/components/chat/input/components/EngineModelPicker.svelte +619 -0
  199. package/frontend/lib/components/chat/input/components/FileAttachmentPreview.svelte +48 -0
  200. package/frontend/lib/components/chat/input/components/LoadingIndicator.svelte +31 -0
  201. package/frontend/lib/components/chat/input/composables/use-animations.svelte.ts +201 -0
  202. package/frontend/lib/components/chat/input/composables/use-chat-actions.svelte.ts +148 -0
  203. package/frontend/lib/components/chat/input/composables/use-file-handling.svelte.ts +216 -0
  204. package/frontend/lib/components/chat/input/composables/use-input-state.svelte.ts +357 -0
  205. package/frontend/lib/components/chat/input/composables/use-textarea-resize.svelte.ts +57 -0
  206. package/frontend/lib/components/chat/message/ChatMessage.svelte +478 -0
  207. package/frontend/lib/components/chat/message/ChatMessages.svelte +541 -0
  208. package/frontend/lib/components/chat/message/DateSeparator.svelte +86 -0
  209. package/frontend/lib/components/chat/message/MessageBubble.svelte +86 -0
  210. package/frontend/lib/components/chat/message/MessageHeader.svelte +157 -0
  211. package/frontend/lib/components/chat/modal/DebugModal.svelte +59 -0
  212. package/frontend/lib/components/chat/modal/TokenUsageModal.svelte +124 -0
  213. package/frontend/lib/components/chat/shared/index.ts +2 -0
  214. package/frontend/lib/components/chat/shared/utils.ts +116 -0
  215. package/frontend/lib/components/chat/tools/BashOutputTool.svelte +35 -0
  216. package/frontend/lib/components/chat/tools/BashTool.svelte +46 -0
  217. package/frontend/lib/components/chat/tools/CustomMcpTool.svelte +139 -0
  218. package/frontend/lib/components/chat/tools/EditTool.svelte +48 -0
  219. package/frontend/lib/components/chat/tools/ExitPlanModeTool.svelte +32 -0
  220. package/frontend/lib/components/chat/tools/GlobTool.svelte +51 -0
  221. package/frontend/lib/components/chat/tools/GrepTool.svelte +90 -0
  222. package/frontend/lib/components/chat/tools/KillShellTool.svelte +26 -0
  223. package/frontend/lib/components/chat/tools/ListMcpResourcesTool.svelte +31 -0
  224. package/frontend/lib/components/chat/tools/NotebookEditTool.svelte +38 -0
  225. package/frontend/lib/components/chat/tools/ReadMcpResourceTool.svelte +34 -0
  226. package/frontend/lib/components/chat/tools/ReadTool.svelte +41 -0
  227. package/frontend/lib/components/chat/tools/TaskTool.svelte +64 -0
  228. package/frontend/lib/components/chat/tools/TodoWriteTool.svelte +75 -0
  229. package/frontend/lib/components/chat/tools/WebFetchTool.svelte +35 -0
  230. package/frontend/lib/components/chat/tools/WebSearchTool.svelte +84 -0
  231. package/frontend/lib/components/chat/tools/WriteTool.svelte +33 -0
  232. package/frontend/lib/components/chat/tools/components/CodeBlock.svelte +79 -0
  233. package/frontend/lib/components/chat/tools/components/DiffBlock.svelte +408 -0
  234. package/frontend/lib/components/chat/tools/components/FileHeader.svelte +45 -0
  235. package/frontend/lib/components/chat/tools/components/InfoLine.svelte +19 -0
  236. package/frontend/lib/components/chat/tools/components/StatsBadges.svelte +27 -0
  237. package/frontend/lib/components/chat/tools/components/TerminalCommand.svelte +54 -0
  238. package/frontend/lib/components/chat/tools/components/index.ts +7 -0
  239. package/frontend/lib/components/chat/tools/index.ts +26 -0
  240. package/frontend/lib/components/chat/widgets/FloatingTodoList.svelte +249 -0
  241. package/frontend/lib/components/chat/widgets/TokenUsage.svelte +78 -0
  242. package/frontend/lib/components/checkpoint/TimelineModal.svelte +391 -0
  243. package/frontend/lib/components/checkpoint/timeline/TimelineEdge.svelte +26 -0
  244. package/frontend/lib/components/checkpoint/timeline/TimelineGraph.svelte +87 -0
  245. package/frontend/lib/components/checkpoint/timeline/TimelineNode.svelte +108 -0
  246. package/frontend/lib/components/checkpoint/timeline/TimelineVersionGroup.svelte +59 -0
  247. package/frontend/lib/components/checkpoint/timeline/animation.ts +168 -0
  248. package/frontend/lib/components/checkpoint/timeline/config.ts +44 -0
  249. package/frontend/lib/components/checkpoint/timeline/graph-builder.ts +304 -0
  250. package/frontend/lib/components/checkpoint/timeline/types.ts +65 -0
  251. package/frontend/lib/components/checkpoint/timeline/utils.ts +53 -0
  252. package/frontend/lib/components/common/Alert.svelte +139 -0
  253. package/frontend/lib/components/common/AvatarBubble.svelte +56 -0
  254. package/frontend/lib/components/common/Button.svelte +71 -0
  255. package/frontend/lib/components/common/Card.svelte +102 -0
  256. package/frontend/lib/components/common/Checkbox.svelte +48 -0
  257. package/frontend/lib/components/common/Dialog.svelte +249 -0
  258. package/frontend/lib/components/common/FolderBrowser.svelte +843 -0
  259. package/frontend/lib/components/common/Icon.svelte +58 -0
  260. package/frontend/lib/components/common/Input.svelte +72 -0
  261. package/frontend/lib/components/common/Lightbox.svelte +233 -0
  262. package/frontend/lib/components/common/LoadingScreen.svelte +52 -0
  263. package/frontend/lib/components/common/LoadingSpinner.svelte +48 -0
  264. package/frontend/lib/components/common/Modal.svelte +177 -0
  265. package/frontend/lib/components/common/ModalProvider.svelte +28 -0
  266. package/frontend/lib/components/common/ModelSelector.svelte +110 -0
  267. package/frontend/lib/components/common/MonacoEditor.svelte +569 -0
  268. package/frontend/lib/components/common/NotificationToast.svelte +113 -0
  269. package/frontend/lib/components/common/PageTemplate.svelte +76 -0
  270. package/frontend/lib/components/common/ProjectUserAvatars.svelte +79 -0
  271. package/frontend/lib/components/common/Select.svelte +98 -0
  272. package/frontend/lib/components/common/Textarea.svelte +80 -0
  273. package/frontend/lib/components/common/ThemeToggle.svelte +44 -0
  274. package/frontend/lib/components/common/lucide-icons.ts +1642 -0
  275. package/frontend/lib/components/common/material-icons.ts +1082 -0
  276. package/frontend/lib/components/common/xterm/XTerm.svelte +796 -0
  277. package/frontend/lib/components/common/xterm/index.ts +16 -0
  278. package/frontend/lib/components/common/xterm/terminal-config.ts +68 -0
  279. package/frontend/lib/components/common/xterm/types.ts +31 -0
  280. package/frontend/lib/components/common/xterm/xterm-service.ts +353 -0
  281. package/frontend/lib/components/files/FileNode.svelte +384 -0
  282. package/frontend/lib/components/files/FileTree.svelte +681 -0
  283. package/frontend/lib/components/files/FileViewer.svelte +728 -0
  284. package/frontend/lib/components/files/SearchResults.svelte +303 -0
  285. package/frontend/lib/components/git/BranchManager.svelte +458 -0
  286. package/frontend/lib/components/git/ChangesSection.svelte +107 -0
  287. package/frontend/lib/components/git/CommitForm.svelte +76 -0
  288. package/frontend/lib/components/git/ConflictResolver.svelte +158 -0
  289. package/frontend/lib/components/git/DiffViewer.svelte +364 -0
  290. package/frontend/lib/components/git/FileChangeItem.svelte +97 -0
  291. package/frontend/lib/components/git/GitButton.svelte +33 -0
  292. package/frontend/lib/components/git/GitLog.svelte +361 -0
  293. package/frontend/lib/components/git/GitModal.svelte +80 -0
  294. package/frontend/lib/components/history/HistoryModal.svelte +563 -0
  295. package/frontend/lib/components/history/HistoryView.svelte +615 -0
  296. package/frontend/lib/components/index.ts +34 -0
  297. package/frontend/lib/components/preview/browser/BrowserPreview.svelte +549 -0
  298. package/frontend/lib/components/preview/browser/components/Canvas.svelte +1058 -0
  299. package/frontend/lib/components/preview/browser/components/ConsolePanel.svelte +757 -0
  300. package/frontend/lib/components/preview/browser/components/Container.svelte +450 -0
  301. package/frontend/lib/components/preview/browser/components/ContextMenu.svelte +236 -0
  302. package/frontend/lib/components/preview/browser/components/SelectDropdown.svelte +224 -0
  303. package/frontend/lib/components/preview/browser/components/Toolbar.svelte +339 -0
  304. package/frontend/lib/components/preview/browser/components/VirtualCursor.svelte +36 -0
  305. package/frontend/lib/components/preview/browser/core/cleanup.svelte.ts +155 -0
  306. package/frontend/lib/components/preview/browser/core/coordinator.svelte.ts +837 -0
  307. package/frontend/lib/components/preview/browser/core/interactions.svelte.ts +113 -0
  308. package/frontend/lib/components/preview/browser/core/mcp-handlers.svelte.ts +296 -0
  309. package/frontend/lib/components/preview/browser/core/native-ui-handlers.svelte.ts +391 -0
  310. package/frontend/lib/components/preview/browser/core/stream-handler.svelte.ts +231 -0
  311. package/frontend/lib/components/preview/browser/core/tab-manager.svelte.ts +210 -0
  312. package/frontend/lib/components/preview/browser/core/tab-operations.svelte.ts +239 -0
  313. package/frontend/lib/components/preview/index.ts +2 -0
  314. package/frontend/lib/components/settings/SettingsModal.svelte +235 -0
  315. package/frontend/lib/components/settings/SettingsView.svelte +36 -0
  316. package/frontend/lib/components/settings/appearance/AppearanceSettings.svelte +51 -0
  317. package/frontend/lib/components/settings/appearance/LayoutPresetSettings.svelte +160 -0
  318. package/frontend/lib/components/settings/appearance/LayoutPreview.svelte +76 -0
  319. package/frontend/lib/components/settings/engines/AIEnginesSettings.svelte +917 -0
  320. package/frontend/lib/components/settings/general/AdvancedSettings.svelte +187 -0
  321. package/frontend/lib/components/settings/general/DataManagementSettings.svelte +203 -0
  322. package/frontend/lib/components/settings/general/GeneralSettings.svelte +10 -0
  323. package/frontend/lib/components/settings/model/ModelSettings.svelte +357 -0
  324. package/frontend/lib/components/settings/notifications/NotificationSettings.svelte +205 -0
  325. package/frontend/lib/components/settings/user/UserSettings.svelte +197 -0
  326. package/frontend/lib/components/terminal/Terminal.svelte +368 -0
  327. package/frontend/lib/components/terminal/TerminalTabs.svelte +87 -0
  328. package/frontend/lib/components/terminal/TerminalView.svelte +55 -0
  329. package/frontend/lib/components/tunnel/TunnelActive.svelte +142 -0
  330. package/frontend/lib/components/tunnel/TunnelButton.svelte +54 -0
  331. package/frontend/lib/components/tunnel/TunnelInactive.svelte +284 -0
  332. package/frontend/lib/components/tunnel/TunnelModal.svelte +47 -0
  333. package/frontend/lib/components/tunnel/TunnelQRCode.svelte +49 -0
  334. package/frontend/lib/components/workspace/DesktopNavigator.svelte +382 -0
  335. package/frontend/lib/components/workspace/MobileNavigator.svelte +403 -0
  336. package/frontend/lib/components/workspace/PanelContainer.svelte +100 -0
  337. package/frontend/lib/components/workspace/PanelHeader.svelte +505 -0
  338. package/frontend/lib/components/workspace/ViewMenu.svelte +162 -0
  339. package/frontend/lib/components/workspace/WorkspaceLayout.svelte +169 -0
  340. package/frontend/lib/components/workspace/layout/DesktopLayout.svelte +15 -0
  341. package/frontend/lib/components/workspace/layout/MobileLayout.svelte +17 -0
  342. package/frontend/lib/components/workspace/layout/split-pane/Container.svelte +42 -0
  343. package/frontend/lib/components/workspace/layout/split-pane/Handle.svelte +85 -0
  344. package/frontend/lib/components/workspace/layout/split-pane/Layout.svelte +37 -0
  345. package/frontend/lib/components/workspace/panels/ChatPanel.svelte +274 -0
  346. package/frontend/lib/components/workspace/panels/FilesPanel.svelte +1261 -0
  347. package/frontend/lib/components/workspace/panels/GitPanel.svelte +1560 -0
  348. package/frontend/lib/components/workspace/panels/PreviewPanel.svelte +150 -0
  349. package/frontend/lib/components/workspace/panels/TerminalPanel.svelte +73 -0
  350. package/frontend/lib/constants/preview.ts +45 -0
  351. package/frontend/lib/services/chat/chat.service.ts +704 -0
  352. package/frontend/lib/services/chat/index.ts +7 -0
  353. package/frontend/lib/services/notification/global-stream-monitor.ts +86 -0
  354. package/frontend/lib/services/notification/index.ts +8 -0
  355. package/frontend/lib/services/notification/push.service.ts +144 -0
  356. package/frontend/lib/services/notification/sound.service.ts +127 -0
  357. package/frontend/lib/services/preview/browser/browser-console.service.ts +61 -0
  358. package/frontend/lib/services/preview/browser/browser-webcodecs.service.ts +1499 -0
  359. package/frontend/lib/services/preview/browser/mcp-integration.svelte.ts +67 -0
  360. package/frontend/lib/services/preview/index.ts +23 -0
  361. package/frontend/lib/services/project/index.ts +8 -0
  362. package/frontend/lib/services/project/status.service.ts +159 -0
  363. package/frontend/lib/services/snapshot/snapshot.service.ts +47 -0
  364. package/frontend/lib/services/terminal/background/index.ts +130 -0
  365. package/frontend/lib/services/terminal/background/session-restore.ts +274 -0
  366. package/frontend/lib/services/terminal/background/stream-manager.ts +286 -0
  367. package/frontend/lib/services/terminal/index.ts +14 -0
  368. package/frontend/lib/services/terminal/persistence.service.ts +260 -0
  369. package/frontend/lib/services/terminal/project.service.ts +953 -0
  370. package/frontend/lib/services/terminal/session.service.ts +364 -0
  371. package/frontend/lib/services/terminal/terminal.service.ts +369 -0
  372. package/frontend/lib/stores/core/app.svelte.ts +117 -0
  373. package/frontend/lib/stores/core/files.svelte.ts +73 -0
  374. package/frontend/lib/stores/core/presence.svelte.ts +48 -0
  375. package/frontend/lib/stores/core/projects.svelte.ts +317 -0
  376. package/frontend/lib/stores/core/sessions.svelte.ts +383 -0
  377. package/frontend/lib/stores/features/claude-accounts.svelte.ts +58 -0
  378. package/frontend/lib/stores/features/models.svelte.ts +89 -0
  379. package/frontend/lib/stores/features/settings.svelte.ts +87 -0
  380. package/frontend/lib/stores/features/terminal.svelte.ts +701 -0
  381. package/frontend/lib/stores/features/tunnel.svelte.ts +161 -0
  382. package/frontend/lib/stores/features/user.svelte.ts +96 -0
  383. package/frontend/lib/stores/ui/chat-input.svelte.ts +57 -0
  384. package/frontend/lib/stores/ui/chat-model.svelte.ts +61 -0
  385. package/frontend/lib/stores/ui/dialog.svelte.ts +59 -0
  386. package/frontend/lib/stores/ui/edit-mode.svelte.ts +214 -0
  387. package/frontend/lib/stores/ui/notification.svelte.ts +166 -0
  388. package/frontend/lib/stores/ui/settings-modal.svelte.ts +88 -0
  389. package/frontend/lib/stores/ui/theme.svelte.ts +179 -0
  390. package/frontend/lib/stores/ui/workspace.svelte.ts +754 -0
  391. package/frontend/lib/types/native-ui.ts +73 -0
  392. package/frontend/lib/utils/chat/date-separator.ts +39 -0
  393. package/frontend/lib/utils/chat/message-grouper.ts +219 -0
  394. package/frontend/lib/utils/chat/message-processor.ts +135 -0
  395. package/frontend/lib/utils/chat/tool-handler.ts +161 -0
  396. package/frontend/lib/utils/chat/virtual-scroll.svelte.ts +142 -0
  397. package/frontend/lib/utils/click-outside.ts +20 -0
  398. package/frontend/lib/utils/context-manager.ts +257 -0
  399. package/frontend/lib/utils/file-icon-mappings.ts +769 -0
  400. package/frontend/lib/utils/folder-icon-mappings.ts +1030 -0
  401. package/frontend/lib/utils/git-status.ts +68 -0
  402. package/frontend/lib/utils/platform.ts +113 -0
  403. package/frontend/lib/utils/port-check.ts +65 -0
  404. package/frontend/lib/utils/terminalFormatter.ts +207 -0
  405. package/frontend/lib/utils/theme.ts +6 -0
  406. package/frontend/lib/utils/tree-visualizer.ts +320 -0
  407. package/frontend/lib/utils/ws.ts +44 -0
  408. package/frontend/main.ts +13 -0
  409. package/index.html +70 -0
  410. package/package.json +111 -0
  411. package/scripts/generate-icons.ts +87 -0
  412. package/scripts/pre-publish-check.sh +142 -0
  413. package/scripts/setup-hooks.sh +134 -0
  414. package/scripts/validate-branch-name.sh +47 -0
  415. package/scripts/validate-commit-msg.sh +42 -0
  416. package/shared/constants/engines.ts +134 -0
  417. package/shared/types/database/connection.ts +16 -0
  418. package/shared/types/database/index.ts +6 -0
  419. package/shared/types/database/schema.ts +141 -0
  420. package/shared/types/engine/index.ts +45 -0
  421. package/shared/types/filesystem/index.ts +22 -0
  422. package/shared/types/git.ts +171 -0
  423. package/shared/types/messaging/index.ts +239 -0
  424. package/shared/types/messaging/tool.ts +526 -0
  425. package/shared/types/network/api.ts +18 -0
  426. package/shared/types/network/index.ts +5 -0
  427. package/shared/types/stores/app.ts +23 -0
  428. package/shared/types/stores/dialog.ts +21 -0
  429. package/shared/types/stores/index.ts +3 -0
  430. package/shared/types/stores/settings.ts +15 -0
  431. package/shared/types/terminal/index.ts +44 -0
  432. package/shared/types/ui/components.ts +61 -0
  433. package/shared/types/ui/icons.ts +23 -0
  434. package/shared/types/ui/index.ts +22 -0
  435. package/shared/types/ui/notifications.ts +14 -0
  436. package/shared/types/ui/theme.ts +12 -0
  437. package/shared/types/websocket/index.ts +43 -0
  438. package/shared/types/window.d.ts +13 -0
  439. package/shared/utils/anonymous-user.ts +168 -0
  440. package/shared/utils/async.ts +10 -0
  441. package/shared/utils/diff-calculator.ts +184 -0
  442. package/shared/utils/file-type-detection.ts +166 -0
  443. package/shared/utils/logger.ts +158 -0
  444. package/shared/utils/message-formatter.ts +79 -0
  445. package/shared/utils/path.ts +47 -0
  446. package/shared/utils/ws-client.ts +768 -0
  447. package/shared/utils/ws-server.ts +660 -0
  448. package/static/audio/notification.ogg +0 -0
  449. package/static/favicon.svg +8 -0
  450. package/static/fonts/dm-sans/dm-sans-italic-latin-ext.woff2 +0 -0
  451. package/static/fonts/dm-sans/dm-sans-italic-latin.woff2 +0 -0
  452. package/static/fonts/dm-sans/dm-sans-normal-latin-ext.woff2 +0 -0
  453. package/static/fonts/dm-sans/dm-sans-normal-latin.woff2 +0 -0
  454. package/static/fonts/dm-sans.css +96 -0
  455. package/svelte.config.js +20 -0
  456. package/vite.config.ts +33 -0
@@ -0,0 +1,704 @@
1
+ /**
2
+ * WebSocket-based Chat Service
3
+ *
4
+ * Modern WebSocket implementation for real-time chat communication
5
+ * Replaces the old SSE-based chat service
6
+ *
7
+ * Key features:
8
+ * - Sequence-based deduplication to prevent duplicate messages
9
+ * - Stream reconnection after browser refresh / project switch
10
+ * - Robust cancel that works even after refresh
11
+ * - Proper presence synchronization
12
+ */
13
+
14
+ import { appState } from '$frontend/lib/stores/core/app.svelte';
15
+ import { chatModelState } from '$frontend/lib/stores/ui/chat-model.svelte';
16
+ import { projectState } from '$frontend/lib/stores/core/projects.svelte';
17
+ import { sessionState, setCurrentSession, createSession, updateSession } from '$frontend/lib/stores/core/sessions.svelte';
18
+ import { addNotification } from '$frontend/lib/stores/ui/notification.svelte';
19
+ import { userStore } from '$frontend/lib/stores/features/user.svelte';
20
+ import { SDK_CONFIG, parseModelId } from '$shared/constants/engines';
21
+ import type { ChatServiceOptions } from '$shared/types/messaging';
22
+ import { buildMetadataFromTransport } from '$shared/utils/message-formatter';
23
+ import { debug } from '$shared/utils/logger';
24
+ import ws from '$frontend/lib/utils/ws';
25
+
26
+ class ChatService {
27
+ private activeProcessId: string | null = null;
28
+ private streamCompleted: boolean = false;
29
+ private currentSessionId: string | null = null;
30
+ private lastEventSeq = new Map<string, number>(); // Sequence-based deduplication
31
+ private cancelledProcessIds = new Set<string>(); // Track ALL cancelled streams to ignore late events
32
+ private reconnected: boolean = false; // Whether we've reconnected to an active stream
33
+
34
+ static loadingTexts: string[] = [
35
+ 'thinking', 'processing', 'analyzing', 'calculating', 'computing',
36
+ 'strategizing', 'learningpatterns', 'updatingweights', 'finetuning',
37
+ 'adaptingmodels', 'trainingnetworks', 'evaluatingoptions', 'planningactions',
38
+ 'executingplans', 'simulatingscenarios', 'predictingoutcomes', 'scanningenvironment',
39
+ 'monitoringsignals', 'processinginputs', 'adjustingparameters', 'optimizing',
40
+ 'generatingresponses', 'refininglogic', 'recognizingpatterns', 'synthesizinginformation',
41
+ 'runninginference', 'validatingoutputs', 'modulatingresponse', 'updatingmemory',
42
+ 'switchingcontext', 'resolvingconflicts', 'allocatingresources', 'prioritizingtasks',
43
+ 'developingawareness', 'buildingstrategies', 'assessingscenarios', 'integratingdata',
44
+ 'bootingreasoning', 'activatingmodules', 'triggeringaction', 'deployinglogic',
45
+ 'maintainingstate', 'clearingcache', 'updating', 'reflecting', 'syncinglogic',
46
+ 'connectingdots', 'compilingideas', 'brainstorming', 'schedulingtasks'
47
+ ].map(text => text + '...');
48
+
49
+ static placeholderTexts: string[] = [
50
+ // Creating new projects
51
+ 'Create a full-stack e-commerce platform with Next.js, Stripe, and PostgreSQL',
52
+ 'Build a real-time chat application using Socket.io with room support and typing indicators',
53
+ 'Create a SaaS dashboard with user management, billing, and analytics',
54
+ // ... (keep the same placeholder texts as before)
55
+ ];
56
+
57
+ constructor() {
58
+ this.setupWebSocketHandlers();
59
+ }
60
+
61
+ /**
62
+ * Check if event should be skipped (sequence-based deduplication)
63
+ */
64
+ private shouldSkipEvent(processId: string, seq: number | undefined): boolean {
65
+ if (seq === undefined || seq === null) return false;
66
+
67
+ const lastSeq = this.lastEventSeq.get(processId) || 0;
68
+ if (seq <= lastSeq) {
69
+ // Skip duplicate
70
+ return true;
71
+ }
72
+
73
+ this.lastEventSeq.set(processId, seq);
74
+ return false;
75
+ }
76
+
77
+ /**
78
+ * Setup WebSocket event handlers
79
+ */
80
+ private setupWebSocketHandlers(): void {
81
+ // Session available event - reset stream state if we were streaming from the old session.
82
+ // With session-scoped routing, this is mostly a safety measure.
83
+ ws.on('sessions:session-available', () => {
84
+ // No-op: with chat session rooms, events are already scoped.
85
+ // Users stay in their current session until they explicitly switch.
86
+ });
87
+
88
+ // Connection event - received by ALL users in the project
89
+ ws.on('chat:connection', (data) => {
90
+ if (this.shouldSkipEvent(data.processId, data.seq)) return;
91
+ // Ignore events from a locally cancelled stream
92
+ if (data.processId && this.cancelledProcessIds.has(data.processId)) return;
93
+
94
+ this.activeProcessId = data.processId;
95
+ this.streamCompleted = false;
96
+ });
97
+
98
+ // Message event
99
+ ws.on('chat:message', (data) => {
100
+ if (this.shouldSkipEvent(data.processId, data.seq)) return;
101
+ // Ignore events from a locally cancelled stream
102
+ if (data.processId && this.cancelledProcessIds.has(data.processId)) return;
103
+
104
+ this.handleMessageEvent(data);
105
+ });
106
+
107
+ // Partial message event (streaming)
108
+ ws.on('chat:partial', (data) => {
109
+ if (this.shouldSkipEvent(data.processId, data.seq)) return;
110
+ // Ignore events from a locally cancelled stream
111
+ if (data.processId && this.cancelledProcessIds.has(data.processId)) return;
112
+
113
+ this.handlePartialEvent(data);
114
+ });
115
+
116
+ // Notification event
117
+ ws.on('chat:notification', (data) => {
118
+ // Notifications don't have processId, use a global key
119
+ if (this.shouldSkipEvent('notification', data.seq)) return;
120
+
121
+ if (data.notification) {
122
+ const notif = data.notification;
123
+ addNotification({
124
+ type: notif.type as any,
125
+ title: notif.title,
126
+ message: notif.message,
127
+ duration: notif.type === 'warning' ? 7000 : 5000
128
+ });
129
+ }
130
+ });
131
+
132
+ // Complete event
133
+ ws.on('chat:complete', async (data) => {
134
+ if (this.shouldSkipEvent(data.processId, data.seq)) return;
135
+ // Ignore late events from a locally cancelled stream
136
+ if (data.processId && this.cancelledProcessIds.has(data.processId)) return;
137
+
138
+ this.streamCompleted = true;
139
+ this.reconnected = false;
140
+ appState.isLoading = false;
141
+ appState.isCancelling = false;
142
+
143
+ // Stream completed successfully — all old cancelled streams' events
144
+ // have definitely been delivered by now, so clear the blacklist.
145
+ this.cancelledProcessIds.clear();
146
+
147
+ // Don't reload messages - they're already added via chat:message events
148
+ // Reloading would cause duplicates
149
+
150
+ // Notifications handled by GlobalStreamMonitor via chat:stream-finished
151
+ });
152
+
153
+ // Cancelled event - broadcast to ALL collaborators when any user cancels
154
+ ws.on('chat:cancelled', async (data) => {
155
+ // Track the cancelled processId so late-arriving events are blocked.
156
+ // This handles the case where a collaborator initiated the cancel
157
+ // (so our local cancelRequest was not called).
158
+ if (data.processId) {
159
+ this.cancelledProcessIds.add(data.processId);
160
+ }
161
+ this.streamCompleted = true;
162
+ this.reconnected = false;
163
+ this.activeProcessId = null;
164
+ appState.isLoading = false;
165
+ appState.isCancelling = false;
166
+
167
+ // Notifications handled by GlobalStreamMonitor via chat:stream-finished
168
+ });
169
+
170
+ // Error event
171
+ ws.on('chat:error', async (data) => {
172
+ if (this.shouldSkipEvent(data.processId, data.seq)) return;
173
+ if (this.streamCompleted) return;
174
+ // Ignore late error events from a locally cancelled stream
175
+ if (data.processId && this.cancelledProcessIds.has(data.processId)) return;
176
+
177
+ // Mark completed immediately to block any duplicate error events that may arrive
178
+ // (e.g. from multiple subscriptions or late-arriving events with different processId/seq)
179
+ this.streamCompleted = true;
180
+ this.reconnected = false;
181
+ appState.isLoading = false;
182
+ appState.isCancelling = false;
183
+
184
+ // Don't show notification for cancel-triggered errors
185
+ if (data.error === 'Stream cancelled') return;
186
+
187
+ // Remove any remaining stream_event messages (streaming placeholders that won't be finalized).
188
+ // The actual error bubble is now emitted as a chat:message from the backend and saved to DB,
189
+ // so it persists across browser refresh. No need to inject a synthetic bubble here.
190
+ for (let i = sessionState.messages.length - 1; i >= 0; i--) {
191
+ const msg = sessionState.messages[i] as any;
192
+ if (msg.type === 'stream_event') {
193
+ sessionState.messages.splice(i, 1);
194
+ }
195
+ }
196
+
197
+ addNotification({
198
+ type: 'error',
199
+ title: 'AI Engine Error',
200
+ message: data.error,
201
+ duration: 5000
202
+ });
203
+ });
204
+ }
205
+
206
+ /**
207
+ * Reconnect to an active stream after browser refresh or project switch.
208
+ * This re-subscribes the connection to receive live stream events.
209
+ * Called from catchupActiveStream in ChatInput.
210
+ */
211
+ reconnectToStream(chatSessionId: string, processId: string): void {
212
+ debug.log('chat', 'Reconnecting to active stream:', { chatSessionId, processId });
213
+
214
+ // Set up local state so events are processed and cancel works
215
+ this.activeProcessId = processId;
216
+ this.currentSessionId = chatSessionId;
217
+ this.streamCompleted = false;
218
+ this.cancelledProcessIds.clear();
219
+ this.reconnected = true;
220
+
221
+ // Tell backend to re-subscribe this connection to the stream
222
+ ws.emit('chat:reconnect', {
223
+ chatSessionId
224
+ });
225
+ }
226
+
227
+ /**
228
+ * Send a message using WebSocket
229
+ */
230
+ async sendMessage(
231
+ message: string,
232
+ options: ChatServiceOptions = {}
233
+ ): Promise<void> {
234
+ if ((!message.trim() && !options.attachedFiles?.length) || appState.isLoading) return;
235
+
236
+ // Check if project is selected
237
+ if (!projectState.currentProject) {
238
+ addNotification({
239
+ type: 'warning',
240
+ title: 'No Project Selected',
241
+ message: 'Please select a project from the sidebar before sending messages',
242
+ duration: 3000
243
+ });
244
+ return;
245
+ }
246
+
247
+ const userMessage = message.trim();
248
+
249
+ // Create a new session if none exists
250
+ if (!sessionState.currentSession) {
251
+ const newSession = await createSession(
252
+ projectState.currentProject.id,
253
+ 'New Chat Session'
254
+ );
255
+ if (newSession) {
256
+ await setCurrentSession(newSession);
257
+ } else {
258
+ addNotification({
259
+ type: 'error',
260
+ title: 'Session Creation Failed',
261
+ message: 'Failed to create chat session. Please try again.',
262
+ duration: 5000
263
+ });
264
+ return;
265
+ }
266
+ }
267
+
268
+ // Ensure we have a valid session before proceeding
269
+ if (!sessionState.currentSession?.id) {
270
+ addNotification({
271
+ type: 'error',
272
+ title: 'No Valid Session',
273
+ message: 'No valid chat session available. Please refresh and try again.',
274
+ duration: 5000
275
+ });
276
+ return;
277
+ }
278
+
279
+ // Set loading state
280
+ appState.isLoading = true;
281
+ appState.isCancelling = false;
282
+ this.streamCompleted = false;
283
+ this.reconnected = false;
284
+ this.currentSessionId = sessionState.currentSession.id;
285
+ // DON'T clear cancelledProcessIds — late events from previously cancelled
286
+ // streams must still be blocked. The set is cleared on stream complete.
287
+ // Clear sequence tracking for new stream
288
+ this.lastEventSeq.clear();
289
+
290
+ // Clean up stale stream_events from any previous cancelled streams.
291
+ // These linger because cancel doesn't remove them, and they cause
292
+ // wrong insertion positions for new reasoning/text streams.
293
+ this.cleanupStreamEvents();
294
+
295
+ try {
296
+ // Build message content (text + optional file attachments)
297
+ let messageContent: any = userMessage;
298
+ if (options.attachedFiles && options.attachedFiles.length > 0) {
299
+ const contentBlocks: any[] = [];
300
+ // Add file attachments first
301
+ for (const file of options.attachedFiles) {
302
+ if (file.type === 'image') {
303
+ contentBlocks.push({
304
+ type: 'image',
305
+ source: { type: 'base64', media_type: file.mediaType, data: file.data }
306
+ });
307
+ } else {
308
+ contentBlocks.push({
309
+ type: 'document',
310
+ source: { type: 'base64', media_type: file.mediaType, data: file.data },
311
+ title: file.fileName
312
+ });
313
+ }
314
+ }
315
+ // Add text block
316
+ if (userMessage) {
317
+ contentBlocks.push({ type: 'text', text: userMessage });
318
+ }
319
+ messageContent = contentBlocks;
320
+ }
321
+
322
+ // Create SDKUserMessage format for prompt
323
+ const sdkUserMessage = {
324
+ type: 'user' as const,
325
+ uuid: crypto.randomUUID(),
326
+ session_id: sessionState.currentSession.id,
327
+ parent_tool_use_id: null,
328
+ message: {
329
+ role: 'user' as const,
330
+ content: messageContent
331
+ }
332
+ };
333
+
334
+ // Optimistic UI: show user message immediately (before server confirms)
335
+ const optimisticMessage = {
336
+ ...sdkUserMessage,
337
+ _optimistic: true,
338
+ metadata: buildMetadataFromTransport({
339
+ timestamp: new Date().toISOString(),
340
+ sender_id: userStore.currentUser?.id || null,
341
+ sender_name: userStore.currentUser?.name || null,
342
+ })
343
+ };
344
+ (sessionState.messages as any[]).push(optimisticMessage);
345
+
346
+ // Parse engine and model from the local chat model state (isolated from Settings)
347
+ const { engine, modelId } = parseModelId(chatModelState.model);
348
+
349
+ // Capture selected engine/model/account before sending
350
+ const selectedEngine = chatModelState.engine || engine;
351
+ const selectedModel = chatModelState.model;
352
+ const selectedAccountId = chatModelState.claudeAccountId;
353
+
354
+ // Send WebSocket message to start streaming
355
+ ws.emit('chat:stream', {
356
+ sessionId: crypto.randomUUID(), // ephemeral session ID for this stream
357
+ chatSessionId: sessionState.currentSession.id,
358
+ projectPath: projectState.currentProject?.path || '',
359
+ prompt: sdkUserMessage,
360
+ messages: sessionState.messages.filter((msg: any) => !msg._optimistic).map(msg => {
361
+ // Convert SDKMessage to API format
362
+ if (msg.type === 'user' && 'message' in msg) {
363
+ return {
364
+ role: msg.message.role,
365
+ content: typeof msg.message.content === 'string' ? msg.message.content : JSON.stringify(msg.message.content)
366
+ };
367
+ } else if (msg.type === 'assistant' && 'message' in msg) {
368
+ return {
369
+ role: msg.message.role,
370
+ content: Array.isArray(msg.message.content)
371
+ ? msg.message.content.map(c => c.type === 'text' ? c.text : JSON.stringify(c)).join(' ')
372
+ : JSON.stringify(msg.message.content)
373
+ };
374
+ }
375
+ return {
376
+ role: 'assistant',
377
+ content: ''
378
+ };
379
+ }).filter(msg => msg.content),
380
+ engine: selectedEngine,
381
+ model: modelId,
382
+ temperature: SDK_CONFIG.DEFAULT_TEMPERATURE,
383
+ senderId: userStore.currentUser?.id,
384
+ senderName: userStore.currentUser?.name,
385
+ ...(selectedEngine === 'claude-code' && selectedAccountId !== null && { claudeAccountId: selectedAccountId }),
386
+ });
387
+
388
+ // Persist engine/model to frontend session state immediately.
389
+ // Backend also saves to DB (for refresh/project-switch restore),
390
+ // but we update the frontend state here so the $effect in
391
+ // EngineModelPicker can see it without a server round-trip.
392
+ // IMPORTANT: Use updateSession() to update BOTH sessionState.currentSession
393
+ // AND sessionState.sessions[] array. A direct spread on currentSession
394
+ // creates a new object, leaving sessions[] stale — causing the model
395
+ // picker to lose the selection when switching projects and back.
396
+ if (sessionState.currentSession) {
397
+ updateSession({
398
+ ...sessionState.currentSession,
399
+ engine: selectedEngine,
400
+ model: selectedModel,
401
+ ...(selectedEngine === 'claude-code' && selectedAccountId !== null && { claude_account_id: selectedAccountId }),
402
+ });
403
+ }
404
+
405
+ } catch (error) {
406
+ this.handleError(error as Error, options);
407
+ }
408
+ }
409
+
410
+ /**
411
+ * Cancel the current request - works for ANY collaborator, not just the sender
412
+ * Also works after browser refresh since it uses sessionState as fallback
413
+ */
414
+ cancelRequest(): void {
415
+ // Use currentSessionId (set by sender) OR sessionState (available to all collaborators)
416
+ const chatSessionId = this.currentSessionId || sessionState.currentSession?.id;
417
+
418
+ if (chatSessionId) {
419
+ ws.emit('chat:cancel', {
420
+ sessionId: crypto.randomUUID(),
421
+ chatSessionId
422
+ });
423
+ }
424
+
425
+ // Track cancelled processId so late-arriving events are ignored.
426
+ // Use a Set to track ALL cancelled streams (not just the last one),
427
+ // preventing late events from any previously cancelled stream from leaking through.
428
+ if (this.activeProcessId) {
429
+ this.cancelledProcessIds.add(this.activeProcessId);
430
+ }
431
+ this.activeProcessId = null;
432
+ this.currentSessionId = null;
433
+ this.streamCompleted = true;
434
+ this.reconnected = false;
435
+ appState.isLoading = false;
436
+ // Prevent presence effect from re-enabling loading before server confirms cancel
437
+ appState.isCancelling = true;
438
+
439
+ // Clean up stale stream_events from the cancelled stream.
440
+ // Without this, stale stream_events remain in the messages array and cause
441
+ // wrong insertion positions when a new stream starts (e.g., reasoning inserted
442
+ // before a stale non-reasoning stream_event instead of at the end).
443
+ this.cleanupStreamEvents();
444
+
445
+ // Safety timeout: clear isCancelling after 10s if WS confirmation never arrives
446
+ // (e.g., network issues, dropped connection)
447
+ setTimeout(() => {
448
+ if (appState.isCancelling) {
449
+ appState.isCancelling = false;
450
+ }
451
+ }, 10000);
452
+ }
453
+
454
+ /**
455
+ * Reset stream state when switching sessions (e.g. collaborator receiving new-chat).
456
+ * Blocks all stale events from the old stream.
457
+ */
458
+ resetForSessionSwitch(): void {
459
+ if (this.activeProcessId) {
460
+ this.cancelledProcessIds.add(this.activeProcessId);
461
+ }
462
+ this.activeProcessId = null;
463
+ this.currentSessionId = null;
464
+ this.streamCompleted = true;
465
+ this.reconnected = false;
466
+ this.lastEventSeq.clear();
467
+ appState.isLoading = false;
468
+ appState.isCancelling = false;
469
+ }
470
+
471
+ /**
472
+ * Handle message events from stream
473
+ */
474
+ private handleMessageEvent(data: any): void {
475
+ const sdkMessage = data.message;
476
+
477
+ // Early return if no message
478
+ if (!sdkMessage) return;
479
+
480
+ // Ignore messages from a completed/cancelled stream
481
+ if (this.streamCompleted) return;
482
+
483
+ // Early return if no valid session
484
+ if (!sessionState.currentSession?.id) {
485
+ return;
486
+ }
487
+
488
+ // If this is a user message from server, replace the optimistic message
489
+ if (sdkMessage.type === 'user' && sdkMessage.message?.role === 'user') {
490
+ const optimisticIndex = sessionState.messages.findIndex(
491
+ (m: any) => m._optimistic && m.type === 'user' && m.uuid === sdkMessage.uuid
492
+ );
493
+ if (optimisticIndex !== -1) {
494
+ // Replace optimistic with server-confirmed message
495
+ const confirmedMessage = {
496
+ ...sdkMessage,
497
+ metadata: buildMetadataFromTransport(data)
498
+ };
499
+ sessionState.messages[optimisticIndex] = confirmedMessage;
500
+ return;
501
+ }
502
+ }
503
+
504
+ // If this is an assistant message, replace the matching streaming message
505
+ if (sdkMessage.message?.role === 'assistant') {
506
+ const isReasoning = sdkMessage.metadata?.reasoning === true;
507
+ if (isReasoning) {
508
+ // Replace reasoning stream_event IN PLACE to preserve message order
509
+ for (let i = sessionState.messages.length - 1; i >= 0; i--) {
510
+ const msg = sessionState.messages[i] as any;
511
+ if (msg.type === 'stream_event' && msg.metadata?.reasoning) {
512
+ const messageFormatter = {
513
+ ...sdkMessage,
514
+ metadata: buildMetadataFromTransport({ ...data, reasoning: true })
515
+ };
516
+ sessionState.messages[i] = messageFormatter;
517
+ return; // Already replaced in-place, skip push below
518
+ }
519
+ }
520
+ // If no reasoning stream_event found, fall through to push at end
521
+ } else {
522
+ // Remove ALL regular (non-reasoning) stream_events, not just the last one
523
+ // This prevents stale stream_events from remaining when message order varies
524
+ for (let i = sessionState.messages.length - 1; i >= 0; i--) {
525
+ const msg = sessionState.messages[i] as any;
526
+ if (msg.type === 'stream_event' && !msg.metadata?.reasoning) {
527
+ sessionState.messages.splice(i, 1);
528
+ break; // Only remove the most recent one
529
+ }
530
+ }
531
+ }
532
+ }
533
+
534
+ // Deduplicate: skip if a message with the same uuid already exists
535
+ if (sdkMessage.uuid) {
536
+ const alreadyExists = sessionState.messages.some(
537
+ (m: any) => m.uuid === sdkMessage.uuid && m.type === sdkMessage.type && !m._optimistic
538
+ );
539
+ if (alreadyExists) return;
540
+ }
541
+
542
+ // Update UI state (message already saved to DB by server)
543
+ const isReasoning = sdkMessage.metadata?.reasoning === true;
544
+ const messageFormatter = {
545
+ ...sdkMessage,
546
+ metadata: buildMetadataFromTransport({
547
+ ...data,
548
+ ...(isReasoning && { reasoning: true }),
549
+ })
550
+ };
551
+
552
+ // For reasoning messages that couldn't find a matching stream_event,
553
+ // insert BEFORE trailing non-reasoning assistant messages (tools/text)
554
+ // to preserve reasoning-before-tool ordering within the same turn.
555
+ if (isReasoning) {
556
+ let insertIdx = sessionState.messages.length;
557
+ for (let i = sessionState.messages.length - 1; i >= 0; i--) {
558
+ const msg = sessionState.messages[i] as any;
559
+ // Stop at user messages or other reasoning messages — they mark turn boundaries
560
+ if (msg.type === 'user' || (msg.type === 'assistant' && msg.metadata?.reasoning)) break;
561
+ // Insert before non-reasoning assistant messages and non-reasoning stream_events
562
+ if (msg.type === 'assistant' || (msg.type === 'stream_event' && !msg.metadata?.reasoning)) {
563
+ insertIdx = i;
564
+ }
565
+ }
566
+ (sessionState.messages as any[]).splice(insertIdx, 0, messageFormatter);
567
+ } else {
568
+ (sessionState.messages as any[]).push(messageFormatter);
569
+ }
570
+ }
571
+
572
+ /**
573
+ * Handle partial message events (streaming)
574
+ */
575
+ private handlePartialEvent(data: any): void {
576
+ // Ignore partials from a completed/cancelled stream
577
+ if (this.streamCompleted) return;
578
+
579
+ // Early return if no valid session
580
+ if (!sessionState.currentSession?.id) {
581
+ return;
582
+ }
583
+
584
+ const { eventType, partialText } = data;
585
+ const isReasoning = data.reasoning === true;
586
+
587
+ if (eventType === 'start') {
588
+ if (isReasoning) {
589
+ // Check if there's already a reasoning stream_event (from catchup)
590
+ const existingReasoning = sessionState.messages.find(
591
+ (m: any) => m.type === 'stream_event' && m.metadata?.reasoning && m.processId === data.processId
592
+ );
593
+ if (existingReasoning) {
594
+ // Already have one, just update it
595
+ (existingReasoning as any).partialText = partialText || '';
596
+ return;
597
+ }
598
+ } else {
599
+ // Check if there's already a regular stream_event for this process (from catchup)
600
+ const existingStream = sessionState.messages.find(
601
+ (m: any) => m.type === 'stream_event' && !m.metadata?.reasoning && m.processId === data.processId
602
+ );
603
+ if (existingStream) {
604
+ // Already have one, just update it
605
+ (existingStream as any).partialText = partialText || '';
606
+ return;
607
+ }
608
+ }
609
+
610
+ // Create new streaming message (reasoning or text)
611
+ const streamingMessage = {
612
+ type: 'stream_event' as const,
613
+ processId: data.processId,
614
+ partialText: partialText || '',
615
+ metadata: buildMetadataFromTransport({
616
+ timestamp: data.timestamp,
617
+ ...(isReasoning && { reasoning: true }),
618
+ })
619
+ };
620
+
621
+ if (isReasoning) {
622
+ // Insert reasoning stream BEFORE any existing non-reasoning stream_event
623
+ // to preserve logical order (reasoning comes before text in the model's output)
624
+ const textStreamIdx = (sessionState.messages as any[]).findIndex(
625
+ (m: any) => m.type === 'stream_event' && !m.metadata?.reasoning
626
+ );
627
+ if (textStreamIdx >= 0) {
628
+ (sessionState.messages as any[]).splice(textStreamIdx, 0, streamingMessage);
629
+ } else {
630
+ (sessionState.messages as any[]).push(streamingMessage);
631
+ }
632
+ } else {
633
+ // Text stream always goes to the end (after any reasoning)
634
+ (sessionState.messages as any[]).push(streamingMessage);
635
+ }
636
+ } else if (eventType === 'update') {
637
+ if (isReasoning) {
638
+ // Update reasoning streaming message — find last reasoning stream_event
639
+ for (let i = sessionState.messages.length - 1; i >= 0; i--) {
640
+ const msg = sessionState.messages[i] as any;
641
+ if (msg.type === 'stream_event' && msg.metadata?.reasoning) {
642
+ msg.partialText = partialText || '';
643
+ break;
644
+ }
645
+ }
646
+ } else {
647
+ // Update regular text streaming message — find matching stream_event
648
+ // Search backwards to find the most recent non-reasoning stream_event
649
+ for (let i = sessionState.messages.length - 1; i >= 0; i--) {
650
+ const msg = sessionState.messages[i] as any;
651
+ if (msg.type === 'stream_event' && !msg.metadata?.reasoning) {
652
+ msg.partialText = partialText || '';
653
+ break;
654
+ }
655
+ }
656
+ }
657
+ }
658
+ // Note: 'end' event is not needed - streaming message will be replaced by final message in handleMessageEvent
659
+ }
660
+
661
+ /**
662
+ * Remove all stream_event messages from the messages array.
663
+ * Called on cancel and new message send to prevent stale streaming
664
+ * placeholders from causing wrong insertion positions.
665
+ */
666
+ private cleanupStreamEvents(): void {
667
+ for (let i = sessionState.messages.length - 1; i >= 0; i--) {
668
+ if ((sessionState.messages[i] as any).type === 'stream_event') {
669
+ sessionState.messages.splice(i, 1);
670
+ }
671
+ }
672
+ }
673
+
674
+ /**
675
+ * Handle general errors
676
+ */
677
+ private handleError(
678
+ error: Error,
679
+ options: ChatServiceOptions
680
+ ): void {
681
+ let errorMessage = 'Failed to connect to AI engine';
682
+ if (error.message.includes('Project path')) {
683
+ errorMessage = error.message;
684
+ } else {
685
+ errorMessage = error.message;
686
+ }
687
+
688
+ addNotification({
689
+ type: 'error',
690
+ title: 'Chat Error',
691
+ message: errorMessage,
692
+ duration: 5000
693
+ });
694
+
695
+ options.onError?.(error);
696
+ appState.isLoading = false;
697
+ }
698
+ }
699
+
700
+ // Export singleton instance
701
+ export const chatService = new ChatService();
702
+
703
+ // Export class for static methods
704
+ export { ChatService };