@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,837 @@
1
+ /**
2
+ * Browser Coordinator
3
+ * Coordinates all browser preview services and state management
4
+ */
5
+
6
+ import { debug } from '$shared/utils/logger';
7
+ import type { DeviceSize, Rotation } from '$frontend/lib/constants/preview';
8
+ import { createTabManager, type TabManager, type PreviewTab, getTabTitle } from './tab-manager.svelte';
9
+ import { createStreamMessageHandler, type StreamMessageHandler } from './stream-handler.svelte';
10
+ import { createNativeUIHandler, type NativeUIHandler } from './native-ui-handlers.svelte';
11
+ import { createMcpHandler, type McpHandler } from './mcp-handlers.svelte';
12
+ import {
13
+ launchBrowser as launchBrowserOp,
14
+ navigateBrowser as navigateBrowserOp,
15
+ destroyBrowser,
16
+ destroyBrowserTab,
17
+ getExistingTabs,
18
+ switchToBackendTab,
19
+ type ExistingTabInfo
20
+ } from './tab-operations.svelte';
21
+ import { browserCleanup } from './cleanup.svelte';
22
+ import { sendInteraction, updateViewport, setInteractionProjectId } from './interactions.svelte';
23
+ import type { BrowserSelectInfo, BrowserContextMenuInfo } from '$frontend/lib/types/native-ui';
24
+
25
+ export interface BrowserCoordinatorConfig {
26
+ // Project ID getter (REQUIRED for project isolation)
27
+ projectId: () => string;
28
+
29
+ // Callbacks from parent component
30
+ onUrlChange?: (url: string) => void;
31
+ onUrlInputChange?: (urlInput: string) => void;
32
+ onSessionChange?: (sessionId: string | null) => void;
33
+ onLoadingChange?: (isLoading: boolean) => void;
34
+ onErrorChange?: (errorMessage: string | null) => void;
35
+
36
+ // Session recovery callback - called when sessions are recovered after page refresh
37
+ onSessionsRecovered?: (tabCount: number) => void;
38
+
39
+ // UI state callbacks
40
+ onSelectOpen?: (selectInfo: BrowserSelectInfo) => void;
41
+ onContextMenuOpen?: (menuInfo: BrowserContextMenuInfo) => void;
42
+ onVirtualCursorUpdate?: (x: number, y: number, clicking?: boolean) => void;
43
+ onVirtualCursorHide?: () => void;
44
+ onMcpCursorUpdate?: (x: number, y: number, clicking?: boolean) => void;
45
+ onMcpCursorHide?: () => void;
46
+
47
+ // Coordinate transformation
48
+ transformBrowserToDisplayCoordinates?: (x: number, y: number) => { x: number, y: number } | null;
49
+ }
50
+
51
+ /**
52
+ * Create browser coordinator
53
+ */
54
+ export function createBrowserCoordinator(config: BrowserCoordinatorConfig) {
55
+ const {
56
+ projectId: getProjectId,
57
+ onUrlChange,
58
+ onUrlInputChange,
59
+ onSessionChange,
60
+ onLoadingChange,
61
+ onErrorChange,
62
+ onSessionsRecovered,
63
+ onSelectOpen,
64
+ onContextMenuOpen,
65
+ onVirtualCursorUpdate,
66
+ onVirtualCursorHide,
67
+ onMcpCursorUpdate,
68
+ onMcpCursorHide,
69
+ transformBrowserToDisplayCoordinates
70
+ } = config;
71
+
72
+ // Restore lock to prevent race conditions
73
+ let isRestoring = $state(false);
74
+ let eventListenersReady = $state(false);
75
+
76
+ // Create managers
77
+ const tabManager = createTabManager();
78
+
79
+ // Create stream handler
80
+ const streamHandler = createStreamMessageHandler({
81
+ tabManager,
82
+ onNavigationUpdate: (tabId, url) => {
83
+ // Only notify parent if this is the active tab
84
+ if (tabId === tabManager.activeTabId) {
85
+ if (onUrlChange) onUrlChange(url);
86
+ if (onUrlInputChange) onUrlInputChange(url);
87
+ }
88
+ },
89
+ onCursorUpdate: (x, y, clicking) => {
90
+ if (onVirtualCursorUpdate) {
91
+ onVirtualCursorUpdate(x, y, clicking);
92
+ }
93
+ },
94
+ onTestCompleted: () => {
95
+ if (onVirtualCursorHide) {
96
+ onVirtualCursorHide();
97
+ }
98
+ },
99
+ transformBrowserToDisplayCoordinates
100
+ });
101
+
102
+ // Create native UI handler
103
+ const nativeUIHandler = createNativeUIHandler({
104
+ tabManager,
105
+ transformBrowserToDisplayCoordinates,
106
+ onSelectOpen,
107
+ onContextMenuOpen,
108
+ onOpenUrlNewTab: (url) => {
109
+ createNewTab(url);
110
+ }
111
+ });
112
+
113
+ // Create MCP handler
114
+ const mcpHandler = createMcpHandler({
115
+ tabManager,
116
+ transformBrowserToDisplayCoordinates,
117
+ onCursorUpdate: (x, y, clicking) => {
118
+ if (onMcpCursorUpdate) {
119
+ onMcpCursorUpdate(x, y, clicking);
120
+ }
121
+ },
122
+ onCursorHide: () => {
123
+ if (onMcpCursorHide) {
124
+ onMcpCursorHide();
125
+ }
126
+ },
127
+ onLaunchRequest: (url, deviceSize, rotation, sessionId) => {
128
+ handleMcpLaunchRequest(url, deviceSize, rotation, sessionId);
129
+ }
130
+ });
131
+
132
+ // Browser session management
133
+
134
+ /**
135
+ * Launch browser for a tab
136
+ */
137
+ async function launchBrowserForTab(tabId: string, tabUrl: string, mcpSessionId?: string) {
138
+ debug.log('preview', `๐Ÿš€ launchBrowserForTab called - tabId: ${tabId}, url: ${tabUrl}${mcpSessionId ? `, mcpSessionId: ${mcpSessionId}` : ''}`);
139
+
140
+ const tab = tabManager.getTab(tabId);
141
+ if (!tab || !tabUrl) {
142
+ debug.error('preview', `โŒ Tab not found or no URL: ${tabId}`);
143
+ return;
144
+ }
145
+
146
+ try {
147
+ tabManager.updateTab(tabId, { isLaunchingBrowser: true, errorMessage: null });
148
+
149
+ // Get current projectId
150
+ const projectId = getProjectId();
151
+ const result = await launchBrowserOp(tabUrl, tab.deviceSize, tab.rotation, projectId, mcpSessionId);
152
+
153
+ if (result.success && result.sessionId && result.sessionInfo) {
154
+ debug.log('preview', `โœ… Browser launched successfully - sessionId: ${result.sessionId}`);
155
+
156
+ // Register session for cleanup tracking
157
+ browserCleanup.registerSession(result.sessionId);
158
+
159
+ // Use the actual URL from backend (which may be different due to redirects)
160
+ const actualUrl = result.sessionInfo.url || tabUrl;
161
+
162
+ tabManager.updateTab(tabId, {
163
+ sessionId: result.sessionId,
164
+ sessionInfo: result.sessionInfo,
165
+ url: actualUrl,
166
+ lastFrameData: null,
167
+ errorMessage: null
168
+ });
169
+
170
+ // Notify parent if active tab
171
+ if (tabId === tabManager.activeTabId) {
172
+ if (onSessionChange) onSessionChange(result.sessionId);
173
+ if (onUrlChange) onUrlChange(actualUrl);
174
+ if (onUrlInputChange) onUrlInputChange(actualUrl);
175
+ if (onErrorChange) onErrorChange(null);
176
+ }
177
+
178
+ } else {
179
+ const errorMsg = result.error || 'Unknown error';
180
+ debug.error('preview', `โŒ Browser launch failed:`, errorMsg);
181
+ tabManager.updateTab(tabId, { errorMessage: errorMsg });
182
+
183
+ if (tabId === tabManager.activeTabId && onErrorChange) {
184
+ onErrorChange(errorMsg);
185
+ }
186
+ }
187
+ } catch (error) {
188
+ const errorMsg = error instanceof Error ? error.message : 'Unknown error';
189
+ debug.error('preview', `๐Ÿ’ฅ Exception in launchBrowserForTab:`, error);
190
+ tabManager.updateTab(tabId, { errorMessage: `Exception: ${errorMsg}` });
191
+
192
+ if (tabId === tabManager.activeTabId && onErrorChange) {
193
+ onErrorChange(`Exception: ${errorMsg}`);
194
+ }
195
+ } finally {
196
+ tabManager.updateTab(tabId, { isLaunchingBrowser: false });
197
+ }
198
+ }
199
+
200
+ /**
201
+ * Navigate browser for a tab
202
+ */
203
+ async function navigateBrowserForTab(tabId: string, newUrl: string) {
204
+ const tab = tabManager.getTab(tabId);
205
+ if (!tab || !tab.sessionId) return;
206
+
207
+ try {
208
+ // Set isNavigating since we have an existing session
209
+ tabManager.updateTab(tabId, { isLoading: true, isNavigating: true, errorMessage: null });
210
+
211
+ // Get current projectId
212
+ const projectId = getProjectId();
213
+ const result = await navigateBrowserOp(newUrl, projectId);
214
+
215
+ if (result.success) {
216
+ const finalUrl = result.finalUrl || newUrl;
217
+ tabManager.updateTab(tabId, {
218
+ url: finalUrl,
219
+ errorMessage: null
220
+ });
221
+
222
+ // Notify parent if active tab
223
+ if (tabId === tabManager.activeTabId) {
224
+ if (onUrlChange) onUrlChange(finalUrl);
225
+ if (onUrlInputChange) onUrlInputChange(finalUrl);
226
+ if (onErrorChange) onErrorChange(null);
227
+ }
228
+ } else {
229
+ const errorMsg = result.error || 'Navigation failed';
230
+ debug.error('preview', `โŒ Browser navigation failed:`, errorMsg);
231
+ tabManager.updateTab(tabId, { errorMessage: errorMsg, isNavigating: false });
232
+
233
+ if (tabId === tabManager.activeTabId && onErrorChange) {
234
+ onErrorChange(errorMsg);
235
+ }
236
+ }
237
+ } catch (error) {
238
+ const errorMsg = error instanceof Error ? error.message : 'Unknown error';
239
+ debug.error('preview', `๐Ÿ’ฅ Exception in navigateBrowserForTab:`, error);
240
+ tabManager.updateTab(tabId, { errorMessage: `Exception: ${errorMsg}`, isNavigating: false });
241
+
242
+ if (tabId === tabManager.activeTabId && onErrorChange) {
243
+ onErrorChange(`Exception: ${errorMsg}`);
244
+ }
245
+ } finally {
246
+ tabManager.updateTab(tabId, { isLoading: false });
247
+ // Note: isNavigating will be cleared when new frame arrives (in stream-handler)
248
+ }
249
+ }
250
+
251
+ /**
252
+ * Create new tab
253
+ */
254
+ function createNewTab(tabUrl: string = ''): string {
255
+ // Prevent creating tabs during restore to avoid race conditions
256
+ if (isRestoring) {
257
+ debug.warn('preview', 'โš ๏ธ Cannot create tab while restoring sessions');
258
+ return '';
259
+ }
260
+
261
+ const tabId = tabManager.createTab(tabUrl);
262
+
263
+ // Update parent state
264
+ if (onUrlChange) onUrlChange(tabUrl);
265
+ if (onUrlInputChange) onUrlInputChange(tabUrl);
266
+
267
+ // Launch browser if URL is provided
268
+ if (tabUrl) {
269
+ setTimeout(() => {
270
+ launchBrowserForTab(tabId, tabUrl);
271
+ }, 0);
272
+ }
273
+
274
+ return tabId;
275
+ }
276
+
277
+ /**
278
+ * Switch to tab
279
+ */
280
+ async function switchToTab(tabId: string) {
281
+ const tab = tabManager.switchTab(tabId);
282
+ if (!tab) return;
283
+
284
+ // If this tab has a backend session, switch backend to it
285
+ // This ensures streaming comes from the correct backend tab
286
+ if (tab.sessionId) {
287
+ const projectId = getProjectId();
288
+ await switchToBackendTab(tab.sessionId, projectId);
289
+ }
290
+
291
+ // Notify parent of state changes
292
+ if (onUrlChange) onUrlChange(tab.url);
293
+ if (onUrlInputChange) onUrlInputChange(tab.url);
294
+ if (onSessionChange) onSessionChange(tab.sessionId);
295
+ if (onLoadingChange) onLoadingChange(tab.isLoading);
296
+ if (onErrorChange) onErrorChange(tab.errorMessage);
297
+ }
298
+
299
+ /**
300
+ * Close tab - explicitly destroys backend session
301
+ */
302
+ function closeTab(tabId: string) {
303
+ const tab = tabManager.getTab(tabId);
304
+ if (!tab) return;
305
+
306
+ // Cleanup session - explicitly destroy the backend tab
307
+ if (tab.sessionId) {
308
+ browserCleanup.unregisterSession(tab.sessionId);
309
+ const projectId = getProjectId();
310
+ destroyBrowserTab(tab.sessionId, projectId); // Use specific tab ID, not active tab
311
+ }
312
+
313
+ const { newActiveTab } = tabManager.closeTab(tabId);
314
+
315
+ // Check if there are any tabs left
316
+ const tabsLeft = tabManager.getAllTabs().length;
317
+
318
+ // If no tabs left, create a new empty tab
319
+ if (tabsLeft === 0) {
320
+ debug.log('preview', '๐Ÿ“ No tabs left after close, creating new empty tab');
321
+ createNewTab('');
322
+ return;
323
+ }
324
+
325
+ // Update parent state if we switched to a new active tab
326
+ if (newActiveTab) {
327
+ if (onUrlChange) onUrlChange(newActiveTab.url);
328
+ if (onUrlInputChange) onUrlInputChange(newActiveTab.url);
329
+ if (onSessionChange) onSessionChange(newActiveTab.sessionId);
330
+ if (onLoadingChange) onLoadingChange(newActiveTab.isLoading);
331
+ if (onErrorChange) onErrorChange(newActiveTab.errorMessage);
332
+ }
333
+ // If newActiveTab is null but tabs still exist, it means we closed a non-active tab
334
+ // In this case, don't update parent state as the active tab hasn't changed
335
+ }
336
+
337
+ /**
338
+ * Handle device size change
339
+ * When changing device, reset to that device's natural default rotation
340
+ */
341
+ function changeDeviceSize(newSize: DeviceSize, scale?: number) {
342
+ if (!tabManager.activeTabId) return;
343
+
344
+ const tab = tabManager.activeTab;
345
+ if (!tab || tab.deviceSize === newSize) return;
346
+
347
+ // Reset to device's natural default rotation:
348
+ // - Desktop/laptop: landscape (natural default)
349
+ // - Tablet/mobile: portrait (natural default)
350
+ const finalRotation: Rotation = (newSize === 'desktop' || newSize === 'laptop') ? 'landscape' : 'portrait';
351
+
352
+ // Update tab state
353
+ tabManager.updateTab(tabManager.activeTabId, {
354
+ deviceSize: newSize,
355
+ rotation: finalRotation
356
+ });
357
+
358
+ // Hot-swap viewport if session is active
359
+ if (tab.sessionId && scale) {
360
+ setTimeout(() => {
361
+ updateViewport(newSize, finalRotation, scale);
362
+ }, 100);
363
+ }
364
+ }
365
+
366
+ /**
367
+ * Toggle rotation
368
+ */
369
+ function toggleRotation(scale?: number) {
370
+ if (!tabManager.activeTabId) return;
371
+
372
+ const tab = tabManager.activeTab;
373
+ if (!tab) return;
374
+
375
+ const newRotation = tab.rotation === 'portrait' ? 'landscape' : 'portrait';
376
+
377
+ // Update tab state
378
+ tabManager.updateTab(tabManager.activeTabId, {
379
+ rotation: newRotation
380
+ });
381
+
382
+ // Hot-swap viewport if session is active
383
+ if (tab.sessionId && scale) {
384
+ setTimeout(() => {
385
+ updateViewport(tab.deviceSize, newRotation, scale);
386
+ }, 100);
387
+ }
388
+ }
389
+
390
+ /**
391
+ * Handle MCP launch request
392
+ */
393
+ function handleMcpLaunchRequest(url: string, deviceSize: string, rotation: string, sessionId?: string) {
394
+ if (tabManager.activeTabId) {
395
+ const tab = tabManager.activeTab;
396
+ if (tab && !tab.sessionId) {
397
+ const finalDeviceSize = (deviceSize || 'laptop') as DeviceSize;
398
+ // Use device-appropriate default rotation if not specified
399
+ const finalRotation = rotation
400
+ ? (rotation as Rotation)
401
+ : ((finalDeviceSize === 'desktop' || finalDeviceSize === 'laptop') ? 'landscape' : 'portrait');
402
+
403
+ // Use existing empty tab
404
+ tabManager.updateTab(tabManager.activeTabId, {
405
+ url: url,
406
+ deviceSize: finalDeviceSize,
407
+ rotation: finalRotation
408
+ });
409
+ launchBrowserForTab(tabManager.activeTabId, url, sessionId);
410
+ } else {
411
+ // Create new tab
412
+ createNewTabWithMcpSession(url, sessionId);
413
+ }
414
+ } else {
415
+ // Create new tab
416
+ createNewTabWithMcpSession(url, sessionId);
417
+ }
418
+ }
419
+
420
+ /**
421
+ * Create new tab with MCP session
422
+ */
423
+ function createNewTabWithMcpSession(tabUrl: string, mcpSessionId?: string): string {
424
+ const tabId = tabManager.createTab(tabUrl);
425
+
426
+ // Update parent state
427
+ if (onUrlChange) onUrlChange(tabUrl);
428
+ if (onUrlInputChange) onUrlInputChange(tabUrl);
429
+
430
+ // Launch browser with MCP sessionId
431
+ if (tabUrl) {
432
+ setTimeout(() => {
433
+ launchBrowserForTab(tabId, tabUrl, mcpSessionId);
434
+ }, 0);
435
+ }
436
+
437
+ return tabId;
438
+ }
439
+
440
+ /**
441
+ * Initialize event listeners
442
+ * NOTE: Session recovery is now handled by the project change watcher in BrowserPreview.svelte
443
+ * to ensure projectId is available before attempting recovery
444
+ */
445
+ function initialize() {
446
+ browserCleanup.initialize();
447
+ nativeUIHandler.setupEventListeners();
448
+ mcpHandler.setupEventListeners();
449
+
450
+ // CRITICAL: Setup event listeners FIRST before any recovery
451
+ // This ensures we don't miss events from backend
452
+ setupTabEventListeners();
453
+ setupNavigationListeners();
454
+
455
+ // Mark event listeners as ready
456
+ eventListenersReady = true;
457
+
458
+ // Note: setInteractionProjectId and recoverExistingSessions are called by switchProject()
459
+ // which is triggered by the project watcher effect after projectId becomes available
460
+ }
461
+
462
+ /**
463
+ * Recover existing browser sessions from backend (after page refresh)
464
+ */
465
+ async function recoverExistingSessions() {
466
+ // Wait for event listeners to be ready before recovering
467
+ // This prevents missing events from backend during recovery
468
+ if (!eventListenersReady) {
469
+ debug.warn('preview', 'โš ๏ธ Event listeners not ready, waiting...');
470
+ await new Promise(resolve => {
471
+ const checkInterval = setInterval(() => {
472
+ if (eventListenersReady) {
473
+ clearInterval(checkInterval);
474
+ resolve(true);
475
+ }
476
+ }, 50);
477
+ });
478
+ }
479
+
480
+ // Set restore lock to prevent race conditions
481
+ isRestoring = true;
482
+
483
+ try {
484
+ const projectId = getProjectId();
485
+ const existingTabs = await getExistingTabs(projectId);
486
+
487
+ if (!existingTabs || existingTabs.count === 0) {
488
+ debug.log('preview', '๐Ÿ“ญ No existing sessions to recover');
489
+ isRestoring = false;
490
+ return;
491
+ }
492
+
493
+ // Track which frontend tab corresponds to the active tab
494
+ let activeTabFrontendId: string | null = null;
495
+ let totalRestored = 0;
496
+
497
+ // Restore backend tabs
498
+ debug.log('preview', `๐Ÿ”„ Recovering ${existingTabs.count} existing browser sessions...`);
499
+
500
+ for (const backendTab of existingTabs.tabs) {
501
+ const frontendId = restoreTabFromBackend(backendTab, backendTab.isActive);
502
+
503
+ // Register session for cleanup tracking
504
+ browserCleanup.registerSession(backendTab.tabId);
505
+
506
+ // Remember which frontend tab should be active
507
+ if (backendTab.isActive && frontendId) {
508
+ activeTabFrontendId = frontendId;
509
+ }
510
+ totalRestored++;
511
+ }
512
+
513
+ // Now switch to the active tab (after all tabs are restored)
514
+ if (activeTabFrontendId) {
515
+ debug.log('preview', `๐ŸŽฏ Switching to active tab: ${activeTabFrontendId}`);
516
+ tabManager.switchTab(activeTabFrontendId);
517
+
518
+ // If active tab has a backend session, notify backend
519
+ const activeTab = tabManager.getTab(activeTabFrontendId);
520
+ if (activeTab?.sessionId) {
521
+ await switchToBackendTab(activeTab.sessionId, projectId);
522
+ }
523
+
524
+ // Update parent state with active tab info
525
+ if (activeTab) {
526
+ if (onUrlChange) onUrlChange(activeTab.url);
527
+ if (onUrlInputChange) onUrlInputChange(activeTab.url);
528
+ if (onSessionChange) onSessionChange(activeTab.sessionId);
529
+ if (onLoadingChange) onLoadingChange(false);
530
+ if (onErrorChange) onErrorChange(null);
531
+ }
532
+ }
533
+
534
+ debug.log('preview', `โœ… Session recovery complete - restored ${totalRestored} tabs`);
535
+
536
+ // Notify parent that sessions were recovered
537
+ if (onSessionsRecovered) {
538
+ onSessionsRecovered(totalRestored);
539
+ }
540
+ } catch (error) {
541
+ debug.error('preview', 'โŒ Error recovering sessions:', error);
542
+ } finally {
543
+ // Always release restore lock
544
+ isRestoring = false;
545
+ }
546
+ }
547
+
548
+ /**
549
+ * Restore a single tab from backend session
550
+ * Returns the frontend tab ID for this restored tab
551
+ */
552
+ function restoreTabFromBackend(backendTab: ExistingTabInfo, isActive: boolean): string {
553
+ debug.log('preview', `๐Ÿ”ง Restoring tab: ${backendTab.tabId} (${backendTab.url})`);
554
+
555
+ // Create frontend tab matching backend tab
556
+ const frontendTabId = tabManager.createTab(backendTab.url);
557
+
558
+ // Update tab with backend session info
559
+ tabManager.updateTab(frontendTabId, {
560
+ sessionId: backendTab.tabId,
561
+ sessionInfo: {
562
+ quality: backendTab.quality,
563
+ url: backendTab.url,
564
+ deviceSize: backendTab.deviceSize as DeviceSize,
565
+ rotation: backendTab.rotation as Rotation
566
+ },
567
+ url: backendTab.url,
568
+ title: backendTab.title || getTabTitle(backendTab.url),
569
+ deviceSize: backendTab.deviceSize as DeviceSize,
570
+ rotation: backendTab.rotation as Rotation,
571
+ isConnected: true,
572
+ isStreamReady: false, // Will be set true when stream reconnects
573
+ isLoading: false,
574
+ isLaunchingBrowser: false,
575
+ isNavigating: false,
576
+ errorMessage: null
577
+ });
578
+
579
+ debug.log('preview', `โœ… Tab restored: ${frontendTabId} โ†’ ${backendTab.tabId}`);
580
+
581
+ return frontendTabId;
582
+ }
583
+
584
+ /**
585
+ * Setup tab event listeners
586
+ * CRITICAL: This must be synchronous to ensure listeners are ready before recovery
587
+ */
588
+ function setupTabEventListeners() {
589
+ debug.log('preview', '๐ŸŽง Setting up tab event listeners...');
590
+
591
+ // Import ws synchronously (should already be loaded in app context)
592
+ // This is critical to ensure listeners are ready before any events arrive
593
+ import('$frontend/lib/utils/ws').then(({ default: ws }) => {
594
+ debug.log('preview', 'โœ… WebSocket module loaded, registering tab event listeners');
595
+
596
+ // Listen for tab opened events (from MCP or backend)
597
+ ws.on('preview:browser-tab-opened', (data: any) => {
598
+ debug.log('preview', `๐Ÿ“ฅ Frontend received preview:browser-tab-opened:`, data);
599
+
600
+ // Skip if we're currently restoring to prevent conflicts
601
+ if (isRestoring) {
602
+ debug.log('preview', `โญ๏ธ Skipping tab-opened event during restore: ${data.tabId}`);
603
+ return;
604
+ }
605
+
606
+ // Check if this tab already exists in frontend (by backend sessionId)
607
+ const existingTab = tabManager.tabs.find(t => t.sessionId === data.tabId);
608
+ if (existingTab) {
609
+ debug.log('preview', `โœ“ Backend tab ${data.tabId} already linked to frontend tab ${existingTab.id}, skipping`);
610
+ return;
611
+ }
612
+
613
+ // Check if there's a tab currently launching without sessionId (race condition fix)
614
+ const launchingTab = tabManager.tabs.find(t => t.isLaunchingBrowser && !t.sessionId);
615
+ if (launchingTab) {
616
+ debug.log('preview', `๐Ÿ”— Found launching tab ${launchingTab.id}, linking to backend tab ${data.tabId}`);
617
+
618
+ // Update the launching tab with backend session info
619
+ tabManager.updateTab(launchingTab.id, {
620
+ sessionId: data.tabId,
621
+ sessionInfo: {
622
+ quality: 'good',
623
+ url: data.url,
624
+ deviceSize: data.deviceSize,
625
+ rotation: data.rotation
626
+ },
627
+ url: data.url,
628
+ title: data.title,
629
+ deviceSize: data.deviceSize || 'laptop',
630
+ rotation: data.rotation || 'landscape',
631
+ isConnected: true,
632
+ isStreamReady: false,
633
+ isLoading: false,
634
+ errorMessage: null
635
+ });
636
+
637
+ // Note: Session registration is handled by launchBrowserForTab
638
+ debug.log('preview', `โœ… Launching tab updated: ${launchingTab.id} โ†’ backend tab: ${data.tabId}`);
639
+ return;
640
+ }
641
+
642
+ // Create frontend tab to match backend tab (only if no launching tab found)
643
+ const frontendTabId = tabManager.createTab(data.url);
644
+
645
+ // Update tab with backend session info including device settings
646
+ tabManager.updateTab(frontendTabId, {
647
+ sessionId: data.tabId, // Backend tab ID is the session ID
648
+ sessionInfo: {
649
+ quality: 'good',
650
+ url: data.url,
651
+ deviceSize: data.deviceSize,
652
+ rotation: data.rotation
653
+ },
654
+ url: data.url,
655
+ title: data.title,
656
+ deviceSize: data.deviceSize || 'laptop',
657
+ rotation: data.rotation || 'landscape',
658
+ isConnected: true,
659
+ isStreamReady: false,
660
+ isLoading: false,
661
+ errorMessage: null
662
+ });
663
+
664
+ // Register session for cleanup tracking
665
+ browserCleanup.registerSession(data.tabId);
666
+
667
+ // Switch to this tab if it's active
668
+ if (data.isActive) {
669
+ tabManager.switchTab(frontendTabId);
670
+ }
671
+
672
+ debug.log('preview', `โœ… Frontend tab created: ${frontendTabId} โ†’ backend tab: ${data.tabId}`);
673
+ });
674
+
675
+ // Listen for tab closed events
676
+ ws.on('preview:browser-tab-closed', (data) => {
677
+ debug.log('preview', `๐Ÿ“ฅ Frontend received preview:browser-tab-closed:`, data);
678
+
679
+ // Find frontend tab by backend session ID
680
+ const tab = tabManager.tabs.find(t => t.sessionId === data.tabId);
681
+ if (tab) {
682
+ tabManager.closeTab(tab.id);
683
+ browserCleanup.unregisterSession(data.tabId);
684
+ debug.log('preview', `โœ… Frontend tab closed: ${tab.id}`);
685
+ }
686
+ });
687
+
688
+ // Listen for tab switched events
689
+ ws.on('preview:browser-tab-switched', (data) => {
690
+ debug.log('preview', `๐Ÿ“ฅ Frontend received preview:browser-tab-switched:`, data);
691
+
692
+ // Find frontend tab by backend session ID
693
+ const tab = tabManager.tabs.find(t => t.sessionId === data.newTabId);
694
+ if (tab) {
695
+ tabManager.switchTab(tab.id);
696
+ debug.log('preview', `โœ… Frontend switched to tab: ${tab.id}`);
697
+ }
698
+ });
699
+
700
+ // Listen for viewport changed events
701
+ ws.on('preview:browser-viewport-changed' as any, (data: any) => {
702
+ debug.log('preview', `๐Ÿ“ฅ Frontend received preview:browser-viewport-changed:`, data);
703
+
704
+ // Find frontend tab by backend session ID
705
+ const tab = tabManager.tabs.find(t => t.sessionId === data.tabId);
706
+ if (tab) {
707
+ // Update tab with new device settings
708
+ tabManager.updateTab(tab.id, {
709
+ deviceSize: data.deviceSize,
710
+ rotation: data.rotation
711
+ });
712
+ debug.log('preview', `โœ… Frontend viewport updated: ${tab.id} โ†’ ${data.deviceSize} (${data.rotation})`);
713
+ }
714
+ });
715
+
716
+ debug.log('preview', `๐ŸŽ‰ All tab event listeners registered and active`);
717
+ }).catch(error => {
718
+ debug.error('preview', 'โŒ Failed to setup tab event listeners:', error);
719
+ });
720
+ }
721
+
722
+ /**
723
+ * Setup navigation event listeners
724
+ */
725
+ function setupNavigationListeners() {
726
+ // Import ws at runtime to avoid circular dependencies
727
+ import('$frontend/lib/utils/ws').then(({ default: ws }) => {
728
+ // Listen for navigation loading events
729
+ ws.on('preview:browser-navigation-loading', (data: { sessionId: string; type: string; url: string; timestamp: number }) => {
730
+ debug.log('preview', `๐Ÿ”„ Navigation loading event received: ${data.sessionId} โ†’ ${data.url}`);
731
+
732
+ // Find tab by sessionId (backend sends backend tab ID as sessionId)
733
+ const tab = tabManager.tabs.find(t => t.sessionId === data.sessionId);
734
+ if (tab) {
735
+ streamHandler.handleStreamMessage({
736
+ type: 'navigation-loading',
737
+ data: { url: data.url }
738
+ }, tab.id);
739
+ } else {
740
+ debug.warn('preview', `Tab not found for sessionId: ${data.sessionId}`);
741
+ }
742
+ });
743
+
744
+ // Listen for navigation completed events
745
+ ws.on('preview:browser-navigation', (data: { sessionId: string; type: string; url: string; timestamp: number }) => {
746
+ debug.log('preview', `โœ… Navigation completed event received: ${data.sessionId} โ†’ ${data.url}`);
747
+
748
+ // Find tab by sessionId (backend sends backend tab ID as sessionId)
749
+ const tab = tabManager.tabs.find(t => t.sessionId === data.sessionId);
750
+ if (tab) {
751
+ streamHandler.handleStreamMessage({
752
+ type: 'navigation',
753
+ data: { url: data.url }
754
+ }, tab.id);
755
+ } else {
756
+ debug.warn('preview', `Tab not found for sessionId: ${data.sessionId}`);
757
+ }
758
+ });
759
+ });
760
+ }
761
+
762
+ /**
763
+ * Cleanup frontend state only (called on component destroy)
764
+ *
765
+ * NOTE: This does NOT destroy backend sessions.
766
+ * Backend sessions are preserved to allow reconnection after page refresh.
767
+ * To explicitly close tabs, use closeTab() which calls destroyBrowser().
768
+ */
769
+ async function cleanup() {
770
+ // Just clear frontend tracking - don't destroy backend sessions
771
+ // Backend sessions will be recovered on next page load
772
+ browserCleanup.clearAll();
773
+ debug.log('preview', '๐Ÿงน Frontend cleanup complete (backend sessions preserved)');
774
+ }
775
+
776
+ /**
777
+ * Switch to a different project
778
+ * Clears current tabs and loads sessions from the new project
779
+ */
780
+ async function switchProject() {
781
+ const newProjectId = getProjectId();
782
+ debug.log('preview', `๐Ÿ”„ Switching to project: ${newProjectId}`);
783
+
784
+ // Set restore lock during project switch
785
+ isRestoring = true;
786
+
787
+ try {
788
+ // Update projectId for interactions
789
+ setInteractionProjectId(newProjectId);
790
+
791
+ // Clear all local tabs
792
+ tabManager.clearAllTabs();
793
+
794
+ // Clear frontend tracking
795
+ browserCleanup.clearAll();
796
+
797
+ // Recover sessions from new project
798
+ // Note: recoverExistingSessions will handle its own lock
799
+ isRestoring = false;
800
+ await recoverExistingSessions();
801
+ } catch (error) {
802
+ debug.error('preview', 'โŒ Error switching project:', error);
803
+ isRestoring = false;
804
+ }
805
+ }
806
+
807
+ return {
808
+ // Managers
809
+ tabManager,
810
+ streamHandler,
811
+ nativeUIHandler,
812
+ mcpHandler,
813
+
814
+ // Tab operations
815
+ createNewTab,
816
+ switchToTab,
817
+ closeTab,
818
+
819
+ // Browser operations
820
+ launchBrowserForTab,
821
+ navigateBrowserForTab,
822
+
823
+ // Device control
824
+ changeDeviceSize,
825
+ toggleRotation,
826
+
827
+ // Lifecycle
828
+ initialize,
829
+ cleanup,
830
+ switchProject,
831
+
832
+ // Utilities
833
+ sendInteraction
834
+ };
835
+ }
836
+
837
+ export type BrowserCoordinator = ReturnType<typeof createBrowserCoordinator>;