@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,1505 @@
1
+ # WebSocket API
2
+
3
+ Type-safe WebSocket API for real-time communication between frontend and backend. Handles both request-response (HTTP-style) and real-time event streaming patterns.
4
+
5
+ ## 📚 Table of Contents
6
+
7
+ - [Overview](#overview)
8
+ - [Quick Start](#quick-start)
9
+ - [Architecture](#architecture)
10
+ - [Creating WebSocket Handlers](#creating-websocket-handlers)
11
+ - [Patterns](#patterns)
12
+ - [API Reference](#api-reference)
13
+ - [Examples](#examples)
14
+ - [Best Practices](#best-practices)
15
+ - [Troubleshooting](#troubleshooting)
16
+
17
+ ---
18
+
19
+ ## Overview
20
+
21
+ **What is WebSocket API?**
22
+ Type-safe WebSocket communication system with two distinct patterns: HTTP-style request-response and real-time event streaming.
23
+
24
+ **Features:**
25
+ - Type-safe with TypeScript
26
+ - Auto-validation with TypeBox
27
+ - Dual pattern support (HTTP + Events)
28
+ - Scope-based event filtering (user, project, global)
29
+ - Auto-wrap response format
30
+ - Router-based modular architecture
31
+ - **Consistent API**: `.emit()` for sending, `.on()` for listening (both frontend & backend)
32
+
33
+ **API Naming Consistency:**
34
+ ```typescript
35
+ // Frontend
36
+ ws.emit('action', data) // Send action to server
37
+ ws.on('event', handler) // Listen to events from server
38
+
39
+ // Backend
40
+ .on('action', handler) // Listen to actions from client
41
+ .emit('event', schema) // Declare events to send to client
42
+ ws.emit.user() // Emit event to specific user
43
+ ws.emit.project() // Emit event to project room (current viewers)
44
+ ws.emit.projectMembers() // Emit event to all users who have the project
45
+ ws.emit.global() // Emit event to all users
46
+ ```
47
+
48
+ **Why `.emit()` everywhere?**
49
+ - Consistent: "emit" always means "send something"
50
+ - Clear: "on" always means "listen to something"
51
+ - Symmetric: Frontend and backend use same terminology
52
+
53
+ ---
54
+
55
+ ## Quick Start
56
+
57
+ ### 1. Create a New Module
58
+
59
+ Create a new folder in `./` (e.g., `todo/`) and create handlers:
60
+
61
+ **File: `./todo/crud.ts`**
62
+ ```typescript
63
+ import { t } from 'elysia';
64
+ import { createRouter } from '$shared/utils/ws-server';
65
+
66
+ export const crudHandler = createRouter()
67
+ // HTTP-style request-response
68
+ .http('todo:create', {
69
+ data: t.Object({
70
+ title: t.String(),
71
+ description: t.Optional(t.String())
72
+ }),
73
+ response: t.Object({
74
+ id: t.String(),
75
+ title: t.String(),
76
+ description: t.String(),
77
+ createdAt: t.String()
78
+ })
79
+ }, async ({ data }) => {
80
+ // Business logic
81
+ const todo = await createTodo(data);
82
+ return todo; // Direct return, no manual wrapping
83
+ })
84
+
85
+ // Real-time event listener
86
+ .on('todo:mark-complete', {
87
+ data: t.Object({
88
+ id: t.String()
89
+ })
90
+ }, async ({ data, conn }) => {
91
+ const todo = await markComplete(data.id);
92
+
93
+ // Emit event to all users in project
94
+ const projectId = ws.getProjectId(conn);
95
+ ws.emit.project(projectId, 'todo:completed', {
96
+ id: todo.id,
97
+ completedAt: new Date().toISOString()
98
+ });
99
+ })
100
+
101
+ // Event declarations (Server → Client)
102
+ .emit('todo:completed', t.Object({
103
+ id: t.String(),
104
+ completedAt: t.String()
105
+ }));
106
+ ```
107
+
108
+ **File: `./todo/index.ts`**
109
+ ```typescript
110
+ import { createRouter } from '$shared/utils/ws-server';
111
+ import { crudHandler } from './crud';
112
+
113
+ export const todoRouter = createRouter()
114
+ .merge(crudHandler);
115
+ ```
116
+
117
+ ### 2. Register the Module
118
+
119
+ Add to `./index.ts`:
120
+
121
+ ```typescript
122
+ import { todoRouter } from './todo';
123
+
124
+ export const wsRouter = createRouter()
125
+ // ... existing routers
126
+ .merge(todoRouter);
127
+ ```
128
+
129
+ ### 3. Use in Frontend
130
+
131
+ ```typescript
132
+ import ws from '$lib/utils/ws';
133
+
134
+ // HTTP-style request-response
135
+ try {
136
+ const todo = await ws.http('todo:create', {
137
+ title: 'Buy milk',
138
+ description: 'Get 2 liters'
139
+ });
140
+ console.log('Created:', todo.id); // Type-safe access
141
+ } catch (error) {
142
+ console.error('Failed:', error.message);
143
+ }
144
+
145
+ // Real-time event listener
146
+ const cleanup = ws.on('todo:completed', (data) => {
147
+ console.log('Todo completed:', data.id);
148
+ updateUI(data);
149
+ });
150
+
151
+ // Emit action (no response expected)
152
+ ws.emit('todo:mark-complete', { id: 'todo-123' });
153
+
154
+ // Cleanup when component unmounts
155
+ cleanup();
156
+ ```
157
+
158
+ ### 4. Done!
159
+
160
+ HTTP endpoint available as: `todo:create`
161
+ Event listener available as: `todo:mark-complete` (frontend emits this)
162
+ Event emission available as: `todo:completed` (backend emits this)
163
+
164
+ ---
165
+
166
+ ## Architecture
167
+
168
+ ```
169
+ backend/ws/
170
+ ├── index.ts # Main router - merges all module routers
171
+ ├── types.ts # Shared types
172
+ ├── chat/ # Chat module
173
+ │ ├── index.ts # Module router
174
+ │ ├── stream.ts # Streaming events
175
+ │ └── background.ts # Background processing
176
+ ├── terminal/ # Terminal module
177
+ │ ├── index.ts # Module router
178
+ │ ├── session.ts # Session management (HTTP)
179
+ │ ├── stream.ts # Real-time I/O (Events)
180
+ │ └── persistence.ts # Stream persistence
181
+ ├── files/ # File operations module
182
+ │ ├── index.ts # Module router
183
+ │ ├── read.ts # Read operations (HTTP)
184
+ │ ├── write.ts # Write operations (HTTP)
185
+ │ └── search.ts # Search operations (HTTP)
186
+ ├── preview/ # Browser preview module
187
+ │ └── browser/
188
+ │ ├── tab.ts # Tab management (HTTP)
189
+ │ ├── interact.ts # User interactions (Events)
190
+ │ ├── webcodecs.ts # Video streaming (Events)
191
+ │ └── ...
192
+ └── ... (other modules)
193
+ ```
194
+
195
+ ### Module Organization
196
+
197
+ **Simple modules** (1-2 files):
198
+ ```
199
+ module/
200
+ ├── index.ts # All handlers here
201
+ └── ...
202
+ ```
203
+
204
+ **Complex modules** (multiple concerns):
205
+ ```
206
+ module/
207
+ ├── index.ts # Module router
208
+ ├── crud.ts # CRUD operations (HTTP)
209
+ ├── stream.ts # Real-time events
210
+ └── helpers.ts # Shared utilities
211
+ ```
212
+
213
+ ### Data Flow
214
+
215
+ ```
216
+ 1. Handler Definition (module/handler.ts)
217
+ └─> createRouter() with .http() or .on()
218
+
219
+ 2. Module Router (module/index.ts)
220
+ └─> Merge handlers into module router
221
+
222
+ 3. Main Router (index.ts)
223
+ └─> Merge all module routers
224
+
225
+ 4. Frontend (ws.http() or ws.on())
226
+ └─> Type-safe API calls
227
+
228
+ 5. Auto-validation & processing
229
+
230
+ 6. Response (HTTP) or Event emission
231
+ ```
232
+
233
+ ### Key Components
234
+
235
+ **`createRouter()`**
236
+ Factory function to create type-safe WebSocket routers.
237
+
238
+ **`.http(action, config, handler)`**
239
+ Define HTTP-style request-response endpoints.
240
+
241
+ **`.on(action, config, handler)`**
242
+ Define event listeners (Client → Server actions).
243
+
244
+ **`.emit(event, schema)`**
245
+ Declare events (Server → Client notifications).
246
+
247
+ **`ws.emit.user()` / `ws.emit.project()` / `ws.emit.projectMembers()` / `ws.emit.global()`**
248
+ Emit events with scope-based filtering.
249
+
250
+ ---
251
+
252
+ ## Creating WebSocket Handlers
253
+
254
+ ### File Structure
255
+
256
+ Each module should be in its own folder under `./`:
257
+
258
+ 1. **Create a folder**: `./your-module/`
259
+ 2. **Create handler files**: Split by concern (crud.ts, stream.ts, etc.)
260
+ 3. **Create index.ts**: Module router that merges handlers
261
+
262
+ Example:
263
+ ```
264
+ your-module/
265
+ ├── index.ts # Module router
266
+ ├── crud.ts # CRUD operations
267
+ └── stream.ts # Real-time events
268
+ ```
269
+
270
+ ### Handler Types
271
+
272
+ There are three types of handlers:
273
+
274
+ #### 1. HTTP-style Request-Response (`.http()`)
275
+
276
+ Use when you need to get data back from the server.
277
+
278
+ ```typescript
279
+ .http('namespace:action', {
280
+ data: t.Object({
281
+ param: t.String()
282
+ }),
283
+ response: t.Object({
284
+ field1: t.String(),
285
+ field2: t.Number()
286
+ })
287
+ }, async ({ data }) => {
288
+ // Business logic
289
+ const result = await doSomething(data.param);
290
+ return result; // Direct return
291
+ })
292
+ ```
293
+
294
+ **Rules:**
295
+ - Response schema ONLY contains data fields
296
+ - NO `success`, `error` fields in schema
297
+ - Handler returns data directly
298
+ - Handler throws error if failed
299
+ - Server auto-wraps with `{ success, data?, error? }`
300
+
301
+ #### 2. Event Listener (`.on()`)
302
+
303
+ Use when client sends an action without expecting a response.
304
+
305
+ ```typescript
306
+ .on('namespace:action', {
307
+ data: t.Object({
308
+ param: t.String()
309
+ })
310
+ }, async ({ data, conn }) => {
311
+ // Process action
312
+ await doSomething(data.param);
313
+
314
+ // Optionally emit events
315
+ const projectId = ws.getProjectId(conn);
316
+ ws.emit.project(projectId, 'namespace:updated', {
317
+ message: 'Update complete'
318
+ });
319
+ })
320
+ ```
321
+
322
+ **Rules:**
323
+ - Use for actions that don't need response
324
+ - Can emit events to notify clients
325
+ - Access WebSocket connection via `conn`
326
+ - Get identity from connection context: `ws.getProjectId(conn)`, `ws.getUserId(conn)`
327
+
328
+ #### 3. Event Declaration (`.emit()`)
329
+
330
+ Declare events that server can emit to clients.
331
+
332
+ ```typescript
333
+ .emit('namespace:event', t.Object({
334
+ field1: t.String(),
335
+ field2: t.Number()
336
+ }))
337
+ ```
338
+
339
+ **Rules:**
340
+ - Direct schema (NOT wrapped)
341
+ - Declare in ALL files that emit this event
342
+ - Match actual emission in handlers
343
+
344
+ ### Input Schema (TypeBox)
345
+
346
+ Define schema as TypeBox objects:
347
+
348
+ ```typescript
349
+ data: t.Object({
350
+ // Required string
351
+ name: t.String(),
352
+
353
+ // Required number with constraints
354
+ age: t.Number({ minimum: 0, maximum: 150 }),
355
+
356
+ // Optional with default
357
+ format: t.Optional(t.String()),
358
+
359
+ // Literal union (enum-like)
360
+ type: t.Union([
361
+ t.Literal('file'),
362
+ t.Literal('directory')
363
+ ]),
364
+
365
+ // Array
366
+ tags: t.Array(t.String()),
367
+
368
+ // Nested object
369
+ metadata: t.Object({
370
+ author: t.String(),
371
+ date: t.String()
372
+ })
373
+ })
374
+ ```
375
+
376
+ ### Handler Function
377
+
378
+ The handler receives validated arguments:
379
+
380
+ ```typescript
381
+ // HTTP handler
382
+ async ({ data }) => {
383
+ try {
384
+ const result = await someOperation(data);
385
+ return result; // Direct return
386
+ } catch (error) {
387
+ throw new Error('Operation failed'); // Throw on error
388
+ }
389
+ }
390
+
391
+ // Event handler
392
+ async ({ data, conn }) => {
393
+ const projectId = ws.getProjectId(conn);
394
+
395
+ // Process action
396
+ await doSomething(data);
397
+
398
+ // Emit events
399
+ ws.emit.project(projectId, 'namespace:event', {
400
+ message: 'Done'
401
+ });
402
+ }
403
+ ```
404
+
405
+ ### Return Format
406
+
407
+ **HTTP handlers:**
408
+ ```typescript
409
+ // Success: return data directly
410
+ return { field1: 'value', field2: 123 };
411
+
412
+ // Error: throw Error
413
+ throw new Error('Something went wrong');
414
+ ```
415
+
416
+ **Event handlers:**
417
+ ```typescript
418
+ // No return value needed
419
+ // Use ws.emit() to notify clients
420
+ ```
421
+
422
+ ---
423
+
424
+ ## Patterns
425
+
426
+ ### Pattern 1: HTTP Request-Response
427
+
428
+ **Backend:**
429
+ ```typescript
430
+ .http('files:read-file', {
431
+ data: t.Object({
432
+ file_path: t.String()
433
+ }),
434
+ response: t.Object({
435
+ content: t.String(),
436
+ size: t.Number(),
437
+ modified: t.String()
438
+ })
439
+ }, async ({ data }) => {
440
+ const result = await readFile(data.file_path);
441
+ return result;
442
+ })
443
+ ```
444
+
445
+ **Frontend:**
446
+ ```typescript
447
+ try {
448
+ const file = await ws.http('files:read-file', {
449
+ file_path: '/path/to/file.txt'
450
+ });
451
+ console.log('Content:', file.content); // Type-safe
452
+ console.log('Size:', file.size);
453
+ } catch (error) {
454
+ console.error('Failed:', error.message);
455
+ }
456
+ ```
457
+
458
+ **Use when:**
459
+ - Client needs immediate response
460
+ - One-time data query
461
+ - Request-response pattern
462
+
463
+ ---
464
+
465
+ ### Pattern 2: Real-Time Events
466
+
467
+ **Backend:**
468
+ ```typescript
469
+ import { ws } from '$backend/lib/utils/ws';
470
+
471
+ .on('terminal:input', {
472
+ data: t.Object({
473
+ sessionId: t.String(),
474
+ content: t.String()
475
+ })
476
+ }, async ({ data, conn }) => {
477
+ // Process input
478
+ pty.write(data.content);
479
+
480
+ // Emit output to project
481
+ const projectId = ws.getProjectId(conn);
482
+ ws.emit.project(projectId, 'terminal:output', {
483
+ sessionId: data.sessionId,
484
+ content: data.content,
485
+ timestamp: new Date().toISOString()
486
+ });
487
+ })
488
+
489
+ .emit('terminal:output', t.Object({
490
+ sessionId: t.String(),
491
+ content: t.String(),
492
+ timestamp: t.String()
493
+ }))
494
+ ```
495
+
496
+ **Frontend:**
497
+ ```typescript
498
+ // Listen to events
499
+ const cleanup = ws.on('terminal:output', (data) => {
500
+ console.log('Output:', data.content);
501
+ terminal.write(data.content);
502
+ });
503
+
504
+ // Emit action
505
+ ws.emit('terminal:input', {
506
+ sessionId: 'term-123',
507
+ content: 'ls -la\n'
508
+ });
509
+
510
+ // Cleanup
511
+ cleanup();
512
+ ```
513
+
514
+ **Use when:**
515
+ - Server pushes updates to client
516
+ - Real-time streaming data
517
+ - Multiple clients need same update
518
+
519
+ ---
520
+
521
+ ### Pattern 3: Scope-Based Event Filtering
522
+
523
+ Events can be scoped to specific users, projects, or broadcast globally.
524
+
525
+ **User-scoped (only sender receives):**
526
+ ```typescript
527
+ ws.emit.user(userId, 'browser:interacted', {
528
+ action: 'click',
529
+ message: 'Success'
530
+ });
531
+ ```
532
+
533
+ **Project-scoped (all users in same project):**
534
+ ```typescript
535
+ const projectId = ws.getProjectId(conn);
536
+ ws.emit.project(projectId, 'terminal:output', {
537
+ sessionId: 'term-123',
538
+ content: 'Hello World\n',
539
+ timestamp: new Date().toISOString()
540
+ });
541
+ ```
542
+
543
+ **Global broadcast (all connected users):**
544
+ ```typescript
545
+ ws.emit.global('system:update', {
546
+ version: '2.0.0',
547
+ message: 'New version available'
548
+ });
549
+ ```
550
+
551
+ **Critical: Room-Based Architecture**
552
+
553
+ Each connection can only be in **one project room at a time**. When a user switches from Project A to Project B, `ws.setProject()` removes their connection from Project A's room and adds it to Project B's room. This means:
554
+
555
+ - `ws.emit.project(projectA, ...)` will **NOT** reach users who switched away from Project A
556
+ - `ws.emit.projectMembers(projectA, ...)` reaches ALL users who have ever joined Project A, even if they switched to another project
557
+ - `ws.emit.user(userId, ...)` reaches the user regardless of which project room they're in
558
+ - `ws.emit.global(...)` reaches all connected clients regardless of project room
559
+
560
+ **4 Emit Scopes:**
561
+
562
+ ```typescript
563
+ ws.emit.user(userId, event, payload) // → specific user (all their connections)
564
+ ws.emit.project(projectId, event, payload) // → connections currently in the project room
565
+ ws.emit.projectMembers(projectId, event, payload) // → all users who have the project (cross-project)
566
+ ws.emit.global(event, payload) // → all connected clients
567
+ ```
568
+
569
+ **How `projectMembers` tracking works:**
570
+ - When `setProject()` or `setUser()` is called, the userId is recorded in a `projectMembers` Map for that project
571
+ - This membership **persists** even after the user switches to a different project
572
+ - `emit.projectMembers()` iterates over all member userIds, then sends to all their connections via `userConnections`
573
+
574
+ **Scope Decision Guide:**
575
+
576
+ | Event Type | Scope | Who Receives |
577
+ |------------|-------|--------------|
578
+ | User interaction feedback | `user` | Only the user who triggered action |
579
+ | Terminal output | `project` | Users **currently viewing** same project |
580
+ | File changes | `project` | Users **currently viewing** same project |
581
+ | Chat messages | `project` | Users **currently viewing** same project |
582
+ | Stream finished notification | `projectMembers` | All users who have the project (any active project) |
583
+ | System notifications | `global` | All connected users |
584
+
585
+ ---
586
+
587
+ ### Pattern 4: External Event Emission
588
+
589
+ Emit events from external sources (PTY, file watchers, etc.):
590
+
591
+ ```typescript
592
+ import { ws } from '$backend/lib/utils/ws';
593
+
594
+ // PTY output
595
+ pty.onData((output) => {
596
+ ws.emit.project(projectId, 'terminal:output', {
597
+ sessionId,
598
+ content: output,
599
+ timestamp: new Date().toISOString()
600
+ });
601
+ });
602
+
603
+ // File watcher
604
+ watcher.on('change', (path) => {
605
+ ws.emit.project(projectId, 'files:changed', {
606
+ path,
607
+ event: 'change',
608
+ timestamp: new Date().toISOString()
609
+ });
610
+ });
611
+
612
+ // Browser screencast
613
+ browser.on('screencast-frame', (frame) => {
614
+ ws.emit.project(projectId, 'browser:frame', {
615
+ sessionId: frame.sessionId,
616
+ frameId: String(frame.frameId),
617
+ timestamp: frame.timestamp,
618
+ data: frame.data // Uint8Array
619
+ });
620
+ });
621
+ ```
622
+
623
+ **Rules:**
624
+ - Import `ws` singleton
625
+ - Use appropriate scope
626
+ - Capture userId/projectId in closure
627
+ - Declare events in router
628
+
629
+ ---
630
+
631
+ ### Pattern 5: User & Project Tracking
632
+
633
+ **Backend - Connection Setup:**
634
+ ```typescript
635
+ import { ws } from '$backend/lib/utils/ws';
636
+
637
+ app.ws('/ws', {
638
+ open(wsRaw) {
639
+ const conn = wsRaw as WSConnection;
640
+ ws.register(conn);
641
+
642
+ // Set user from auth
643
+ const userId = getUserFromAuth(conn);
644
+ ws.setUser(conn, userId);
645
+ },
646
+ close(wsRaw) {
647
+ const conn = wsRaw as WSConnection;
648
+ ws.unregister(conn);
649
+ }
650
+ });
651
+ ```
652
+
653
+ **Backend - Connection Context (`ws:set-context`):**
654
+
655
+ The frontend automatically sets `userId` and `projectId` on the connection via `ws:set-context` before sending any events. This is handled by the WSClient's `syncContext()` method.
656
+
657
+ ```typescript
658
+ // Frontend sets context (handled automatically by WSClient)
659
+ ws.setUser('user-123'); // Calls ws:set-context { userId }
660
+ ws.setProject('proj-456'); // Calls ws:set-context { projectId }
661
+ ```
662
+
663
+ **Backend - Reading Context in Handlers:**
664
+ ```typescript
665
+ // In any handler, get identity from connection (single source of truth)
666
+ async ({ data, conn }) => {
667
+ const projectId = ws.getProjectId(conn); // Throws if not set
668
+ const userId = ws.getUserId(conn); // Throws if not set
669
+
670
+ // Use for business logic and event emission
671
+ ws.emit.project(projectId, 'namespace:event', { ... });
672
+ ws.emit.user(userId, 'namespace:feedback', { ... });
673
+ }
674
+ ```
675
+
676
+ ---
677
+
678
+ ## API Reference
679
+
680
+ ### Main Router
681
+
682
+ **File: `./index.ts`**
683
+
684
+ ```typescript
685
+ export const wsRouter = createRouter()
686
+ .merge(chatRouter)
687
+ .merge(terminalRouter)
688
+ .merge(filesRouter)
689
+ // ... other modules
690
+
691
+ export type WSAPI = typeof wsRouter['$api'];
692
+ ```
693
+
694
+ ### Router Methods
695
+
696
+ #### `.http(action, config, handler)`
697
+
698
+ Define HTTP-style endpoint.
699
+
700
+ ```typescript
701
+ .http('namespace:action', {
702
+ data: t.Object({ ... }),
703
+ response: t.Object({ ... })
704
+ }, async ({ data }) => {
705
+ return result;
706
+ })
707
+ ```
708
+
709
+ **Parameters:**
710
+ - `action` - Action name (format: `namespace:action`)
711
+ - `config.data` - Input schema (TypeBox)
712
+ - `config.response` - Response schema (TypeBox, data only)
713
+ - `handler` - Async function that returns data or throws error
714
+
715
+ #### `.on(action, config, handler)`
716
+
717
+ Define event listener.
718
+
719
+ ```typescript
720
+ .on('namespace:action', {
721
+ data: t.Object({ ... })
722
+ }, async ({ data, conn }) => {
723
+ // Process action
724
+ })
725
+ ```
726
+
727
+ **Parameters:**
728
+ - `action` - Action name (format: `namespace:action`)
729
+ - `config.data` - Input schema (TypeBox)
730
+ - `handler` - Async function with `data` and `conn` params
731
+
732
+ #### `.emit(event, schema)`
733
+
734
+ Declare event.
735
+
736
+ ```typescript
737
+ .emit('namespace:event', t.Object({ ... }))
738
+ ```
739
+
740
+ **Parameters:**
741
+ - `event` - Event name (format: `namespace:event`)
742
+ - `schema` - Event data schema (TypeBox, direct)
743
+
744
+ #### `.merge(router)`
745
+
746
+ Merge another router.
747
+
748
+ ```typescript
749
+ .merge(otherRouter)
750
+ ```
751
+
752
+ ### WebSocket Singleton API
753
+
754
+ **File: `backend/lib/utils/ws.ts`**
755
+
756
+ ```typescript
757
+ import { ws } from '$backend/lib/utils/ws';
758
+
759
+ // Emit events
760
+ ws.emit.user(userId, 'event', payload)
761
+ ws.emit.project(projectId, 'event', payload)
762
+ ws.emit.projectMembers(projectId, 'event', payload)
763
+ ws.emit.global('event', payload)
764
+
765
+ // Connection context (single source of truth - throws if not set)
766
+ ws.getProjectId(conn) // Returns string, throws if not set
767
+ ws.getUserId(conn) // Returns string, throws if not set
768
+
769
+ // Context setters (used internally by ws:set-context)
770
+ ws.setUser(conn, userId)
771
+ ws.setProject(conn, projectId)
772
+
773
+ // Connection management
774
+ ws.register(conn)
775
+ ws.unregister(conn)
776
+ ws.addCleanup(conn, fn)
777
+ ws.removeCleanup(conn, fn)
778
+ ws.getConnections(projectId?)
779
+ ws.getConnectionCount(projectId?)
780
+ ```
781
+
782
+ ### Frontend Client API
783
+
784
+ **File: `frontend/lib/utils/ws.ts`**
785
+
786
+ ```typescript
787
+ import ws from '$lib/utils/ws';
788
+
789
+ // HTTP request
790
+ const data = await ws.http('namespace:action', { ... });
791
+
792
+ // Emit action (no response)
793
+ ws.emit('namespace:action', { ... });
794
+
795
+ // Listen to events
796
+ const cleanup = ws.on('namespace:event', (data) => {
797
+ console.log(data);
798
+ });
799
+
800
+ // Cleanup
801
+ cleanup();
802
+ ```
803
+
804
+ ---
805
+
806
+ ## Examples
807
+
808
+ ### Example 1: Simple CRUD (Files Module)
809
+
810
+ **Backend: `./files/read.ts`**
811
+ ```typescript
812
+ import { t } from 'elysia';
813
+ import { createRouter } from '$shared/utils/ws-server';
814
+
815
+ export const readHandler = createRouter()
816
+ .http('files:read-file', {
817
+ data: t.Object({
818
+ file_path: t.String()
819
+ }),
820
+ response: t.Object({
821
+ content: t.String(),
822
+ size: t.Number(),
823
+ modified: t.String()
824
+ })
825
+ }, async ({ data }) => {
826
+ const result = await readFile(data.file_path);
827
+ return result;
828
+ })
829
+
830
+ .http('files:list-tree', {
831
+ data: t.Object({
832
+ project_path: t.String(),
833
+ expanded: t.Optional(t.String())
834
+ }),
835
+ response: t.Recursive((Self) => t.Union([
836
+ t.Object({ type: t.Literal('file'), ... }),
837
+ t.Object({ type: t.Literal('directory'), ... })
838
+ ]))
839
+ }, async ({ data }) => {
840
+ const tree = await buildFileTree(data.project_path);
841
+ return tree;
842
+ });
843
+ ```
844
+
845
+ **Frontend:**
846
+ ```typescript
847
+ // Read file
848
+ const file = await ws.http('files:read-file', {
849
+ file_path: '/path/to/file.txt'
850
+ });
851
+ console.log(file.content);
852
+
853
+ // List tree
854
+ const tree = await ws.http('files:list-tree', {
855
+ project_path: '/path/to/project'
856
+ });
857
+ renderTree(tree);
858
+ ```
859
+
860
+ ---
861
+
862
+ ### Example 2: Real-Time Streaming (Terminal Module)
863
+
864
+ **Backend: `./terminal/stream.ts`**
865
+ ```typescript
866
+ import { t } from 'elysia';
867
+ import { createRouter } from '$shared/utils/ws-server';
868
+ import { ws } from '$backend/lib/utils/ws';
869
+
870
+ export const streamHandler = createRouter()
871
+ .on('terminal:input', {
872
+ data: t.Object({
873
+ sessionId: t.String(),
874
+ data: t.Any()
875
+ })
876
+ }, async ({ data, conn }) => {
877
+ const projectId = ws.getProjectId(conn);
878
+
879
+ // Write to PTY
880
+ ptySessionManager.write(data.sessionId, data.data);
881
+
882
+ // PTY will emit output via external event
883
+ })
884
+
885
+ .emit('terminal:output', t.Object({
886
+ sessionId: t.String(),
887
+ content: t.String(),
888
+ timestamp: t.String()
889
+ }))
890
+
891
+ .emit('terminal:exit', t.Object({
892
+ sessionId: t.String(),
893
+ exitCode: t.Number()
894
+ }));
895
+
896
+ // External event emission (in PTY manager)
897
+ pty.onData((output) => {
898
+ ws.emit.project(projectId, 'terminal:output', {
899
+ sessionId,
900
+ content: output,
901
+ timestamp: new Date().toISOString()
902
+ });
903
+ });
904
+
905
+ pty.onExit((exitCode) => {
906
+ ws.emit.project(projectId, 'terminal:exit', {
907
+ sessionId,
908
+ exitCode
909
+ });
910
+ });
911
+ ```
912
+
913
+ **Frontend:**
914
+ ```typescript
915
+ // Listen to output
916
+ const cleanupOutput = ws.on('terminal:output', (data) => {
917
+ terminal.write(data.content);
918
+ });
919
+
920
+ const cleanupExit = ws.on('terminal:exit', (data) => {
921
+ console.log('Exited with code:', data.exitCode);
922
+ });
923
+
924
+ // Emit input
925
+ ws.emit('terminal:input', {
926
+ sessionId: 'term-123',
927
+ data: 'ls -la\n'
928
+ });
929
+
930
+ // Cleanup
931
+ onDestroy(() => {
932
+ cleanupOutput();
933
+ cleanupExit();
934
+ });
935
+ ```
936
+
937
+ ---
938
+
939
+ ### Example 3: User Interaction Feedback (Browser Module)
940
+
941
+ **Backend: `./preview/browser/interact.ts`**
942
+ ```typescript
943
+ import { t } from 'elysia';
944
+ import { createRouter } from '$shared/utils/ws-server';
945
+ import { ws } from '$backend/lib/utils/ws';
946
+
947
+ export const interactHandler = createRouter()
948
+ .on('browser:interact', {
949
+ data: t.Object({
950
+ sessionId: t.String(),
951
+ action: t.Object({
952
+ type: t.Union([
953
+ t.Literal('click'),
954
+ t.Literal('type'),
955
+ t.Literal('scroll')
956
+ ]),
957
+ x: t.Optional(t.Number()),
958
+ y: t.Optional(t.Number()),
959
+ text: t.Optional(t.String())
960
+ })
961
+ })
962
+ }, async ({ data, conn }) => {
963
+ const userId = ws.getUserId(conn);
964
+
965
+ try {
966
+ // Perform interaction
967
+ const session = browserService.getSession(data.sessionId);
968
+
969
+ if (data.action.type === 'click') {
970
+ await session.page.mouse.click(data.action.x, data.action.y);
971
+ }
972
+
973
+ // User-scoped feedback
974
+ ws.emit.user(userId, 'browser:interacted', {
975
+ action: data.action.type,
976
+ message: 'Success'
977
+ });
978
+ } catch (error) {
979
+ ws.emit.user(userId, 'browser:error', {
980
+ message: error.message
981
+ });
982
+ }
983
+ })
984
+
985
+ .emit('browser:interacted', t.Object({
986
+ action: t.String(),
987
+ message: t.String()
988
+ }))
989
+
990
+ .emit('browser:error', t.Object({
991
+ message: t.String()
992
+ }));
993
+ ```
994
+
995
+ **Frontend:**
996
+ ```typescript
997
+ // Listen to feedback
998
+ const cleanup = ws.on('browser:interacted', (data) => {
999
+ showNotification(`${data.action}: ${data.message}`);
1000
+ });
1001
+
1002
+ // Emit interaction
1003
+ ws.emit('browser:interact', {
1004
+ sessionId: 'browser-123',
1005
+ action: {
1006
+ type: 'click',
1007
+ x: 100,
1008
+ y: 200
1009
+ }
1010
+ });
1011
+ ```
1012
+
1013
+ ---
1014
+
1015
+ ## Best Practices
1016
+
1017
+ ### 1. One Endpoint = One Purpose
1018
+
1019
+ **DO:**
1020
+ ```typescript
1021
+ .http('files:write-file', { ... })
1022
+ .http('files:create-file', { ... })
1023
+ .http('files:delete', { ... })
1024
+ ```
1025
+
1026
+ **DON'T:**
1027
+ ```typescript
1028
+ .http('files:operation', {
1029
+ data: t.Object({
1030
+ action: t.Union([
1031
+ t.Literal('write'),
1032
+ t.Literal('create'),
1033
+ t.Literal('delete')
1034
+ ]),
1035
+ // ...
1036
+ })
1037
+ }, async ({ data }) => {
1038
+ switch (data.action) { // ❌ Branching logic
1039
+ case 'write': ...
1040
+ case 'create': ...
1041
+ }
1042
+ })
1043
+ ```
1044
+
1045
+ **Why:**
1046
+ - Better type safety
1047
+ - Clearer API contract
1048
+ - Easier to maintain
1049
+ - No switch/if logic
1050
+
1051
+ ---
1052
+
1053
+ ### 2. TypeBox Schemas Inline
1054
+
1055
+ **DO:**
1056
+ ```typescript
1057
+ .http('files:read-file', {
1058
+ data: t.Object({
1059
+ file_path: t.String()
1060
+ }),
1061
+ response: t.Object({
1062
+ content: t.String(),
1063
+ size: t.Number()
1064
+ })
1065
+ }, async ({ data }) => { ... })
1066
+ ```
1067
+
1068
+ **DON'T:**
1069
+ ```typescript
1070
+ const ReadFileResponse = t.Object({ ... });
1071
+
1072
+ .http('files:read-file', {
1073
+ response: ReadFileResponse
1074
+ }, ...)
1075
+ ```
1076
+
1077
+ **Why:**
1078
+ - Schema visible when reading code
1079
+ - No need to scroll to find definition
1080
+ - Easier to develop
1081
+
1082
+ ---
1083
+
1084
+ ### 3. Direct Return, No Manual Wrapping
1085
+
1086
+ **DO:**
1087
+ ```typescript
1088
+ .http('files:read-file', {
1089
+ response: t.Object({
1090
+ content: t.String()
1091
+ })
1092
+ }, async ({ data }) => {
1093
+ const result = await readFile(data.file_path);
1094
+ return result; // ✅ Direct return
1095
+ })
1096
+ ```
1097
+
1098
+ **DON'T:**
1099
+ ```typescript
1100
+ .http('files:read-file', {
1101
+ response: t.Object({
1102
+ success: t.Boolean(), // ❌ Manual wrapper
1103
+ data: t.Optional(t.Object({ ... })),
1104
+ error: t.Optional(t.String())
1105
+ })
1106
+ }, async ({ data }) => {
1107
+ try {
1108
+ return { success: true, data: result }; // ❌
1109
+ } catch (error) {
1110
+ return { success: false, error: error.message }; // ❌
1111
+ }
1112
+ })
1113
+ ```
1114
+
1115
+ **Why:**
1116
+ - Server auto-wraps response
1117
+ - Cleaner handler code
1118
+ - Consistent error handling
1119
+
1120
+ ---
1121
+
1122
+ ### 4. Event vs HTTP Decision
1123
+
1124
+ Choose the right pattern based on your use case:
1125
+
1126
+ | Use Case | Direction | Pattern | Example |
1127
+ |----------|-----------|---------|---------|
1128
+ | **Request-Response** | Client → Server | `.http()` | `files:read-file`, `browser:get-info` |
1129
+ | **Real-time Streaming** | Server → Client | Backend: `.emit()` declaration + emission<br>Frontend: `.on()` listener | `terminal:output`, `browser:frame` |
1130
+ | **Fire-and-Forget Action** | Client → Server | Frontend: `.emit()`<br>Backend: `.on()` | `terminal:input`, `browser:interact` |
1131
+ | **Action with Confirmation** | Client → Server → Client | Frontend: `.emit()` + `.on()`<br>Backend: `.on()` + `.emit()` | `browser:start-stream` → `browser:stream-started` |
1132
+
1133
+ **Key Differences:**
1134
+
1135
+ - **`.http()`**: Request needs immediate response (like REST API)
1136
+ - **`.emit()` + `.on()`**: One-way communication (fire-and-forget or push notifications)
1137
+ - **Combined**: Bidirectional flow (action triggers server event)
1138
+
1139
+ **Don't duplicate:**
1140
+ ```typescript
1141
+ // ❌ WRONG - Same data available via two patterns
1142
+ .http('browser:get-frame', ...) // HTTP endpoint to fetch frame
1143
+ .emit('browser:frame', ...) // Real-time frame streaming
1144
+ // Choose ONE! If streaming exists, remove HTTP endpoint.
1145
+ ```
1146
+
1147
+ **When to use what:**
1148
+ - Use `.http()` when you need **immediate data back**
1149
+ - Use `.emit()` + `.on()` when you need **real-time updates**
1150
+ - Don't mix both for the same data
1151
+
1152
+ ---
1153
+
1154
+ ### 5. Declare Only Emitted Events
1155
+
1156
+ **DO:**
1157
+ ```typescript
1158
+ // Declare events that are actually emitted
1159
+ .emit('terminal:output', ...) // ✅ Emitted in line 53
1160
+ .emit('terminal:exit', ...) // ✅ Emitted in line 68
1161
+ ```
1162
+
1163
+ **DON'T:**
1164
+ ```typescript
1165
+ // Declare events that are never emitted
1166
+ .emit('terminal:resize', ...) // ❌ Never emitted
1167
+ .emit('terminal:test', ...) // ❌ Never emitted
1168
+ ```
1169
+
1170
+ **Why:**
1171
+ - Type safety matches reality
1172
+ - No dead code
1173
+ - Frontend doesn't listen to non-existent events
1174
+
1175
+ ---
1176
+
1177
+ ### 6. Security - Connection Context as Single Source of Truth
1178
+
1179
+ **DO:**
1180
+ ```typescript
1181
+ .on('browser:interact', {
1182
+ data: t.Object({
1183
+ sessionId: t.String()
1184
+ // ✅ NO userId/projectId in payload - use connection context
1185
+ })
1186
+ }, async ({ data, conn }) => {
1187
+ const userId = ws.getUserId(conn); // ✅ From connection state
1188
+ const projectId = ws.getProjectId(conn); // ✅ From connection state
1189
+ ws.emit.user(userId, 'browser:interacted', { ... });
1190
+ })
1191
+ ```
1192
+
1193
+ **DON'T:**
1194
+ ```typescript
1195
+ .on('browser:interact', {
1196
+ data: t.Object({
1197
+ sessionId: t.String(),
1198
+ userId: t.String(), // ❌ Client can fake
1199
+ projectId: t.String() // ❌ Client can fake
1200
+ })
1201
+ }, async ({ data }) => {
1202
+ ws.emit.user(data.userId, 'event', { ... }); // ❌ Using client-sent identity
1203
+ })
1204
+ ```
1205
+
1206
+ **Why:**
1207
+ - `ws.getProjectId()`/`ws.getUserId()` are the **single source of truth**
1208
+ - Identity comes from server-side connection state, not client payload
1209
+ - Cannot be faked by client
1210
+ - Throws if context not set (fail-fast, no silent empty strings)
1211
+ - Frontend sets context via `ws:set-context` before sending any events
1212
+
1213
+ ---
1214
+
1215
+ ### 7. Error Handling
1216
+
1217
+ **HTTP handlers - throw errors:**
1218
+ ```typescript
1219
+ async ({ data }) => {
1220
+ if (!isValid(data)) {
1221
+ throw new Error('Validation failed');
1222
+ }
1223
+
1224
+ const result = await operation(data);
1225
+ return result;
1226
+ }
1227
+ ```
1228
+
1229
+ **Event handlers - emit error events:**
1230
+ ```typescript
1231
+ async ({ data, conn }) => {
1232
+ try {
1233
+ await operation(data);
1234
+ } catch (error) {
1235
+ const userId = ws.getUserId(conn);
1236
+ ws.emit.user(userId, 'namespace:error', {
1237
+ message: error.message
1238
+ });
1239
+ }
1240
+ }
1241
+ ```
1242
+
1243
+ ---
1244
+
1245
+ ### 8. Resource Cleanup
1246
+
1247
+ Use `ws.addCleanup()` to register cleanup functions that run automatically when the connection closes:
1248
+
1249
+ ```typescript
1250
+ .on('terminal:create', {
1251
+ data: t.Object({ ... })
1252
+ }, async ({ data, conn }) => {
1253
+ const sessionId = await createSession(data);
1254
+
1255
+ // Register cleanup - runs automatically on disconnect
1256
+ ws.addCleanup(conn, () => {
1257
+ cleanupSession(sessionId);
1258
+ });
1259
+ })
1260
+ ```
1261
+
1262
+ ---
1263
+
1264
+ ### 9. TypeBox Type Constraints
1265
+
1266
+ **Use literal unions for enums:**
1267
+ ```typescript
1268
+ type: t.Union([
1269
+ t.Literal('file'),
1270
+ t.Literal('directory'),
1271
+ t.Literal('drive')
1272
+ ])
1273
+ ```
1274
+
1275
+ **Use string for free-form text:**
1276
+ ```typescript
1277
+ description: t.String()
1278
+ content: t.String()
1279
+ ```
1280
+
1281
+ **Use constraints for validation:**
1282
+ ```typescript
1283
+ age: t.Number({ minimum: 0, maximum: 150 })
1284
+ email: t.String({ format: 'email' })
1285
+ ```
1286
+
1287
+ ---
1288
+
1289
+ ## Troubleshooting
1290
+
1291
+ ### Handler Not Working
1292
+
1293
+ **Problem:** WebSocket handler doesn't respond.
1294
+
1295
+ **Solutions:**
1296
+ 1. Verify module router is imported in `./index.ts`
1297
+ 2. Check handler is merged in module's `index.ts`
1298
+ 3. Verify action name format: `namespace:action`
1299
+ 4. Check console for TypeBox validation errors
1300
+ 5. Run `bun run check` for TypeScript errors
1301
+
1302
+ ---
1303
+
1304
+ ### Type Errors
1305
+
1306
+ **Problem:** TypeScript errors in handlers.
1307
+
1308
+ **Solutions:**
1309
+ 1. Ensure TypeBox schemas are inline in `.http()` / `.on()`
1310
+ 2. Verify response schema has NO `success` or `error` fields
1311
+ 3. Check handler returns data directly (not wrapped)
1312
+ 4. Ensure `.emit()` uses direct schema (not wrapped)
1313
+ 5. Run `bun run check` to see all errors
1314
+
1315
+ **Common mistakes:**
1316
+ ```typescript
1317
+ // ❌ Wrong - wrapped schema
1318
+ .emit('event', { schema: t.Object({ ... }) })
1319
+
1320
+ // ✅ Correct - direct schema
1321
+ .emit('event', t.Object({ ... }))
1322
+
1323
+ // ❌ Wrong - manual wrapper in response
1324
+ response: t.Object({
1325
+ success: t.Boolean(),
1326
+ data: t.Object({ ... })
1327
+ })
1328
+
1329
+ // ✅ Correct - data only
1330
+ response: t.Object({
1331
+ field1: t.String(),
1332
+ field2: t.Number()
1333
+ })
1334
+ ```
1335
+
1336
+ ---
1337
+
1338
+ ### Events Not Received
1339
+
1340
+ **Problem:** Frontend doesn't receive events.
1341
+
1342
+ **Check:**
1343
+ 1. Event is declared with `.emit()` in router
1344
+ 2. Event name matches between backend and frontend
1345
+ 3. Event is actually emitted (search for `ws.emit`)
1346
+ 4. Correct scope is used (`user`, `project`, `global`)
1347
+ 5. User/project tracking is set up correctly
1348
+ 6. Frontend listener is set up before event is emitted
1349
+
1350
+ **Debug:**
1351
+ ```typescript
1352
+ // Backend - log emission
1353
+ ws.emit.project(projectId, 'event', payload);
1354
+ console.log('Emitted event to project:', projectId);
1355
+
1356
+ // Frontend - log reception
1357
+ ws.on('event', (data) => {
1358
+ console.log('Received event:', data);
1359
+ });
1360
+ ```
1361
+
1362
+ ---
1363
+
1364
+ ### Validation Errors
1365
+
1366
+ **Problem:** TypeBox validation fails.
1367
+
1368
+ **Solutions:**
1369
+ 1. Check payload matches schema exactly
1370
+ 2. Verify all required fields are present
1371
+ 3. Check field types match (string vs number)
1372
+ 4. Ensure literal unions use exact values
1373
+ 5. Look at error message for specific field
1374
+
1375
+ **Example error:**
1376
+ ```
1377
+ Expected type: { sessionId: string, content: string }
1378
+ Received: { sessionId: "term-123" }
1379
+ Error: Missing required property 'content'
1380
+ ```
1381
+
1382
+ ---
1383
+
1384
+ ### Scope Filtering Issues
1385
+
1386
+ **Problem:** Events received by wrong users/projects.
1387
+
1388
+ **Check:**
1389
+ 1. Correct scope is used in `ws.emit()`
1390
+ 2. User tracking is set up (`ws.setUser()`)
1391
+ 3. Project tracking is set up (`ws.setProject()`)
1392
+ 4. Connection context is passed to `ws.emit()`
1393
+
1394
+ **Debug:**
1395
+ ```typescript
1396
+ // Check current user/project
1397
+ const userId = ws.getUserId(conn);
1398
+ const projectId = ws.getProjectId(conn);
1399
+ console.log('User:', userId, 'Project:', projectId);
1400
+
1401
+ // Emit with logging
1402
+ console.log('Emitting to project:', projectId);
1403
+ ws.emit.project(projectId, 'event', payload);
1404
+ ```
1405
+
1406
+ ---
1407
+
1408
+ ## Module Audit Checklist
1409
+
1410
+ Use this checklist when creating or auditing modules:
1411
+
1412
+ ### 1. Pattern Duplication
1413
+ - [ ] Check if data is available via HTTP AND Event
1414
+ - [ ] If streaming exists, remove HTTP endpoint for same data
1415
+ - [ ] If HTTP is sufficient, don't create Event for same data
1416
+
1417
+ ### 2. Event Declaration Discipline
1418
+ - [ ] List all `.emit()` declarations
1419
+ - [ ] Search for actual `ws.emit()` calls
1420
+ - [ ] Remove declarations that are never emitted
1421
+
1422
+ ### 3. Frontend Listener Audit
1423
+ - [ ] List all `ws.on()` listeners in frontend
1424
+ - [ ] Verify backend actually emits these events
1425
+ - [ ] Remove listeners for non-existent events
1426
+
1427
+ ### 4. File Structure
1428
+ - [ ] Are there files with only 1 endpoint?
1429
+ - [ ] Can they be merged into other files?
1430
+ - [ ] Are there redundant files with streaming?
1431
+
1432
+ ### 5. Scope Consistency
1433
+ - [ ] Same event uses same scope across files
1434
+ - [ ] User-specific events use `user` scope
1435
+ - [ ] Project-shared events use `project` scope
1436
+
1437
+ ---
1438
+
1439
+ ## Additional Resources
1440
+
1441
+ - [TypeBox Documentation](https://github.com/sinclairzx81/typebox)
1442
+ - [Elysia Documentation](https://elysiajs.com/)
1443
+ - [WebSocket API (MDN)](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket)
1444
+
1445
+ ---
1446
+
1447
+ ## Quick Reference
1448
+
1449
+ ### Backend Handler Types
1450
+
1451
+ ```typescript
1452
+ // HTTP request-response
1453
+ .http('namespace:action', {
1454
+ data: t.Object({ ... }),
1455
+ response: t.Object({ ... })
1456
+ }, async ({ data }) => {
1457
+ return result;
1458
+ })
1459
+
1460
+ // Event listener
1461
+ .on('namespace:action', {
1462
+ data: t.Object({ ... })
1463
+ }, async ({ data, conn }) => {
1464
+ // Process action
1465
+ })
1466
+
1467
+ // Event declaration
1468
+ .emit('namespace:event', t.Object({ ... }))
1469
+ ```
1470
+
1471
+ ### Frontend API
1472
+
1473
+ ```typescript
1474
+ // HTTP request
1475
+ const data = await ws.http('namespace:action', { ... });
1476
+
1477
+ // Emit action
1478
+ ws.emit('namespace:action', { ... });
1479
+
1480
+ // Listen to events
1481
+ const cleanup = ws.on('namespace:event', (data) => { ... });
1482
+ cleanup();
1483
+ ```
1484
+
1485
+ ### Event Emission
1486
+
1487
+ ```typescript
1488
+ import { ws } from '$backend/lib/utils/ws';
1489
+
1490
+ // User-specific (all connections of a user)
1491
+ ws.emit.user(userId, 'event', payload);
1492
+
1493
+ // Project room (connections currently viewing the project)
1494
+ ws.emit.project(projectId, 'event', payload);
1495
+
1496
+ // Project members (all users who have the project, even if viewing another project)
1497
+ ws.emit.projectMembers(projectId, 'event', payload);
1498
+
1499
+ // Global broadcast (all connections)
1500
+ ws.emit.global('event', payload);
1501
+ ```
1502
+
1503
+ ---
1504
+
1505
+ **Happy coding! 🚀**