@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,944 @@
1
+ /**
2
+ * Backend WebSocket Server Singleton - Optimized
3
+ *
4
+ * High-performance WebSocket event emitter with:
5
+ * - Room-based architecture for O(1) project/user lookup
6
+ * - Stable connection identity via WeakMap on raw Bun ServerWebSocket
7
+ * - External state storage (not on ephemeral Elysia wrapper objects)
8
+ * - Backpressure handling to prevent OOM
9
+ * - Connection health monitoring
10
+ * - Clean emit API: ws.emit.project(), ws.emit.user(), ws.emit.global()
11
+ *
12
+ * CRITICAL: Elysia creates NEW ElysiaWS wrapper objects per handler call.
13
+ * We use `(ws as any).raw` (the underlying persistent Bun ServerWebSocket)
14
+ * as a stable identity key via WeakMap, and store all connection state
15
+ * in external Maps (connectionState). No properties are set on the wrapper.
16
+ *
17
+ * Usage:
18
+ * ```ts
19
+ * import { ws } from '$backend/lib/ws'
20
+ *
21
+ * // Project-specific event
22
+ * ws.emit.project(projectId, 'terminal:output', { content });
23
+ *
24
+ * // User-specific event
25
+ * ws.emit.user(userId, 'chat:error', { error });
26
+ *
27
+ * // System-wide event
28
+ * ws.emit.global('system:update', { version });
29
+ * ```
30
+ */
31
+
32
+ import type { WSAPI } from '$backend/ws';
33
+ import type { WSConnection } from '$shared/utils/ws-server';
34
+ import { encodeBinaryMessage, isBinaryAction } from '$shared/utils/ws-server';
35
+ import { debug } from '$shared/utils/logger';
36
+
37
+ /**
38
+ * Performance configuration
39
+ */
40
+ const CONFIG = {
41
+ /** Maximum send buffer before dropping frames (bytes) */
42
+ MAX_BUFFER_SIZE: 1024 * 1024, // 1MB
43
+ /** Enable backpressure handling */
44
+ ENABLE_BACKPRESSURE: true,
45
+ /** Log dropped frames */
46
+ LOG_DROPPED_FRAMES: true
47
+ };
48
+
49
+ /**
50
+ * Metrics for monitoring
51
+ */
52
+ interface WSMetrics {
53
+ totalConnections: number;
54
+ activeProjects: number;
55
+ activeUsers: number;
56
+ droppedFrames: number;
57
+ totalEmits: number;
58
+ }
59
+
60
+ /**
61
+ * External connection state (stored separately from ephemeral ws wrappers)
62
+ */
63
+ interface ConnectionState {
64
+ userId: string | null;
65
+ projectId: string | null;
66
+ /** Chat session IDs this connection is subscribed to */
67
+ chatSessionIds: Set<string>;
68
+ /** Cleanup functions called automatically on unregister (connection close) */
69
+ cleanups: Set<() => void>;
70
+ }
71
+
72
+ /**
73
+ * High-Performance WebSocket Server Manager
74
+ *
75
+ * Uses WeakMap<rawSocket, wsId> for stable identity across Elysia wrapper instances,
76
+ * and external Maps for all connection state (userId, projectId).
77
+ */
78
+ class WSServer {
79
+ /** All connections by ID */
80
+ private connections = new Map<string, WSConnection>();
81
+
82
+ /** Raw Bun ServerWebSocket → wsId for stable identity across wrapper instances */
83
+ private rawToId = new WeakMap<object, string>();
84
+
85
+ /** External connection state (userId, projectId) keyed by wsId */
86
+ private connectionState = new Map<string, ConnectionState>();
87
+
88
+ /** Room-based: Project ID → Map<wsId, WSConnection> */
89
+ private projectRooms = new Map<string, Map<string, WSConnection>>();
90
+
91
+ /** Room-based: User ID → Map<wsId, WSConnection> */
92
+ private userConnections = new Map<string, Map<string, WSConnection>>();
93
+
94
+ /** Room-based: Chat Session ID → Map<wsId, WSConnection> */
95
+ private chatSessionRooms = new Map<string, Map<string, WSConnection>>();
96
+
97
+ /** Project membership: Project ID → Set<userId> (persists across project switches) */
98
+ private projectMembers = new Map<string, Set<string>>();
99
+
100
+ /** Metrics tracking */
101
+ private metrics: WSMetrics = {
102
+ totalConnections: 0,
103
+ activeProjects: 0,
104
+ activeUsers: 0,
105
+ droppedFrames: 0,
106
+ totalEmits: 0
107
+ };
108
+
109
+ /**
110
+ * Resolve stable wsId from any ws wrapper using the raw Bun ServerWebSocket.
111
+ * Returns null if the connection has never been registered.
112
+ */
113
+ private resolveId(conn: WSConnection): string | null {
114
+ const raw = (conn as any).raw;
115
+ if (!raw) return null;
116
+ return this.rawToId.get(raw) ?? null;
117
+ }
118
+
119
+ /**
120
+ * Register a WebSocket connection (idempotent based on raw socket identity).
121
+ * Called automatically by Elysia WebSocket plugin on connection open.
122
+ * Also called lazily by ensureRegistered() if a message arrives before
123
+ * the async open handler completes (race condition with await import).
124
+ */
125
+ register(conn: WSConnection): string {
126
+ // Check if already registered via raw socket identity
127
+ const existingId = this.resolveId(conn);
128
+ if (existingId && this.connections.has(existingId)) {
129
+ // Update stored wrapper reference (so room maps point to latest wrapper)
130
+ this.connections.set(existingId, conn);
131
+ return existingId;
132
+ }
133
+
134
+ // New connection: create fresh registration
135
+ const raw = (conn as any).raw;
136
+ if (!raw) {
137
+ throw new Error('No raw socket found on ws wrapper - cannot register connection');
138
+ }
139
+
140
+ const id = crypto.randomUUID();
141
+ this.rawToId.set(raw, id);
142
+ this.connections.set(id, conn);
143
+ this.connectionState.set(id, { userId: null, projectId: null, chatSessionIds: new Set(), cleanups: new Set() });
144
+
145
+ this.metrics.totalConnections = this.connections.size;
146
+ debug.log('websocket', `Connection registered: ${id} (total: ${this.connections.size})`);
147
+ return id;
148
+ }
149
+
150
+ /**
151
+ * Ensure a connection is registered, hydrated, and return its wsId.
152
+ * Handles the race condition where message handlers fire before the async
153
+ * open handler completes its await import('$backend/lib/utils/ws').
154
+ */
155
+ private ensureRegistered(conn: WSConnection): string {
156
+ const existingId = this.resolveId(conn);
157
+ if (existingId && this.connections.has(existingId)) {
158
+ this.connections.set(existingId, conn);
159
+ return existingId;
160
+ }
161
+ return this.register(conn);
162
+ }
163
+
164
+ /**
165
+ * Unregister a WebSocket connection.
166
+ * Called automatically on connection close.
167
+ * Runs all registered cleanup functions before removing state.
168
+ */
169
+ unregister(conn: WSConnection): void {
170
+ const id = this.resolveId(conn);
171
+ if (!id) return;
172
+
173
+ // Get state from external storage (NOT from wrapper which may be stale)
174
+ const state = this.connectionState.get(id);
175
+
176
+ // Run all registered cleanup functions
177
+ if (state?.cleanups.size) {
178
+ for (const cleanup of state.cleanups) {
179
+ try { cleanup(); } catch { /* ignore cleanup errors */ }
180
+ }
181
+ debug.log('websocket', `Ran ${state.cleanups.size} cleanup(s) for connection ${id}`);
182
+ }
183
+
184
+ // Remove from project room
185
+ const projectId = state?.projectId;
186
+ if (projectId) {
187
+ const room = this.projectRooms.get(projectId);
188
+ if (room) {
189
+ room.delete(id);
190
+ if (room.size === 0) {
191
+ this.projectRooms.delete(projectId);
192
+ }
193
+ }
194
+ }
195
+
196
+ // Remove from all chat session rooms
197
+ if (state?.chatSessionIds) {
198
+ for (const csId of state.chatSessionIds) {
199
+ const csRoom = this.chatSessionRooms.get(csId);
200
+ if (csRoom) {
201
+ csRoom.delete(id);
202
+ if (csRoom.size === 0) {
203
+ this.chatSessionRooms.delete(csId);
204
+ }
205
+ }
206
+ }
207
+ }
208
+
209
+ // Remove from user connections
210
+ const userId = state?.userId;
211
+ if (userId) {
212
+ const userConns = this.userConnections.get(userId);
213
+ if (userConns) {
214
+ userConns.delete(id);
215
+ if (userConns.size === 0) {
216
+ this.userConnections.delete(userId);
217
+ }
218
+ }
219
+ }
220
+
221
+ // Clean up all state
222
+ this.connections.delete(id);
223
+ this.connectionState.delete(id);
224
+
225
+ // Clean up raw socket mapping
226
+ const raw = (conn as any).raw;
227
+ if (raw) {
228
+ this.rawToId.delete(raw);
229
+ }
230
+
231
+ this.metrics.totalConnections = this.connections.size;
232
+ this.metrics.activeProjects = this.projectRooms.size;
233
+ this.metrics.activeUsers = this.userConnections.size;
234
+
235
+ debug.log('websocket', `Connection unregistered: ${id} (total: ${this.connections.size})`);
236
+ }
237
+
238
+ /**
239
+ * Register a cleanup function for a connection.
240
+ * Will be called automatically when the connection is unregistered (closed).
241
+ * Uses raw socket identity so it works across Elysia wrapper instances.
242
+ */
243
+ addCleanup(conn: WSConnection, cleanup: () => void): void {
244
+ const wsId = this.ensureRegistered(conn);
245
+ const state = this.connectionState.get(wsId);
246
+ state?.cleanups.add(cleanup);
247
+ }
248
+
249
+ /**
250
+ * Remove a previously registered cleanup function.
251
+ */
252
+ removeCleanup(conn: WSConnection, cleanup: () => void): void {
253
+ const id = this.resolveId(conn);
254
+ if (!id) return;
255
+ const state = this.connectionState.get(id);
256
+ state?.cleanups.delete(cleanup);
257
+ }
258
+
259
+ /**
260
+ * Set user ID for a connection.
261
+ * Tracks which user owns this connection (for per-user events).
262
+ */
263
+ setUser(conn: WSConnection, userId: string | null): void {
264
+ const wsId = this.ensureRegistered(conn);
265
+
266
+ // Get OLD userId from external state (NOT from wrapper which may be stale)
267
+ const state = this.connectionState.get(wsId);
268
+ const oldUserId = state?.userId;
269
+
270
+ // Remove from old user group by ID
271
+ if (oldUserId && oldUserId !== userId) {
272
+ const oldUserConns = this.userConnections.get(oldUserId);
273
+ if (oldUserConns) {
274
+ oldUserConns.delete(wsId);
275
+ if (oldUserConns.size === 0) {
276
+ this.userConnections.delete(oldUserId);
277
+ }
278
+ }
279
+ }
280
+
281
+ // Update connection state
282
+ if (state) {
283
+ state.userId = userId;
284
+ }
285
+
286
+ // Add to new user group by ID (Map.set replaces if same key → no duplicates)
287
+ if (userId) {
288
+ if (!this.userConnections.has(userId)) {
289
+ this.userConnections.set(userId, new Map());
290
+ }
291
+ this.userConnections.get(userId)!.set(wsId, conn);
292
+
293
+ // If connection already has a project, track membership
294
+ // (handles case where setUser is called after setProject)
295
+ const currentProjectId = state?.projectId;
296
+ if (currentProjectId) {
297
+ if (!this.projectMembers.has(currentProjectId)) {
298
+ this.projectMembers.set(currentProjectId, new Set());
299
+ }
300
+ this.projectMembers.get(currentProjectId)!.add(userId);
301
+ }
302
+ }
303
+
304
+ this.metrics.activeUsers = this.userConnections.size;
305
+ debug.log('websocket', `Connection ${wsId} set to user: ${userId}`);
306
+ }
307
+
308
+ /**
309
+ * Get user ID for a connection.
310
+ * Throws if not set - ensures single source of truth.
311
+ * Client must call ws:set-context before sending events.
312
+ */
313
+ getUserId(conn: WSConnection): string {
314
+ const id = this.resolveId(conn);
315
+ if (!id) {
316
+ throw new Error('Connection not registered. Cannot resolve userId.');
317
+ }
318
+ const userId = this.connectionState.get(id)?.userId;
319
+ if (!userId) {
320
+ throw new Error('No userId on connection. Client must call ws:set-context first.');
321
+ }
322
+ return userId;
323
+ }
324
+
325
+ /**
326
+ * Set current project for a connection.
327
+ * Moves connection between project rooms efficiently using ID-based lookup.
328
+ */
329
+ setProject(conn: WSConnection, projectId: string | null): void {
330
+ const wsId = this.ensureRegistered(conn);
331
+
332
+ // Get OLD projectId from external state (NOT from wrapper which may be stale)
333
+ const state = this.connectionState.get(wsId);
334
+ const oldProjectId = state?.projectId;
335
+
336
+ // Remove from old project room by ID
337
+ if (oldProjectId && oldProjectId !== projectId) {
338
+ const oldRoom = this.projectRooms.get(oldProjectId);
339
+ if (oldRoom) {
340
+ oldRoom.delete(wsId);
341
+ if (oldRoom.size === 0) {
342
+ this.projectRooms.delete(oldProjectId);
343
+ }
344
+ }
345
+ }
346
+
347
+ // Update connection state
348
+ if (state) {
349
+ state.projectId = projectId;
350
+ }
351
+
352
+ // Add to new project room by ID (Map.set replaces if same key → no duplicates)
353
+ if (projectId) {
354
+ if (!this.projectRooms.has(projectId)) {
355
+ this.projectRooms.set(projectId, new Map());
356
+ }
357
+ this.projectRooms.get(projectId)!.set(wsId, conn);
358
+
359
+ // Track project membership (persists across project switches)
360
+ // Used by emit.projectMembers() for cross-project notifications
361
+ const userId = state?.userId;
362
+ if (userId) {
363
+ if (!this.projectMembers.has(projectId)) {
364
+ this.projectMembers.set(projectId, new Set());
365
+ }
366
+ this.projectMembers.get(projectId)!.add(userId);
367
+ }
368
+ }
369
+
370
+ this.metrics.activeProjects = this.projectRooms.size;
371
+ debug.log('websocket', `Connection ${wsId} set to project: ${projectId}`);
372
+ }
373
+
374
+ /**
375
+ * Get project ID for a connection.
376
+ * Throws if not set - ensures single source of truth.
377
+ * Client must call ws:set-context before sending events.
378
+ */
379
+ getProjectId(conn: WSConnection): string {
380
+ const id = this.resolveId(conn);
381
+ if (!id) {
382
+ throw new Error('Connection not registered. Cannot resolve projectId.');
383
+ }
384
+ const projectId = this.connectionState.get(id)?.projectId;
385
+ if (!projectId) {
386
+ throw new Error('No projectId on connection. Client must call ws:set-context first.');
387
+ }
388
+ return projectId;
389
+ }
390
+
391
+ /**
392
+ * Join a chat session room.
393
+ * A connection can be in multiple chat session rooms simultaneously,
394
+ * but typically only one at a time (leave old before joining new).
395
+ */
396
+ joinChatSession(conn: WSConnection, chatSessionId: string): void {
397
+ const wsId = this.ensureRegistered(conn);
398
+ const state = this.connectionState.get(wsId);
399
+
400
+ if (!state) return;
401
+
402
+ // Add to chat session room
403
+ if (!this.chatSessionRooms.has(chatSessionId)) {
404
+ this.chatSessionRooms.set(chatSessionId, new Map());
405
+ }
406
+ this.chatSessionRooms.get(chatSessionId)!.set(wsId, conn);
407
+
408
+ // Track on connection state
409
+ state.chatSessionIds.add(chatSessionId);
410
+
411
+ debug.log('websocket', `Connection ${wsId} joined chat session: ${chatSessionId}`);
412
+ }
413
+
414
+ /**
415
+ * Leave a chat session room.
416
+ */
417
+ leaveChatSession(conn: WSConnection, chatSessionId: string): void {
418
+ const wsId = this.resolveId(conn);
419
+ if (!wsId) return;
420
+
421
+ const state = this.connectionState.get(wsId);
422
+ if (state) {
423
+ state.chatSessionIds.delete(chatSessionId);
424
+ }
425
+
426
+ const room = this.chatSessionRooms.get(chatSessionId);
427
+ if (room) {
428
+ room.delete(wsId);
429
+ if (room.size === 0) {
430
+ this.chatSessionRooms.delete(chatSessionId);
431
+ }
432
+ }
433
+
434
+ debug.log('websocket', `Connection ${wsId} left chat session: ${chatSessionId}`);
435
+ }
436
+
437
+ /**
438
+ * Leave all chat session rooms for a connection.
439
+ */
440
+ leaveAllChatSessions(conn: WSConnection): void {
441
+ const wsId = this.resolveId(conn);
442
+ if (!wsId) return;
443
+
444
+ const state = this.connectionState.get(wsId);
445
+ if (!state) return;
446
+
447
+ for (const csId of state.chatSessionIds) {
448
+ const room = this.chatSessionRooms.get(csId);
449
+ if (room) {
450
+ room.delete(wsId);
451
+ if (room.size === 0) {
452
+ this.chatSessionRooms.delete(csId);
453
+ }
454
+ }
455
+ }
456
+ state.chatSessionIds.clear();
457
+ }
458
+
459
+ /**
460
+ * Get user info for all connections in a chat session room.
461
+ * Returns deduplicated list of { userId, userName } for users currently viewing the session.
462
+ */
463
+ getChatSessionUsers(chatSessionId: string): { userId: string; userName: string }[] {
464
+ const room = this.chatSessionRooms.get(chatSessionId);
465
+ if (!room || room.size === 0) return [];
466
+
467
+ const seen = new Map<string, string>(); // userId → userName
468
+ for (const [wsId] of room) {
469
+ const state = this.connectionState.get(wsId);
470
+ if (state?.userId) {
471
+ // Resolve userName from user connections or fallback
472
+ if (!seen.has(state.userId)) {
473
+ seen.set(state.userId, state.userId); // default to userId
474
+ }
475
+ }
476
+ }
477
+ return Array.from(seen.entries()).map(([userId, userName]) => ({ userId, userName }));
478
+ }
479
+
480
+ /**
481
+ * Get all chat session IDs that have active connections for a given project.
482
+ * Used for per-session presence data.
483
+ */
484
+ getProjectChatSessions(projectId: string): Map<string, { userId: string }[]> {
485
+ const result = new Map<string, { userId: string }[]>();
486
+
487
+ // Iterate through all chat session rooms
488
+ for (const [chatSessionId, room] of this.chatSessionRooms) {
489
+ const users: { userId: string }[] = [];
490
+ const seenUsers = new Set<string>();
491
+
492
+ for (const [wsId] of room) {
493
+ const state = this.connectionState.get(wsId);
494
+ // Only include connections that belong to this project
495
+ if (state?.projectId === projectId && state?.userId && !seenUsers.has(state.userId)) {
496
+ seenUsers.add(state.userId);
497
+ users.push({ userId: state.userId });
498
+ }
499
+ }
500
+
501
+ if (users.length > 0) {
502
+ result.set(chatSessionId, users);
503
+ }
504
+ }
505
+
506
+ return result;
507
+ }
508
+
509
+ /**
510
+ * Get raw connection state for a connection.
511
+ * Used internally by ws:set-context to read back current values.
512
+ */
513
+ getConnectionState(conn: WSConnection): ConnectionState | undefined {
514
+ const id = this.resolveId(conn);
515
+ if (!id) return undefined;
516
+ return this.connectionState.get(id);
517
+ }
518
+
519
+ /**
520
+ * Check if connection can receive data (backpressure check)
521
+ */
522
+ private canSend(conn: WSConnection): boolean {
523
+ if (!CONFIG.ENABLE_BACKPRESSURE) return true;
524
+
525
+ // Check WebSocket ready state
526
+ if (conn.readyState !== 1) return false;
527
+
528
+ // Check buffer size if available (Bun/uWebSockets)
529
+ const rawWs = conn as any;
530
+ if (typeof rawWs.getBufferedAmount === 'function') {
531
+ const buffered = rawWs.getBufferedAmount();
532
+ if (buffered > CONFIG.MAX_BUFFER_SIZE) {
533
+ if (CONFIG.LOG_DROPPED_FRAMES) {
534
+ this.metrics.droppedFrames++;
535
+ const id = this.resolveId(conn) || 'unknown';
536
+ debug.warn('websocket', `Backpressure: dropping frame for ${id} (buffer: ${buffered})`);
537
+ }
538
+ return false;
539
+ }
540
+ }
541
+
542
+ return true;
543
+ }
544
+
545
+ /**
546
+ * Send message to a single connection with backpressure handling
547
+ */
548
+ private sendToConnection(conn: WSConnection, message: string | ArrayBuffer): boolean {
549
+ if (!this.canSend(conn)) {
550
+ return false;
551
+ }
552
+
553
+ try {
554
+ // For binary data (ArrayBuffer), wrap in Buffer for Bun/Elysia compatibility
555
+ if (message instanceof ArrayBuffer) {
556
+ conn.send(Buffer.from(message));
557
+ } else {
558
+ conn.send(message);
559
+ }
560
+ return true;
561
+ } catch (err) {
562
+ const id = this.resolveId(conn) || 'unknown';
563
+ debug.error('websocket', `Send error for ${id}:`, err);
564
+ return false;
565
+ }
566
+ }
567
+
568
+ /**
569
+ * Clean stale connections from a room Map.
570
+ * Returns number of stale connections removed.
571
+ */
572
+ private cleanStaleFromRoom(room: Map<string, WSConnection>): number {
573
+ const staleIds: string[] = [];
574
+ for (const [wsId, wsConn] of room) {
575
+ if (wsConn.readyState !== 1) {
576
+ staleIds.push(wsId);
577
+ }
578
+ }
579
+ for (const staleId of staleIds) {
580
+ const staleConn = room.get(staleId);
581
+ room.delete(staleId);
582
+ if (staleConn) {
583
+ this.unregister(staleConn);
584
+ }
585
+ }
586
+ return staleIds.length;
587
+ }
588
+
589
+ /**
590
+ * Emit API - Clean interface for sending events
591
+ */
592
+ emit = {
593
+ /**
594
+ * Emit event to all connections in a project room
595
+ */
596
+ project: <K extends keyof WSAPI['server']>(
597
+ projectId: string,
598
+ event: K,
599
+ payload: WSAPI['server'][K]
600
+ ): void => {
601
+ this.metrics.totalEmits++;
602
+
603
+ const room = this.projectRooms.get(projectId);
604
+ if (!room || room.size === 0) {
605
+ debug.warn('websocket', `No connections in project room: ${projectId} for event: ${String(event)}`);
606
+ return;
607
+ }
608
+
609
+ // Clean stale connections before sending
610
+ const staleCount = this.cleanStaleFromRoom(room);
611
+ if (staleCount > 0) {
612
+ debug.log('websocket', `Cleaned ${staleCount} stale connections from project room ${projectId}`);
613
+ }
614
+
615
+ if (room.size === 0) {
616
+ this.projectRooms.delete(projectId);
617
+ return;
618
+ }
619
+
620
+ // Check if payload contains binary data - use binary encoding if so
621
+ const eventStr = String(event);
622
+ const hasBinary = isBinaryAction(eventStr, payload);
623
+ const message = hasBinary
624
+ ? encodeBinaryMessage(eventStr, payload)
625
+ : JSON.stringify({ action: event, payload });
626
+
627
+ let sentCount = 0;
628
+
629
+ for (const wsConn of room.values()) {
630
+ if (this.sendToConnection(wsConn, message)) {
631
+ sentCount++;
632
+ }
633
+ }
634
+
635
+ debug.log('websocket', `Emitted ${eventStr}${hasBinary ? ' (binary)' : ''} to ${sentCount}/${room.size} connections in project ${projectId}`);
636
+ },
637
+
638
+ /**
639
+ * Emit event to all connections of a specific user
640
+ */
641
+ user: <K extends keyof WSAPI['server']>(
642
+ userId: string,
643
+ event: K,
644
+ payload: WSAPI['server'][K]
645
+ ): void => {
646
+ this.metrics.totalEmits++;
647
+
648
+ const userConns = this.userConnections.get(userId);
649
+ if (!userConns || userConns.size === 0) {
650
+ debug.warn('websocket', `No connections for user: ${userId} for event: ${String(event)}`);
651
+ return;
652
+ }
653
+
654
+ // Clean stale connections
655
+ const staleCount = this.cleanStaleFromRoom(userConns);
656
+ if (staleCount > 0) {
657
+ debug.log('websocket', `Cleaned ${staleCount} stale user connections for user ${userId}`);
658
+ }
659
+
660
+ if (userConns.size === 0) {
661
+ this.userConnections.delete(userId);
662
+ return;
663
+ }
664
+
665
+ // Check if payload contains binary data - use binary encoding if so
666
+ const eventStr = String(event);
667
+ const hasBinary = isBinaryAction(eventStr, payload);
668
+ const message = hasBinary
669
+ ? encodeBinaryMessage(eventStr, payload)
670
+ : JSON.stringify({ action: event, payload });
671
+
672
+ let sentCount = 0;
673
+
674
+ for (const wsConn of userConns.values()) {
675
+ if (this.sendToConnection(wsConn, message)) {
676
+ sentCount++;
677
+ }
678
+ }
679
+
680
+ debug.log('websocket', `Emitted ${eventStr}${hasBinary ? ' (binary)' : ''} to ${sentCount}/${userConns.size} connections for user ${userId}`);
681
+ },
682
+
683
+ /**
684
+ * Emit event to all users who have ever joined a project.
685
+ * Unlike emit.project() which only reaches connections currently in the room,
686
+ * this reaches ALL connections of users who have been associated with the project
687
+ * (even if they switched to a different project).
688
+ * Used for cross-project notifications (e.g., stream finished).
689
+ */
690
+ projectMembers: <K extends keyof WSAPI['server']>(
691
+ projectId: string,
692
+ event: K,
693
+ payload: WSAPI['server'][K]
694
+ ): void => {
695
+ this.metrics.totalEmits++;
696
+
697
+ const memberIds = this.projectMembers.get(projectId);
698
+ if (!memberIds || memberIds.size === 0) {
699
+ debug.warn('websocket', `No members for project: ${projectId} for event: ${String(event)}`);
700
+ return;
701
+ }
702
+
703
+ // Check if payload contains binary data - use binary encoding if so
704
+ const eventStr = String(event);
705
+ const hasBinary = isBinaryAction(eventStr, payload);
706
+ const message = hasBinary
707
+ ? encodeBinaryMessage(eventStr, payload)
708
+ : JSON.stringify({ action: event, payload });
709
+
710
+ let sentCount = 0;
711
+ let totalConns = 0;
712
+
713
+ // Send to all connections of all member users
714
+ for (const userId of memberIds) {
715
+ const userConns = this.userConnections.get(userId);
716
+ if (!userConns || userConns.size === 0) continue;
717
+
718
+ // Clean stale connections
719
+ this.cleanStaleFromRoom(userConns);
720
+ if (userConns.size === 0) {
721
+ this.userConnections.delete(userId);
722
+ continue;
723
+ }
724
+
725
+ totalConns += userConns.size;
726
+ for (const wsConn of userConns.values()) {
727
+ if (this.sendToConnection(wsConn, message)) {
728
+ sentCount++;
729
+ }
730
+ }
731
+ }
732
+
733
+ debug.log('websocket', `Emitted ${eventStr}${hasBinary ? ' (binary)' : ''} to ${sentCount}/${totalConns} connections of ${memberIds.size} members in project ${projectId}`);
734
+ },
735
+
736
+ /**
737
+ * Emit event to all connections in a chat session room.
738
+ * Used for session-scoped chat events (message, partial, complete, etc.)
739
+ */
740
+ chatSession: <K extends keyof WSAPI['server']>(
741
+ chatSessionId: string,
742
+ event: K,
743
+ payload: WSAPI['server'][K]
744
+ ): void => {
745
+ this.metrics.totalEmits++;
746
+
747
+ const room = this.chatSessionRooms.get(chatSessionId);
748
+ if (!room || room.size === 0) {
749
+ debug.warn('websocket', `No connections in chat session room: ${chatSessionId} for event: ${String(event)}`);
750
+ return;
751
+ }
752
+
753
+ // Clean stale connections before sending
754
+ const staleCount = this.cleanStaleFromRoom(room);
755
+ if (staleCount > 0) {
756
+ debug.log('websocket', `Cleaned ${staleCount} stale connections from chat session room ${chatSessionId}`);
757
+ }
758
+
759
+ if (room.size === 0) {
760
+ this.chatSessionRooms.delete(chatSessionId);
761
+ return;
762
+ }
763
+
764
+ const eventStr = String(event);
765
+ const hasBinary = isBinaryAction(eventStr, payload);
766
+ const message = hasBinary
767
+ ? encodeBinaryMessage(eventStr, payload)
768
+ : JSON.stringify({ action: event, payload });
769
+
770
+ let sentCount = 0;
771
+
772
+ for (const wsConn of room.values()) {
773
+ if (this.sendToConnection(wsConn, message)) {
774
+ sentCount++;
775
+ }
776
+ }
777
+
778
+ debug.log('websocket', `Emitted ${eventStr}${hasBinary ? ' (binary)' : ''} to ${sentCount}/${room.size} connections in chat session ${chatSessionId.slice(0, 8)}`);
779
+ },
780
+
781
+ /**
782
+ * Emit event to all connections (broadcast)
783
+ */
784
+ global: <K extends keyof WSAPI['server']>(
785
+ event: K,
786
+ payload: WSAPI['server'][K]
787
+ ): void => {
788
+ this.metrics.totalEmits++;
789
+
790
+ // Check if payload contains binary data - use binary encoding if so
791
+ const eventStr = String(event);
792
+ const hasBinary = isBinaryAction(eventStr, payload);
793
+ const message = hasBinary
794
+ ? encodeBinaryMessage(eventStr, payload)
795
+ : JSON.stringify({ action: event, payload });
796
+
797
+ let sentCount = 0;
798
+
799
+ for (const wsConn of this.connections.values()) {
800
+ if (this.sendToConnection(wsConn, message)) {
801
+ sentCount++;
802
+ }
803
+ }
804
+
805
+ debug.log('websocket', `Emitted ${eventStr}${hasBinary ? ' (binary)' : ''} to ${sentCount}/${this.connections.size} connections (global)`);
806
+ }
807
+ };
808
+
809
+ /**
810
+ * Emit binary event to project room
811
+ */
812
+ emitBinary = {
813
+ /**
814
+ * Emit binary data to all connections in a project room
815
+ */
816
+ project: <K extends keyof WSAPI['server']>(
817
+ projectId: string,
818
+ _event: K,
819
+ binaryMessage: ArrayBuffer
820
+ ): void => {
821
+ this.metrics.totalEmits++;
822
+
823
+ const room = this.projectRooms.get(projectId);
824
+ if (!room || room.size === 0) return;
825
+
826
+ let sentCount = 0;
827
+ for (const ws of room.values()) {
828
+ if (this.sendToConnection(ws, binaryMessage)) {
829
+ sentCount++;
830
+ }
831
+ }
832
+
833
+ debug.log('websocket', `Emitted binary to ${sentCount}/${room.size} connections in project ${projectId}`);
834
+ },
835
+
836
+ /**
837
+ * Emit binary data to all connections of a specific user
838
+ */
839
+ user: <K extends keyof WSAPI['server']>(
840
+ userId: string,
841
+ _event: K,
842
+ binaryMessage: ArrayBuffer
843
+ ): void => {
844
+ this.metrics.totalEmits++;
845
+
846
+ const userConns = this.userConnections.get(userId);
847
+ if (!userConns || userConns.size === 0) return;
848
+
849
+ let sentCount = 0;
850
+ for (const ws of userConns.values()) {
851
+ if (this.sendToConnection(ws, binaryMessage)) {
852
+ sentCount++;
853
+ }
854
+ }
855
+
856
+ debug.log('websocket', `Emitted binary to ${sentCount}/${userConns.size} connections for user ${userId}`);
857
+ },
858
+
859
+ /**
860
+ * Emit binary data to all connections (broadcast)
861
+ */
862
+ global: <K extends keyof WSAPI['server']>(
863
+ _event: K,
864
+ binaryMessage: ArrayBuffer
865
+ ): void => {
866
+ this.metrics.totalEmits++;
867
+
868
+ let sentCount = 0;
869
+ for (const ws of this.connections.values()) {
870
+ if (this.sendToConnection(ws, binaryMessage)) {
871
+ sentCount++;
872
+ }
873
+ }
874
+
875
+ debug.log('websocket', `Emitted binary to ${sentCount}/${this.connections.size} connections (global)`);
876
+ }
877
+ };
878
+
879
+ /**
880
+ * Get all connections (optionally filtered by project)
881
+ */
882
+ getConnections(projectId?: string): WSConnection[] {
883
+ if (projectId) {
884
+ const room = this.projectRooms.get(projectId);
885
+ return room ? Array.from(room.values()) : [];
886
+ }
887
+ return Array.from(this.connections.values());
888
+ }
889
+
890
+ /**
891
+ * Get connection count (optionally filtered by project)
892
+ */
893
+ getConnectionCount(projectId?: string): number {
894
+ if (projectId) {
895
+ return this.projectRooms.get(projectId)?.size || 0;
896
+ }
897
+ return this.connections.size;
898
+ }
899
+
900
+ /**
901
+ * Get all active project IDs
902
+ */
903
+ getActiveProjects(): Set<string> {
904
+ return new Set(this.projectRooms.keys());
905
+ }
906
+
907
+ /**
908
+ * Get all active user IDs
909
+ */
910
+ getActiveUsers(): Set<string> {
911
+ return new Set(this.userConnections.keys());
912
+ }
913
+
914
+ /**
915
+ * Get current metrics for monitoring
916
+ */
917
+ getMetrics(): WSMetrics {
918
+ return { ...this.metrics };
919
+ }
920
+
921
+ /**
922
+ * Get room sizes for debugging
923
+ */
924
+ getRoomSizes(): { projects: Map<string, number>; users: Map<string, number> } {
925
+ const projects = new Map<string, number>();
926
+ const users = new Map<string, number>();
927
+
928
+ for (const [id, room] of this.projectRooms) {
929
+ projects.set(id, room.size);
930
+ }
931
+
932
+ for (const [id, conns] of this.userConnections) {
933
+ users.set(id, conns.size);
934
+ }
935
+
936
+ return { projects, users };
937
+ }
938
+ }
939
+
940
+ /**
941
+ * Singleton instance
942
+ * Import this in any backend file to emit events
943
+ */
944
+ export const ws = new WSServer();