@myrialabs/clopen 0.0.4 → 0.0.6

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 (453) hide show
  1. package/.env.example +5 -5
  2. package/.github/workflows/{release.yml → ci.yml} +86 -60
  3. package/CONTRIBUTING.md +499 -499
  4. package/LICENSE +21 -21
  5. package/README.md +209 -209
  6. package/backend/index.ts +168 -156
  7. package/backend/lib/chat/helpers.ts +42 -42
  8. package/backend/lib/chat/index.ts +1 -1
  9. package/backend/lib/chat/stream-manager.ts +1126 -1126
  10. package/backend/lib/database/README.md +76 -76
  11. package/backend/lib/database/index.ts +118 -118
  12. package/backend/lib/database/migrations/001_create_projects_table.ts +30 -30
  13. package/backend/lib/database/migrations/002_create_chat_sessions_table.ts +32 -32
  14. package/backend/lib/database/migrations/003_create_messages_table.ts +31 -31
  15. package/backend/lib/database/migrations/004_create_prompt_templates_table.ts +34 -34
  16. package/backend/lib/database/migrations/005_create_settings_table.ts +23 -23
  17. package/backend/lib/database/migrations/006_add_user_to_messages.ts +57 -57
  18. package/backend/lib/database/migrations/007_create_stream_states_table.ts +40 -40
  19. package/backend/lib/database/migrations/008_create_message_snapshots_table.ts +61 -61
  20. package/backend/lib/database/migrations/009_add_delta_snapshot_fields.ts +41 -41
  21. package/backend/lib/database/migrations/010_add_soft_delete_and_branch_support.ts +70 -70
  22. package/backend/lib/database/migrations/011_git_like_commit_graph.ts +156 -156
  23. package/backend/lib/database/migrations/012_add_file_change_statistics.ts +41 -41
  24. package/backend/lib/database/migrations/013_checkpoint_tree_state.ts +118 -118
  25. package/backend/lib/database/migrations/014_add_engine_to_sessions.ts +18 -18
  26. package/backend/lib/database/migrations/015_add_model_to_sessions.ts +18 -18
  27. package/backend/lib/database/migrations/016_create_user_projects_table.ts +34 -34
  28. package/backend/lib/database/migrations/017_add_current_session_to_user_projects.ts +32 -32
  29. package/backend/lib/database/migrations/018_create_claude_accounts_table.ts +24 -24
  30. package/backend/lib/database/migrations/019_add_claude_account_to_sessions.ts +18 -18
  31. package/backend/lib/database/migrations/020_add_snapshot_tree_hash.ts +32 -32
  32. package/backend/lib/database/migrations/021_drop_prompt_templates_table.ts +33 -33
  33. package/backend/lib/database/migrations/index.ts +153 -153
  34. package/backend/lib/database/queries/checkpoint-queries.ts +87 -87
  35. package/backend/lib/database/queries/engine-queries.ts +75 -75
  36. package/backend/lib/database/queries/index.ts +8 -8
  37. package/backend/lib/database/queries/message-queries.ts +471 -471
  38. package/backend/lib/database/queries/project-queries.ts +117 -117
  39. package/backend/lib/database/queries/session-queries.ts +270 -270
  40. package/backend/lib/database/queries/settings-queries.ts +33 -33
  41. package/backend/lib/database/queries/snapshot-queries.ts +325 -325
  42. package/backend/lib/database/queries/utils-queries.ts +58 -58
  43. package/backend/lib/database/seeders/index.ts +12 -12
  44. package/backend/lib/database/seeders/settings_seeder.ts +83 -83
  45. package/backend/lib/database/utils/connection.ts +173 -173
  46. package/backend/lib/database/utils/index.ts +3 -3
  47. package/backend/lib/database/utils/migration-runner.ts +117 -117
  48. package/backend/lib/database/utils/seeder-runner.ts +120 -120
  49. package/backend/lib/engine/adapters/claude/environment.ts +160 -164
  50. package/backend/lib/engine/adapters/claude/error-handler.ts +60 -60
  51. package/backend/lib/engine/adapters/claude/index.ts +1 -1
  52. package/backend/lib/engine/adapters/claude/path-utils.ts +38 -38
  53. package/backend/lib/engine/adapters/claude/stream.ts +177 -177
  54. package/backend/lib/engine/adapters/opencode/index.ts +2 -2
  55. package/backend/lib/engine/adapters/opencode/message-converter.ts +862 -862
  56. package/backend/lib/engine/adapters/opencode/server.ts +104 -104
  57. package/backend/lib/engine/adapters/opencode/stream.ts +755 -755
  58. package/backend/lib/engine/index.ts +196 -196
  59. package/backend/lib/engine/types.ts +58 -58
  60. package/backend/lib/files/file-operations.ts +478 -478
  61. package/backend/lib/files/file-reading.ts +308 -308
  62. package/backend/lib/files/file-watcher.ts +383 -383
  63. package/backend/lib/files/path-browsing.ts +382 -382
  64. package/backend/lib/git/git-executor.ts +89 -88
  65. package/backend/lib/git/git-parser.ts +411 -411
  66. package/backend/lib/git/git-service.ts +505 -505
  67. package/backend/lib/mcp/README.md +1144 -1144
  68. package/backend/lib/mcp/config.ts +317 -316
  69. package/backend/lib/mcp/index.ts +35 -35
  70. package/backend/lib/mcp/project-context.ts +236 -236
  71. package/backend/lib/mcp/servers/browser-automation/actions.ts +156 -156
  72. package/backend/lib/mcp/servers/browser-automation/browser.ts +419 -419
  73. package/backend/lib/mcp/servers/browser-automation/index.ts +791 -791
  74. package/backend/lib/mcp/servers/browser-automation/inspection.ts +501 -501
  75. package/backend/lib/mcp/servers/helper.ts +143 -143
  76. package/backend/lib/mcp/servers/index.ts +44 -44
  77. package/backend/lib/mcp/servers/weather/get-temperature.ts +56 -56
  78. package/backend/lib/mcp/servers/weather/index.ts +31 -31
  79. package/backend/lib/mcp/stdio-server.ts +103 -103
  80. package/backend/lib/mcp/types.ts +65 -65
  81. package/backend/lib/preview/browser/browser-audio-capture.ts +86 -86
  82. package/backend/lib/preview/browser/browser-console-manager.ts +262 -262
  83. package/backend/lib/preview/browser/browser-dialog-handler.ts +222 -222
  84. package/backend/lib/preview/browser/browser-interaction-handler.ts +421 -421
  85. package/backend/lib/preview/browser/browser-mcp-control.ts +415 -415
  86. package/backend/lib/preview/browser/browser-native-ui-handler.ts +512 -512
  87. package/backend/lib/preview/browser/browser-navigation-tracker.ts +103 -103
  88. package/backend/lib/preview/browser/browser-pool.ts +357 -357
  89. package/backend/lib/preview/browser/browser-preview-service.ts +882 -882
  90. package/backend/lib/preview/browser/browser-tab-manager.ts +935 -935
  91. package/backend/lib/preview/browser/browser-video-capture.ts +695 -695
  92. package/backend/lib/preview/browser/scripts/audio-stream.ts +292 -292
  93. package/backend/lib/preview/browser/scripts/cursor-tracking.ts +85 -85
  94. package/backend/lib/preview/browser/scripts/video-stream.ts +438 -438
  95. package/backend/lib/preview/browser/types.ts +359 -359
  96. package/backend/lib/preview/index.ts +23 -23
  97. package/backend/lib/project/index.ts +1 -1
  98. package/backend/lib/project/status-manager.ts +181 -181
  99. package/backend/lib/shared/env.ts +117 -0
  100. package/backend/lib/shared/index.ts +5 -2
  101. package/backend/lib/shared/port-utils.ts +25 -25
  102. package/backend/lib/shared/process-manager.ts +280 -280
  103. package/backend/lib/snapshot/blob-store.ts +227 -227
  104. package/backend/lib/snapshot/gitignore.ts +307 -307
  105. package/backend/lib/snapshot/helpers.ts +397 -397
  106. package/backend/lib/snapshot/snapshot-service.ts +483 -483
  107. package/backend/lib/terminal/helpers.ts +14 -14
  108. package/backend/lib/terminal/index.ts +7 -7
  109. package/backend/lib/terminal/pty-manager.ts +3 -3
  110. package/backend/lib/terminal/pty-session-manager.ts +370 -387
  111. package/backend/lib/terminal/shell-utils.ts +315 -312
  112. package/backend/lib/terminal/stream-manager.ts +292 -292
  113. package/backend/lib/tunnel/global-tunnel-manager.ts +266 -243
  114. package/backend/lib/tunnel/project-tunnel-manager.ts +311 -311
  115. package/backend/lib/user/helpers.ts +87 -87
  116. package/backend/lib/utils/ws.ts +944 -944
  117. package/backend/lib/vite-dev.ts +295 -295
  118. package/backend/middleware/cors.ts +16 -15
  119. package/backend/middleware/error-handler.ts +50 -49
  120. package/backend/middleware/logger.ts +9 -9
  121. package/backend/types/api.ts +24 -24
  122. package/backend/ws/README.md +1505 -1505
  123. package/backend/ws/chat/background.ts +198 -198
  124. package/backend/ws/chat/index.ts +21 -21
  125. package/backend/ws/chat/stream.ts +707 -707
  126. package/backend/ws/engine/claude/accounts.ts +399 -401
  127. package/backend/ws/engine/claude/index.ts +13 -13
  128. package/backend/ws/engine/claude/status.ts +43 -43
  129. package/backend/ws/engine/index.ts +14 -14
  130. package/backend/ws/engine/opencode/index.ts +11 -11
  131. package/backend/ws/engine/opencode/status.ts +30 -30
  132. package/backend/ws/engine/utils.ts +36 -36
  133. package/backend/ws/files/index.ts +30 -30
  134. package/backend/ws/files/read.ts +189 -189
  135. package/backend/ws/files/search.ts +453 -453
  136. package/backend/ws/files/watch.ts +124 -124
  137. package/backend/ws/files/write.ts +143 -143
  138. package/backend/ws/git/branch.ts +106 -106
  139. package/backend/ws/git/commit.ts +39 -39
  140. package/backend/ws/git/conflict.ts +68 -68
  141. package/backend/ws/git/diff.ts +69 -69
  142. package/backend/ws/git/index.ts +24 -24
  143. package/backend/ws/git/log.ts +41 -41
  144. package/backend/ws/git/remote.ts +214 -214
  145. package/backend/ws/git/staging.ts +84 -84
  146. package/backend/ws/git/status.ts +90 -90
  147. package/backend/ws/index.ts +69 -69
  148. package/backend/ws/mcp/index.ts +61 -61
  149. package/backend/ws/messages/crud.ts +74 -74
  150. package/backend/ws/messages/index.ts +14 -14
  151. package/backend/ws/preview/browser/cleanup.ts +129 -129
  152. package/backend/ws/preview/browser/console.ts +114 -114
  153. package/backend/ws/preview/browser/interact.ts +513 -513
  154. package/backend/ws/preview/browser/mcp.ts +129 -129
  155. package/backend/ws/preview/browser/native-ui.ts +235 -235
  156. package/backend/ws/preview/browser/stats.ts +55 -55
  157. package/backend/ws/preview/browser/tab-info.ts +126 -126
  158. package/backend/ws/preview/browser/tab.ts +166 -166
  159. package/backend/ws/preview/browser/webcodecs.ts +293 -293
  160. package/backend/ws/preview/index.ts +146 -146
  161. package/backend/ws/projects/crud.ts +113 -113
  162. package/backend/ws/projects/index.ts +25 -25
  163. package/backend/ws/projects/presence.ts +46 -46
  164. package/backend/ws/projects/status.ts +116 -116
  165. package/backend/ws/sessions/crud.ts +327 -327
  166. package/backend/ws/sessions/index.ts +33 -33
  167. package/backend/ws/settings/crud.ts +112 -112
  168. package/backend/ws/settings/index.ts +14 -14
  169. package/backend/ws/snapshot/index.ts +17 -17
  170. package/backend/ws/snapshot/restore.ts +173 -173
  171. package/backend/ws/snapshot/timeline.ts +141 -141
  172. package/backend/ws/system/index.ts +14 -14
  173. package/backend/ws/system/operations.ts +49 -49
  174. package/backend/ws/terminal/index.ts +40 -40
  175. package/backend/ws/terminal/persistence.ts +153 -153
  176. package/backend/ws/terminal/session.ts +382 -382
  177. package/backend/ws/terminal/stream.ts +79 -79
  178. package/backend/ws/tunnel/index.ts +14 -14
  179. package/backend/ws/tunnel/operations.ts +91 -91
  180. package/backend/ws/types.ts +20 -20
  181. package/backend/ws/user/crud.ts +156 -156
  182. package/backend/ws/user/index.ts +14 -14
  183. package/bin/clopen.ts +307 -307
  184. package/bun.lock +1353 -1352
  185. package/frontend/App.svelte +38 -34
  186. package/frontend/app.css +313 -313
  187. package/frontend/lib/app-environment.ts +10 -10
  188. package/frontend/lib/components/chat/ChatInterface.svelte +406 -406
  189. package/frontend/lib/components/chat/formatters/ErrorMessage.svelte +56 -56
  190. package/frontend/lib/components/chat/formatters/MessageFormatter.svelte +223 -223
  191. package/frontend/lib/components/chat/formatters/TextMessage.svelte +394 -394
  192. package/frontend/lib/components/chat/formatters/Tools.svelte +69 -69
  193. package/frontend/lib/components/chat/formatters/index.ts +2 -2
  194. package/frontend/lib/components/chat/input/ChatInput.svelte +421 -421
  195. package/frontend/lib/components/chat/input/components/ChatInputActions.svelte +78 -78
  196. package/frontend/lib/components/chat/input/components/DragDropOverlay.svelte +30 -30
  197. package/frontend/lib/components/chat/input/components/EditModeIndicator.svelte +33 -33
  198. package/frontend/lib/components/chat/input/components/EngineModelPicker.svelte +619 -619
  199. package/frontend/lib/components/chat/input/components/FileAttachmentPreview.svelte +48 -48
  200. package/frontend/lib/components/chat/input/components/LoadingIndicator.svelte +31 -31
  201. package/frontend/lib/components/chat/input/composables/use-animations.svelte.ts +201 -201
  202. package/frontend/lib/components/chat/input/composables/use-chat-actions.svelte.ts +148 -148
  203. package/frontend/lib/components/chat/input/composables/use-file-handling.svelte.ts +216 -216
  204. package/frontend/lib/components/chat/input/composables/use-input-state.svelte.ts +357 -357
  205. package/frontend/lib/components/chat/input/composables/use-textarea-resize.svelte.ts +57 -57
  206. package/frontend/lib/components/chat/message/ChatMessage.svelte +478 -478
  207. package/frontend/lib/components/chat/message/ChatMessages.svelte +541 -541
  208. package/frontend/lib/components/chat/message/DateSeparator.svelte +86 -86
  209. package/frontend/lib/components/chat/message/MessageBubble.svelte +86 -86
  210. package/frontend/lib/components/chat/message/MessageHeader.svelte +157 -157
  211. package/frontend/lib/components/chat/modal/DebugModal.svelte +59 -59
  212. package/frontend/lib/components/chat/modal/TokenUsageModal.svelte +124 -124
  213. package/frontend/lib/components/chat/shared/index.ts +1 -1
  214. package/frontend/lib/components/chat/shared/utils.ts +115 -115
  215. package/frontend/lib/components/chat/tools/BashOutputTool.svelte +35 -35
  216. package/frontend/lib/components/chat/tools/BashTool.svelte +45 -45
  217. package/frontend/lib/components/chat/tools/CustomMcpTool.svelte +139 -139
  218. package/frontend/lib/components/chat/tools/EditTool.svelte +47 -47
  219. package/frontend/lib/components/chat/tools/ExitPlanModeTool.svelte +31 -31
  220. package/frontend/lib/components/chat/tools/GlobTool.svelte +50 -50
  221. package/frontend/lib/components/chat/tools/GrepTool.svelte +89 -89
  222. package/frontend/lib/components/chat/tools/KillShellTool.svelte +25 -25
  223. package/frontend/lib/components/chat/tools/ListMcpResourcesTool.svelte +30 -30
  224. package/frontend/lib/components/chat/tools/NotebookEditTool.svelte +37 -37
  225. package/frontend/lib/components/chat/tools/ReadMcpResourceTool.svelte +33 -33
  226. package/frontend/lib/components/chat/tools/ReadTool.svelte +40 -40
  227. package/frontend/lib/components/chat/tools/TaskTool.svelte +63 -63
  228. package/frontend/lib/components/chat/tools/TodoWriteTool.svelte +74 -74
  229. package/frontend/lib/components/chat/tools/WebFetchTool.svelte +34 -34
  230. package/frontend/lib/components/chat/tools/WebSearchTool.svelte +83 -83
  231. package/frontend/lib/components/chat/tools/WriteTool.svelte +32 -32
  232. package/frontend/lib/components/chat/tools/components/CodeBlock.svelte +78 -78
  233. package/frontend/lib/components/chat/tools/components/DiffBlock.svelte +407 -407
  234. package/frontend/lib/components/chat/tools/components/FileHeader.svelte +45 -45
  235. package/frontend/lib/components/chat/tools/components/InfoLine.svelte +18 -18
  236. package/frontend/lib/components/chat/tools/components/StatsBadges.svelte +26 -26
  237. package/frontend/lib/components/chat/tools/components/TerminalCommand.svelte +53 -53
  238. package/frontend/lib/components/chat/tools/components/index.ts +7 -7
  239. package/frontend/lib/components/chat/tools/index.ts +25 -25
  240. package/frontend/lib/components/chat/widgets/FloatingTodoList.svelte +248 -248
  241. package/frontend/lib/components/chat/widgets/TokenUsage.svelte +78 -78
  242. package/frontend/lib/components/checkpoint/TimelineModal.svelte +391 -391
  243. package/frontend/lib/components/checkpoint/timeline/TimelineEdge.svelte +26 -26
  244. package/frontend/lib/components/checkpoint/timeline/TimelineGraph.svelte +86 -86
  245. package/frontend/lib/components/checkpoint/timeline/TimelineNode.svelte +108 -108
  246. package/frontend/lib/components/checkpoint/timeline/TimelineVersionGroup.svelte +59 -59
  247. package/frontend/lib/components/checkpoint/timeline/animation.ts +168 -168
  248. package/frontend/lib/components/checkpoint/timeline/config.ts +44 -44
  249. package/frontend/lib/components/checkpoint/timeline/graph-builder.ts +304 -304
  250. package/frontend/lib/components/checkpoint/timeline/types.ts +65 -65
  251. package/frontend/lib/components/checkpoint/timeline/utils.ts +53 -53
  252. package/frontend/lib/components/common/Alert.svelte +138 -138
  253. package/frontend/lib/components/common/AvatarBubble.svelte +55 -55
  254. package/frontend/lib/components/common/Button.svelte +71 -71
  255. package/frontend/lib/components/common/Card.svelte +102 -102
  256. package/frontend/lib/components/common/Checkbox.svelte +48 -48
  257. package/frontend/lib/components/common/Dialog.svelte +248 -248
  258. package/frontend/lib/components/common/FolderBrowser.svelte +842 -842
  259. package/frontend/lib/components/common/Icon.svelte +57 -57
  260. package/frontend/lib/components/common/Input.svelte +72 -72
  261. package/frontend/lib/components/common/Lightbox.svelte +232 -232
  262. package/frontend/lib/components/common/LoadingScreen.svelte +52 -52
  263. package/frontend/lib/components/common/LoadingSpinner.svelte +48 -48
  264. package/frontend/lib/components/common/Modal.svelte +177 -177
  265. package/frontend/lib/components/common/ModalProvider.svelte +27 -27
  266. package/frontend/lib/components/common/ModelSelector.svelte +110 -110
  267. package/frontend/lib/components/common/MonacoEditor.svelte +568 -568
  268. package/frontend/lib/components/common/NotificationToast.svelte +113 -113
  269. package/frontend/lib/components/common/PageTemplate.svelte +75 -75
  270. package/frontend/lib/components/common/ProjectUserAvatars.svelte +79 -79
  271. package/frontend/lib/components/common/Select.svelte +97 -97
  272. package/frontend/lib/components/common/Textarea.svelte +79 -79
  273. package/frontend/lib/components/common/ThemeToggle.svelte +44 -44
  274. package/frontend/lib/components/common/lucide-icons.ts +1642 -1642
  275. package/frontend/lib/components/common/material-icons.ts +1082 -1082
  276. package/frontend/lib/components/common/xterm/XTerm.svelte +809 -795
  277. package/frontend/lib/components/common/xterm/index.ts +15 -15
  278. package/frontend/lib/components/common/xterm/terminal-config.ts +67 -67
  279. package/frontend/lib/components/common/xterm/types.ts +30 -30
  280. package/frontend/lib/components/common/xterm/xterm-service.ts +379 -353
  281. package/frontend/lib/components/files/FileNode.svelte +383 -383
  282. package/frontend/lib/components/files/FileTree.svelte +681 -681
  283. package/frontend/lib/components/files/FileViewer.svelte +728 -728
  284. package/frontend/lib/components/files/SearchResults.svelte +303 -303
  285. package/frontend/lib/components/git/BranchManager.svelte +458 -458
  286. package/frontend/lib/components/git/ChangesSection.svelte +107 -107
  287. package/frontend/lib/components/git/CommitForm.svelte +76 -76
  288. package/frontend/lib/components/git/ConflictResolver.svelte +158 -158
  289. package/frontend/lib/components/git/DiffViewer.svelte +364 -364
  290. package/frontend/lib/components/git/FileChangeItem.svelte +97 -97
  291. package/frontend/lib/components/git/GitButton.svelte +33 -33
  292. package/frontend/lib/components/git/GitLog.svelte +361 -361
  293. package/frontend/lib/components/git/GitModal.svelte +80 -80
  294. package/frontend/lib/components/history/HistoryModal.svelte +563 -563
  295. package/frontend/lib/components/history/HistoryView.svelte +614 -614
  296. package/frontend/lib/components/index.ts +34 -34
  297. package/frontend/lib/components/preview/browser/BrowserPreview.svelte +549 -549
  298. package/frontend/lib/components/preview/browser/components/Canvas.svelte +1058 -1058
  299. package/frontend/lib/components/preview/browser/components/ConsolePanel.svelte +756 -756
  300. package/frontend/lib/components/preview/browser/components/Container.svelte +450 -450
  301. package/frontend/lib/components/preview/browser/components/ContextMenu.svelte +236 -236
  302. package/frontend/lib/components/preview/browser/components/SelectDropdown.svelte +224 -224
  303. package/frontend/lib/components/preview/browser/components/Toolbar.svelte +338 -338
  304. package/frontend/lib/components/preview/browser/components/VirtualCursor.svelte +35 -35
  305. package/frontend/lib/components/preview/browser/core/cleanup.svelte.ts +155 -155
  306. package/frontend/lib/components/preview/browser/core/coordinator.svelte.ts +837 -837
  307. package/frontend/lib/components/preview/browser/core/interactions.svelte.ts +113 -113
  308. package/frontend/lib/components/preview/browser/core/mcp-handlers.svelte.ts +296 -296
  309. package/frontend/lib/components/preview/browser/core/native-ui-handlers.svelte.ts +391 -391
  310. package/frontend/lib/components/preview/browser/core/stream-handler.svelte.ts +231 -231
  311. package/frontend/lib/components/preview/browser/core/tab-manager.svelte.ts +210 -210
  312. package/frontend/lib/components/preview/browser/core/tab-operations.svelte.ts +239 -239
  313. package/frontend/lib/components/preview/index.ts +1 -1
  314. package/frontend/lib/components/settings/SettingsModal.svelte +235 -235
  315. package/frontend/lib/components/settings/SettingsView.svelte +36 -36
  316. package/frontend/lib/components/settings/appearance/AppearanceSettings.svelte +51 -51
  317. package/frontend/lib/components/settings/appearance/LayoutPresetSettings.svelte +160 -160
  318. package/frontend/lib/components/settings/appearance/LayoutPreview.svelte +76 -76
  319. package/frontend/lib/components/settings/engines/AIEnginesSettings.svelte +917 -917
  320. package/frontend/lib/components/settings/general/AdvancedSettings.svelte +187 -187
  321. package/frontend/lib/components/settings/general/DataManagementSettings.svelte +203 -203
  322. package/frontend/lib/components/settings/general/GeneralSettings.svelte +10 -10
  323. package/frontend/lib/components/settings/model/ModelSettings.svelte +357 -357
  324. package/frontend/lib/components/settings/notifications/NotificationSettings.svelte +205 -205
  325. package/frontend/lib/components/settings/user/UserSettings.svelte +197 -197
  326. package/frontend/lib/components/terminal/Terminal.svelte +367 -367
  327. package/frontend/lib/components/terminal/TerminalTabs.svelte +87 -87
  328. package/frontend/lib/components/terminal/TerminalView.svelte +54 -54
  329. package/frontend/lib/components/tunnel/TunnelActive.svelte +157 -142
  330. package/frontend/lib/components/tunnel/TunnelButton.svelte +60 -54
  331. package/frontend/lib/components/tunnel/TunnelInactive.svelte +285 -284
  332. package/frontend/lib/components/tunnel/TunnelModal.svelte +48 -47
  333. package/frontend/lib/components/tunnel/TunnelQRCode.svelte +49 -49
  334. package/frontend/lib/components/workspace/DesktopNavigator.svelte +382 -382
  335. package/frontend/lib/components/workspace/MobileNavigator.svelte +394 -403
  336. package/frontend/lib/components/workspace/PanelContainer.svelte +100 -100
  337. package/frontend/lib/components/workspace/PanelHeader.svelte +505 -505
  338. package/frontend/lib/components/workspace/ViewMenu.svelte +162 -162
  339. package/frontend/lib/components/workspace/WorkspaceLayout.svelte +169 -169
  340. package/frontend/lib/components/workspace/layout/DesktopLayout.svelte +15 -15
  341. package/frontend/lib/components/workspace/layout/MobileLayout.svelte +17 -17
  342. package/frontend/lib/components/workspace/layout/split-pane/Container.svelte +42 -42
  343. package/frontend/lib/components/workspace/layout/split-pane/Handle.svelte +84 -84
  344. package/frontend/lib/components/workspace/layout/split-pane/Layout.svelte +37 -37
  345. package/frontend/lib/components/workspace/panels/ChatPanel.svelte +274 -274
  346. package/frontend/lib/components/workspace/panels/FilesPanel.svelte +1261 -1261
  347. package/frontend/lib/components/workspace/panels/GitPanel.svelte +1560 -1560
  348. package/frontend/lib/components/workspace/panels/PreviewPanel.svelte +150 -150
  349. package/frontend/lib/components/workspace/panels/TerminalPanel.svelte +73 -73
  350. package/frontend/lib/constants/preview.ts +44 -44
  351. package/frontend/lib/services/chat/chat.service.ts +704 -704
  352. package/frontend/lib/services/chat/index.ts +6 -6
  353. package/frontend/lib/services/notification/global-stream-monitor.ts +86 -86
  354. package/frontend/lib/services/notification/index.ts +7 -7
  355. package/frontend/lib/services/notification/push.service.ts +143 -143
  356. package/frontend/lib/services/notification/sound.service.ts +126 -126
  357. package/frontend/lib/services/preview/browser/browser-console.service.ts +61 -61
  358. package/frontend/lib/services/preview/browser/browser-webcodecs.service.ts +1499 -1499
  359. package/frontend/lib/services/preview/browser/mcp-integration.svelte.ts +67 -67
  360. package/frontend/lib/services/preview/index.ts +22 -22
  361. package/frontend/lib/services/project/index.ts +7 -7
  362. package/frontend/lib/services/project/status.service.ts +159 -159
  363. package/frontend/lib/services/snapshot/snapshot.service.ts +47 -47
  364. package/frontend/lib/services/terminal/background/index.ts +129 -129
  365. package/frontend/lib/services/terminal/background/session-restore.ts +273 -273
  366. package/frontend/lib/services/terminal/background/stream-manager.ts +285 -285
  367. package/frontend/lib/services/terminal/index.ts +13 -13
  368. package/frontend/lib/services/terminal/persistence.service.ts +260 -260
  369. package/frontend/lib/services/terminal/project.service.ts +952 -952
  370. package/frontend/lib/services/terminal/session.service.ts +363 -363
  371. package/frontend/lib/services/terminal/terminal.service.ts +369 -369
  372. package/frontend/lib/stores/core/app.svelte.ts +117 -117
  373. package/frontend/lib/stores/core/files.svelte.ts +72 -72
  374. package/frontend/lib/stores/core/presence.svelte.ts +48 -48
  375. package/frontend/lib/stores/core/projects.svelte.ts +317 -317
  376. package/frontend/lib/stores/core/sessions.svelte.ts +383 -383
  377. package/frontend/lib/stores/features/claude-accounts.svelte.ts +58 -58
  378. package/frontend/lib/stores/features/models.svelte.ts +89 -89
  379. package/frontend/lib/stores/features/settings.svelte.ts +87 -87
  380. package/frontend/lib/stores/features/terminal.svelte.ts +700 -700
  381. package/frontend/lib/stores/features/tunnel.svelte.ts +163 -161
  382. package/frontend/lib/stores/features/user.svelte.ts +95 -95
  383. package/frontend/lib/stores/ui/chat-input.svelte.ts +56 -56
  384. package/frontend/lib/stores/ui/chat-model.svelte.ts +61 -61
  385. package/frontend/lib/stores/ui/dialog.svelte.ts +58 -58
  386. package/frontend/lib/stores/ui/edit-mode.svelte.ts +214 -214
  387. package/frontend/lib/stores/ui/notification.svelte.ts +166 -166
  388. package/frontend/lib/stores/ui/settings-modal.svelte.ts +88 -88
  389. package/frontend/lib/stores/ui/theme.svelte.ts +179 -179
  390. package/frontend/lib/stores/ui/workspace.svelte.ts +754 -754
  391. package/frontend/lib/types/native-ui.ts +73 -73
  392. package/frontend/lib/utils/chat/date-separator.ts +38 -38
  393. package/frontend/lib/utils/chat/message-grouper.ts +218 -218
  394. package/frontend/lib/utils/chat/message-processor.ts +134 -134
  395. package/frontend/lib/utils/chat/tool-handler.ts +160 -160
  396. package/frontend/lib/utils/chat/virtual-scroll.svelte.ts +142 -142
  397. package/frontend/lib/utils/click-outside.ts +20 -20
  398. package/frontend/lib/utils/context-manager.ts +256 -256
  399. package/frontend/lib/utils/file-icon-mappings.ts +768 -768
  400. package/frontend/lib/utils/folder-icon-mappings.ts +1029 -1029
  401. package/frontend/lib/utils/git-status.ts +68 -68
  402. package/frontend/lib/utils/platform.ts +112 -112
  403. package/frontend/lib/utils/port-check.ts +64 -64
  404. package/frontend/lib/utils/terminalFormatter.ts +206 -206
  405. package/frontend/lib/utils/theme.ts +6 -6
  406. package/frontend/lib/utils/tree-visualizer.ts +320 -320
  407. package/frontend/lib/utils/ws.ts +44 -44
  408. package/frontend/main.ts +13 -13
  409. package/index.html +70 -70
  410. package/package.json +111 -111
  411. package/scripts/generate-icons.ts +86 -86
  412. package/scripts/pre-publish-check.sh +142 -142
  413. package/scripts/setup-hooks.sh +134 -134
  414. package/scripts/validate-branch-name.sh +47 -47
  415. package/scripts/validate-commit-msg.sh +42 -42
  416. package/shared/constants/engines.ts +134 -134
  417. package/shared/types/database/connection.ts +15 -15
  418. package/shared/types/database/index.ts +5 -5
  419. package/shared/types/database/schema.ts +140 -140
  420. package/shared/types/engine/index.ts +45 -45
  421. package/shared/types/filesystem/index.ts +21 -21
  422. package/shared/types/git.ts +171 -171
  423. package/shared/types/messaging/index.ts +238 -238
  424. package/shared/types/messaging/tool.ts +525 -525
  425. package/shared/types/network/api.ts +17 -17
  426. package/shared/types/network/index.ts +4 -4
  427. package/shared/types/stores/app.ts +22 -22
  428. package/shared/types/stores/dialog.ts +20 -20
  429. package/shared/types/stores/index.ts +2 -2
  430. package/shared/types/stores/settings.ts +15 -15
  431. package/shared/types/terminal/index.ts +43 -43
  432. package/shared/types/ui/components.ts +60 -60
  433. package/shared/types/ui/icons.ts +22 -22
  434. package/shared/types/ui/index.ts +21 -21
  435. package/shared/types/ui/notifications.ts +13 -13
  436. package/shared/types/ui/theme.ts +11 -11
  437. package/shared/types/websocket/index.ts +43 -43
  438. package/shared/types/window.d.ts +12 -12
  439. package/shared/utils/anonymous-user.ts +167 -167
  440. package/shared/utils/async.ts +10 -10
  441. package/shared/utils/diff-calculator.ts +184 -184
  442. package/shared/utils/file-type-detection.ts +165 -165
  443. package/shared/utils/logger.ts +144 -158
  444. package/shared/utils/message-formatter.ts +79 -79
  445. package/shared/utils/path.ts +47 -47
  446. package/shared/utils/ws-client.ts +768 -768
  447. package/shared/utils/ws-server.ts +660 -660
  448. package/static/favicon.svg +7 -7
  449. package/static/fonts/dm-sans.css +96 -96
  450. package/svelte.config.js +20 -20
  451. package/tsconfig.json +41 -41
  452. package/vite.config.ts +33 -33
  453. package/.github/workflows/test.yml +0 -40
