@myrialabs/clopen 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (456) hide show
  1. package/.env.example +6 -0
  2. package/.github/workflows/release.yml +60 -0
  3. package/.github/workflows/test.yml +40 -0
  4. package/CONTRIBUTING.md +499 -0
  5. package/LICENSE +21 -0
  6. package/README.md +209 -0
  7. package/backend/index.ts +156 -0
  8. package/backend/lib/chat/helpers.ts +42 -0
  9. package/backend/lib/chat/index.ts +2 -0
  10. package/backend/lib/chat/stream-manager.ts +1126 -0
  11. package/backend/lib/database/README.md +77 -0
  12. package/backend/lib/database/index.ts +119 -0
  13. package/backend/lib/database/migrations/001_create_projects_table.ts +31 -0
  14. package/backend/lib/database/migrations/002_create_chat_sessions_table.ts +33 -0
  15. package/backend/lib/database/migrations/003_create_messages_table.ts +32 -0
  16. package/backend/lib/database/migrations/004_create_prompt_templates_table.ts +34 -0
  17. package/backend/lib/database/migrations/005_create_settings_table.ts +24 -0
  18. package/backend/lib/database/migrations/006_add_user_to_messages.ts +58 -0
  19. package/backend/lib/database/migrations/007_create_stream_states_table.ts +41 -0
  20. package/backend/lib/database/migrations/008_create_message_snapshots_table.ts +62 -0
  21. package/backend/lib/database/migrations/009_add_delta_snapshot_fields.ts +41 -0
  22. package/backend/lib/database/migrations/010_add_soft_delete_and_branch_support.ts +70 -0
  23. package/backend/lib/database/migrations/011_git_like_commit_graph.ts +156 -0
  24. package/backend/lib/database/migrations/012_add_file_change_statistics.ts +41 -0
  25. package/backend/lib/database/migrations/013_checkpoint_tree_state.ts +118 -0
  26. package/backend/lib/database/migrations/014_add_engine_to_sessions.ts +18 -0
  27. package/backend/lib/database/migrations/015_add_model_to_sessions.ts +18 -0
  28. package/backend/lib/database/migrations/016_create_user_projects_table.ts +34 -0
  29. package/backend/lib/database/migrations/017_add_current_session_to_user_projects.ts +32 -0
  30. package/backend/lib/database/migrations/018_create_claude_accounts_table.ts +24 -0
  31. package/backend/lib/database/migrations/019_add_claude_account_to_sessions.ts +18 -0
  32. package/backend/lib/database/migrations/020_add_snapshot_tree_hash.ts +32 -0
  33. package/backend/lib/database/migrations/021_drop_prompt_templates_table.ts +33 -0
  34. package/backend/lib/database/migrations/index.ts +154 -0
  35. package/backend/lib/database/queries/checkpoint-queries.ts +87 -0
  36. package/backend/lib/database/queries/engine-queries.ts +75 -0
  37. package/backend/lib/database/queries/index.ts +9 -0
  38. package/backend/lib/database/queries/message-queries.ts +472 -0
  39. package/backend/lib/database/queries/project-queries.ts +117 -0
  40. package/backend/lib/database/queries/session-queries.ts +271 -0
  41. package/backend/lib/database/queries/settings-queries.ts +34 -0
  42. package/backend/lib/database/queries/snapshot-queries.ts +326 -0
  43. package/backend/lib/database/queries/utils-queries.ts +59 -0
  44. package/backend/lib/database/seeders/index.ts +13 -0
  45. package/backend/lib/database/seeders/settings_seeder.ts +84 -0
  46. package/backend/lib/database/utils/connection.ts +174 -0
  47. package/backend/lib/database/utils/index.ts +4 -0
  48. package/backend/lib/database/utils/migration-runner.ts +118 -0
  49. package/backend/lib/database/utils/seeder-runner.ts +121 -0
  50. package/backend/lib/engine/adapters/claude/environment.ts +164 -0
  51. package/backend/lib/engine/adapters/claude/error-handler.ts +60 -0
  52. package/backend/lib/engine/adapters/claude/index.ts +1 -0
  53. package/backend/lib/engine/adapters/claude/path-utils.ts +38 -0
  54. package/backend/lib/engine/adapters/claude/stream.ts +177 -0
  55. package/backend/lib/engine/adapters/opencode/index.ts +2 -0
  56. package/backend/lib/engine/adapters/opencode/message-converter.ts +862 -0
  57. package/backend/lib/engine/adapters/opencode/server.ts +104 -0
  58. package/backend/lib/engine/adapters/opencode/stream.ts +755 -0
  59. package/backend/lib/engine/index.ts +196 -0
  60. package/backend/lib/engine/types.ts +58 -0
  61. package/backend/lib/files/file-operations.ts +478 -0
  62. package/backend/lib/files/file-reading.ts +308 -0
  63. package/backend/lib/files/file-watcher.ts +383 -0
  64. package/backend/lib/files/path-browsing.ts +382 -0
  65. package/backend/lib/git/git-executor.ts +88 -0
  66. package/backend/lib/git/git-parser.ts +411 -0
  67. package/backend/lib/git/git-service.ts +505 -0
  68. package/backend/lib/mcp/README.md +1144 -0
  69. package/backend/lib/mcp/config.ts +316 -0
  70. package/backend/lib/mcp/index.ts +35 -0
  71. package/backend/lib/mcp/project-context.ts +236 -0
  72. package/backend/lib/mcp/servers/browser-automation/actions.ts +156 -0
  73. package/backend/lib/mcp/servers/browser-automation/browser.ts +419 -0
  74. package/backend/lib/mcp/servers/browser-automation/index.ts +791 -0
  75. package/backend/lib/mcp/servers/browser-automation/inspection.ts +501 -0
  76. package/backend/lib/mcp/servers/helper.ts +143 -0
  77. package/backend/lib/mcp/servers/index.ts +45 -0
  78. package/backend/lib/mcp/servers/weather/get-temperature.ts +56 -0
  79. package/backend/lib/mcp/servers/weather/index.ts +31 -0
  80. package/backend/lib/mcp/stdio-server.ts +103 -0
  81. package/backend/lib/mcp/types.ts +65 -0
  82. package/backend/lib/preview/browser/browser-audio-capture.ts +86 -0
  83. package/backend/lib/preview/browser/browser-console-manager.ts +263 -0
  84. package/backend/lib/preview/browser/browser-dialog-handler.ts +222 -0
  85. package/backend/lib/preview/browser/browser-interaction-handler.ts +421 -0
  86. package/backend/lib/preview/browser/browser-mcp-control.ts +415 -0
  87. package/backend/lib/preview/browser/browser-native-ui-handler.ts +512 -0
  88. package/backend/lib/preview/browser/browser-navigation-tracker.ts +104 -0
  89. package/backend/lib/preview/browser/browser-pool.ts +357 -0
  90. package/backend/lib/preview/browser/browser-preview-service.ts +882 -0
  91. package/backend/lib/preview/browser/browser-tab-manager.ts +935 -0
  92. package/backend/lib/preview/browser/browser-video-capture.ts +695 -0
  93. package/backend/lib/preview/browser/scripts/audio-stream.ts +292 -0
  94. package/backend/lib/preview/browser/scripts/cursor-tracking.ts +85 -0
  95. package/backend/lib/preview/browser/scripts/video-stream.ts +438 -0
  96. package/backend/lib/preview/browser/types.ts +359 -0
  97. package/backend/lib/preview/index.ts +24 -0
  98. package/backend/lib/project/index.ts +2 -0
  99. package/backend/lib/project/status-manager.ts +182 -0
  100. package/backend/lib/shared/index.ts +2 -0
  101. package/backend/lib/shared/port-utils.ts +25 -0
  102. package/backend/lib/shared/process-manager.ts +281 -0
  103. package/backend/lib/snapshot/blob-store.ts +227 -0
  104. package/backend/lib/snapshot/gitignore.ts +307 -0
  105. package/backend/lib/snapshot/helpers.ts +397 -0
  106. package/backend/lib/snapshot/snapshot-service.ts +483 -0
  107. package/backend/lib/terminal/helpers.ts +14 -0
  108. package/backend/lib/terminal/index.ts +8 -0
  109. package/backend/lib/terminal/pty-manager.ts +4 -0
  110. package/backend/lib/terminal/pty-session-manager.ts +387 -0
  111. package/backend/lib/terminal/shell-utils.ts +313 -0
  112. package/backend/lib/terminal/stream-manager.ts +293 -0
  113. package/backend/lib/tunnel/global-tunnel-manager.ts +243 -0
  114. package/backend/lib/tunnel/project-tunnel-manager.ts +311 -0
  115. package/backend/lib/user/helpers.ts +87 -0
  116. package/backend/lib/utils/ws.ts +944 -0
  117. package/backend/lib/vite-dev.ts +353 -0
  118. package/backend/middleware/cors.ts +15 -0
  119. package/backend/middleware/error-handler.ts +49 -0
  120. package/backend/middleware/logger.ts +9 -0
  121. package/backend/types/api.ts +24 -0
  122. package/backend/ws/README.md +1505 -0
  123. package/backend/ws/chat/background.ts +198 -0
  124. package/backend/ws/chat/index.ts +21 -0
  125. package/backend/ws/chat/stream.ts +707 -0
  126. package/backend/ws/engine/claude/accounts.ts +401 -0
  127. package/backend/ws/engine/claude/index.ts +13 -0
  128. package/backend/ws/engine/claude/status.ts +43 -0
  129. package/backend/ws/engine/index.ts +14 -0
  130. package/backend/ws/engine/opencode/index.ts +11 -0
  131. package/backend/ws/engine/opencode/status.ts +30 -0
  132. package/backend/ws/engine/utils.ts +36 -0
  133. package/backend/ws/files/index.ts +30 -0
  134. package/backend/ws/files/read.ts +189 -0
  135. package/backend/ws/files/search.ts +453 -0
  136. package/backend/ws/files/watch.ts +124 -0
  137. package/backend/ws/files/write.ts +143 -0
  138. package/backend/ws/git/branch.ts +106 -0
  139. package/backend/ws/git/commit.ts +39 -0
  140. package/backend/ws/git/conflict.ts +68 -0
  141. package/backend/ws/git/diff.ts +69 -0
  142. package/backend/ws/git/index.ts +24 -0
  143. package/backend/ws/git/log.ts +41 -0
  144. package/backend/ws/git/remote.ts +214 -0
  145. package/backend/ws/git/staging.ts +84 -0
  146. package/backend/ws/git/status.ts +90 -0
  147. package/backend/ws/index.ts +69 -0
  148. package/backend/ws/mcp/index.ts +61 -0
  149. package/backend/ws/messages/crud.ts +74 -0
  150. package/backend/ws/messages/index.ts +14 -0
  151. package/backend/ws/preview/browser/cleanup.ts +129 -0
  152. package/backend/ws/preview/browser/console.ts +114 -0
  153. package/backend/ws/preview/browser/interact.ts +513 -0
  154. package/backend/ws/preview/browser/mcp.ts +129 -0
  155. package/backend/ws/preview/browser/native-ui.ts +235 -0
  156. package/backend/ws/preview/browser/stats.ts +55 -0
  157. package/backend/ws/preview/browser/tab-info.ts +126 -0
  158. package/backend/ws/preview/browser/tab.ts +166 -0
  159. package/backend/ws/preview/browser/webcodecs.ts +293 -0
  160. package/backend/ws/preview/index.ts +146 -0
  161. package/backend/ws/projects/crud.ts +113 -0
  162. package/backend/ws/projects/index.ts +25 -0
  163. package/backend/ws/projects/presence.ts +46 -0
  164. package/backend/ws/projects/status.ts +116 -0
  165. package/backend/ws/sessions/crud.ts +327 -0
  166. package/backend/ws/sessions/index.ts +33 -0
  167. package/backend/ws/settings/crud.ts +112 -0
  168. package/backend/ws/settings/index.ts +14 -0
  169. package/backend/ws/snapshot/index.ts +17 -0
  170. package/backend/ws/snapshot/restore.ts +173 -0
  171. package/backend/ws/snapshot/timeline.ts +141 -0
  172. package/backend/ws/system/index.ts +14 -0
  173. package/backend/ws/system/operations.ts +49 -0
  174. package/backend/ws/terminal/index.ts +40 -0
  175. package/backend/ws/terminal/persistence.ts +153 -0
  176. package/backend/ws/terminal/session.ts +382 -0
  177. package/backend/ws/terminal/stream.ts +79 -0
  178. package/backend/ws/tunnel/index.ts +14 -0
  179. package/backend/ws/tunnel/operations.ts +91 -0
  180. package/backend/ws/types.ts +20 -0
  181. package/backend/ws/user/crud.ts +156 -0
  182. package/backend/ws/user/index.ts +14 -0
  183. package/bin/clopen.ts +307 -0
  184. package/bun.lock +1352 -0
  185. package/frontend/App.svelte +34 -0
  186. package/frontend/app.css +313 -0
  187. package/frontend/lib/app-environment.ts +10 -0
  188. package/frontend/lib/components/chat/ChatInterface.svelte +407 -0
  189. package/frontend/lib/components/chat/formatters/ErrorMessage.svelte +57 -0
  190. package/frontend/lib/components/chat/formatters/MessageFormatter.svelte +224 -0
  191. package/frontend/lib/components/chat/formatters/TextMessage.svelte +395 -0
  192. package/frontend/lib/components/chat/formatters/Tools.svelte +70 -0
  193. package/frontend/lib/components/chat/formatters/index.ts +3 -0
  194. package/frontend/lib/components/chat/input/ChatInput.svelte +421 -0
  195. package/frontend/lib/components/chat/input/components/ChatInputActions.svelte +78 -0
  196. package/frontend/lib/components/chat/input/components/DragDropOverlay.svelte +30 -0
  197. package/frontend/lib/components/chat/input/components/EditModeIndicator.svelte +33 -0
  198. package/frontend/lib/components/chat/input/components/EngineModelPicker.svelte +619 -0
  199. package/frontend/lib/components/chat/input/components/FileAttachmentPreview.svelte +48 -0
  200. package/frontend/lib/components/chat/input/components/LoadingIndicator.svelte +31 -0
  201. package/frontend/lib/components/chat/input/composables/use-animations.svelte.ts +201 -0
  202. package/frontend/lib/components/chat/input/composables/use-chat-actions.svelte.ts +148 -0
  203. package/frontend/lib/components/chat/input/composables/use-file-handling.svelte.ts +216 -0
  204. package/frontend/lib/components/chat/input/composables/use-input-state.svelte.ts +357 -0
  205. package/frontend/lib/components/chat/input/composables/use-textarea-resize.svelte.ts +57 -0
  206. package/frontend/lib/components/chat/message/ChatMessage.svelte +478 -0
  207. package/frontend/lib/components/chat/message/ChatMessages.svelte +541 -0
  208. package/frontend/lib/components/chat/message/DateSeparator.svelte +86 -0
  209. package/frontend/lib/components/chat/message/MessageBubble.svelte +86 -0
  210. package/frontend/lib/components/chat/message/MessageHeader.svelte +157 -0
  211. package/frontend/lib/components/chat/modal/DebugModal.svelte +59 -0
  212. package/frontend/lib/components/chat/modal/TokenUsageModal.svelte +124 -0
  213. package/frontend/lib/components/chat/shared/index.ts +2 -0
  214. package/frontend/lib/components/chat/shared/utils.ts +116 -0
  215. package/frontend/lib/components/chat/tools/BashOutputTool.svelte +35 -0
  216. package/frontend/lib/components/chat/tools/BashTool.svelte +46 -0
  217. package/frontend/lib/components/chat/tools/CustomMcpTool.svelte +139 -0
  218. package/frontend/lib/components/chat/tools/EditTool.svelte +48 -0
  219. package/frontend/lib/components/chat/tools/ExitPlanModeTool.svelte +32 -0
  220. package/frontend/lib/components/chat/tools/GlobTool.svelte +51 -0
  221. package/frontend/lib/components/chat/tools/GrepTool.svelte +90 -0
  222. package/frontend/lib/components/chat/tools/KillShellTool.svelte +26 -0
  223. package/frontend/lib/components/chat/tools/ListMcpResourcesTool.svelte +31 -0
  224. package/frontend/lib/components/chat/tools/NotebookEditTool.svelte +38 -0
  225. package/frontend/lib/components/chat/tools/ReadMcpResourceTool.svelte +34 -0
  226. package/frontend/lib/components/chat/tools/ReadTool.svelte +41 -0
  227. package/frontend/lib/components/chat/tools/TaskTool.svelte +64 -0
  228. package/frontend/lib/components/chat/tools/TodoWriteTool.svelte +75 -0
  229. package/frontend/lib/components/chat/tools/WebFetchTool.svelte +35 -0
  230. package/frontend/lib/components/chat/tools/WebSearchTool.svelte +84 -0
  231. package/frontend/lib/components/chat/tools/WriteTool.svelte +33 -0
  232. package/frontend/lib/components/chat/tools/components/CodeBlock.svelte +79 -0
  233. package/frontend/lib/components/chat/tools/components/DiffBlock.svelte +408 -0
  234. package/frontend/lib/components/chat/tools/components/FileHeader.svelte +45 -0
  235. package/frontend/lib/components/chat/tools/components/InfoLine.svelte +19 -0
  236. package/frontend/lib/components/chat/tools/components/StatsBadges.svelte +27 -0
  237. package/frontend/lib/components/chat/tools/components/TerminalCommand.svelte +54 -0
  238. package/frontend/lib/components/chat/tools/components/index.ts +7 -0
  239. package/frontend/lib/components/chat/tools/index.ts +26 -0
  240. package/frontend/lib/components/chat/widgets/FloatingTodoList.svelte +249 -0
  241. package/frontend/lib/components/chat/widgets/TokenUsage.svelte +78 -0
  242. package/frontend/lib/components/checkpoint/TimelineModal.svelte +391 -0
  243. package/frontend/lib/components/checkpoint/timeline/TimelineEdge.svelte +26 -0
  244. package/frontend/lib/components/checkpoint/timeline/TimelineGraph.svelte +87 -0
  245. package/frontend/lib/components/checkpoint/timeline/TimelineNode.svelte +108 -0
  246. package/frontend/lib/components/checkpoint/timeline/TimelineVersionGroup.svelte +59 -0
  247. package/frontend/lib/components/checkpoint/timeline/animation.ts +168 -0
  248. package/frontend/lib/components/checkpoint/timeline/config.ts +44 -0
  249. package/frontend/lib/components/checkpoint/timeline/graph-builder.ts +304 -0
  250. package/frontend/lib/components/checkpoint/timeline/types.ts +65 -0
  251. package/frontend/lib/components/checkpoint/timeline/utils.ts +53 -0
  252. package/frontend/lib/components/common/Alert.svelte +139 -0
  253. package/frontend/lib/components/common/AvatarBubble.svelte +56 -0
  254. package/frontend/lib/components/common/Button.svelte +71 -0
  255. package/frontend/lib/components/common/Card.svelte +102 -0
  256. package/frontend/lib/components/common/Checkbox.svelte +48 -0
  257. package/frontend/lib/components/common/Dialog.svelte +249 -0
  258. package/frontend/lib/components/common/FolderBrowser.svelte +843 -0
  259. package/frontend/lib/components/common/Icon.svelte +58 -0
  260. package/frontend/lib/components/common/Input.svelte +72 -0
  261. package/frontend/lib/components/common/Lightbox.svelte +233 -0
  262. package/frontend/lib/components/common/LoadingScreen.svelte +52 -0
  263. package/frontend/lib/components/common/LoadingSpinner.svelte +48 -0
  264. package/frontend/lib/components/common/Modal.svelte +177 -0
  265. package/frontend/lib/components/common/ModalProvider.svelte +28 -0
  266. package/frontend/lib/components/common/ModelSelector.svelte +110 -0
  267. package/frontend/lib/components/common/MonacoEditor.svelte +569 -0
  268. package/frontend/lib/components/common/NotificationToast.svelte +113 -0
  269. package/frontend/lib/components/common/PageTemplate.svelte +76 -0
  270. package/frontend/lib/components/common/ProjectUserAvatars.svelte +79 -0
  271. package/frontend/lib/components/common/Select.svelte +98 -0
  272. package/frontend/lib/components/common/Textarea.svelte +80 -0
  273. package/frontend/lib/components/common/ThemeToggle.svelte +44 -0
  274. package/frontend/lib/components/common/lucide-icons.ts +1642 -0
  275. package/frontend/lib/components/common/material-icons.ts +1082 -0
  276. package/frontend/lib/components/common/xterm/XTerm.svelte +796 -0
  277. package/frontend/lib/components/common/xterm/index.ts +16 -0
  278. package/frontend/lib/components/common/xterm/terminal-config.ts +68 -0
  279. package/frontend/lib/components/common/xterm/types.ts +31 -0
  280. package/frontend/lib/components/common/xterm/xterm-service.ts +353 -0
  281. package/frontend/lib/components/files/FileNode.svelte +384 -0
  282. package/frontend/lib/components/files/FileTree.svelte +681 -0
  283. package/frontend/lib/components/files/FileViewer.svelte +728 -0
  284. package/frontend/lib/components/files/SearchResults.svelte +303 -0
  285. package/frontend/lib/components/git/BranchManager.svelte +458 -0
  286. package/frontend/lib/components/git/ChangesSection.svelte +107 -0
  287. package/frontend/lib/components/git/CommitForm.svelte +76 -0
  288. package/frontend/lib/components/git/ConflictResolver.svelte +158 -0
  289. package/frontend/lib/components/git/DiffViewer.svelte +364 -0
  290. package/frontend/lib/components/git/FileChangeItem.svelte +97 -0
  291. package/frontend/lib/components/git/GitButton.svelte +33 -0
  292. package/frontend/lib/components/git/GitLog.svelte +361 -0
  293. package/frontend/lib/components/git/GitModal.svelte +80 -0
  294. package/frontend/lib/components/history/HistoryModal.svelte +563 -0
  295. package/frontend/lib/components/history/HistoryView.svelte +615 -0
  296. package/frontend/lib/components/index.ts +34 -0
  297. package/frontend/lib/components/preview/browser/BrowserPreview.svelte +549 -0
  298. package/frontend/lib/components/preview/browser/components/Canvas.svelte +1058 -0
  299. package/frontend/lib/components/preview/browser/components/ConsolePanel.svelte +757 -0
  300. package/frontend/lib/components/preview/browser/components/Container.svelte +450 -0
  301. package/frontend/lib/components/preview/browser/components/ContextMenu.svelte +236 -0
  302. package/frontend/lib/components/preview/browser/components/SelectDropdown.svelte +224 -0
  303. package/frontend/lib/components/preview/browser/components/Toolbar.svelte +339 -0
  304. package/frontend/lib/components/preview/browser/components/VirtualCursor.svelte +36 -0
  305. package/frontend/lib/components/preview/browser/core/cleanup.svelte.ts +155 -0
  306. package/frontend/lib/components/preview/browser/core/coordinator.svelte.ts +837 -0
  307. package/frontend/lib/components/preview/browser/core/interactions.svelte.ts +113 -0
  308. package/frontend/lib/components/preview/browser/core/mcp-handlers.svelte.ts +296 -0
  309. package/frontend/lib/components/preview/browser/core/native-ui-handlers.svelte.ts +391 -0
  310. package/frontend/lib/components/preview/browser/core/stream-handler.svelte.ts +231 -0
  311. package/frontend/lib/components/preview/browser/core/tab-manager.svelte.ts +210 -0
  312. package/frontend/lib/components/preview/browser/core/tab-operations.svelte.ts +239 -0
  313. package/frontend/lib/components/preview/index.ts +2 -0
  314. package/frontend/lib/components/settings/SettingsModal.svelte +235 -0
  315. package/frontend/lib/components/settings/SettingsView.svelte +36 -0
  316. package/frontend/lib/components/settings/appearance/AppearanceSettings.svelte +51 -0
  317. package/frontend/lib/components/settings/appearance/LayoutPresetSettings.svelte +160 -0
  318. package/frontend/lib/components/settings/appearance/LayoutPreview.svelte +76 -0
  319. package/frontend/lib/components/settings/engines/AIEnginesSettings.svelte +917 -0
  320. package/frontend/lib/components/settings/general/AdvancedSettings.svelte +187 -0
  321. package/frontend/lib/components/settings/general/DataManagementSettings.svelte +203 -0
  322. package/frontend/lib/components/settings/general/GeneralSettings.svelte +10 -0
  323. package/frontend/lib/components/settings/model/ModelSettings.svelte +357 -0
  324. package/frontend/lib/components/settings/notifications/NotificationSettings.svelte +205 -0
  325. package/frontend/lib/components/settings/user/UserSettings.svelte +197 -0
  326. package/frontend/lib/components/terminal/Terminal.svelte +368 -0
  327. package/frontend/lib/components/terminal/TerminalTabs.svelte +87 -0
  328. package/frontend/lib/components/terminal/TerminalView.svelte +55 -0
  329. package/frontend/lib/components/tunnel/TunnelActive.svelte +142 -0
  330. package/frontend/lib/components/tunnel/TunnelButton.svelte +54 -0
  331. package/frontend/lib/components/tunnel/TunnelInactive.svelte +284 -0
  332. package/frontend/lib/components/tunnel/TunnelModal.svelte +47 -0
  333. package/frontend/lib/components/tunnel/TunnelQRCode.svelte +49 -0
  334. package/frontend/lib/components/workspace/DesktopNavigator.svelte +382 -0
  335. package/frontend/lib/components/workspace/MobileNavigator.svelte +403 -0
  336. package/frontend/lib/components/workspace/PanelContainer.svelte +100 -0
  337. package/frontend/lib/components/workspace/PanelHeader.svelte +505 -0
  338. package/frontend/lib/components/workspace/ViewMenu.svelte +162 -0
  339. package/frontend/lib/components/workspace/WorkspaceLayout.svelte +169 -0
  340. package/frontend/lib/components/workspace/layout/DesktopLayout.svelte +15 -0
  341. package/frontend/lib/components/workspace/layout/MobileLayout.svelte +17 -0
  342. package/frontend/lib/components/workspace/layout/split-pane/Container.svelte +42 -0
  343. package/frontend/lib/components/workspace/layout/split-pane/Handle.svelte +85 -0
  344. package/frontend/lib/components/workspace/layout/split-pane/Layout.svelte +37 -0
  345. package/frontend/lib/components/workspace/panels/ChatPanel.svelte +274 -0
  346. package/frontend/lib/components/workspace/panels/FilesPanel.svelte +1261 -0
  347. package/frontend/lib/components/workspace/panels/GitPanel.svelte +1560 -0
  348. package/frontend/lib/components/workspace/panels/PreviewPanel.svelte +150 -0
  349. package/frontend/lib/components/workspace/panels/TerminalPanel.svelte +73 -0
  350. package/frontend/lib/constants/preview.ts +45 -0
  351. package/frontend/lib/services/chat/chat.service.ts +704 -0
  352. package/frontend/lib/services/chat/index.ts +7 -0
  353. package/frontend/lib/services/notification/global-stream-monitor.ts +86 -0
  354. package/frontend/lib/services/notification/index.ts +8 -0
  355. package/frontend/lib/services/notification/push.service.ts +144 -0
  356. package/frontend/lib/services/notification/sound.service.ts +127 -0
  357. package/frontend/lib/services/preview/browser/browser-console.service.ts +61 -0
  358. package/frontend/lib/services/preview/browser/browser-webcodecs.service.ts +1499 -0
  359. package/frontend/lib/services/preview/browser/mcp-integration.svelte.ts +67 -0
  360. package/frontend/lib/services/preview/index.ts +23 -0
  361. package/frontend/lib/services/project/index.ts +8 -0
  362. package/frontend/lib/services/project/status.service.ts +159 -0
  363. package/frontend/lib/services/snapshot/snapshot.service.ts +47 -0
  364. package/frontend/lib/services/terminal/background/index.ts +130 -0
  365. package/frontend/lib/services/terminal/background/session-restore.ts +274 -0
  366. package/frontend/lib/services/terminal/background/stream-manager.ts +286 -0
  367. package/frontend/lib/services/terminal/index.ts +14 -0
  368. package/frontend/lib/services/terminal/persistence.service.ts +260 -0
  369. package/frontend/lib/services/terminal/project.service.ts +953 -0
  370. package/frontend/lib/services/terminal/session.service.ts +364 -0
  371. package/frontend/lib/services/terminal/terminal.service.ts +369 -0
  372. package/frontend/lib/stores/core/app.svelte.ts +117 -0
  373. package/frontend/lib/stores/core/files.svelte.ts +73 -0
  374. package/frontend/lib/stores/core/presence.svelte.ts +48 -0
  375. package/frontend/lib/stores/core/projects.svelte.ts +317 -0
  376. package/frontend/lib/stores/core/sessions.svelte.ts +383 -0
  377. package/frontend/lib/stores/features/claude-accounts.svelte.ts +58 -0
  378. package/frontend/lib/stores/features/models.svelte.ts +89 -0
  379. package/frontend/lib/stores/features/settings.svelte.ts +87 -0
  380. package/frontend/lib/stores/features/terminal.svelte.ts +701 -0
  381. package/frontend/lib/stores/features/tunnel.svelte.ts +161 -0
  382. package/frontend/lib/stores/features/user.svelte.ts +96 -0
  383. package/frontend/lib/stores/ui/chat-input.svelte.ts +57 -0
  384. package/frontend/lib/stores/ui/chat-model.svelte.ts +61 -0
  385. package/frontend/lib/stores/ui/dialog.svelte.ts +59 -0
  386. package/frontend/lib/stores/ui/edit-mode.svelte.ts +214 -0
  387. package/frontend/lib/stores/ui/notification.svelte.ts +166 -0
  388. package/frontend/lib/stores/ui/settings-modal.svelte.ts +88 -0
  389. package/frontend/lib/stores/ui/theme.svelte.ts +179 -0
  390. package/frontend/lib/stores/ui/workspace.svelte.ts +754 -0
  391. package/frontend/lib/types/native-ui.ts +73 -0
  392. package/frontend/lib/utils/chat/date-separator.ts +39 -0
  393. package/frontend/lib/utils/chat/message-grouper.ts +219 -0
  394. package/frontend/lib/utils/chat/message-processor.ts +135 -0
  395. package/frontend/lib/utils/chat/tool-handler.ts +161 -0
  396. package/frontend/lib/utils/chat/virtual-scroll.svelte.ts +142 -0
  397. package/frontend/lib/utils/click-outside.ts +20 -0
  398. package/frontend/lib/utils/context-manager.ts +257 -0
  399. package/frontend/lib/utils/file-icon-mappings.ts +769 -0
  400. package/frontend/lib/utils/folder-icon-mappings.ts +1030 -0
  401. package/frontend/lib/utils/git-status.ts +68 -0
  402. package/frontend/lib/utils/platform.ts +113 -0
  403. package/frontend/lib/utils/port-check.ts +65 -0
  404. package/frontend/lib/utils/terminalFormatter.ts +207 -0
  405. package/frontend/lib/utils/theme.ts +6 -0
  406. package/frontend/lib/utils/tree-visualizer.ts +320 -0
  407. package/frontend/lib/utils/ws.ts +44 -0
  408. package/frontend/main.ts +13 -0
  409. package/index.html +70 -0
  410. package/package.json +111 -0
  411. package/scripts/generate-icons.ts +87 -0
  412. package/scripts/pre-publish-check.sh +142 -0
  413. package/scripts/setup-hooks.sh +134 -0
  414. package/scripts/validate-branch-name.sh +47 -0
  415. package/scripts/validate-commit-msg.sh +42 -0
  416. package/shared/constants/engines.ts +134 -0
  417. package/shared/types/database/connection.ts +16 -0
  418. package/shared/types/database/index.ts +6 -0
  419. package/shared/types/database/schema.ts +141 -0
  420. package/shared/types/engine/index.ts +45 -0
  421. package/shared/types/filesystem/index.ts +22 -0
  422. package/shared/types/git.ts +171 -0
  423. package/shared/types/messaging/index.ts +239 -0
  424. package/shared/types/messaging/tool.ts +526 -0
  425. package/shared/types/network/api.ts +18 -0
  426. package/shared/types/network/index.ts +5 -0
  427. package/shared/types/stores/app.ts +23 -0
  428. package/shared/types/stores/dialog.ts +21 -0
  429. package/shared/types/stores/index.ts +3 -0
  430. package/shared/types/stores/settings.ts +15 -0
  431. package/shared/types/terminal/index.ts +44 -0
  432. package/shared/types/ui/components.ts +61 -0
  433. package/shared/types/ui/icons.ts +23 -0
  434. package/shared/types/ui/index.ts +22 -0
  435. package/shared/types/ui/notifications.ts +14 -0
  436. package/shared/types/ui/theme.ts +12 -0
  437. package/shared/types/websocket/index.ts +43 -0
  438. package/shared/types/window.d.ts +13 -0
  439. package/shared/utils/anonymous-user.ts +168 -0
  440. package/shared/utils/async.ts +10 -0
  441. package/shared/utils/diff-calculator.ts +184 -0
  442. package/shared/utils/file-type-detection.ts +166 -0
  443. package/shared/utils/logger.ts +158 -0
  444. package/shared/utils/message-formatter.ts +79 -0
  445. package/shared/utils/path.ts +47 -0
  446. package/shared/utils/ws-client.ts +768 -0
  447. package/shared/utils/ws-server.ts +660 -0
  448. package/static/audio/notification.ogg +0 -0
  449. package/static/favicon.svg +8 -0
  450. package/static/fonts/dm-sans/dm-sans-italic-latin-ext.woff2 +0 -0
  451. package/static/fonts/dm-sans/dm-sans-italic-latin.woff2 +0 -0
  452. package/static/fonts/dm-sans/dm-sans-normal-latin-ext.woff2 +0 -0
  453. package/static/fonts/dm-sans/dm-sans-normal-latin.woff2 +0 -0
  454. package/static/fonts/dm-sans.css +96 -0
  455. package/svelte.config.js +20 -0
  456. package/vite.config.ts +33 -0
