@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,768 @@
1
+ /**
2
+ * WebSocket Client Core Library - Optimized
3
+ *
4
+ * High-performance WebSocket client with:
5
+ * - 100% type inference from backend API
6
+ * - Singleton TextEncoder/Decoder for performance
7
+ * - Automatic context sync (user/project)
8
+ * - Automatic reconnection with exponential backoff
9
+ * - Binary message support for efficient data transfer
10
+ */
11
+
12
+ import { debug } from './logger';
13
+
14
+ // ============================================================================
15
+ // Singleton Encoders (Performance Optimization)
16
+ // ============================================================================
17
+
18
+ /** Singleton TextEncoder - reused across all encode operations */
19
+ const textEncoder = new TextEncoder();
20
+
21
+ /** Singleton TextDecoder - reused across all decode operations */
22
+ const textDecoder = new TextDecoder();
23
+
24
+ // ============================================================================
25
+ // Pre-computed Binary Actions
26
+ // ============================================================================
27
+
28
+ /** Actions known to contain binary data - skip containsBinary() check */
29
+ const BINARY_ACTIONS = new Set<string>([
30
+ 'preview:frame',
31
+ 'file:upload',
32
+ 'file:download',
33
+ 'terminal:binary'
34
+ ]);
35
+
36
+ // ============================================================================
37
+ // Client Options
38
+ // ============================================================================
39
+
40
+ /**
41
+ * WebSocket client options
42
+ */
43
+ export interface WSClientOptions {
44
+ /** Auto-reconnect on connection loss */
45
+ autoReconnect?: boolean;
46
+ /** Maximum reconnection attempts (0 = infinite) */
47
+ maxReconnectAttempts?: number;
48
+ /** Initial reconnect delay in ms */
49
+ reconnectDelay?: number;
50
+ /** Maximum reconnect delay in ms */
51
+ maxReconnectDelay?: number;
52
+ }
53
+
54
+ // ============================================================================
55
+ // Binary Message Utilities (Optimized)
56
+ // ============================================================================
57
+
58
+ /**
59
+ * Check if payload contains binary data
60
+ * Optimized with early return and iterative approach
61
+ */
62
+ function containsBinary(obj: any): boolean {
63
+ if (obj instanceof Uint8Array || obj instanceof ArrayBuffer) return true;
64
+ if (typeof obj !== 'object' || obj === null) return false;
65
+
66
+ const stack = [obj];
67
+ while (stack.length > 0) {
68
+ const current = stack.pop();
69
+ for (const value of Object.values(current)) {
70
+ if (value instanceof Uint8Array || value instanceof ArrayBuffer) return true;
71
+ if (typeof value === 'object' && value !== null) {
72
+ stack.push(value);
73
+ }
74
+ }
75
+ }
76
+ return false;
77
+ }
78
+
79
+ /**
80
+ * Fast check using pre-computed binary actions
81
+ */
82
+ function isBinaryAction(action: string, payload: any): boolean {
83
+ if (BINARY_ACTIONS.has(action)) return true;
84
+ return containsBinary(payload);
85
+ }
86
+
87
+ /**
88
+ * Extract binary field and metadata from payload
89
+ */
90
+ function extractBinaryFields(payload: any): { binaryData: Uint8Array; metadata: Record<string, any> } {
91
+ const metadata: Record<string, any> = {};
92
+ let binaryData: Uint8Array = new Uint8Array(0);
93
+
94
+ for (const [key, value] of Object.entries(payload)) {
95
+ if (value instanceof Uint8Array) {
96
+ binaryData = value;
97
+ } else if (value instanceof ArrayBuffer) {
98
+ binaryData = new Uint8Array(value);
99
+ } else {
100
+ metadata[key] = value;
101
+ }
102
+ }
103
+
104
+ return { binaryData, metadata };
105
+ }
106
+
107
+ /**
108
+ * Encode a binary message with action and metadata
109
+ *
110
+ * Binary Message Format:
111
+ * ┌─────────────────┬────────────────┬─────────────────┬──────────────┬─────────────┐
112
+ * │ Action Length │ Action String │ Metadata Length │ Metadata JSON│ Binary Data │
113
+ * │ (1 byte) │ (N bytes) │ (4 bytes) │ (M bytes) │ (rest) │
114
+ * └─────────────────┴────────────────┴─────────────────┴──────────────┴─────────────┘
115
+ */
116
+ function encodeBinaryMessage(action: string, payload: any): ArrayBuffer {
117
+ const { binaryData, metadata } = extractBinaryFields(payload);
118
+
119
+ const actionBytes = textEncoder.encode(action);
120
+ const metaBytes = textEncoder.encode(JSON.stringify(metadata));
121
+
122
+ const totalLength = 1 + actionBytes.length + 4 + metaBytes.length + binaryData.length;
123
+ const buffer = new ArrayBuffer(totalLength);
124
+ const view = new DataView(buffer);
125
+ const uint8 = new Uint8Array(buffer);
126
+
127
+ let offset = 0;
128
+
129
+ // Action length (1 byte)
130
+ view.setUint8(offset, actionBytes.length);
131
+ offset += 1;
132
+
133
+ // Action string
134
+ uint8.set(actionBytes, offset);
135
+ offset += actionBytes.length;
136
+
137
+ // Metadata length (4 bytes)
138
+ view.setUint32(offset, metaBytes.length);
139
+ offset += 4;
140
+
141
+ // Metadata JSON
142
+ uint8.set(metaBytes, offset);
143
+ offset += metaBytes.length;
144
+
145
+ // Binary data
146
+ uint8.set(binaryData, offset);
147
+
148
+ return buffer;
149
+ }
150
+
151
+ /**
152
+ * Decode a binary message back to action and payload
153
+ */
154
+ function decodeBinaryMessage(buffer: ArrayBuffer): { action: string; payload: any } {
155
+ const view = new DataView(buffer);
156
+ const uint8 = new Uint8Array(buffer);
157
+
158
+ let offset = 0;
159
+
160
+ // Read action length (1 byte)
161
+ const actionLength = view.getUint8(offset);
162
+ offset += 1;
163
+
164
+ // Read action string
165
+ const actionBytes = uint8.slice(offset, offset + actionLength);
166
+ const action = textDecoder.decode(actionBytes);
167
+ offset += actionLength;
168
+
169
+ // Read metadata length (4 bytes)
170
+ const metaLength = view.getUint32(offset);
171
+ offset += 4;
172
+
173
+ // Read metadata JSON
174
+ const metaBytes = uint8.slice(offset, offset + metaLength);
175
+ const metadata = JSON.parse(textDecoder.decode(metaBytes));
176
+ offset += metaLength;
177
+
178
+ // Read binary data (rest of buffer)
179
+ const binaryData = uint8.slice(offset);
180
+
181
+ // Reconstruct payload with binary data
182
+ const payload = {
183
+ ...metadata,
184
+ data: binaryData // Binary field always named 'data'
185
+ };
186
+
187
+ return { action, payload };
188
+ }
189
+
190
+ // ============================================================================
191
+ // WebSocket Client
192
+ // ============================================================================
193
+
194
+ /**
195
+ * Type-safe WebSocket Client with Binary Support and Context Sync
196
+ */
197
+ export class WSClient<TAPI extends { client: any; server: any }> {
198
+ private ws: WebSocket | null = null;
199
+ private url: string;
200
+ private options: Required<WSClientOptions>;
201
+ private reconnectAttempts = 0;
202
+ private reconnectTimeout: ReturnType<typeof setTimeout> | null = null;
203
+ private listeners = new Map<string, Set<(payload: any) => void>>();
204
+ private messageQueue: Array<{ action: string; payload: any }> = [];
205
+ private isConnected = false;
206
+ private shouldReconnect = true;
207
+
208
+ /** Current context (synced with server) */
209
+ private context: {
210
+ userId: string | null;
211
+ projectId: string | null;
212
+ } = {
213
+ userId: null,
214
+ projectId: null
215
+ };
216
+
217
+ /** Pending context sync (for reconnection) */
218
+ private pendingContextSync = false;
219
+
220
+ /** Resolvers waiting for connection to be fully ready */
221
+ private connectResolvers: Array<() => void> = [];
222
+
223
+ constructor(url: string, options: WSClientOptions = {}) {
224
+ this.url = url;
225
+ this.options = {
226
+ autoReconnect: options.autoReconnect ?? true,
227
+ maxReconnectAttempts: options.maxReconnectAttempts ?? 5,
228
+ reconnectDelay: options.reconnectDelay ?? 1000,
229
+ maxReconnectDelay: options.maxReconnectDelay ?? 30000
230
+ };
231
+
232
+ this.connect();
233
+ }
234
+
235
+ /**
236
+ * Establish WebSocket connection
237
+ */
238
+ private connect(): void {
239
+ try {
240
+ // CRITICAL: Close any existing connection before creating a new one
241
+ // This prevents zombie connections from accumulating on the server
242
+ // (e.g., during reconnection or HMR, old connections may linger)
243
+ if (this.ws) {
244
+ try {
245
+ this.ws.onclose = null; // Prevent triggering reconnect from this close
246
+ this.ws.onerror = null;
247
+ this.ws.onmessage = null;
248
+ this.ws.close();
249
+ } catch {
250
+ // Ignore close errors on stale socket
251
+ }
252
+ this.ws = null;
253
+ }
254
+
255
+ debug.log('websocket', 'Connecting to', this.url);
256
+ this.ws = new WebSocket(this.url);
257
+
258
+ // IMPORTANT: Enable binary message handling
259
+ this.ws.binaryType = 'arraybuffer';
260
+
261
+ this.ws.onopen = async () => {
262
+ debug.log('websocket', 'Connected');
263
+ this.isConnected = true;
264
+ this.reconnectAttempts = 0;
265
+
266
+ // Sync context on reconnection - MUST await before flushing queue
267
+ if (this.context.userId || this.context.projectId) {
268
+ try {
269
+ await this.syncContext();
270
+ debug.log('websocket', 'Context synced after reconnection');
271
+ } catch (err) {
272
+ debug.error('websocket', 'Failed to sync context on reconnection:', err);
273
+ }
274
+ }
275
+
276
+ // Flush queued messages AFTER context is synced
277
+ while (this.messageQueue.length > 0) {
278
+ const msg = this.messageQueue.shift();
279
+ if (msg) {
280
+ this.sendRaw(msg.action, msg.payload);
281
+ }
282
+ }
283
+
284
+ // Resolve waitUntilConnected() callers AFTER context sync + queue flush
285
+ for (const resolve of this.connectResolvers) {
286
+ resolve();
287
+ }
288
+ this.connectResolvers = [];
289
+ };
290
+
291
+ this.ws.onmessage = (event) => {
292
+ try {
293
+ if (event.data instanceof ArrayBuffer) {
294
+ // Binary message
295
+ debug.log('websocket', `Received ArrayBuffer: ${event.data.byteLength} bytes`);
296
+ this.handleBinaryMessage(event.data);
297
+ } else if (event.data instanceof Blob) {
298
+ // Blob message - convert to ArrayBuffer
299
+ debug.log('websocket', `Received Blob: ${event.data.size} bytes, converting to ArrayBuffer`);
300
+ event.data.arrayBuffer().then((buffer) => {
301
+ this.handleBinaryMessage(buffer);
302
+ });
303
+ } else {
304
+ // JSON message
305
+ this.handleTextMessage(event.data);
306
+ }
307
+ } catch (err) {
308
+ debug.error('websocket', 'Message handling error:', err);
309
+ }
310
+ };
311
+
312
+ this.ws.onerror = (error) => {
313
+ debug.error('websocket', 'WebSocket error:', error);
314
+ };
315
+
316
+ this.ws.onclose = () => {
317
+ debug.log('websocket', 'Disconnected');
318
+ this.isConnected = false;
319
+ this.ws = null;
320
+
321
+ // Auto-reconnect
322
+ if (this.shouldReconnect && this.options.autoReconnect) {
323
+ this.scheduleReconnect();
324
+ }
325
+ };
326
+ } catch (err) {
327
+ debug.error('websocket', 'Connection error:', err);
328
+ if (this.shouldReconnect && this.options.autoReconnect) {
329
+ this.scheduleReconnect();
330
+ }
331
+ }
332
+ }
333
+
334
+ /**
335
+ * Handle text/JSON message from server
336
+ */
337
+ private handleTextMessage(data: string): void {
338
+ // Check if data looks like binary (not valid JSON start)
339
+ if (data && data.length > 0 && data[0] !== '{' && data[0] !== '[') {
340
+ debug.warn('websocket', `Received non-JSON text data (length: ${data.length}), first char code: ${data.charCodeAt(0)}`);
341
+ return;
342
+ }
343
+
344
+ const parsed = JSON.parse(data);
345
+ const { action, payload } = parsed;
346
+
347
+ if (!action) {
348
+ debug.log('websocket', 'Invalid message format:', parsed);
349
+ return;
350
+ }
351
+
352
+ this.dispatchToListeners(action, payload);
353
+ }
354
+
355
+ /**
356
+ * Handle binary message from server
357
+ */
358
+ private handleBinaryMessage(buffer: ArrayBuffer): void {
359
+ const { action, payload } = decodeBinaryMessage(buffer);
360
+
361
+ if (!action) {
362
+ debug.log('websocket', 'Invalid binary message format');
363
+ return;
364
+ }
365
+
366
+ debug.log('websocket', 'Received binary:', action, `(${buffer.byteLength} bytes)`);
367
+ this.dispatchToListeners(action, payload);
368
+ }
369
+
370
+ /**
371
+ * Dispatch message to registered listeners
372
+ */
373
+ private dispatchToListeners(action: string, payload: any): void {
374
+ const callbacks = this.listeners.get(action);
375
+ if (callbacks) {
376
+ callbacks.forEach((cb) => {
377
+ try {
378
+ cb(payload);
379
+ } catch (err) {
380
+ debug.error('websocket', `Listener error for ${action}:`, err);
381
+ }
382
+ });
383
+ }
384
+ }
385
+
386
+ /**
387
+ * Schedule reconnection with exponential backoff
388
+ */
389
+ private scheduleReconnect(): void {
390
+ if (this.options.maxReconnectAttempts > 0 && this.reconnectAttempts >= this.options.maxReconnectAttempts) {
391
+ debug.error('websocket', 'Max reconnect attempts reached');
392
+ return;
393
+ }
394
+
395
+ this.reconnectAttempts++;
396
+ const delay = Math.min(
397
+ this.options.reconnectDelay * Math.pow(2, this.reconnectAttempts - 1),
398
+ this.options.maxReconnectDelay
399
+ );
400
+
401
+ debug.log('websocket', `Reconnecting in ${delay}ms (attempt ${this.reconnectAttempts})`);
402
+
403
+ this.reconnectTimeout = setTimeout(() => {
404
+ this.connect();
405
+ }, delay);
406
+ }
407
+
408
+ /**
409
+ * Emit message to server (type-safe)
410
+ * Automatically detects binary data and sends as binary message
411
+ */
412
+ emit<TEvent extends keyof TAPI['client']>(
413
+ action: TEvent,
414
+ payload: TAPI['client'][TEvent]
415
+ ): void {
416
+ if (this.isConnected && this.ws) {
417
+ this.sendRaw(action as string, payload);
418
+ } else {
419
+ // Queue message for sending when connected
420
+ debug.log('websocket', 'Queueing message (not connected):', action);
421
+ this.messageQueue.push({ action: action as string, payload });
422
+ }
423
+ }
424
+
425
+ /**
426
+ * Send raw message (JSON or Binary based on payload content)
427
+ */
428
+ private sendRaw(action: string, payload: any): void {
429
+ if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
430
+ debug.log('websocket', 'Cannot send, socket not ready:', action);
431
+ return;
432
+ }
433
+
434
+ try {
435
+ if (isBinaryAction(action, payload)) {
436
+ // Send as binary message
437
+ const binaryMessage = encodeBinaryMessage(action, payload);
438
+ this.ws.send(binaryMessage);
439
+ debug.log('websocket', 'Sent binary:', action, `(${binaryMessage.byteLength} bytes)`);
440
+ } else {
441
+ // Send as JSON message
442
+ this.ws.send(JSON.stringify({ action, payload }));
443
+ debug.log('websocket', 'Sent JSON:', action);
444
+ }
445
+ } catch (err) {
446
+ debug.error('websocket', 'Send error:', err);
447
+ }
448
+ }
449
+
450
+ /**
451
+ * Listen for server messages (type-safe)
452
+ */
453
+ on<TEvent extends keyof TAPI['server']>(
454
+ action: TEvent,
455
+ callback: (payload: TAPI['server'][TEvent]) => void
456
+ ): () => void {
457
+ const actionStr = action as string;
458
+
459
+ if (!this.listeners.has(actionStr)) {
460
+ this.listeners.set(actionStr, new Set());
461
+ }
462
+
463
+ this.listeners.get(actionStr)!.add(callback);
464
+ debug.log('websocket', 'Listener added:', actionStr);
465
+
466
+ // Return unsubscribe function
467
+ return () => {
468
+ const callbacks = this.listeners.get(actionStr);
469
+ if (callbacks) {
470
+ callbacks.delete(callback);
471
+ if (callbacks.size === 0) {
472
+ this.listeners.delete(actionStr);
473
+ }
474
+ debug.log('websocket', 'Listener removed:', actionStr);
475
+ }
476
+ };
477
+ }
478
+
479
+ /**
480
+ * Remove all listeners for an action
481
+ */
482
+ off<TEvent extends keyof TAPI['server']>(action: TEvent): void {
483
+ const actionStr = action as string;
484
+ this.listeners.delete(actionStr);
485
+ debug.log('websocket', 'All listeners removed:', actionStr);
486
+ }
487
+
488
+ /**
489
+ * Disconnect and cleanup
490
+ */
491
+ disconnect(): void {
492
+ this.shouldReconnect = false;
493
+
494
+ if (this.reconnectTimeout) {
495
+ clearTimeout(this.reconnectTimeout);
496
+ this.reconnectTimeout = null;
497
+ }
498
+
499
+ if (this.ws) {
500
+ this.ws.close();
501
+ this.ws = null;
502
+ }
503
+
504
+ this.listeners.clear();
505
+ this.messageQueue = [];
506
+ this.isConnected = false;
507
+
508
+ debug.log('websocket', 'Disconnected and cleaned up');
509
+ }
510
+
511
+ /**
512
+ * Check if connected
513
+ */
514
+ connected(): boolean {
515
+ return this.isConnected;
516
+ }
517
+
518
+ /**
519
+ * Wait until WebSocket is fully connected and ready (context synced, queue flushed).
520
+ * Resolves immediately if already connected.
521
+ */
522
+ waitUntilConnected(timeout = 10000): Promise<void> {
523
+ if (this.isConnected) return Promise.resolve();
524
+
525
+ return new Promise<void>((resolve, reject) => {
526
+ const timer = setTimeout(() => {
527
+ const idx = this.connectResolvers.indexOf(doResolve);
528
+ if (idx >= 0) this.connectResolvers.splice(idx, 1);
529
+ reject(new Error(`WebSocket connection timeout (${timeout}ms)`));
530
+ }, timeout);
531
+
532
+ const doResolve = () => {
533
+ clearTimeout(timer);
534
+ resolve();
535
+ };
536
+
537
+ this.connectResolvers.push(doResolve);
538
+ });
539
+ }
540
+
541
+ /**
542
+ * Manual reconnect
543
+ */
544
+ reconnect(): void {
545
+ this.disconnect();
546
+ this.shouldReconnect = true;
547
+ this.reconnectAttempts = 0;
548
+ this.connect();
549
+ }
550
+
551
+ // =========================================================================
552
+ // Context Management
553
+ // =========================================================================
554
+
555
+ /** Promise for ongoing context sync */
556
+ private contextSyncPromise: Promise<void> | null = null;
557
+
558
+ /**
559
+ * Set user context (auto-syncs with server)
560
+ * @returns Promise that resolves when context is synced with server
561
+ */
562
+ async setUser(userId: string | null): Promise<void> {
563
+ if (this.context.userId === userId) return;
564
+
565
+ this.context.userId = userId;
566
+ debug.log('websocket', 'Context: user set to', userId);
567
+
568
+ if (this.isConnected) {
569
+ await this.syncContext();
570
+ }
571
+ }
572
+
573
+ /**
574
+ * Set project context (auto-syncs with server)
575
+ * @returns Promise that resolves when context is synced with server
576
+ */
577
+ async setProject(projectId: string | null): Promise<void> {
578
+ if (this.context.projectId === projectId) return;
579
+
580
+ this.context.projectId = projectId;
581
+ debug.log('websocket', 'Context: project set to', projectId);
582
+
583
+ if (this.isConnected) {
584
+ await this.syncContext();
585
+ }
586
+ }
587
+
588
+ /**
589
+ * Get current context
590
+ */
591
+ getContext(): { userId: string | null; projectId: string | null } {
592
+ return { ...this.context };
593
+ }
594
+
595
+ /**
596
+ * Wait for any pending context sync to complete
597
+ */
598
+ async waitForContextSync(): Promise<void> {
599
+ if (this.contextSyncPromise) {
600
+ await this.contextSyncPromise;
601
+ }
602
+ }
603
+
604
+ /**
605
+ * Sync context with server (awaitable)
606
+ * Waits for server confirmation before resolving
607
+ */
608
+ private async syncContext(): Promise<void> {
609
+ // If there's already a pending sync, wait for it
610
+ if (this.contextSyncPromise) {
611
+ await this.contextSyncPromise;
612
+ }
613
+
614
+ // Create a new sync promise
615
+ this.contextSyncPromise = this.doSyncContext();
616
+
617
+ try {
618
+ await this.contextSyncPromise;
619
+ } finally {
620
+ this.contextSyncPromise = null;
621
+ }
622
+ }
623
+
624
+ /**
625
+ * Actually perform the context sync
626
+ */
627
+ private async doSyncContext(): Promise<void> {
628
+ const requestId = `context-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
629
+
630
+ return new Promise<void>((resolve, reject) => {
631
+ let unsubResponse: (() => void) | null = null;
632
+ let timeoutId: ReturnType<typeof setTimeout> | null = null;
633
+
634
+ const cleanup = () => {
635
+ unsubResponse?.();
636
+ if (timeoutId) {
637
+ clearTimeout(timeoutId);
638
+ timeoutId = null;
639
+ }
640
+ };
641
+
642
+ // Response handler
643
+ const handleResponse = (response: any) => {
644
+ if (response?.requestId !== requestId) return;
645
+
646
+ cleanup();
647
+
648
+ if (response.success) {
649
+ debug.log('websocket', 'Context sync confirmed by server:', response.data);
650
+ resolve();
651
+ } else {
652
+ debug.error('websocket', 'Context sync failed:', response.error);
653
+ reject(new Error(response.error || 'Context sync failed'));
654
+ }
655
+ };
656
+
657
+ // Timeout handler (2 seconds — localhost round-trip should be near-instant)
658
+ timeoutId = setTimeout(() => {
659
+ cleanup();
660
+ debug.warn('websocket', 'Context sync timeout, assuming success');
661
+ // Don't reject on timeout - just warn and continue
662
+ resolve();
663
+ }, 2000);
664
+
665
+ // Register response listener
666
+ unsubResponse = this.on('ws:set-context:response' as any, handleResponse);
667
+
668
+ // Send context sync request
669
+ this.emit('ws:set-context' as any, {
670
+ requestId,
671
+ data: {
672
+ userId: this.context.userId,
673
+ projectId: this.context.projectId
674
+ }
675
+ } as any);
676
+
677
+ debug.log('websocket', 'Context sync sent:', this.context);
678
+ });
679
+ }
680
+
681
+ // =========================================================================
682
+ // HTTP-like Request-Response Pattern
683
+ // =========================================================================
684
+
685
+ /**
686
+ * HTTP-like request-response pattern over WebSocket
687
+ *
688
+ * This method provides a simplified API for request-response pattern.
689
+ * The server always returns `{ success, data?, error? }` response.
690
+ * This method unwraps the response:
691
+ * - If `success: true` → returns `data` directly
692
+ * - If `success: false` → throws Error with `error` message
693
+ * - Timeout → throws Error
694
+ */
695
+ http<TAction extends keyof TAPI['client']>(
696
+ action: TAction,
697
+ data?: TAPI['client'][TAction] extends { data: infer D } ? D : never,
698
+ timeout: number = 30000
699
+ ): Promise<
700
+ TAPI['server'][`${TAction & string}:response`] extends { success: boolean; data?: infer TData }
701
+ ? TData
702
+ : any
703
+ > {
704
+ const responseAction = `${action as string}:response` as any;
705
+ // Generate unique request ID to match request with response
706
+ const requestId = `${action as string}-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
707
+
708
+ return new Promise((resolve, reject) => {
709
+ let unsubResponse: (() => void) | null = null;
710
+ let timeoutId: ReturnType<typeof setTimeout> | null = null;
711
+
712
+ // Cleanup function
713
+ const cleanup = () => {
714
+ unsubResponse?.();
715
+ if (timeoutId) {
716
+ clearTimeout(timeoutId);
717
+ timeoutId = null;
718
+ }
719
+ };
720
+
721
+ // Response handler - unwrap { success, data, error } response
722
+ const handleResponse = (response: any) => {
723
+ // Only handle response that matches this request ID
724
+ if (response && response.requestId !== requestId) {
725
+ return; // Ignore responses for other requests
726
+ }
727
+
728
+ cleanup();
729
+
730
+ // Check if response has success field
731
+ if (response && typeof response === 'object' && 'success' in response) {
732
+ if (response.success) {
733
+ // Success: return data directly (unwrapped)
734
+ resolve(response.data);
735
+ } else {
736
+ // Failure: throw error with message from response.error
737
+ reject(new Error(response.error || 'Unknown error'));
738
+ }
739
+ } else {
740
+ // Legacy response format or unexpected structure
741
+ resolve(response);
742
+ }
743
+ };
744
+
745
+ // Timeout handler
746
+ if (timeout > 0) {
747
+ timeoutId = setTimeout(() => {
748
+ cleanup();
749
+ reject(new Error(`Request timeout: ${action as string} (${timeout}ms)`));
750
+ }, timeout);
751
+ }
752
+
753
+ // Register response listener
754
+ unsubResponse = this.on(responseAction, handleResponse);
755
+
756
+ // Send request with data structure and requestId
757
+ const payload = {
758
+ requestId,
759
+ data: data || {}
760
+ };
761
+
762
+ this.emit(action as any, payload as any);
763
+ });
764
+ }
765
+ }
766
+
767
+ // Export binary utilities for advanced use cases
768
+ export { encodeBinaryMessage, decodeBinaryMessage, containsBinary, isBinaryAction };