@@ -1,882 +1,882 @@
1
- import { EventEmitter } from 'events';
2
- import type { Page } from 'puppeteer-core';
3
- import { BrowserTabManager } from './browser-tab-manager.js';
4
- import { BrowserConsoleManager } from './browser-console-manager.js';
5
- import { BrowserInteractionHandler } from './browser-interaction-handler.js';
6
- import { BrowserNavigationTracker } from './browser-navigation-tracker.js';
7
- import { BrowserVideoCapture } from './browser-video-capture.js';
8
- import { BrowserDialogHandler } from './browser-dialog-handler.js';
9
- import { BrowserNativeUIHandler } from './browser-native-ui-handler.js';
10
- import { browserMcpControl } from './browser-mcp-control.js';
11
- import { ws } from '$backend/lib/utils/ws';
12
- import { debug } from '$shared/utils/logger';
13
- import type {
14
- BrowserTab,
15
- BrowserTabInfo,
16
- BrowserConsoleMessage,
17
- BrowserAutonomousAction,
18
- DeviceSize,
19
- Rotation,
20
- BrowserDialogResponse,
21
- BrowserSelectResponse,
22
- BrowserContextMenuResponse,
23
- BrowserContextMenuInfo
24
- } from './types';
25
-
26
- /**
27
- * Browser Preview Service
28
- *
29
- * Main orchestrator for browser preview functionality.
30
- * Tab-centric architecture - all operations work with tabs.
31
- *
32
- * Architecture:
33
- * - Tabs are the primary unit (no separate session concept)
34
- * - Each tab = isolated browser context + page
35
- * - Event-driven communication with frontend
36
- * - Manages all browser operations: streaming, interaction, console, etc.
37
- * - **PROJECT ISOLATION**: Each instance is isolated per project
38
- */
39
- export class BrowserPreviewService extends EventEmitter {
40
- private tabManager: BrowserTabManager;
41
- private consoleManager: BrowserConsoleManager;
42
- private interactionHandler: BrowserInteractionHandler;
43
- private navigationTracker: BrowserNavigationTracker;
44
- private videoCapture: BrowserVideoCapture;
45
- private dialogHandler: BrowserDialogHandler;
46
- private nativeUIHandler: BrowserNativeUIHandler;
47
-
48
- // Store context menu info for later action execution
49
- private contextMenus = new Map<string, BrowserContextMenuInfo>();
50
-
51
- // Project ID for isolation (REQUIRED)
52
- private projectId: string;
53
-
54
- constructor(projectId: string) {
55
- super();
56
-
57
- if (!projectId) {
58
- throw new Error('projectId is required for BrowserPreviewService');
59
- }
60
-
61
- this.projectId = projectId;
62
-
63
- // Initialize managers with projectId for isolation
64
- this.tabManager = new BrowserTabManager(projectId);
65
- this.consoleManager = new BrowserConsoleManager();
66
- this.interactionHandler = new BrowserInteractionHandler();
67
- this.navigationTracker = new BrowserNavigationTracker();
68
- this.videoCapture = new BrowserVideoCapture();
69
- this.dialogHandler = new BrowserDialogHandler();
70
- this.nativeUIHandler = new BrowserNativeUIHandler();
71
-
72
- // Forward events from handlers to main service
73
- this.setupEventForwarding();
74
- }
75
-
76
- private setupEventForwarding() {
77
- // Forward console events
78
- this.consoleManager.on('console-message', (data) => {
79
- this.emit('preview:browser-console-message', data);
80
- });
81
- this.consoleManager.on('console-clear', (data) => {
82
- this.emit('preview:browser-console-clear', data);
83
- });
84
-
85
- // Forward interaction events (MCP cursor)
86
- this.interactionHandler.on('cursor-position', (data) => {
87
- this.emit('preview:browser-mcp-cursor-position', data);
88
- });
89
- this.interactionHandler.on('cursor-click', (data) => {
90
- this.emit('preview:browser-mcp-cursor-click', data);
91
- });
92
- this.interactionHandler.on('test-completed', (data) => {
93
- this.emit('preview:browser-mcp-test-completed', data);
94
- });
95
-
96
- // Forward navigation events and handle video streaming restart
97
- this.navigationTracker.on('navigation', async (data) => {
98
- this.emit('preview:browser-navigation', data);
99
-
100
- // After navigation completes, restart video streaming for the tab
101
- // This re-injects the peer script and restarts CDP screencast
102
- const { sessionId } = data;
103
- if (this.videoCapture.isStreaming(sessionId)) {
104
- const tab = this.getTab(sessionId);
105
- if (tab) {
106
- // Small delay to ensure page is fully loaded
107
- setTimeout(async () => {
108
- try {
109
- const success = await this.videoCapture.handleNavigation(sessionId, tab);
110
- if (success) {
111
- this.emit('preview:browser-navigation-streaming-ready', { sessionId });
112
- }
113
- } catch (error) {
114
- // Silently fail - frontend will request refresh if needed
115
- }
116
- }, 100);
117
- }
118
- }
119
- });
120
-
121
- // Forward navigation loading events
122
- this.navigationTracker.on('navigation-loading', (data) => {
123
- this.emit('preview:browser-navigation-loading', data);
124
- });
125
-
126
- // Forward new window events
127
- this.tabManager.on('new-window', (data) => {
128
- this.emit('preview:browser-new-window', data);
129
- });
130
-
131
- // Forward tab events (already have correct event names from tab manager)
132
- this.tabManager.on('preview:browser-tab-opened', (data) => {
133
- this.emit('preview:browser-tab-opened', data);
134
- });
135
- this.tabManager.on('preview:browser-tab-closed', (data) => {
136
- this.emit('preview:browser-tab-closed', data);
137
- });
138
- this.tabManager.on('preview:browser-tab-switched', (data) => {
139
- this.emit('preview:browser-tab-switched', data);
140
- });
141
- this.tabManager.on('preview:browser-tab-navigated', (data) => {
142
- this.emit('preview:browser-tab-navigated', data);
143
- });
144
-
145
- // Forward video capture events
146
- this.videoCapture.on('ice-candidate', (data) => {
147
- this.emit('preview:browser-webcodecs-ice-candidate', data);
148
- });
149
- this.videoCapture.on('connection-state', (data) => {
150
- this.emit('preview:browser-webcodecs-connection-state', data);
151
- });
152
- this.videoCapture.on('cursor-change', (data) => {
153
- this.emit('preview:browser-cursor-change', data);
154
- });
155
- this.videoCapture.on('navigation-streaming-ready', (data) => {
156
- this.emit('preview:browser-navigation-streaming-ready', data);
157
- });
158
-
159
- // Forward dialog events
160
- this.dialogHandler.on('dialog', (data) => {
161
- this.emit('preview:browser-dialog', data);
162
- });
163
- this.dialogHandler.on('print', (data) => {
164
- this.emit('preview:browser-print', data);
165
- });
166
-
167
- // Forward native UI events
168
- this.nativeUIHandler.on('copy-to-clipboard', (data) => {
169
- this.emit('preview:browser-copy-to-clipboard', data);
170
- });
171
- this.nativeUIHandler.on('open-url-new-tab', (data) => {
172
- this.emit('preview:browser-open-url-new-tab', data);
173
- });
174
- this.nativeUIHandler.on('download-image', (data) => {
175
- this.emit('preview:browser-download-image', data);
176
- });
177
- this.nativeUIHandler.on('copy-image-to-clipboard', (data) => {
178
- this.emit('preview:browser-copy-image-to-clipboard', data);
179
- });
180
- }
181
-
182
- /**
183
- * Get project ID for this service instance
184
- */
185
- getProjectId(): string {
186
- return this.projectId;
187
- }
188
-
189
- // ============================================================================
190
- // Tab Management Methods
191
- // ============================================================================
192
-
193
- /**
194
- * Create a new tab with optional URL
195
- *
196
- * If URL is provided, navigate to it immediately.
197
- * If URL is not provided, create blank tab (about:blank).
198
- *
199
- * Default rotation depends on device size:
200
- * - Desktop/laptop: landscape
201
- * - Tablet/mobile: portrait
202
- */
203
- async createTab(url?: string, deviceSize: DeviceSize = 'laptop', rotation?: Rotation): Promise<BrowserTab> {
204
- // Use device-appropriate default rotation if not specified
205
- const actualRotation = rotation || ((deviceSize === 'desktop' || deviceSize === 'laptop') ? 'landscape' : 'portrait');
206
- // Pre-navigation setup callback for dialog bindings
207
- const preNavigationSetup = async (page: Page) => {
208
- // Note: We'll setup dialog bindings using tabId after tab is created
209
- };
210
-
211
- // Create tab
212
- const tab = await this.tabManager.createTab(url, deviceSize, actualRotation, {
213
- setActive: true,
214
- preNavigationSetup
215
- });
216
-
217
- // Setup console and navigation tracking
218
- await this.consoleManager.setupConsoleLogging(tab.id, tab.page, tab);
219
- await this.navigationTracker.setupNavigationTracking(tab.id, tab.page, tab);
220
-
221
- // Setup dialog bindings and handling
222
- await this.dialogHandler.setupDialogBindings(tab.id, tab.page);
223
- await this.dialogHandler.setupDialogHandling(tab.id, tab.page, tab);
224
-
225
- return tab;
226
- }
227
-
228
- /**
229
- * Navigate tab to a new URL
230
- */
231
- async navigateTab(tabId: string, url: string): Promise<string> {
232
- const actualUrl = await this.tabManager.navigateTab(tabId, url);
233
-
234
- // Mark navigation for frame deduplication
235
- this.markNavigation(tabId, url);
236
-
237
- return actualUrl;
238
- }
239
-
240
- /**
241
- * Close a tab and cleanup its resources
242
- */
243
- async closeTab(tabId: string): Promise<{ success: boolean; newActiveTabId: string | null }> {
244
- const tab = this.tabManager.getTab(tabId);
245
- if (!tab) {
246
- return { success: false, newActiveTabId: null };
247
- }
248
-
249
- // Stop WebCodecs streaming first
250
- await this.stopWebCodecsStreaming(tabId);
251
-
252
- // Clear cursor tracking for this tab
253
- this.interactionHandler.clearSessionCursor(tabId);
254
-
255
- // Clear dialogs for this tab
256
- this.dialogHandler.clearSessionDialogs(tabId);
257
-
258
- // Close the tab (this will cleanup context, page, etc.)
259
- const result = await this.tabManager.closeTab(tabId);
260
-
261
- // Emit tab closed event (for MCP control manager and other listeners)
262
- this.emit('preview:browser-tab-destroyed', { tabId });
263
-
264
- return result;
265
- }
266
-
267
- /**
268
- * Switch to a specific tab
269
- */
270
- switchTab(tabId: string): boolean {
271
- return this.tabManager.setActiveTab(tabId);
272
- }
273
-
274
- /**
275
- * Get a tab by ID
276
- */
277
- getTab(tabId: string): BrowserTab | null {
278
- return this.tabManager.getTab(tabId);
279
- }
280
-
281
- /**
282
- * Get the active tab
283
- */
284
- getActiveTab(): BrowserTab | null {
285
- return this.tabManager.getActiveTab();
286
- }
287
-
288
- /**
289
- * Get all tabs
290
- */
291
- getAllTabs(): BrowserTab[] {
292
- return this.tabManager.getAllTabs();
293
- }
294
-
295
- /**
296
- * Change viewport settings (device size and rotation) for an existing tab
297
- */
298
- async setViewport(tabId: string, deviceSize: DeviceSize, rotation: Rotation): Promise<boolean> {
299
- return await this.tabManager.setViewport(tabId, deviceSize, rotation);
300
- }
301
-
302
- /**
303
- * Get tab count
304
- */
305
- getTabCount(): number {
306
- return this.tabManager.getTabCount();
307
- }
308
-
309
- /**
310
- * Get tab info
311
- */
312
- getTabInfo(tabId: string): BrowserTabInfo | null {
313
- return this.tabManager.getTabInfo(tabId);
314
- }
315
-
316
- /**
317
- * Get all tabs info
318
- */
319
- getAllTabsInfo(): BrowserTabInfo[] {
320
- return this.tabManager.getAllTabsInfo();
321
- }
322
-
323
- /**
324
- * Get available tab IDs
325
- */
326
- getAvailableTabIds(): string[] {
327
- return this.tabManager.getAvailableTabIds();
328
- }
329
-
330
- /**
331
- * Get tabs status (for admin/debugging)
332
- */
333
- getTabsStatus() {
334
- return this.tabManager.getTabsStatus();
335
- }
336
-
337
- /**
338
- * Update tab title from URL
339
- */
340
- updateTabTitleFromUrl(tabId: string, url: string): void {
341
- this.tabManager.updateTabTitleFromUrl(tabId, url);
342
- }
343
-
344
- /**
345
- * Check if tab is valid
346
- */
347
- isValidTab(tabId: string): boolean {
348
- const tab = this.getTab(tabId);
349
- return tab !== null && !tab.isDestroyed;
350
- }
351
-
352
- // ============================================================================
353
- // WebCodecs Streaming Methods (optimized, ~20-40ms, lower bandwidth)
354
- // ============================================================================
355
- async startWebCodecsStreaming(tabId: string): Promise<boolean> {
356
- const tab = this.getTab(tabId);
357
- if (!tab) {
358
- return false;
359
- }
360
- return await this.videoCapture.startStreaming(
361
- tabId,
362
- tab,
363
- () => this.isValidTab(tabId)
364
- );
365
- }
366
-
367
- async stopWebCodecsStreaming(tabId: string): Promise<void> {
368
- const tab = this.getTab(tabId);
369
- await this.videoCapture.stopStreaming(tabId, tab ?? undefined);
370
- }
371
-
372
- async updateWebCodecsScale(tabId: string, newScale: number): Promise<boolean> {
373
- const tab = this.getTab(tabId);
374
- if (!tab) {
375
- return false;
376
- }
377
- return await this.videoCapture.updateScale(tabId, tab, newScale);
378
- }
379
-
380
- async updateWebCodecsViewport(tabId: string, width: number, height: number, newScale: number): Promise<boolean> {
381
- const tab = this.getTab(tabId);
382
- if (!tab) {
383
- return false;
384
- }
385
- return await this.videoCapture.updateViewport(tabId, tab, width, height, newScale);
386
- }
387
-
388
- async getWebCodecsOffer(tabId: string): Promise<RTCSessionDescriptionInit | null> {
389
- const tab = this.getTab(tabId);
390
- if (!tab) {
391
- return null;
392
- }
393
- return await this.videoCapture.createOffer(tabId, tab);
394
- }
395
-
396
- async handleWebCodecsAnswer(tabId: string, answer: RTCSessionDescriptionInit): Promise<boolean> {
397
- const tab = this.getTab(tabId);
398
- if (!tab) {
399
- return false;
400
- }
401
- return await this.videoCapture.handleAnswer(tabId, tab, answer);
402
- }
403
-
404
- async addWebCodecsIceCandidate(tabId: string, candidate: RTCIceCandidateInit): Promise<boolean> {
405
- const tab = this.getTab(tabId);
406
- if (!tab) {
407
- return false;
408
- }
409
- return await this.videoCapture.addIceCandidate(tabId, tab, candidate);
410
- }
411
-
412
- isWebCodecsActive(tabId: string): boolean {
413
- return this.videoCapture.isStreaming(tabId);
414
- }
415
-
416
- async getWebCodecsStats(tabId: string) {
417
- const tab = this.getTab(tabId);
418
- if (!tab) {
419
- return null;
420
- }
421
- return await this.videoCapture.getStats(tabId, tab);
422
- }
423
-
424
- markUserInteraction(tabId: string): void {
425
- this.tabManager.markTabActivity(tabId);
426
- }
427
-
428
- // Public method to mark tab activity (called from WS handlers)
429
- markActiveTabActivity(): void {
430
- const tab = this.getActiveTab();
431
- if (tab) {
432
- this.tabManager.markTabActivity(tab.id);
433
- }
434
- }
435
-
436
- markNavigation(tabId: string, _newUrl?: string): void {
437
- // Navigation tracking is now handled by WebCodecs automatically
438
- // This method is kept for API compatibility
439
- }
440
-
441
- // ============================================================================
442
- // Console Management Methods
443
- // ============================================================================
444
- getConsoleLogs(tabId: string): BrowserConsoleMessage[] {
445
- const tab = this.getTab(tabId);
446
- return tab ? this.consoleManager.getConsoleLogs(tab) : [];
447
- }
448
-
449
- clearConsoleLogs(tabId: string): boolean {
450
- const tab = this.getTab(tabId);
451
- return tab ? this.consoleManager.clearConsoleLogs(tab) : false;
452
- }
453
-
454
- toggleConsoleLogging(tabId: string, enabled: boolean): boolean {
455
- const tab = this.getTab(tabId);
456
- return tab ? this.consoleManager.toggleConsoleLogging(tab, enabled) : false;
457
- }
458
-
459
- async executeConsoleCommand(tabId: string, command: string): Promise<any> {
460
- const tab = this.getTab(tabId);
461
- if (!tab) throw new Error('Tab not found or invalid');
462
- return this.consoleManager.executeConsoleCommand(tab, command);
463
- }
464
-
465
- // ============================================================================
466
- // Interaction & Autonomous Actions Methods
467
- // ============================================================================
468
- async performAutonomousActions(tabId: string, actions: BrowserAutonomousAction[]) {
469
- const tab = this.getTab(tabId);
470
- if (!tab) throw new Error('Tab not found or invalid');
471
-
472
- const results = await this.interactionHandler.performAutonomousActions(
473
- tabId,
474
- tab,
475
- actions,
476
- () => this.isValidTab(tabId)
477
- );
478
-
479
- return results;
480
- }
481
-
482
- /**
483
- * Perform autonomous actions using tab object directly
484
- * More efficient when tab is already available
485
- */
486
- async performAutonomousActionsWithTab(tab: BrowserTab, actions: BrowserAutonomousAction[]) {
487
- const results = await this.interactionHandler.performAutonomousActions(
488
- tab.id,
489
- tab,
490
- actions,
491
- () => this.isValidTab(tab.id)
492
- );
493
-
494
- return results;
495
- }
496
-
497
- // ============================================================================
498
- // Dialog Management Methods
499
- // ============================================================================
500
- async respondToDialog(response: BrowserDialogResponse): Promise<boolean> {
501
- return await this.dialogHandler.respondToDialog(response);
502
- }
503
-
504
- // ============================================================================
505
- // Native UI Methods (Select & Context Menu)
506
- // ============================================================================
507
- async checkForSelectElement(tabId: string, x: number, y: number) {
508
- const tab = this.getTab(tabId);
509
- if (!tab) return null;
510
-
511
- const selectInfo = await this.nativeUIHandler.checkForSelect(tabId, tab.page, x, y);
512
- if (selectInfo) {
513
- this.emit('preview:browser-select', selectInfo);
514
- }
515
- return selectInfo;
516
- }
517
-
518
- async handleSelectResponse(tabId: string, response: BrowserSelectResponse): Promise<boolean> {
519
- const tab = this.getTab(tabId);
520
- if (!tab) return false;
521
-
522
- return await this.nativeUIHandler.handleSelectResponse(tab.page, response);
523
- }
524
-
525
- async checkForContextMenu(tabId: string, x: number, y: number) {
526
- const tab = this.getTab(tabId);
527
- if (!tab) return null;
528
-
529
- const menuInfo = await this.nativeUIHandler.checkForContextMenu(tabId, tab.page, x, y);
530
- if (menuInfo) {
531
- // Store menu info for later action execution
532
- this.contextMenus.set(menuInfo.menuId, menuInfo);
533
- this.emit('preview:browser-context-menu', menuInfo);
534
- }
535
- return menuInfo;
536
- }
537
-
538
- async handleContextMenuResponse(tabId: string, response: BrowserContextMenuResponse, clipboardText?: string): Promise<boolean> {
539
- const tab = this.getTab(tabId);
540
- if (!tab) return false;
541
-
542
- // Get stored menu info
543
- const menuInfo = this.contextMenus.get(response.menuId);
544
- if (!menuInfo) return false;
545
-
546
- const result = await this.nativeUIHandler.handleContextMenuResponse(tab.page, response, menuInfo, clipboardText);
547
-
548
- // Clean up stored menu info
549
- this.contextMenus.delete(response.menuId);
550
-
551
- return result;
552
- }
553
-
554
- // ============================================================================
555
- // Cleanup Methods
556
- // ============================================================================
557
- async cleanup() {
558
- // Clear all cursor tracking
559
- this.interactionHandler.clearAllSessionCursors();
560
- // Cleanup tabs (this will also cleanup all contexts/pages/browser pool)
561
- await this.tabManager.cleanup();
562
- }
563
-
564
- async cleanupInactiveTabs() {
565
- return this.tabManager.cleanupInactiveTabs();
566
- }
567
-
568
- async forceCleanupAll() {
569
- // First try normal cleanup
570
- await this.cleanup();
571
-
572
- // Cleanup video capture sessions
573
- await this.videoCapture.cleanup();
574
-
575
- // Clear all dialogs and context menus
576
- this.dialogHandler.clearAllDialogs();
577
- this.contextMenus.clear();
578
-
579
- // Remove all listeners to prevent memory leaks
580
- this.removeAllListeners();
581
- this.consoleManager.removeAllListeners();
582
- this.interactionHandler.removeAllListeners();
583
- this.navigationTracker.removeAllListeners();
584
- this.videoCapture.removeAllListeners();
585
- this.dialogHandler.removeAllListeners();
586
- this.nativeUIHandler.removeAllListeners();
587
- }
588
- }
589
-
590
- /**
591
- * Browser Preview Service Manager
592
- *
593
- * Manages BrowserPreviewService instances per project.
594
- * Provides project isolation - each project has its own browser tabs and state.
595
- */
596
- class BrowserPreviewServiceManager {
597
- private services = new Map<string, BrowserPreviewService>();
598
-
599
- /**
600
- * Get or create a BrowserPreviewService for a project
601
- */
602
- getService(projectId: string): BrowserPreviewService {
603
- if (!projectId) {
604
- throw new Error('projectId is required and cannot be empty');
605
- }
606
-
607
- if (!this.services.has(projectId)) {
608
- debug.log('preview', `🆕 Creating new BrowserPreviewService for project: ${projectId}`);
609
- const service = new BrowserPreviewService(projectId);
610
- this.services.set(projectId, service);
611
-
612
- // Setup WebSocket event forwarding for this service
613
- this.setupWebSocketForwarding(service, projectId);
614
- debug.log('preview', `✅ BrowserPreviewService fully initialized for project: ${projectId}`);
615
- }
616
-
617
- return this.services.get(projectId)!;
618
- }
619
-
620
- /**
621
- * Setup WebSocket event forwarding for a service instance
622
- * Events are emitted to the specific project only
623
- */
624
- private setupWebSocketForwarding(service: BrowserPreviewService, projectId: string): void {
625
- debug.log('preview', `🔌 Setting up WebSocket forwarding for project: ${projectId}...`);
626
-
627
- // Forward WebCodecs events
628
- service.on('preview:browser-webcodecs-ice-candidate', (data) => {
629
- ws.emit.project(projectId, 'preview:browser-stream-ice', {
630
- sessionId: data.sessionId,
631
- candidate: data.candidate,
632
- from: data.from
633
- });
634
- });
635
-
636
- service.on('preview:browser-webcodecs-connection-state', (data) => {
637
- ws.emit.project(projectId, 'preview:browser-stream-state', data);
638
- });
639
-
640
- service.on('preview:browser-cursor-change', (data) => {
641
- ws.emit.project(projectId, 'preview:browser-cursor-change', data);
642
- });
643
-
644
- // Forward navigation events
645
- service.on('preview:browser-navigation-loading', (data) => {
646
- ws.emit.project(projectId, 'preview:browser-navigation-loading', data);
647
- });
648
-
649
- service.on('preview:browser-navigation', (data) => {
650
- ws.emit.project(projectId, 'preview:browser-navigation', data);
651
- });
652
-
653
- // Forward tab events
654
- service.on('preview:browser-tab-opened', (data) => {
655
- debug.log('preview', `🚀 Forwarding preview:browser-tab-opened to project ${projectId}:`, data);
656
- ws.emit.project(projectId, 'preview:browser-tab-opened', data);
657
- });
658
-
659
- service.on('preview:browser-tab-closed', (data) => {
660
- ws.emit.project(projectId, 'preview:browser-tab-closed', data);
661
- });
662
-
663
- service.on('preview:browser-tab-switched', (data) => {
664
- ws.emit.project(projectId, 'preview:browser-tab-switched', data);
665
- });
666
-
667
- service.on('preview:browser-tab-navigated', (data) => {
668
- ws.emit.project(projectId, 'preview:browser-tab-navigated', data);
669
- });
670
-
671
- // Forward console events
672
- service.on('preview:browser-console-message', (data) => {
673
- ws.emit.project(projectId, 'preview:browser-console-message', data);
674
- });
675
-
676
- service.on('preview:browser-console-clear', (data) => {
677
- ws.emit.project(projectId, 'preview:browser-console-clear', data);
678
- });
679
-
680
- // Forward MCP events
681
- service.on('preview:browser-mcp-cursor-position', (data) => {
682
- ws.emit.project(projectId, 'preview:browser-mcp-cursor-position', data);
683
- });
684
-
685
- service.on('preview:browser-mcp-cursor-click', (data) => {
686
- ws.emit.project(projectId, 'preview:browser-mcp-cursor-click', data);
687
- });
688
-
689
- service.on('preview:browser-mcp-test-completed', (data) => {
690
- ws.emit.project(projectId, 'preview:browser-mcp-test-completed', data);
691
- });
692
-
693
- // Forward dialog events
694
- service.on('preview:browser-dialog', (data) => {
695
- ws.emit.project(projectId, 'preview:browser-dialog', data);
696
- });
697
-
698
- service.on('preview:browser-print', (data) => {
699
- ws.emit.project(projectId, 'preview:browser-print', data);
700
- });
701
-
702
- // Forward native UI events
703
- service.on('preview:browser-select', (data) => {
704
- ws.emit.project(projectId, 'preview:browser-select', data);
705
- });
706
-
707
- service.on('preview:browser-context-menu', (data) => {
708
- ws.emit.project(projectId, 'preview:browser-context-menu', data);
709
- });
710
-
711
- service.on('preview:browser-copy-to-clipboard', (data) => {
712
- ws.emit.project(projectId, 'preview:browser-copy-to-clipboard', data);
713
- });
714
-
715
- service.on('preview:browser-open-url-new-tab', (data) => {
716
- ws.emit.project(projectId, 'preview:browser-open-url-new-tab', data);
717
- });
718
-
719
- service.on('preview:browser-download-image', (data) => {
720
- ws.emit.project(projectId, 'preview:browser-download-image', data);
721
- });
722
-
723
- service.on('preview:browser-copy-image-to-clipboard', (data) => {
724
- ws.emit.project(projectId, 'preview:browser-copy-image-to-clipboard', data);
725
- });
726
-
727
- // Forward new window events
728
- service.on('preview:browser-new-window', (data) => {
729
- ws.emit.project(projectId, 'preview:browser-new-window', data);
730
- });
731
-
732
- // Forward MCP control events (from singleton browserMcpControl)
733
- browserMcpControl.on('control-start', (data) => {
734
- debug.log('preview', `🚀 Forwarding mcp-control-start to project ${projectId}:`, data);
735
- ws.emit.project(projectId, 'preview:browser-mcp-control-start', {
736
- browserSessionId: data.browserTabId,
737
- mcpSessionId: data.mcpSessionId,
738
- timestamp: data.timestamp
739
- });
740
- });
741
-
742
- browserMcpControl.on('control-end', (data) => {
743
- debug.log('preview', `🚀 Forwarding mcp-control-end to project ${projectId}:`, data);
744
- ws.emit.project(projectId, 'preview:browser-mcp-control-end', {
745
- browserSessionId: data.browserTabId,
746
- timestamp: data.timestamp
747
- });
748
- });
749
-
750
- // Forward MCP cursor events
751
- browserMcpControl.on('cursor-position', (data) => {
752
- ws.emit.project(projectId, 'preview:browser-mcp-cursor-position', {
753
- sessionId: data.tabId,
754
- x: data.x,
755
- y: data.y,
756
- timestamp: data.timestamp,
757
- source: 'mcp'
758
- });
759
- });
760
-
761
- browserMcpControl.on('cursor-click', (data) => {
762
- ws.emit.project(projectId, 'preview:browser-mcp-cursor-click', {
763
- sessionId: data.tabId,
764
- x: data.x,
765
- y: data.y,
766
- timestamp: data.timestamp,
767
- source: 'mcp'
768
- });
769
- });
770
-
771
- browserMcpControl.on('test-completed', (data) => {
772
- ws.emit.project(projectId, 'preview:browser-mcp-test-completed', {
773
- sessionId: data.tabId,
774
- timestamp: data.timestamp,
775
- source: 'mcp'
776
- });
777
- });
778
-
779
- debug.log('preview', `🎉 All WebSocket event listeners registered for project: ${projectId}`);
780
- }
781
-
782
- /**
783
- * Check if a service exists for a project
784
- */
785
- hasService(projectId: string): boolean {
786
- if (!projectId) {
787
- throw new Error('projectId is required and cannot be empty');
788
- }
789
- return this.services.has(projectId);
790
- }
791
-
792
- /**
793
- * Remove a service for a project (cleanup)
794
- */
795
- async removeService(projectId: string): Promise<void> {
796
- if (!projectId) {
797
- throw new Error('projectId is required and cannot be empty');
798
- }
799
-
800
- const service = this.services.get(projectId);
801
-
802
- if (service) {
803
- await service.forceCleanupAll();
804
- this.services.delete(projectId);
805
- }
806
- }
807
-
808
- /**
809
- * Cleanup all services
810
- */
811
- async cleanup(): Promise<void> {
812
- const cleanupPromises = Array.from(this.services.values()).map(service =>
813
- service.forceCleanupAll().catch(error => {
814
- console.error('Error cleaning up service:', error);
815
- })
816
- );
817
-
818
- await Promise.all(cleanupPromises);
819
- this.services.clear();
820
- }
821
-
822
- /**
823
- * Get all active project IDs
824
- */
825
- getActiveProjects(): string[] {
826
- return Array.from(this.services.keys());
827
- }
828
-
829
- /**
830
- * Get stats for all services
831
- */
832
- getStats() {
833
- const stats = new Map<string, any>();
834
-
835
- for (const [projectId, service] of this.services.entries()) {
836
- stats.set(projectId, {
837
- projectId,
838
- tabs: service.getTabsStatus()
839
- });
840
- }
841
-
842
- return stats;
843
- }
844
- }
845
-
846
- // Service manager instance (singleton)
847
- export const browserPreviewServiceManager = new BrowserPreviewServiceManager();
848
-
849
- // Graceful shutdown handlers
850
- const gracefulShutdown = async (signal: string) => {
851
- try {
852
- await browserPreviewServiceManager.cleanup();
853
- process.exit(0);
854
- } catch (error) {
855
- process.exit(1);
856
- }
857
- };
858
-
859
- // Handle various termination signals
860
- process.on('SIGINT', () => gracefulShutdown('SIGINT'));
861
- process.on('SIGTERM', () => gracefulShutdown('SIGTERM'));
862
- process.on('SIGHUP', () => gracefulShutdown('SIGHUP'));
863
-
864
- // Handle Windows-specific signals
865
- if (process.platform === 'win32') {
866
- process.on('SIGBREAK', () => gracefulShutdown('SIGBREAK'));
867
- }
868
-
869
- // Handle uncaught exceptions and unhandled rejections
870
- process.on('uncaughtException', async (error) => {
871
- await browserPreviewServiceManager.cleanup();
872
- process.exit(1);
873
- });
874
-
875
- process.on('unhandledRejection', async (reason, promise) => {
876
- await browserPreviewServiceManager.cleanup();
877
- process.exit(1);
878
- });
879
-
880
- // Handle process exit
881
- process.on('exit', (code) => {
882
- });
1
+ import { EventEmitter } from 'events';
2
+ import type { Page } from 'puppeteer-core';
3
+ import { BrowserTabManager } from './browser-tab-manager.js';
4
+ import { BrowserConsoleManager } from './browser-console-manager.js';
5
+ import { BrowserInteractionHandler } from './browser-interaction-handler.js';
6
+ import { BrowserNavigationTracker } from './browser-navigation-tracker.js';
7
+ import { BrowserVideoCapture } from './browser-video-capture.js';
8
+ import { BrowserDialogHandler } from './browser-dialog-handler.js';
9
+ import { BrowserNativeUIHandler } from './browser-native-ui-handler.js';
10
+ import { browserMcpControl } from './browser-mcp-control.js';
11
+ import { ws } from '$backend/lib/utils/ws';
12
+ import { debug } from '$shared/utils/logger';
13
+ import type {
14
+ BrowserTab,
15
+ BrowserTabInfo,
16
+ BrowserConsoleMessage,
17
+ BrowserAutonomousAction,
18
+ DeviceSize,
19
+ Rotation,
20
+ BrowserDialogResponse,
21
+ BrowserSelectResponse,
22
+ BrowserContextMenuResponse,
23
+ BrowserContextMenuInfo
24
+ } from './types';
25
+
26
+ /**
27
+ * Browser Preview Service
28
+ *
29
+ * Main orchestrator for browser preview functionality.
30
+ * Tab-centric architecture - all operations work with tabs.
31
+ *
32
+ * Architecture:
33
+ * - Tabs are the primary unit (no separate session concept)
34
+ * - Each tab = isolated browser context + page
35
+ * - Event-driven communication with frontend
36
+ * - Manages all browser operations: streaming, interaction, console, etc.
37
+ * - **PROJECT ISOLATION**: Each instance is isolated per project
38
+ */
39
+ export class BrowserPreviewService extends EventEmitter {
40
+ private tabManager: BrowserTabManager;
41
+ private consoleManager: BrowserConsoleManager;
42
+ private interactionHandler: BrowserInteractionHandler;
43
+ private navigationTracker: BrowserNavigationTracker;
44
+ private videoCapture: BrowserVideoCapture;
45
+ private dialogHandler: BrowserDialogHandler;
46
+ private nativeUIHandler: BrowserNativeUIHandler;
47
+
48
+ // Store context menu info for later action execution
49
+ private contextMenus = new Map<string, BrowserContextMenuInfo>();
50
+
51
+ // Project ID for isolation (REQUIRED)
52
+ private projectId: string;
53
+
54
+ constructor(projectId: string) {
55
+ super();
56
+
57
+ if (!projectId) {
58
+ throw new Error('projectId is required for BrowserPreviewService');
59
+ }
60
+
61
+ this.projectId = projectId;
62
+
63
+ // Initialize managers with projectId for isolation
64
+ this.tabManager = new BrowserTabManager(projectId);
65
+ this.consoleManager = new BrowserConsoleManager();
66
+ this.interactionHandler = new BrowserInteractionHandler();
67
+ this.navigationTracker = new BrowserNavigationTracker();
68
+ this.videoCapture = new BrowserVideoCapture();
69
+ this.dialogHandler = new BrowserDialogHandler();
70
+ this.nativeUIHandler = new BrowserNativeUIHandler();
71
+
72
+ // Forward events from handlers to main service
73
+ this.setupEventForwarding();
74
+ }
75
+
76
+ private setupEventForwarding() {
77
+ // Forward console events
78
+ this.consoleManager.on('console-message', (data) => {
79
+ this.emit('preview:browser-console-message', data);
80
+ });
81
+ this.consoleManager.on('console-clear', (data) => {
82
+ this.emit('preview:browser-console-clear', data);
83
+ });
84
+
85
+ // Forward interaction events (MCP cursor)
86
+ this.interactionHandler.on('cursor-position', (data) => {
87
+ this.emit('preview:browser-mcp-cursor-position', data);
88
+ });
89
+ this.interactionHandler.on('cursor-click', (data) => {
90
+ this.emit('preview:browser-mcp-cursor-click', data);
91
+ });
92
+ this.interactionHandler.on('test-completed', (data) => {
93
+ this.emit('preview:browser-mcp-test-completed', data);
94
+ });
95
+
96
+ // Forward navigation events and handle video streaming restart
97
+ this.navigationTracker.on('navigation', async (data) => {
98
+ this.emit('preview:browser-navigation', data);
99
+
100
+ // After navigation completes, restart video streaming for the tab
101
+ // This re-injects the peer script and restarts CDP screencast
102
+ const { sessionId } = data;
103
+ if (this.videoCapture.isStreaming(sessionId)) {
104
+ const tab = this.getTab(sessionId);
105
+ if (tab) {
106
+ // Small delay to ensure page is fully loaded
107
+ setTimeout(async () => {
108
+ try {
109
+ const success = await this.videoCapture.handleNavigation(sessionId, tab);
110
+ if (success) {
111
+ this.emit('preview:browser-navigation-streaming-ready', { sessionId });
112
+ }
113
+ } catch (error) {
114
+ // Silently fail - frontend will request refresh if needed
115
+ }
116
+ }, 100);
117
+ }
118
+ }
119
+ });
120
+
121
+ // Forward navigation loading events
122
+ this.navigationTracker.on('navigation-loading', (data) => {
123
+ this.emit('preview:browser-navigation-loading', data);
124
+ });
125
+
126
+ // Forward new window events
127
+ this.tabManager.on('new-window', (data) => {
128
+ this.emit('preview:browser-new-window', data);
129
+ });
130
+
131
+ // Forward tab events (already have correct event names from tab manager)
132
+ this.tabManager.on('preview:browser-tab-opened', (data) => {
133
+ this.emit('preview:browser-tab-opened', data);
134
+ });
135
+ this.tabManager.on('preview:browser-tab-closed', (data) => {
136
+ this.emit('preview:browser-tab-closed', data);
137
+ });
138
+ this.tabManager.on('preview:browser-tab-switched', (data) => {
139
+ this.emit('preview:browser-tab-switched', data);
140
+ });
141
+ this.tabManager.on('preview:browser-tab-navigated', (data) => {
142
+ this.emit('preview:browser-tab-navigated', data);
143
+ });
144
+
145
+ // Forward video capture events
146
+ this.videoCapture.on('ice-candidate', (data) => {
147
+ this.emit('preview:browser-webcodecs-ice-candidate', data);
148
+ });
149
+ this.videoCapture.on('connection-state', (data) => {
150
+ this.emit('preview:browser-webcodecs-connection-state', data);
151
+ });
152
+ this.videoCapture.on('cursor-change', (data) => {
153
+ this.emit('preview:browser-cursor-change', data);
154
+ });
155
+ this.videoCapture.on('navigation-streaming-ready', (data) => {
156
+ this.emit('preview:browser-navigation-streaming-ready', data);
157
+ });
158
+
159
+ // Forward dialog events
160
+ this.dialogHandler.on('dialog', (data) => {
161
+ this.emit('preview:browser-dialog', data);
162
+ });
163
+ this.dialogHandler.on('print', (data) => {
164
+ this.emit('preview:browser-print', data);
165
+ });
166
+
167
+ // Forward native UI events
168
+ this.nativeUIHandler.on('copy-to-clipboard', (data) => {
169
+ this.emit('preview:browser-copy-to-clipboard', data);
170
+ });
171
+ this.nativeUIHandler.on('open-url-new-tab', (data) => {
172
+ this.emit('preview:browser-open-url-new-tab', data);
173
+ });
174
+ this.nativeUIHandler.on('download-image', (data) => {
175
+ this.emit('preview:browser-download-image', data);
176
+ });
177
+ this.nativeUIHandler.on('copy-image-to-clipboard', (data) => {
178
+ this.emit('preview:browser-copy-image-to-clipboard', data);
179
+ });
180
+ }
181
+
182
+ /**
183
+ * Get project ID for this service instance
184
+ */
185
+ getProjectId(): string {
186
+ return this.projectId;
187
+ }
188
+
189
+ // ============================================================================
190
+ // Tab Management Methods
191
+ // ============================================================================
192
+
193
+ /**
194
+ * Create a new tab with optional URL
195
+ *
196
+ * If URL is provided, navigate to it immediately.
197
+ * If URL is not provided, create blank tab (about:blank).
198
+ *
199
+ * Default rotation depends on device size:
200
+ * - Desktop/laptop: landscape
201
+ * - Tablet/mobile: portrait
202
+ */
203
+ async createTab(url?: string, deviceSize: DeviceSize = 'laptop', rotation?: Rotation): Promise<BrowserTab> {
204
+ // Use device-appropriate default rotation if not specified
205
+ const actualRotation = rotation || ((deviceSize === 'desktop' || deviceSize === 'laptop') ? 'landscape' : 'portrait');
206
+ // Pre-navigation setup callback for dialog bindings
207
+ const preNavigationSetup = async (page: Page) => {
208
+ // Note: We'll setup dialog bindings using tabId after tab is created
209
+ };
210
+
211
+ // Create tab
212
+ const tab = await this.tabManager.createTab(url, deviceSize, actualRotation, {
213
+ setActive: true,
214
+ preNavigationSetup
215
+ });
216
+
217
+ // Setup console and navigation tracking
218
+ await this.consoleManager.setupConsoleLogging(tab.id, tab.page, tab);
219
+ await this.navigationTracker.setupNavigationTracking(tab.id, tab.page, tab);
220
+
221
+ // Setup dialog bindings and handling
222
+ await this.dialogHandler.setupDialogBindings(tab.id, tab.page);
223
+ await this.dialogHandler.setupDialogHandling(tab.id, tab.page, tab);
224
+
225
+ return tab;
226
+ }
227
+
228
+ /**
229
+ * Navigate tab to a new URL
230
+ */
231
+ async navigateTab(tabId: string, url: string): Promise<string> {
232
+ const actualUrl = await this.tabManager.navigateTab(tabId, url);
233
+
234
+ // Mark navigation for frame deduplication
235
+ this.markNavigation(tabId, url);
236
+
237
+ return actualUrl;
238
+ }
239
+
240
+ /**
241
+ * Close a tab and cleanup its resources
242
+ */
243
+ async closeTab(tabId: string): Promise<{ success: boolean; newActiveTabId: string | null }> {
244
+ const tab = this.tabManager.getTab(tabId);
245
+ if (!tab) {
246
+ return { success: false, newActiveTabId: null };
247
+ }
248
+
249
+ // Stop WebCodecs streaming first
250
+ await this.stopWebCodecsStreaming(tabId);
251
+
252
+ // Clear cursor tracking for this tab
253
+ this.interactionHandler.clearSessionCursor(tabId);
254
+
255
+ // Clear dialogs for this tab
256
+ this.dialogHandler.clearSessionDialogs(tabId);
257
+
258
+ // Close the tab (this will cleanup context, page, etc.)
259
+ const result = await this.tabManager.closeTab(tabId);
260
+
261
+ // Emit tab closed event (for MCP control manager and other listeners)
262
+ this.emit('preview:browser-tab-destroyed', { tabId });
263
+
264
+ return result;
265
+ }
266
+
267
+ /**
268
+ * Switch to a specific tab
269
+ */
270
+ switchTab(tabId: string): boolean {
271
+ return this.tabManager.setActiveTab(tabId);
272
+ }
273
+
274
+ /**
275
+ * Get a tab by ID
276
+ */
277
+ getTab(tabId: string): BrowserTab | null {
278
+ return this.tabManager.getTab(tabId);
279
+ }
280
+
281
+ /**
282
+ * Get the active tab
283
+ */
284
+ getActiveTab(): BrowserTab | null {
285
+ return this.tabManager.getActiveTab();
286
+ }
287
+
288
+ /**
289
+ * Get all tabs
290
+ */
291
+ getAllTabs(): BrowserTab[] {
292
+ return this.tabManager.getAllTabs();
293
+ }
294
+
295
+ /**
296
+ * Change viewport settings (device size and rotation) for an existing tab
297
+ */
298
+ async setViewport(tabId: string, deviceSize: DeviceSize, rotation: Rotation): Promise<boolean> {
299
+ return await this.tabManager.setViewport(tabId, deviceSize, rotation);
300
+ }
301
+
302
+ /**
303
+ * Get tab count
304
+ */
305
+ getTabCount(): number {
306
+ return this.tabManager.getTabCount();
307
+ }
308
+
309
+ /**
310
+ * Get tab info
311
+ */
312
+ getTabInfo(tabId: string): BrowserTabInfo | null {
313
+ return this.tabManager.getTabInfo(tabId);
314
+ }
315
+
316
+ /**
317
+ * Get all tabs info
318
+ */
319
+ getAllTabsInfo(): BrowserTabInfo[] {
320
+ return this.tabManager.getAllTabsInfo();
321
+ }
322
+
323
+ /**
324
+ * Get available tab IDs
325
+ */
326
+ getAvailableTabIds(): string[] {
327
+ return this.tabManager.getAvailableTabIds();
328
+ }
329
+
330
+ /**
331
+ * Get tabs status (for admin/debugging)
332
+ */
333
+ getTabsStatus() {
334
+ return this.tabManager.getTabsStatus();
335
+ }
336
+
337
+ /**
338
+ * Update tab title from URL
339
+ */
340
+ updateTabTitleFromUrl(tabId: string, url: string): void {
341
+ this.tabManager.updateTabTitleFromUrl(tabId, url);
342
+ }
343
+
344
+ /**
345
+ * Check if tab is valid
346
+ */
347
+ isValidTab(tabId: string): boolean {
348
+ const tab = this.getTab(tabId);
349
+ return tab !== null && !tab.isDestroyed;
350
+ }
351
+
352
+ // ============================================================================
353
+ // WebCodecs Streaming Methods (optimized, ~20-40ms, lower bandwidth)
354
+ // ============================================================================
355
+ async startWebCodecsStreaming(tabId: string): Promise<boolean> {
356
+ const tab = this.getTab(tabId);
357
+ if (!tab) {
358
+ return false;
359
+ }
360
+ return await this.videoCapture.startStreaming(
361
+ tabId,
362
+ tab,
363
+ () => this.isValidTab(tabId)
364
+ );
365
+ }
366
+
367
+ async stopWebCodecsStreaming(tabId: string): Promise<void> {
368
+ const tab = this.getTab(tabId);
369
+ await this.videoCapture.stopStreaming(tabId, tab ?? undefined);
370
+ }
371
+
372
+ async updateWebCodecsScale(tabId: string, newScale: number): Promise<boolean> {
373
+ const tab = this.getTab(tabId);
374
+ if (!tab) {
375
+ return false;
376
+ }
377
+ return await this.videoCapture.updateScale(tabId, tab, newScale);
378
+ }
379
+
380
+ async updateWebCodecsViewport(tabId: string, width: number, height: number, newScale: number): Promise<boolean> {
381
+ const tab = this.getTab(tabId);
382
+ if (!tab) {
383
+ return false;
384
+ }
385
+ return await this.videoCapture.updateViewport(tabId, tab, width, height, newScale);
386
+ }
387
+
388
+ async getWebCodecsOffer(tabId: string): Promise<RTCSessionDescriptionInit | null> {
389
+ const tab = this.getTab(tabId);
390
+ if (!tab) {
391
+ return null;
392
+ }
393
+ return await this.videoCapture.createOffer(tabId, tab);
394
+ }
395
+
396
+ async handleWebCodecsAnswer(tabId: string, answer: RTCSessionDescriptionInit): Promise<boolean> {
397
+ const tab = this.getTab(tabId);
398
+ if (!tab) {
399
+ return false;
400
+ }
401
+ return await this.videoCapture.handleAnswer(tabId, tab, answer);
402
+ }
403
+
404
+ async addWebCodecsIceCandidate(tabId: string, candidate: RTCIceCandidateInit): Promise<boolean> {
405
+ const tab = this.getTab(tabId);
406
+ if (!tab) {
407
+ return false;
408
+ }
409
+ return await this.videoCapture.addIceCandidate(tabId, tab, candidate);
410
+ }
411
+
412
+ isWebCodecsActive(tabId: string): boolean {
413
+ return this.videoCapture.isStreaming(tabId);
414
+ }
415
+
416
+ async getWebCodecsStats(tabId: string) {
417
+ const tab = this.getTab(tabId);
418
+ if (!tab) {
419
+ return null;
420
+ }
421
+ return await this.videoCapture.getStats(tabId, tab);
422
+ }
423
+
424
+ markUserInteraction(tabId: string): void {
425
+ this.tabManager.markTabActivity(tabId);
426
+ }
427
+
428
+ // Public method to mark tab activity (called from WS handlers)
429
+ markActiveTabActivity(): void {
430
+ const tab = this.getActiveTab();
431
+ if (tab) {
432
+ this.tabManager.markTabActivity(tab.id);
433
+ }
434
+ }
435
+
436
+ markNavigation(tabId: string, _newUrl?: string): void {
437
+ // Navigation tracking is now handled by WebCodecs automatically
438
+ // This method is kept for API compatibility
439
+ }
440
+
441
+ // ============================================================================
442
+ // Console Management Methods
443
+ // ============================================================================
444
+ getConsoleLogs(tabId: string): BrowserConsoleMessage[] {
445
+ const tab = this.getTab(tabId);
446
+ return tab ? this.consoleManager.getConsoleLogs(tab) : [];
447
+ }
448
+
449
+ clearConsoleLogs(tabId: string): boolean {
450
+ const tab = this.getTab(tabId);
451
+ return tab ? this.consoleManager.clearConsoleLogs(tab) : false;
452
+ }
453
+
454
+ toggleConsoleLogging(tabId: string, enabled: boolean): boolean {
455
+ const tab = this.getTab(tabId);
456
+ return tab ? this.consoleManager.toggleConsoleLogging(tab, enabled) : false;
457
+ }
458
+
459
+ async executeConsoleCommand(tabId: string, command: string): Promise<any> {
460
+ const tab = this.getTab(tabId);
461
+ if (!tab) throw new Error('Tab not found or invalid');
462
+ return this.consoleManager.executeConsoleCommand(tab, command);
463
+ }
464
+
465
+ // ============================================================================
466
+ // Interaction & Autonomous Actions Methods
467
+ // ============================================================================
468
+ async performAutonomousActions(tabId: string, actions: BrowserAutonomousAction[]) {
469
+ const tab = this.getTab(tabId);
470
+ if (!tab) throw new Error('Tab not found or invalid');
471
+
472
+ const results = await this.interactionHandler.performAutonomousActions(
473
+ tabId,
474
+ tab,
475
+ actions,
476
+ () => this.isValidTab(tabId)
477
+ );
478
+
479
+ return results;
480
+ }
481
+
482
+ /**
483
+ * Perform autonomous actions using tab object directly
484
+ * More efficient when tab is already available
485
+ */
486
+ async performAutonomousActionsWithTab(tab: BrowserTab, actions: BrowserAutonomousAction[]) {
487
+ const results = await this.interactionHandler.performAutonomousActions(
488
+ tab.id,
489
+ tab,
490
+ actions,
491
+ () => this.isValidTab(tab.id)
492
+ );
493
+
494
+ return results;
495
+ }
496
+
497
+ // ============================================================================
498
+ // Dialog Management Methods
499
+ // ============================================================================
500
+ async respondToDialog(response: BrowserDialogResponse): Promise<boolean> {
501
+ return await this.dialogHandler.respondToDialog(response);
502
+ }
503
+
504
+ // ============================================================================
505
+ // Native UI Methods (Select & Context Menu)
506
+ // ============================================================================
507
+ async checkForSelectElement(tabId: string, x: number, y: number) {
508
+ const tab = this.getTab(tabId);
509
+ if (!tab) return null;
510
+
511
+ const selectInfo = await this.nativeUIHandler.checkForSelect(tabId, tab.page, x, y);
512
+ if (selectInfo) {
513
+ this.emit('preview:browser-select', selectInfo);
514
+ }
515
+ return selectInfo;
516
+ }
517
+
518
+ async handleSelectResponse(tabId: string, response: BrowserSelectResponse): Promise<boolean> {
519
+ const tab = this.getTab(tabId);
520
+ if (!tab) return false;
521
+
522
+ return await this.nativeUIHandler.handleSelectResponse(tab.page, response);
523
+ }
524
+
525
+ async checkForContextMenu(tabId: string, x: number, y: number) {
526
+ const tab = this.getTab(tabId);
527
+ if (!tab) return null;
528
+
529
+ const menuInfo = await this.nativeUIHandler.checkForContextMenu(tabId, tab.page, x, y);
530
+ if (menuInfo) {
531
+ // Store menu info for later action execution
532
+ this.contextMenus.set(menuInfo.menuId, menuInfo);
533
+ this.emit('preview:browser-context-menu', menuInfo);
534
+ }
535
+ return menuInfo;
536
+ }
537
+
538
+ async handleContextMenuResponse(tabId: string, response: BrowserContextMenuResponse, clipboardText?: string): Promise<boolean> {
539
+ const tab = this.getTab(tabId);
540
+ if (!tab) return false;
541
+
542
+ // Get stored menu info
543
+ const menuInfo = this.contextMenus.get(response.menuId);
544
+ if (!menuInfo) return false;
545
+
546
+ const result = await this.nativeUIHandler.handleContextMenuResponse(tab.page, response, menuInfo, clipboardText);
547
+
548
+ // Clean up stored menu info
549
+ this.contextMenus.delete(response.menuId);
550
+
551
+ return result;
552
+ }
553
+
554
+ // ============================================================================
555
+ // Cleanup Methods
556
+ // ============================================================================
557
+ async cleanup() {
558
+ // Clear all cursor tracking
559
+ this.interactionHandler.clearAllSessionCursors();
560
+ // Cleanup tabs (this will also cleanup all contexts/pages/browser pool)
561
+ await this.tabManager.cleanup();
562
+ }
563
+
564
+ async cleanupInactiveTabs() {
565
+ return this.tabManager.cleanupInactiveTabs();
566
+ }
567
+
568
+ async forceCleanupAll() {
569
+ // First try normal cleanup
570
+ await this.cleanup();
571
+
572
+ // Cleanup video capture sessions
573
+ await this.videoCapture.cleanup();
574
+
575
+ // Clear all dialogs and context menus
576
+ this.dialogHandler.clearAllDialogs();
577
+ this.contextMenus.clear();
578
+
579
+ // Remove all listeners to prevent memory leaks
580
+ this.removeAllListeners();
581
+ this.consoleManager.removeAllListeners();
582
+ this.interactionHandler.removeAllListeners();
583
+ this.navigationTracker.removeAllListeners();
584
+ this.videoCapture.removeAllListeners();
585
+ this.dialogHandler.removeAllListeners();
586
+ this.nativeUIHandler.removeAllListeners();
587
+ }
588
+ }
589
+
590
+ /**
591
+ * Browser Preview Service Manager
592
+ *
593
+ * Manages BrowserPreviewService instances per project.
594
+ * Provides project isolation - each project has its own browser tabs and state.
595
+ */
596
+ class BrowserPreviewServiceManager {
597
+ private services = new Map<string, BrowserPreviewService>();
598
+
599
+ /**
600
+ * Get or create a BrowserPreviewService for a project
601
+ */
602
+ getService(projectId: string): BrowserPreviewService {
603
+ if (!projectId) {
604
+ throw new Error('projectId is required and cannot be empty');
605
+ }
606
+
607
+ if (!this.services.has(projectId)) {
608
+ debug.log('preview', `🆕 Creating new BrowserPreviewService for project: ${projectId}`);
609
+ const service = new BrowserPreviewService(projectId);
610
+ this.services.set(projectId, service);
611
+
612
+ // Setup WebSocket event forwarding for this service
613
+ this.setupWebSocketForwarding(service, projectId);
614
+ debug.log('preview', `✅ BrowserPreviewService fully initialized for project: ${projectId}`);
615
+ }
616
+
617
+ return this.services.get(projectId)!;
618
+ }
619
+
620
+ /**
621
+ * Setup WebSocket event forwarding for a service instance
622
+ * Events are emitted to the specific project only
623
+ */
624
+ private setupWebSocketForwarding(service: BrowserPreviewService, projectId: string): void {
625
+ debug.log('preview', `🔌 Setting up WebSocket forwarding for project: ${projectId}...`);
626
+
627
+ // Forward WebCodecs events
628
+ service.on('preview:browser-webcodecs-ice-candidate', (data) => {
629
+ ws.emit.project(projectId, 'preview:browser-stream-ice', {
630
+ sessionId: data.sessionId,
631
+ candidate: data.candidate,
632
+ from: data.from
633
+ });
634
+ });
635
+
636
+ service.on('preview:browser-webcodecs-connection-state', (data) => {
637
+ ws.emit.project(projectId, 'preview:browser-stream-state', data);
638
+ });
639
+
640
+ service.on('preview:browser-cursor-change', (data) => {
641
+ ws.emit.project(projectId, 'preview:browser-cursor-change', data);
642
+ });
643
+
644
+ // Forward navigation events
645
+ service.on('preview:browser-navigation-loading', (data) => {
646
+ ws.emit.project(projectId, 'preview:browser-navigation-loading', data);
647
+ });
648
+
649
+ service.on('preview:browser-navigation', (data) => {
650
+ ws.emit.project(projectId, 'preview:browser-navigation', data);
651
+ });
652
+
653
+ // Forward tab events
654
+ service.on('preview:browser-tab-opened', (data) => {
655
+ debug.log('preview', `🚀 Forwarding preview:browser-tab-opened to project ${projectId}:`, data);
656
+ ws.emit.project(projectId, 'preview:browser-tab-opened', data);
657
+ });
658
+
659
+ service.on('preview:browser-tab-closed', (data) => {
660
+ ws.emit.project(projectId, 'preview:browser-tab-closed', data);
661
+ });
662
+
663
+ service.on('preview:browser-tab-switched', (data) => {
664
+ ws.emit.project(projectId, 'preview:browser-tab-switched', data);
665
+ });
666
+
667
+ service.on('preview:browser-tab-navigated', (data) => {
668
+ ws.emit.project(projectId, 'preview:browser-tab-navigated', data);
669
+ });
670
+
671
+ // Forward console events
672
+ service.on('preview:browser-console-message', (data) => {
673
+ ws.emit.project(projectId, 'preview:browser-console-message', data);
674
+ });
675
+
676
+ service.on('preview:browser-console-clear', (data) => {
677
+ ws.emit.project(projectId, 'preview:browser-console-clear', data);
678
+ });
679
+
680
+ // Forward MCP events
681
+ service.on('preview:browser-mcp-cursor-position', (data) => {
682
+ ws.emit.project(projectId, 'preview:browser-mcp-cursor-position', data);
683
+ });
684
+
685
+ service.on('preview:browser-mcp-cursor-click', (data) => {
686
+ ws.emit.project(projectId, 'preview:browser-mcp-cursor-click', data);
687
+ });
688
+
689
+ service.on('preview:browser-mcp-test-completed', (data) => {
690
+ ws.emit.project(projectId, 'preview:browser-mcp-test-completed', data);
691
+ });
692
+
693
+ // Forward dialog events
694
+ service.on('preview:browser-dialog', (data) => {
695
+ ws.emit.project(projectId, 'preview:browser-dialog', data);
696
+ });
697
+
698
+ service.on('preview:browser-print', (data) => {
699
+ ws.emit.project(projectId, 'preview:browser-print', data);
700
+ });
701
+
702
+ // Forward native UI events
703
+ service.on('preview:browser-select', (data) => {
704
+ ws.emit.project(projectId, 'preview:browser-select', data);
705
+ });
706
+
707
+ service.on('preview:browser-context-menu', (data) => {
708
+ ws.emit.project(projectId, 'preview:browser-context-menu', data);
709
+ });
710
+
711
+ service.on('preview:browser-copy-to-clipboard', (data) => {
712
+ ws.emit.project(projectId, 'preview:browser-copy-to-clipboard', data);
713
+ });
714
+
715
+ service.on('preview:browser-open-url-new-tab', (data) => {
716
+ ws.emit.project(projectId, 'preview:browser-open-url-new-tab', data);
717
+ });
718
+
719
+ service.on('preview:browser-download-image', (data) => {
720
+ ws.emit.project(projectId, 'preview:browser-download-image', data);
721
+ });
722
+
723
+ service.on('preview:browser-copy-image-to-clipboard', (data) => {
724
+ ws.emit.project(projectId, 'preview:browser-copy-image-to-clipboard', data);
725
+ });
726
+
727
+ // Forward new window events
728
+ service.on('preview:browser-new-window', (data) => {
729
+ ws.emit.project(projectId, 'preview:browser-new-window', data);
730
+ });
731
+
732
+ // Forward MCP control events (from singleton browserMcpControl)
733
+ browserMcpControl.on('control-start', (data) => {
734
+ debug.log('preview', `🚀 Forwarding mcp-control-start to project ${projectId}:`, data);
735
+ ws.emit.project(projectId, 'preview:browser-mcp-control-start', {
736
+ browserSessionId: data.browserTabId,
737
+ mcpSessionId: data.mcpSessionId,
738
+ timestamp: data.timestamp
739
+ });
740
+ });
741
+
742
+ browserMcpControl.on('control-end', (data) => {
743
+ debug.log('preview', `🚀 Forwarding mcp-control-end to project ${projectId}:`, data);
744
+ ws.emit.project(projectId, 'preview:browser-mcp-control-end', {
745
+ browserSessionId: data.browserTabId,
746
+ timestamp: data.timestamp
747
+ });
748
+ });
749
+
750
+ // Forward MCP cursor events
751
+ browserMcpControl.on('cursor-position', (data) => {
752
+ ws.emit.project(projectId, 'preview:browser-mcp-cursor-position', {
753
+ sessionId: data.tabId,
754
+ x: data.x,
755
+ y: data.y,
756
+ timestamp: data.timestamp,
757
+ source: 'mcp'
758
+ });
759
+ });
760
+
761
+ browserMcpControl.on('cursor-click', (data) => {
762
+ ws.emit.project(projectId, 'preview:browser-mcp-cursor-click', {
763
+ sessionId: data.tabId,
764
+ x: data.x,
765
+ y: data.y,
766
+ timestamp: data.timestamp,
767
+ source: 'mcp'
768
+ });
769
+ });
770
+
771
+ browserMcpControl.on('test-completed', (data) => {
772
+ ws.emit.project(projectId, 'preview:browser-mcp-test-completed', {
773
+ sessionId: data.tabId,
774
+ timestamp: data.timestamp,
775
+ source: 'mcp'
776
+ });
777
+ });
778
+
779
+ debug.log('preview', `🎉 All WebSocket event listeners registered for project: ${projectId}`);
780
+ }
781
+
782
+ /**
783
+ * Check if a service exists for a project
784
+ */
785
+ hasService(projectId: string): boolean {
786
+ if (!projectId) {
787
+ throw new Error('projectId is required and cannot be empty');
788
+ }
789
+ return this.services.has(projectId);
790
+ }
791
+
792
+ /**
793
+ * Remove a service for a project (cleanup)
794
+ */
795
+ async removeService(projectId: string): Promise<void> {
796
+ if (!projectId) {
797
+ throw new Error('projectId is required and cannot be empty');
798
+ }
799
+
800
+ const service = this.services.get(projectId);
801
+
802
+ if (service) {
803
+ await service.forceCleanupAll();
804
+ this.services.delete(projectId);
805
+ }
806
+ }
807
+
808
+ /**
809
+ * Cleanup all services
810
+ */
811
+ async cleanup(): Promise<void> {
812
+ const cleanupPromises = Array.from(this.services.values()).map(service =>
813
+ service.forceCleanupAll().catch(error => {
814
+ console.error('Error cleaning up service:', error);
815
+ })
816
+ );
817
+
818
+ await Promise.all(cleanupPromises);
819
+ this.services.clear();
820
+ }
821
+
822
+ /**
823
+ * Get all active project IDs
824
+ */
825
+ getActiveProjects(): string[] {
826
+ return Array.from(this.services.keys());
827
+ }
828
+
829
+ /**
830
+ * Get stats for all services
831
+ */
832
+ getStats() {
833
+ const stats = new Map<string, any>();
834
+
835
+ for (const [projectId, service] of this.services.entries()) {
836
+ stats.set(projectId, {
837
+ projectId,
838
+ tabs: service.getTabsStatus()
839
+ });
840
+ }
841
+
842
+ return stats;
843
+ }
844
+ }
845
+
846
+ // Service manager instance (singleton)
847
+ export const browserPreviewServiceManager = new BrowserPreviewServiceManager();
848
+
849
+ // Graceful shutdown handlers
850
+ const gracefulShutdown = async (signal: string) => {
851
+ try {
852
+ await browserPreviewServiceManager.cleanup();
853
+ process.exit(0);
854
+ } catch (error) {
855
+ process.exit(1);
856
+ }
857
+ };
858
+
859
+ // Handle various termination signals
860
+ process.on('SIGINT', () => gracefulShutdown('SIGINT'));
861
+ process.on('SIGTERM', () => gracefulShutdown('SIGTERM'));
862
+ process.on('SIGHUP', () => gracefulShutdown('SIGHUP'));
863
+
864
+ // Handle Windows-specific signals
865
+ if (process.platform === 'win32') {
866
+ process.on('SIGBREAK', () => gracefulShutdown('SIGBREAK'));
867
+ }
868
+
869
+ // Handle uncaught exceptions and unhandled rejections
870
+ process.on('uncaughtException', async (error) => {
871
+ await browserPreviewServiceManager.cleanup();
872
+ process.exit(1);
873
+ });
874
+
875
+ process.on('unhandledRejection', async (reason, promise) => {
876
+ await browserPreviewServiceManager.cleanup();
877
+ process.exit(1);
878
+ });
879
+
880
+ // Handle process exit
881
+ process.on('exit', (code) => {
882
+ });