@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,513 @@
1
+ /**
2
+ * Browser Interact WebSocket Handler
3
+ * Handles mouse/keyboard interactions with browser
4
+ * **PROJECT ISOLATION**: Uses project-specific BrowserPreviewService instances
5
+ */
6
+
7
+ import { t } from 'elysia';
8
+ import { createRouter } from '$shared/utils/ws-server';
9
+ import { browserPreviewServiceManager } from '../../../lib/preview/index';
10
+ import { ws } from '$backend/lib/utils/ws';
11
+ import type { KeyInput } from 'puppeteer';
12
+ import { debug } from '$shared/utils/logger';
13
+ import { sleep } from '$shared/utils/async';
14
+
15
+ // Helper function to check if error is navigation-related
16
+ function isNavigationError(error: Error): boolean {
17
+ const msg = error.message.toLowerCase();
18
+ return msg.includes('execution context was destroyed') ||
19
+ msg.includes('target closed') ||
20
+ msg.includes('page is loading') ||
21
+ msg.includes('waiting for navigation') ||
22
+ msg.includes('inspected target navigated') ||
23
+ msg.includes('cannot find context');
24
+ }
25
+
26
+ export const interactPreviewHandler = createRouter()
27
+ // Action: Client requests browser interaction
28
+ .on('preview:browser-interact', {
29
+ data: t.Object({
30
+ action: t.Object({
31
+ type: t.String(),
32
+ x: t.Optional(t.Number()),
33
+ y: t.Optional(t.Number()),
34
+ startX: t.Optional(t.Number()),
35
+ startY: t.Optional(t.Number()),
36
+ endX: t.Optional(t.Number()),
37
+ endY: t.Optional(t.Number()),
38
+ currentX: t.Optional(t.Number()),
39
+ currentY: t.Optional(t.Number()),
40
+ text: t.Optional(t.String()),
41
+ key: t.Optional(t.String()),
42
+ button: t.Optional(t.String()),
43
+ delay: t.Optional(t.Number()),
44
+ steps: t.Optional(t.Number()),
45
+ deltaX: t.Optional(t.Number()),
46
+ deltaY: t.Optional(t.Number()),
47
+ selector: t.Optional(t.String()),
48
+ value: t.Optional(t.String()),
49
+ optionIndex: t.Optional(t.Number()),
50
+ ctrlKey: t.Optional(t.Boolean()),
51
+ metaKey: t.Optional(t.Boolean()),
52
+ altKey: t.Optional(t.Boolean()),
53
+ shiftKey: t.Optional(t.Boolean()),
54
+ clearFirst: t.Optional(t.Boolean()),
55
+ scale: t.Optional(t.Number()),
56
+ width: t.Optional(t.Number()),
57
+ height: t.Optional(t.Number()),
58
+ deviceSize: t.Optional(t.String()),
59
+ rotation: t.Optional(t.String())
60
+ }),
61
+ })
62
+ }, async ({ data, conn }) => {
63
+ const userId = ws.getUserId(conn);
64
+ const projectId = ws.getProjectId(conn);
65
+
66
+ // Get project-specific preview service
67
+ const previewService = browserPreviewServiceManager.getService(projectId);
68
+
69
+ try {
70
+ const { action } = data;
71
+
72
+ // Get active tab (mirip MCP pattern)
73
+ const tab = previewService.getActiveTab();
74
+ if (!tab) {
75
+ debug.error('preview', `❌ NO ACTIVE TAB for project ${projectId} - Available tabs: ${previewService.getTabCount()}`);
76
+ ws.emit.user(userId, 'preview:browser-error', {
77
+ message: 'No active tab. Please open or switch to a tab first.'
78
+ });
79
+ return;
80
+ }
81
+
82
+ const session = tab;
83
+ const tabId = tab.id;
84
+
85
+ // Check if page is still open and valid
86
+ if (!session.page || session.page.isClosed()) {
87
+ debug.warn('preview', `⚠️ Cannot execute interaction: page is closed for tab ${tabId}`);
88
+ ws.emit.user(userId, 'preview:browser-error', {
89
+ message: 'Browser page is closed'
90
+ });
91
+ return;
92
+ }
93
+
94
+ // Check if browser is still connected
95
+ if (!session.browser || !session.browser.connected) {
96
+ debug.warn('preview', `⚠️ Cannot execute interaction: browser disconnected for tab ${tabId}`);
97
+ ws.emit.user(userId, 'preview:browser-error', {
98
+ message: 'Browser is disconnected'
99
+ });
100
+ return;
101
+ }
102
+
103
+ // Mark tab activity to prevent automatic cleanup
104
+ previewService.markActiveTabActivity();
105
+
106
+ // Execute the interaction based on type with navigation-safe error handling
107
+ try {
108
+ switch (action.type) {
109
+ case 'mousedown':
110
+ try {
111
+ // Reset mouse state first to ensure clean state
112
+ try { await session.page.mouse.up(); } catch { }
113
+ // Move to position and press button
114
+ await session.page.mouse.move(action.x!, action.y!, { steps: 1 });
115
+ await session.page.mouse.down({ button: action.button === 'right' ? 'right' : 'left' });
116
+ } catch (error) {
117
+ if (error instanceof Error && isNavigationError(error)) {
118
+ ws.emit.user(userId, 'preview:browser-interacted', { action: action.type, message: 'Action deferred (navigation)', deferred: true });
119
+ return;
120
+ }
121
+ throw error;
122
+ }
123
+ break;
124
+
125
+ case 'mouseup':
126
+ try {
127
+ await session.page.mouse.up({ button: action.button === 'right' ? 'right' : 'left' });
128
+ } catch (error) {
129
+ if (error instanceof Error && isNavigationError(error)) {
130
+ ws.emit.user(userId, 'preview:browser-interacted', { action: action.type, message: 'Action deferred (navigation)', deferred: true });
131
+ return;
132
+ }
133
+ // Ignore "not pressed" errors - might have been released already
134
+ if (error instanceof Error && error.message.includes('not pressed')) {
135
+ break;
136
+ }
137
+ throw error;
138
+ }
139
+ break;
140
+
141
+ case 'click':
142
+ try {
143
+ // Reset mouse state before click to prevent "already pressed" errors
144
+ // This ensures a clean state for each click operation
145
+ try {
146
+ await session.page.mouse.up();
147
+ } catch { /* Ignore - mouse might not be pressed */ }
148
+
149
+ // IMPORTANT: Check for select element BEFORE clicking
150
+ // If it's a select, we'll emit event to frontend instead of clicking
151
+ const selectInfo = await previewService.checkForSelectElement(session.id, action.x!, action.y!);
152
+ if (selectInfo) {
153
+ // Select element detected - event emitted by checkForSelectElement
154
+ // Don't perform regular click, frontend will show dropdown overlay
155
+ debug.log('preview', `✅ Select element detected at (${action.x}, ${action.y}), skipping regular click`);
156
+ break;
157
+ }
158
+
159
+ // Perform the click - this does down + up atomically
160
+ await session.page.mouse.click(action.x!, action.y!);
161
+ } catch (error) {
162
+ if (error instanceof Error) {
163
+ if (isNavigationError(error)) {
164
+ ws.emit.user(userId, 'preview:browser-interacted', { action: action.type, message: 'Click triggered navigation', deferred: true });
165
+ return;
166
+ }
167
+ // These errors are recoverable - the click likely worked
168
+ if (error.message.includes('already pressed') || error.message.includes('not pressed')) {
169
+ // Mouse state errors are recoverable, continue without fallback
170
+ break;
171
+ }
172
+ }
173
+ throw error;
174
+ }
175
+ break;
176
+
177
+ case 'type':
178
+ if (action.text) {
179
+ try {
180
+ // For user manual typing, clearFirst defaults to false (append behavior)
181
+ // User can explicitly set clearFirst: true if they want to clear
182
+ const shouldClear = action.clearFirst === true; // default false for user
183
+
184
+ if (shouldClear) {
185
+ // Select all text first (Ctrl+A), then type will overwrite
186
+ await session.page.keyboard.down('Control');
187
+ await session.page.keyboard.press('KeyA');
188
+ await session.page.keyboard.up('Control');
189
+ await sleep(50); // Small delay for selection
190
+ }
191
+
192
+ await session.page.keyboard.type(action.text, {
193
+ delay: action.delay || 50
194
+ });
195
+ } catch (error) {
196
+ if (error instanceof Error && isNavigationError(error)) {
197
+ ws.emit.user(userId, 'preview:browser-interacted', { action: action.type, message: 'Action deferred (navigation)', deferred: true });
198
+ return;
199
+ }
200
+ throw error;
201
+ }
202
+ }
203
+ break;
204
+
205
+ case 'key':
206
+ if (action.key) {
207
+ try {
208
+ // Handle modifier keys (Ctrl+C, Ctrl+V, etc.)
209
+ const modifiers: string[] = [];
210
+ if (action.ctrlKey) modifiers.push('Control');
211
+ if (action.metaKey) modifiers.push('Meta');
212
+ if (action.altKey) modifiers.push('Alt');
213
+ if (action.shiftKey) modifiers.push('Shift');
214
+
215
+ if (modifiers.length > 0) {
216
+ // Press modifiers first
217
+ for (const mod of modifiers) {
218
+ await session.page.keyboard.down(mod as KeyInput);
219
+ }
220
+ // Press the main key
221
+ await session.page.keyboard.press(action.key as KeyInput);
222
+ // Release modifiers in reverse order
223
+ for (const mod of modifiers.reverse()) {
224
+ await session.page.keyboard.up(mod as KeyInput);
225
+ }
226
+ } else {
227
+ // No modifiers, just press the key
228
+ await session.page.keyboard.press(action.key as KeyInput);
229
+ }
230
+ } catch (error) {
231
+ if (error instanceof Error && isNavigationError(error)) {
232
+ ws.emit.user(userId, 'preview:browser-interacted', { action: action.type, message: 'Action deferred (navigation)', deferred: true });
233
+ return;
234
+ }
235
+ throw error;
236
+ }
237
+ }
238
+ break;
239
+
240
+ case 'mousemove':
241
+ try {
242
+ // Use minimal steps for smoother, faster mouse movement
243
+ await session.page.mouse.move(action.x!, action.y!, {
244
+ steps: action.steps || 1 // Reduced from 5 to 1 for faster response
245
+ });
246
+ // Update cursor position in browser context (fire-and-forget, don't await)
247
+ session.page.evaluate((data) => {
248
+ const { x, y } = data;
249
+ if ((window as any).__cursorInfo) {
250
+ (window as any).__cursorInfo.x = x;
251
+ (window as any).__cursorInfo.y = y;
252
+ (window as any).__cursorInfo.timestamp = Date.now();
253
+ }
254
+ }, { x: action.x!, y: action.y! }).catch(() => { /* Ignore evaluation errors */ });
255
+ } catch (error) {
256
+ if (error instanceof Error && isNavigationError(error)) {
257
+ ws.emit.user(userId, 'preview:browser-interacted', { action: action.type, message: 'Action deferred (navigation)', deferred: true });
258
+ return;
259
+ }
260
+ throw error;
261
+ }
262
+ break;
263
+
264
+ case 'scroll':
265
+ try {
266
+ await session.page.mouse.wheel({ deltaX: action.deltaX || 0, deltaY: action.deltaY || 0 });
267
+ } catch (error) {
268
+ if (error instanceof Error && isNavigationError(error)) {
269
+ ws.emit.user(userId, 'preview:browser-interacted', { action: action.type, message: 'Action deferred (navigation)', deferred: true });
270
+ return;
271
+ }
272
+ throw error;
273
+ }
274
+ break;
275
+
276
+ case 'doubleclick':
277
+ try {
278
+ // Reset mouse state first
279
+ try { await session.page.mouse.up(); } catch { }
280
+ await session.page.mouse.click(action.x!, action.y!, { clickCount: 2 });
281
+ } catch (error) {
282
+ if (error instanceof Error) {
283
+ if (isNavigationError(error)) {
284
+ ws.emit.user(userId, 'preview:browser-interacted', { action: action.type, message: 'Action deferred (navigation)', deferred: true });
285
+ return;
286
+ }
287
+ if (error.message.includes('already pressed') || error.message.includes('not pressed')) break;
288
+ }
289
+ throw error;
290
+ }
291
+ break;
292
+
293
+ case 'rightclick':
294
+ try {
295
+ // Reset mouse state first
296
+ try { await session.page.mouse.up(); } catch { }
297
+
298
+ // IMPORTANT: Check for context menu
299
+ // We'll emit context menu event to frontend for custom overlay
300
+ const menuInfo = await previewService.checkForContextMenu(session.id, action.x!, action.y!);
301
+ if (menuInfo) {
302
+ // Context menu detected - event emitted by checkForContextMenu
303
+ debug.log('preview', `✅ Context menu detected at (${action.x}, ${action.y})`);
304
+ // Don't perform regular right-click, frontend will show context menu overlay
305
+ break;
306
+ }
307
+
308
+ await session.page.mouse.click(action.x!, action.y!, { button: 'right' });
309
+ } catch (error) {
310
+ if (error instanceof Error) {
311
+ if (isNavigationError(error)) {
312
+ ws.emit.user(userId, 'preview:browser-interacted', { action: action.type, message: 'Action deferred (navigation)', deferred: true });
313
+ return;
314
+ }
315
+ if (error.message.includes('already pressed') || error.message.includes('not pressed')) break;
316
+ }
317
+ throw error;
318
+ }
319
+ break;
320
+
321
+ case 'drag':
322
+ try {
323
+ // Ensure clean state before drag
324
+ try { await session.page.mouse.up(); } catch { }
325
+
326
+ // Move to start, press, drag, release
327
+ await session.page.mouse.move(action.startX!, action.startY!, { steps: 1 });
328
+ await session.page.mouse.down();
329
+ await session.page.mouse.move(action.endX!, action.endY!, { steps: 3 });
330
+ await session.page.mouse.up();
331
+ } catch (error) {
332
+ try { await session.page.mouse.up(); } catch { }
333
+ if (error instanceof Error && isNavigationError(error)) {
334
+ ws.emit.user(userId, 'preview:browser-interacted', { action: action.type, message: 'Drag during navigation', deferred: true });
335
+ return;
336
+ }
337
+ }
338
+ break;
339
+
340
+ case 'dragmove':
341
+ try {
342
+ await session.page.mouse.move(action.currentX!, action.currentY!, { steps: 1 });
343
+ } catch { }
344
+ break;
345
+
346
+ case 'keynav':
347
+ if (action.key) {
348
+ const modifiers: string[] = [];
349
+ if (action.ctrlKey) modifiers.push('Control');
350
+ if (action.metaKey) modifiers.push('Meta');
351
+ if (action.altKey) modifiers.push('Alt');
352
+ if (action.shiftKey) modifiers.push('Shift');
353
+
354
+ if (modifiers.length > 0) {
355
+ for (const mod of modifiers) {
356
+ await session.page.keyboard.down(mod as KeyInput);
357
+ }
358
+ await session.page.keyboard.press(action.key as KeyInput);
359
+ for (const mod of modifiers.reverse()) {
360
+ await session.page.keyboard.up(mod as KeyInput);
361
+ }
362
+ } else {
363
+ await session.page.keyboard.press(action.key as KeyInput);
364
+ }
365
+
366
+ if (['ArrowDown', 'ArrowUp'].includes(action.key)) {
367
+ try {
368
+ await sleep(50);
369
+ } catch { }
370
+ }
371
+ }
372
+ break;
373
+
374
+ case 'checkselectoptions':
375
+ if (action.x && action.y) {
376
+ try {
377
+ await session.page.evaluate((coordinates) => {
378
+ const { x, y } = coordinates;
379
+ const element = document.elementFromPoint(x, y);
380
+
381
+ if (!element) return null;
382
+
383
+ const isSelect = element.tagName === 'SELECT';
384
+ const isOption = element.tagName === 'OPTION' || element.getAttribute('role') === 'option';
385
+ const isDropdown = element.classList.contains('dropdown') ||
386
+ element.classList.contains('select') ||
387
+ element.getAttribute('role') === 'listbox';
388
+
389
+ let options: Array<{ index: number, value: string, text: string, selected: boolean }> = [];
390
+ if (isSelect || isDropdown || isOption) {
391
+ const selectElement = isSelect ? element :
392
+ element.closest('select') ||
393
+ element.closest('[role="listbox"]') ||
394
+ element.closest('.dropdown');
395
+
396
+ if (selectElement) {
397
+ if (selectElement.tagName === 'SELECT') {
398
+ options = Array.from(selectElement.querySelectorAll('option')).map((opt, index) => ({
399
+ index,
400
+ value: opt.value || '',
401
+ text: opt.textContent || '',
402
+ selected: opt.selected
403
+ }));
404
+ } else {
405
+ options = Array.from(selectElement.querySelectorAll('[role="option"], .option, .dropdown-item')).map((opt, index) => ({
406
+ index,
407
+ value: opt.getAttribute('data-value') || opt.textContent || '',
408
+ text: opt.textContent || '',
409
+ selected: opt.getAttribute('aria-selected') === 'true' || opt.classList.contains('selected')
410
+ }));
411
+ }
412
+ }
413
+ }
414
+
415
+ return {
416
+ elementType: element.tagName,
417
+ isSelect,
418
+ isOption,
419
+ isDropdown,
420
+ hasOptions: options.length > 0,
421
+ options: options.slice(0, 10),
422
+ elementId: element.id,
423
+ elementClass: element.className,
424
+ elementText: element.textContent?.slice(0, 100)
425
+ };
426
+ }, { x: action.x, y: action.y });
427
+ } catch (error) {
428
+ debug.error('preview', 'Error checking for select options:', error);
429
+ }
430
+ }
431
+ break;
432
+
433
+ case 'scale-update':
434
+ // Handle scale update from frontend (hot-swap, no reconnection)
435
+ if (action.scale && action.scale > 0 && action.scale <= 1) {
436
+ session.scale = action.scale;
437
+ debug.log('preview', `📐 Scale updated for tab ${tabId}: ${action.scale}`);
438
+
439
+ // Hot-swap resolution without reconnection
440
+ const updated = await previewService.updateWebCodecsScale(session.id, action.scale);
441
+ if (!updated) {
442
+ debug.warn('preview', `⚠️ Hot-swap failed, falling back to restart`);
443
+ // Fallback: restart if hot-swap fails
444
+ await previewService.stopWebCodecsStreaming(session.id);
445
+ await previewService.startWebCodecsStreaming(session.id);
446
+ }
447
+ }
448
+ break;
449
+
450
+ case 'viewport-update':
451
+ // Handle viewport change (device/rotation) without reconnection
452
+ if (action.width && action.height && action.scale && action.scale > 0 && action.scale <= 1) {
453
+ session.scale = action.scale;
454
+ if (action.deviceSize) session.deviceSize = action.deviceSize as any;
455
+ if (action.rotation) session.rotation = action.rotation as any;
456
+
457
+ debug.log('preview', `📱 Viewport updated for tab ${tabId}: ${action.width}x${action.height} (scale: ${action.scale})`);
458
+
459
+ // Hot-swap viewport without reconnection
460
+ const updated = await previewService.updateWebCodecsViewport(session.id, action.width, action.height, action.scale);
461
+ if (!updated) {
462
+ debug.warn('preview', `⚠️ Viewport hot-swap failed`);
463
+ }
464
+ }
465
+ break;
466
+
467
+ default:
468
+ ws.emit.user(userId, 'preview:browser-error', {
469
+ message: `Unknown action type: ${action.type}`
470
+ });
471
+ return;
472
+ }
473
+ } catch (interactionError) {
474
+ if (interactionError instanceof Error) {
475
+ const errorMsg = interactionError.message.toLowerCase();
476
+
477
+ if (errorMsg.includes('execution context was destroyed') ||
478
+ errorMsg.includes('target closed') ||
479
+ errorMsg.includes('page is loading') ||
480
+ errorMsg.includes('waiting for navigation')) {
481
+
482
+ ws.emit.user(userId, 'preview:browser-interacted', {
483
+ action: action.type,
484
+ message: `Action ${action.type} deferred due to page loading`,
485
+ deferred: true
486
+ });
487
+ return;
488
+ }
489
+ }
490
+ throw interactionError;
491
+ }
492
+
493
+ ws.emit.user(userId, 'preview:browser-interacted', {
494
+ action: action.type,
495
+ message: `Action ${action.type} executed successfully`
496
+ });
497
+ } catch (error) {
498
+ debug.error('preview', 'Error executing browser interaction:', error);
499
+ ws.emit.user(userId, 'preview:browser-error', {
500
+ message: error instanceof Error ? error.message : 'Unknown error'
501
+ });
502
+ }
503
+ })
504
+
505
+ // Server → Client Events (independent declarations)
506
+ .emit('preview:browser-interacted', t.Object({
507
+ action: t.String(),
508
+ message: t.String(),
509
+ deferred: t.Optional(t.Boolean())
510
+ }))
511
+ .emit('preview:browser-error', t.Object({
512
+ message: t.String()
513
+ }));
@@ -0,0 +1,129 @@
1
+ /**
2
+ * MCP Tab Handlers
3
+ *
4
+ * HTTP endpoints for MCP tab coordination between backend and frontend.
5
+ */
6
+
7
+ import { createRouter } from '$shared/utils/ws-server';
8
+ import { t } from 'elysia';
9
+ import { browserMcpControl } from '$backend/lib/preview';
10
+ import { debug } from '$shared/utils/logger';
11
+
12
+ // Tab response types
13
+ const TabSchema = t.Object({
14
+ id: t.String(),
15
+ url: t.String(),
16
+ title: t.String(),
17
+ sessionId: t.Union([t.String(), t.Null()]),
18
+ isActive: t.Boolean()
19
+ });
20
+
21
+ export const mcpPreviewHandler = createRouter()
22
+ // Tabs list
23
+ .http('preview:mcp-tab-list', {
24
+ data: t.Object({
25
+ requestId: t.String(),
26
+ tabs: t.Array(TabSchema)
27
+ }),
28
+ response: t.Object({
29
+ success: t.Boolean()
30
+ })
31
+ }, async ({ data }) => {
32
+ debug.log('mcp', `📋 Received tabs list for request: ${data.requestId}`);
33
+
34
+ const resolved = browserMcpControl.resolveTabRequest(data.requestId, {
35
+ tabs: data.tabs
36
+ });
37
+
38
+ return { success: resolved };
39
+ })
40
+
41
+ // Active tab
42
+ .http('preview:mcp-active-tab', {
43
+ data: t.Object({
44
+ requestId: t.String(),
45
+ tab: t.Union([TabSchema, t.Null()])
46
+ }),
47
+ response: t.Object({
48
+ success: t.Boolean()
49
+ })
50
+ }, async ({ data }) => {
51
+ debug.log('mcp', `📋 Received active tab for request: ${data.requestId}`);
52
+
53
+ const resolved = browserMcpControl.resolveTabRequest(data.requestId, {
54
+ tab: data.tab
55
+ });
56
+
57
+ return { success: resolved };
58
+ })
59
+
60
+ // Switch tab
61
+ .http('preview:mcp-switch-tab', {
62
+ data: t.Object({
63
+ requestId: t.String(),
64
+ success: t.Boolean(),
65
+ tab: t.Optional(TabSchema),
66
+ error: t.Optional(t.String())
67
+ }),
68
+ response: t.Object({
69
+ success: t.Boolean()
70
+ })
71
+ }, async ({ data }) => {
72
+ debug.log('mcp', `📋 Received switch tab for request: ${data.requestId}`);
73
+
74
+ const resolved = browserMcpControl.resolveTabRequest(data.requestId, {
75
+ success: data.success,
76
+ tab: data.tab,
77
+ error: data.error
78
+ });
79
+
80
+ return { success: resolved };
81
+ })
82
+
83
+ // Open tab
84
+ .http('preview:mcp-open-tab', {
85
+ data: t.Object({
86
+ requestId: t.String(),
87
+ success: t.Boolean(),
88
+ tab: t.Optional(TabSchema),
89
+ error: t.Optional(t.String())
90
+ }),
91
+ response: t.Object({
92
+ success: t.Boolean()
93
+ })
94
+ }, async ({ data }) => {
95
+ debug.log('mcp', `📋 Received open tab for request: ${data.requestId}`);
96
+
97
+ const resolved = browserMcpControl.resolveTabRequest(data.requestId, {
98
+ success: data.success,
99
+ tab: data.tab,
100
+ error: data.error
101
+ });
102
+
103
+ return { success: resolved };
104
+ })
105
+
106
+ // Close tab
107
+ .http('preview:mcp-close-tab', {
108
+ data: t.Object({
109
+ requestId: t.String(),
110
+ success: t.Boolean(),
111
+ closedTabId: t.Optional(t.String()),
112
+ newActiveTab: t.Optional(TabSchema),
113
+ error: t.Optional(t.String())
114
+ }),
115
+ response: t.Object({
116
+ success: t.Boolean()
117
+ })
118
+ }, async ({ data }) => {
119
+ debug.log('mcp', `📋 Received close tab for request: ${data.requestId}`);
120
+
121
+ const resolved = browserMcpControl.resolveTabRequest(data.requestId, {
122
+ success: data.success,
123
+ closedTabId: data.closedTabId,
124
+ newActiveTab: data.newActiveTab,
125
+ error: data.error
126
+ });
127
+
128
+ return { success: resolved };
129
+ });