@@ -0,0 +1,935 @@
1
+ import type { Browser, BrowserContext, Page } from 'puppeteer';
2
+ import { EventEmitter } from 'events';
3
+ import { getViewportDimensions } from '$frontend/lib/constants/preview.js';
4
+ import type { BrowserTab, BrowserTabInfo, DeviceSize, Rotation } from './types';
5
+ import { DEFAULT_STREAMING_CONFIG } from './types';
6
+ import { browserPool } from './browser-pool';
7
+ import { BrowserAudioCapture } from './browser-audio-capture';
8
+ import { cursorTrackingScript } from './scripts/cursor-tracking';
9
+ import { browserMcpControl } from './browser-mcp-control';
10
+ import { debug } from '$shared/utils/logger';
11
+
12
+ // Tab cleanup configuration
13
+ const INACTIVE_TIMEOUT = 5 * 60 * 1000; // 5 minutes
14
+ const CLEANUP_INTERVAL = 60 * 1000; // Check every minute
15
+
16
+ /**
17
+ * Browser Tab Manager
18
+ *
19
+ * Tab-centric architecture where each tab represents a complete browser instance.
20
+ * Manages tab lifecycle, creation, navigation, and cleanup.
21
+ *
22
+ * ARCHITECTURE:
23
+ * - Tabs are the primary unit (no separate "session" concept)
24
+ * - Each tab has its own isolated browser context + page from the pool
25
+ * - 1 shared browser + isolated contexts = ~20 MB per tab
26
+ * - Active tab tracking for operations
27
+ * - Event-driven for frontend sync
28
+ * - **PROJECT ISOLATION**: Sessions are prefixed with projectId
29
+ *
30
+ * ISOLATION GUARANTEE:
31
+ * Each tab gets its own BrowserContext which provides:
32
+ * - Separate cookies
33
+ * - Separate localStorage/sessionStorage
34
+ * - Separate cache
35
+ * - Separate service workers
36
+ * - No data leakage between tabs
37
+ * - No data leakage between projects (via projectId-prefixed sessionIds)
38
+ */
39
+ export class BrowserTabManager extends EventEmitter {
40
+ private tabs = new Map<string, BrowserTab>();
41
+ private activeTabId: string | null = null;
42
+ private nextTabNumber = 1;
43
+
44
+ // Tab activity tracking for cleanup
45
+ private tabActivity = new Map<string, number>();
46
+ private cleanupInterval: NodeJS.Timeout | null = null;
47
+
48
+ // Audio capture manager
49
+ private audioCapture = new BrowserAudioCapture();
50
+
51
+ // Project ID for session 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 BrowserTabManager');
59
+ }
60
+
61
+ this.projectId = projectId;
62
+ // Initialize periodic cleanup
63
+ this.initializeCleanup();
64
+ }
65
+
66
+ /**
67
+ * Create a new tab with optional URL
68
+ *
69
+ * If URL is provided, navigate to it immediately.
70
+ * If URL is not provided, create blank tab (about:blank).
71
+ *
72
+ * Default rotation depends on device size:
73
+ * - Desktop/laptop: landscape
74
+ * - Tablet/mobile: portrait
75
+ */
76
+ async createTab(
77
+ url?: string,
78
+ deviceSize: DeviceSize = 'laptop',
79
+ rotation: Rotation = 'landscape',
80
+ options?: {
81
+ setActive?: boolean;
82
+ preNavigationSetup?: (page: Page) => Promise<void>;
83
+ }
84
+ ): Promise<BrowserTab> {
85
+ const tabId = `tab-${this.nextTabNumber++}`;
86
+ const finalUrl = url || 'about:blank';
87
+
88
+ debug.log('preview', `🟡🟡🟡 Creating new tab: ${tabId} for project: ${this.projectId} 🟡🟡🟡`);
89
+ debug.log('preview', `📁 Tab URL: ${finalUrl}, deviceSize: ${deviceSize}, rotation: ${rotation}`);
90
+
91
+ let browser: Browser;
92
+ let context: BrowserContext;
93
+ let page: Page;
94
+
95
+ try {
96
+ // Create project-scoped sessionId for isolation
97
+ // Format: "projectId:tabId" ensures complete isolation between projects
98
+ const sessionId = `${this.projectId}:${tabId}`;
99
+
100
+ // Create isolated context via puppeteer-cluster
101
+ // This provides full isolation: cookies, localStorage, sessionStorage, cache
102
+ const pooledSession = await browserPool.createSession(sessionId);
103
+ browser = await browserPool.getBrowser();
104
+ context = pooledSession.context;
105
+ page = pooledSession.page;
106
+
107
+ debug.log('preview', `🔐 Session ID: ${sessionId} (project-scoped)`);
108
+ } catch (poolError) {
109
+ debug.error('preview', `❌ Browser pool error:`, poolError);
110
+ throw poolError;
111
+ }
112
+
113
+ debug.log('preview', `✅ Isolated context created for tab: ${tabId}`);
114
+
115
+ // Setup page (viewport, headers, etc.)
116
+ debug.log('preview', `⚙️ Setting up page...`);
117
+ await this.setupPage(page, deviceSize, rotation);
118
+ debug.log('preview', `✅ Page setup complete`);
119
+
120
+ // Run pre-navigation setup if provided (e.g., dialog handling)
121
+ if (options?.preNavigationSetup) {
122
+ debug.log('preview', `🔧 Running pre-navigation setup...`);
123
+ await options.preNavigationSetup(page);
124
+ debug.log('preview', `✅ Pre-navigation setup complete`);
125
+ }
126
+
127
+ // Navigate to URL (or about:blank)
128
+ debug.log('preview', `🌐 Navigating to: ${finalUrl}`);
129
+ const actualUrl = await this.navigateWithRetry(page, finalUrl);
130
+ debug.log('preview', `✅ Navigation complete - final URL: ${actualUrl}`);
131
+
132
+ // Get title from URL
133
+ const title = this.getTitleFromUrl(actualUrl);
134
+
135
+ // Create tab object
136
+ const tab: BrowserTab = {
137
+ // Identity
138
+ id: tabId,
139
+ url: actualUrl,
140
+ title,
141
+ isActive: false,
142
+
143
+ // Browser instances
144
+ browser,
145
+ context,
146
+ page,
147
+
148
+ // Streaming
149
+ isStreaming: false,
150
+ quality: 'good',
151
+
152
+ // Device
153
+ deviceSize,
154
+ rotation,
155
+
156
+ // Console
157
+ consoleLogs: [],
158
+ consoleEnabled: true,
159
+
160
+ // Navigation
161
+ isLoading: false,
162
+ canGoBack: false,
163
+ canGoForward: false,
164
+ currentUrl: actualUrl,
165
+
166
+ // Timestamps
167
+ createdAt: Date.now(),
168
+ lastAccessedAt: Date.now(),
169
+
170
+ // Internal
171
+ isDestroyed: false,
172
+ lastFrameHash: undefined,
173
+ duplicateFrameCount: 0,
174
+ lastInteractionTime: undefined,
175
+ lastNavigationTime: undefined
176
+ };
177
+
178
+ this.tabs.set(tabId, tab);
179
+ this.setupBrowserHandlers(tabId, browser, context, page);
180
+
181
+ // Mark tab as active immediately
182
+ this.markTabActivity(tabId);
183
+
184
+ // Set as active if requested or if it's the first tab
185
+ if (options?.setActive !== false) {
186
+ this.setActiveTab(tabId);
187
+ }
188
+
189
+ // Emit tab created event with device info
190
+ const tabOpenedEvent = {
191
+ tabId,
192
+ url: actualUrl,
193
+ title,
194
+ isActive: tab.isActive,
195
+ deviceSize: tab.deviceSize,
196
+ rotation: tab.rotation,
197
+ timestamp: Date.now()
198
+ };
199
+
200
+ debug.log('preview', `📤 Emitting preview:browser-tab-opened event:`, tabOpenedEvent);
201
+ this.emit('preview:browser-tab-opened', tabOpenedEvent);
202
+
203
+ debug.log('preview', `✅ Tab created: ${tabId} (active: ${tab.isActive})`);
204
+
205
+ // Log pool stats
206
+ const stats = browserPool.getStats();
207
+ debug.log('preview', `📊 Pool stats: ${stats.activeSessions}/${stats.maxConcurrency} tabs active`);
208
+
209
+ return tab;
210
+ }
211
+
212
+ /**
213
+ * Navigate tab to a new URL
214
+ */
215
+ async navigateTab(tabId: string, url: string): Promise<string> {
216
+ const tab = this.tabs.get(tabId);
217
+ if (!tab) {
218
+ throw new Error(`Tab not found: ${tabId}`);
219
+ }
220
+
221
+ debug.log('preview', `🌐 Navigating tab ${tabId} to: ${url}`);
222
+
223
+ // Mark as loading
224
+ tab.isLoading = true;
225
+
226
+ try {
227
+ // Navigate (streaming continues, handlers reused)
228
+ const actualUrl = await this.navigateWithRetry(tab.page, url);
229
+
230
+ // Update tab properties
231
+ tab.url = actualUrl;
232
+ tab.currentUrl = actualUrl;
233
+ tab.title = this.getTitleFromUrl(actualUrl);
234
+ tab.lastNavigationTime = Date.now();
235
+ tab.isLoading = false;
236
+
237
+ // Update navigation state
238
+ tab.canGoBack = (await tab.page.evaluate(() => window.history.length)) > 1;
239
+ tab.canGoForward = false;
240
+
241
+ // Mark activity
242
+ this.markTabActivity(tabId);
243
+
244
+ // Emit navigation event
245
+ this.emit('preview:browser-tab-navigated', {
246
+ tabId,
247
+ url: actualUrl,
248
+ title: tab.title,
249
+ timestamp: Date.now()
250
+ });
251
+
252
+ debug.log('preview', `✅ Tab ${tabId} navigated to: ${actualUrl}`);
253
+
254
+ return actualUrl;
255
+ } catch (error) {
256
+ tab.isLoading = false;
257
+ throw error;
258
+ }
259
+ }
260
+
261
+ /**
262
+ * Close a tab and cleanup its resources
263
+ */
264
+ async closeTab(tabId: string): Promise<{ success: boolean; newActiveTabId: string | null }> {
265
+ const tab = this.tabs.get(tabId);
266
+ if (!tab) {
267
+ debug.warn('preview', `❌ Tab not found: ${tabId}`);
268
+ return { success: false, newActiveTabId: null };
269
+ }
270
+
271
+ debug.log('preview', `🗑️ Closing tab: ${tabId}`);
272
+
273
+ const wasActive = tab.isActive;
274
+
275
+ // Auto-release MCP control if this tab is being controlled
276
+ browserMcpControl.autoReleaseForTab(tabId);
277
+
278
+ // IMMEDIATELY set destroyed flag and stop streaming
279
+ tab.isDestroyed = true;
280
+ tab.isStreaming = false;
281
+
282
+ // Clear all intervals immediately
283
+ if (tab.screenshotInterval) {
284
+ clearInterval(tab.screenshotInterval);
285
+ tab.screenshotInterval = undefined;
286
+ }
287
+ if (tab.streamingInterval) {
288
+ clearInterval(tab.streamingInterval);
289
+ tab.streamingInterval = undefined;
290
+ }
291
+
292
+ // Wait a moment for streaming loop to detect the flags and stop
293
+ await new Promise(resolve => setTimeout(resolve, 500));
294
+
295
+ // Clean up the isolated context
296
+ await this.cleanupContext(tab);
297
+
298
+ // Remove from map
299
+ this.tabs.delete(tabId);
300
+ this.tabActivity.delete(tabId);
301
+
302
+ // If closing active tab, switch to another tab
303
+ let newActiveTabId: string | null = null;
304
+ if (wasActive && this.tabs.size > 0) {
305
+ // Get the first available tab
306
+ const nextTab = Array.from(this.tabs.values())[0];
307
+ if (nextTab) {
308
+ this.setActiveTab(nextTab.id);
309
+ newActiveTabId = nextTab.id;
310
+ } else {
311
+ this.activeTabId = null;
312
+ }
313
+ } else if (this.tabs.size === 0) {
314
+ this.activeTabId = null;
315
+ }
316
+
317
+ // Emit tab closed event
318
+ this.emit('preview:browser-tab-closed', {
319
+ tabId,
320
+ newActiveTabId,
321
+ timestamp: Date.now()
322
+ });
323
+
324
+ debug.log('preview', `✅ Tab closed: ${tabId} (new active: ${newActiveTabId || 'none'})`);
325
+
326
+ // Log pool stats after cleanup
327
+ const stats = browserPool.getStats();
328
+ debug.log('preview', `📊 Pool stats after cleanup: ${stats.activeSessions}/${stats.maxConcurrency} tabs active`);
329
+
330
+ return { success: true, newActiveTabId };
331
+ }
332
+
333
+ /**
334
+ * Switch to a specific tab
335
+ */
336
+ setActiveTab(tabId: string): boolean {
337
+ const tab = this.tabs.get(tabId);
338
+ if (!tab) {
339
+ debug.warn('preview', `❌ Cannot switch to tab: ${tabId} (not found)`);
340
+ return false;
341
+ }
342
+
343
+ const previousTabId = this.activeTabId;
344
+
345
+ // Deactivate previous active tab
346
+ if (previousTabId && previousTabId !== tabId) {
347
+ const previousTab = this.tabs.get(previousTabId);
348
+ if (previousTab) {
349
+ previousTab.isActive = false;
350
+ }
351
+ }
352
+
353
+ // Activate new tab
354
+ tab.isActive = true;
355
+ tab.lastAccessedAt = Date.now();
356
+ this.activeTabId = tabId;
357
+
358
+ // Mark tab activity
359
+ this.markTabActivity(tabId);
360
+
361
+ // Emit tab switched event
362
+ if (previousTabId !== tabId) {
363
+ this.emit('preview:browser-tab-switched', {
364
+ previousTabId: previousTabId || '',
365
+ newTabId: tabId,
366
+ timestamp: Date.now()
367
+ });
368
+
369
+ debug.log('preview', `🔄 Switched tab: ${previousTabId || 'none'} → ${tabId}`);
370
+ }
371
+
372
+ return true;
373
+ }
374
+
375
+ /**
376
+ * Get a tab by ID
377
+ */
378
+ getTab(tabId: string): BrowserTab | null {
379
+ const tab = this.tabs.get(tabId);
380
+ if (!tab) {
381
+ return null;
382
+ }
383
+
384
+ // Validate tab before returning
385
+ if (!this.isValidTab(tabId)) {
386
+ return null;
387
+ }
388
+
389
+ return tab;
390
+ }
391
+
392
+ /**
393
+ * Get the active tab
394
+ */
395
+ getActiveTab(): BrowserTab | null {
396
+ if (!this.activeTabId) return null;
397
+ return this.getTab(this.activeTabId);
398
+ }
399
+
400
+ /**
401
+ * Change viewport settings (device size and rotation) for an existing tab
402
+ */
403
+ async setViewport(tabId: string, deviceSize: DeviceSize, rotation: Rotation): Promise<boolean> {
404
+ const tab = this.tabs.get(tabId);
405
+ if (!tab) {
406
+ debug.warn('preview', `❌ Cannot set viewport: Tab ${tabId} not found`);
407
+ return false;
408
+ }
409
+
410
+ // Get new viewport dimensions
411
+ const { width: viewportWidth, height: viewportHeight } = getViewportDimensions(deviceSize, rotation);
412
+
413
+ try {
414
+ // Update viewport on the page
415
+ await tab.page.setViewport({ width: viewportWidth, height: viewportHeight });
416
+
417
+ // Update tab metadata
418
+ tab.deviceSize = deviceSize;
419
+ tab.rotation = rotation;
420
+
421
+ // Mark tab activity
422
+ this.markTabActivity(tabId);
423
+
424
+ // Emit viewport changed event
425
+ this.emit('preview:browser-viewport-changed', {
426
+ tabId,
427
+ deviceSize,
428
+ rotation,
429
+ width: viewportWidth,
430
+ height: viewportHeight,
431
+ timestamp: Date.now()
432
+ });
433
+
434
+ debug.log('preview', `📱 Viewport changed for tab ${tabId}: ${deviceSize} (${rotation}) - ${viewportWidth}x${viewportHeight}`);
435
+
436
+ return true;
437
+ } catch (error) {
438
+ debug.error('preview', `❌ Failed to set viewport for tab ${tabId}:`, error);
439
+ return false;
440
+ }
441
+ }
442
+
443
+ /**
444
+ * Get all tabs
445
+ */
446
+ getAllTabs(): BrowserTab[] {
447
+ return Array.from(this.tabs.values());
448
+ }
449
+
450
+ /**
451
+ * Get tab count
452
+ */
453
+ getTabCount(): number {
454
+ return this.tabs.size;
455
+ }
456
+
457
+ /**
458
+ * Check if a tab exists
459
+ */
460
+ hasTab(tabId: string): boolean {
461
+ return this.tabs.has(tabId);
462
+ }
463
+
464
+ /**
465
+ * Get active tab ID
466
+ */
467
+ getActiveTabId(): string | null {
468
+ return this.activeTabId;
469
+ }
470
+
471
+ /**
472
+ * Get tab info
473
+ */
474
+ getTabInfo(tabId: string): BrowserTabInfo | null {
475
+ const tab = this.getTab(tabId);
476
+ if (!tab) return null;
477
+
478
+ return {
479
+ id: tab.id,
480
+ url: tab.url,
481
+ title: tab.title,
482
+ quality: tab.quality,
483
+ isStreaming: tab.isStreaming,
484
+ deviceSize: tab.deviceSize,
485
+ rotation: tab.rotation,
486
+ isActive: tab.isActive
487
+ };
488
+ }
489
+
490
+ /**
491
+ * Get all tabs info
492
+ */
493
+ getAllTabsInfo(): BrowserTabInfo[] {
494
+ return Array.from(this.tabs.values()).map(tab => ({
495
+ id: tab.id,
496
+ url: tab.url,
497
+ title: tab.title,
498
+ quality: tab.quality,
499
+ isStreaming: tab.isStreaming,
500
+ deviceSize: tab.deviceSize,
501
+ rotation: tab.rotation,
502
+ isActive: tab.isActive
503
+ }));
504
+ }
505
+
506
+ /**
507
+ * Get tabs status (for admin/debugging)
508
+ */
509
+ getTabsStatus() {
510
+ const tabs = Array.from(this.tabs.entries()).map(([id, tab]) => ({
511
+ id,
512
+ url: tab.url,
513
+ title: tab.title,
514
+ isStreaming: tab.isStreaming,
515
+ isDestroyed: tab.isDestroyed || false,
516
+ browserConnected: tab.browser?.connected || false,
517
+ pageClosed: tab.page?.isClosed() || true,
518
+ deviceSize: tab.deviceSize,
519
+ rotation: tab.rotation,
520
+ consoleLogs: tab.consoleLogs.length,
521
+ lastInteractionTime: tab.lastInteractionTime,
522
+ duplicateFrameCount: tab.duplicateFrameCount || 0,
523
+ isActive: tab.isActive,
524
+ createdAt: tab.createdAt,
525
+ lastAccessedAt: tab.lastAccessedAt
526
+ }));
527
+
528
+ return {
529
+ totalTabs: tabs.length,
530
+ activeTabs: tabs.filter(t => t.isStreaming && t.browserConnected && !t.pageClosed && !t.isDestroyed).length,
531
+ inactiveTabs: tabs.filter(t => t.isDestroyed || !t.browserConnected || t.pageClosed || !t.isStreaming).length,
532
+ tabs
533
+ };
534
+ }
535
+
536
+ /**
537
+ * Update tab title
538
+ */
539
+ updateTabTitle(tabId: string, title: string): void {
540
+ const tab = this.tabs.get(tabId);
541
+ if (tab) {
542
+ tab.title = title;
543
+ }
544
+ }
545
+
546
+ /**
547
+ * Update tab title from URL
548
+ */
549
+ updateTabTitleFromUrl(tabId: string, url: string): void {
550
+ const tab = this.tabs.get(tabId);
551
+ if (tab) {
552
+ tab.title = this.getTitleFromUrl(url);
553
+ }
554
+ }
555
+
556
+ /**
557
+ * Get project-scoped session ID for a tab
558
+ */
559
+ private getSessionId(tabId: string): string {
560
+ return this.projectId ? `${this.projectId}:${tabId}` : tabId;
561
+ }
562
+
563
+ /**
564
+ * Validate tab
565
+ */
566
+ private isValidTab(tabId: string): boolean {
567
+ const tab = this.tabs.get(tabId);
568
+ if (!tab) {
569
+ return false;
570
+ }
571
+
572
+ // Check if tab is already destroyed
573
+ if (tab.isDestroyed) {
574
+ debug.warn('preview', `⚠️ Tab ${tabId}: already destroyed`);
575
+ return false;
576
+ }
577
+
578
+ // Check if browser is still connected (shared browser)
579
+ if (!tab.browser || !tab.browser.connected) {
580
+ debug.warn('preview', `⚠️ Tab ${tabId}: shared browser disconnected`);
581
+ this.closeTab(tabId).catch(console.error);
582
+ return false;
583
+ }
584
+
585
+ // Check if session is still valid in the pool (use project-scoped sessionId)
586
+ const sessionId = this.getSessionId(tabId);
587
+ const isPoolValid = browserPool.isSessionValid(sessionId);
588
+ if (!isPoolValid) {
589
+ debug.warn('preview', `⚠️ Tab ${tabId}: session no longer valid in pool`);
590
+ this.closeTab(tabId).catch(console.error);
591
+ return false;
592
+ }
593
+
594
+ // Check if page is still open
595
+ if (!tab.page || tab.page.isClosed()) {
596
+ debug.warn('preview', `⚠️ Tab ${tabId}: page closed`);
597
+ this.closeTab(tabId).catch(console.error);
598
+ return false;
599
+ }
600
+
601
+ return true;
602
+ }
603
+
604
+ /**
605
+ * Mark tab activity (prevent cleanup)
606
+ */
607
+ markTabActivity(tabId: string): void {
608
+ const now = Date.now();
609
+ this.tabActivity.set(tabId, now);
610
+ }
611
+
612
+ /**
613
+ * Setup page (viewport, headers, injections)
614
+ */
615
+ private async setupPage(page: Page, deviceSize: DeviceSize, rotation: Rotation) {
616
+ // Get viewport dimensions from config
617
+ const { width: viewportWidth, height: viewportHeight } = getViewportDimensions(deviceSize, rotation);
618
+
619
+ await page.setViewport({ width: viewportWidth, height: viewportHeight });
620
+
621
+ // Set page timeouts - more generous for stability
622
+ page.setDefaultTimeout(30000);
623
+ page.setDefaultNavigationTimeout(30000);
624
+
625
+ // Configure page for stability
626
+ await page.setExtraHTTPHeaders({
627
+ 'Accept-Language': 'en-US,en;q=0.9',
628
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
629
+ });
630
+
631
+ // Optimize font loading to prevent screenshot timeouts
632
+ await page.evaluateOnNewDocument(() => {
633
+ // Disable font loading wait
634
+ Object.defineProperty(document, 'fonts', {
635
+ value: {
636
+ ready: Promise.resolve(),
637
+ load: () => Promise.resolve([]),
638
+ check: () => true,
639
+ addEventListener: () => {},
640
+ removeEventListener: () => {}
641
+ }
642
+ });
643
+ });
644
+
645
+ // Inject audio capture script BEFORE page loads to intercept AudioContext
646
+ await this.audioCapture.setupAudioCapture(page, DEFAULT_STREAMING_CONFIG.audio);
647
+
648
+ // Simplified cursor tracking for visual feedback only
649
+ await this.injectCursorTracking(page);
650
+ }
651
+
652
+ /**
653
+ * Inject cursor tracking script
654
+ */
655
+ private async injectCursorTracking(page: Page) {
656
+ await page.evaluateOnNewDocument(cursorTrackingScript);
657
+ }
658
+
659
+ /**
660
+ * Navigate with retry
661
+ */
662
+ private async navigateWithRetry(page: Page, url: string): Promise<string> {
663
+ let retries = 3;
664
+ let actualUrl = '';
665
+
666
+ while (retries > 0) {
667
+ try {
668
+ await page.goto(url, {
669
+ waitUntil: 'domcontentloaded',
670
+ timeout: 30000
671
+ });
672
+ actualUrl = page.url();
673
+ break;
674
+ } catch (error) {
675
+ retries--;
676
+ debug.warn('preview', `⚠️ Navigation failed, ${retries} retries left:`, error);
677
+ if (retries === 0) throw error;
678
+
679
+ // Wait before retry
680
+ await new Promise(resolve => setTimeout(resolve, 2000));
681
+ }
682
+ }
683
+
684
+ return actualUrl;
685
+ }
686
+
687
+ /**
688
+ * Setup browser event handlers
689
+ */
690
+ private setupBrowserHandlers(tabId: string, browser: Browser, context: BrowserContext, page: Page) {
691
+ // Add error handlers for browser disconnection
692
+ // Note: With shared browser, we only clean up THIS tab, not close the browser
693
+ browser.on('disconnected', () => {
694
+ const tab = this.tabs.get(tabId);
695
+ if (tab && !tab.isDestroyed) {
696
+ debug.warn('preview', `⚠️ Shared browser disconnected, cleaning up tab ${tabId}`);
697
+ tab.isDestroyed = true;
698
+ this.closeTab(tabId).catch(console.error);
699
+ }
700
+ });
701
+
702
+ // Handle page errors
703
+ page.on('error', (error) => {
704
+ const tab = this.tabs.get(tabId);
705
+ if (tab && !tab.isDestroyed) {
706
+ debug.error('preview', `💥 Page error for tab ${tabId}: ${error.message}, cleaning up`);
707
+ tab.isDestroyed = true;
708
+ this.closeTab(tabId).catch(console.error);
709
+ }
710
+ });
711
+
712
+ // Track page close event
713
+ page.on('close', () => {
714
+ debug.warn('preview', `⚠️ Page close event for tab ${tabId}`);
715
+ });
716
+
717
+ // Handle popup/new window events within this context
718
+ context.on('targetcreated', async (target) => {
719
+ if (target.type() === 'page') {
720
+ const newPage = await target.page();
721
+ if (newPage && newPage !== page) {
722
+ const popupUrl = newPage.url();
723
+
724
+ // Emit event for frontend to handle
725
+ this.emit('new-window', {
726
+ tabId,
727
+ url: popupUrl,
728
+ timestamp: Date.now()
729
+ });
730
+
731
+ // Close the popup to prevent resource leak
732
+ try {
733
+ await newPage.close();
734
+ } catch (error) {
735
+ debug.warn('preview', 'Failed to close popup:', error);
736
+ }
737
+ }
738
+ }
739
+ });
740
+ }
741
+
742
+ /**
743
+ * Clean up the isolated context for a tab
744
+ */
745
+ private async cleanupContext(tab: BrowserTab) {
746
+ try {
747
+ // Close the page first
748
+ if (tab.page && !tab.page.isClosed()) {
749
+ await tab.page.close().catch((error) =>
750
+ debug.warn('preview', `⚠️ Error closing page:`, error instanceof Error ? error.message : error)
751
+ );
752
+ }
753
+
754
+ // Destroy the isolated session via browser pool (use project-scoped sessionId)
755
+ const sessionId = this.getSessionId(tab.id);
756
+ await browserPool.destroySession(sessionId);
757
+ } catch (error) {
758
+ debug.warn('preview', `⚠️ Error during context cleanup for ${tab.id}:`, error instanceof Error ? error.message : error);
759
+ }
760
+ }
761
+
762
+ /**
763
+ * Helper: Get title from URL
764
+ */
765
+ private getTitleFromUrl(url: string): string {
766
+ if (!url || url === 'about:blank') return 'New Tab';
767
+ try {
768
+ return new URL(url).hostname;
769
+ } catch {
770
+ return url.length > 30 ? url.slice(0, 30) + '...' : url;
771
+ }
772
+ }
773
+
774
+ /**
775
+ * Initialize periodic cleanup of inactive tabs
776
+ */
777
+ private initializeCleanup(): void {
778
+ // Don't initialize twice
779
+ if (this.cleanupInterval) {
780
+ return;
781
+ }
782
+
783
+ // Start periodic cleanup
784
+ this.cleanupInterval = setInterval(() => {
785
+ this.performCleanup();
786
+ }, CLEANUP_INTERVAL);
787
+
788
+ // Cleanup on shutdown
789
+ const cleanup = () => {
790
+ if (this.cleanupInterval) clearInterval(this.cleanupInterval);
791
+ this.tabActivity.clear();
792
+ };
793
+
794
+ process.on('SIGTERM', cleanup);
795
+ process.on('SIGINT', cleanup);
796
+ }
797
+
798
+ /**
799
+ * Perform cleanup of inactive tabs
800
+ */
801
+ private performCleanup(): void {
802
+ const now = Date.now();
803
+
804
+ for (const [tabId, tab] of this.tabs.entries()) {
805
+ const lastActivity = this.tabActivity.get(tabId);
806
+
807
+ // If no activity recorded, mark it as active now and skip cleanup
808
+ if (!lastActivity) {
809
+ this.tabActivity.set(tabId, now);
810
+ continue;
811
+ }
812
+
813
+ const inactiveTime = now - lastActivity;
814
+
815
+ // Skip if tab has recent activity
816
+ if (inactiveTime < INACTIVE_TIMEOUT) {
817
+ continue;
818
+ }
819
+
820
+ // Only cleanup if tab is truly orphaned
821
+ if (tab.isDestroyed || (tab.page?.isClosed() && !tab.browser?.connected)) {
822
+ debug.log('preview', `🧹 Auto-cleaning up inactive tab: ${tabId} (inactive for ${Math.round(inactiveTime / 1000)}s)`);
823
+
824
+ // Close tab
825
+ this.closeTab(tabId).catch(console.error);
826
+ }
827
+ }
828
+ }
829
+
830
+ /**
831
+ * Cleanup inactive tabs
832
+ */
833
+ async cleanupInactiveTabs() {
834
+ const tabIds = Array.from(this.tabs.keys());
835
+ const inactiveTabs: string[] = [];
836
+ const activeTabs: string[] = [];
837
+
838
+ // Categorize tabs by activity
839
+ for (const tabId of tabIds) {
840
+ const tab = this.tabs.get(tabId);
841
+ if (!tab) {
842
+ inactiveTabs.push(tabId);
843
+ continue;
844
+ }
845
+
846
+ // Check if tab is truly inactive
847
+ const isInactive =
848
+ tab.isDestroyed ||
849
+ !tab.browser?.connected ||
850
+ tab.page?.isClosed() ||
851
+ !tab.isStreaming;
852
+
853
+ if (isInactive) {
854
+ inactiveTabs.push(tabId);
855
+ } else {
856
+ activeTabs.push(tabId);
857
+ }
858
+ }
859
+
860
+ // Only cleanup inactive tabs
861
+ if (inactiveTabs.length > 0) {
862
+ const cleanupPromises = inactiveTabs.map(tabId =>
863
+ this.closeTab(tabId).catch(error =>
864
+ debug.warn('preview', `⚠️ Error destroying inactive tab ${tabId}:`, error)
865
+ )
866
+ );
867
+
868
+ try {
869
+ await Promise.race([
870
+ Promise.all(cleanupPromises),
871
+ new Promise((_, reject) => setTimeout(() => reject(new Error('Inactive tab cleanup timeout')), 10000))
872
+ ]);
873
+ } catch (error) {
874
+ debug.warn('preview', '⚠️ Inactive tab cleanup timeout:', error);
875
+ }
876
+ }
877
+
878
+ return {
879
+ activeTabsCount: activeTabs.length,
880
+ inactiveTabsDestroyed: inactiveTabs.length,
881
+ activeTabs,
882
+ cleanedTabs: inactiveTabs
883
+ };
884
+ }
885
+
886
+ /**
887
+ * Cleanup all tabs
888
+ */
889
+ async cleanup(): Promise<void> {
890
+ debug.log('preview', `🧹 Cleaning up ${this.tabs.size} tabs...`);
891
+
892
+ // Stop cleanup interval
893
+ if (this.cleanupInterval) {
894
+ clearInterval(this.cleanupInterval);
895
+ this.cleanupInterval = null;
896
+ }
897
+
898
+ const tabIds = Array.from(this.tabs.keys());
899
+
900
+ if (tabIds.length > 0) {
901
+ debug.log('preview', `🗑️ Destroying ${tabIds.length} tabs...`);
902
+
903
+ // Destroy all tabs in parallel
904
+ const cleanupPromises = tabIds.map((tabId) =>
905
+ this.closeTab(tabId).catch((error) => debug.warn('preview', `⚠️ Error destroying tab ${tabId}:`, error))
906
+ );
907
+
908
+ try {
909
+ await Promise.race([
910
+ Promise.all(cleanupPromises),
911
+ new Promise((_, reject) => setTimeout(() => reject(new Error('Tab cleanup timeout')), 15000))
912
+ ]);
913
+ } catch (error) {
914
+ debug.warn('preview', '⚠️ Tab cleanup timeout:', error);
915
+ }
916
+ }
917
+
918
+ // Force clear tabs map
919
+ this.tabs.clear();
920
+ this.activeTabId = null;
921
+ this.tabActivity.clear();
922
+
923
+ // Clean up the browser pool (closes all contexts and the shared browser)
924
+ await browserPool.cleanup();
925
+
926
+ debug.log('preview', '✅ All tabs cleaned up');
927
+ }
928
+
929
+ /**
930
+ * Get all tab IDs
931
+ */
932
+ getAvailableTabIds(): string[] {
933
+ return Array.from(this.tabs.keys());
934
+ }
935
+ }