@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,1058 @@
1
+ <script lang="ts">
2
+ import { onDestroy } from 'svelte';
3
+ import { getViewportDimensions, type DeviceSize, type Rotation } from '$frontend/lib/constants/preview';
4
+ import { BrowserWebCodecsService, type BrowserWebCodecsStreamStats } from '$frontend/lib/services/preview/browser/browser-webcodecs.service';
5
+ import { debug } from '$shared/utils/logger';
6
+
7
+ let {
8
+ projectId = '', // REQUIRED for project isolation (read-only from parent)
9
+ sessionId = $bindable<string | null>(null),
10
+ sessionInfo = $bindable<any>(null),
11
+ deviceSize = $bindable<DeviceSize>('laptop'),
12
+ rotation = $bindable<Rotation>('portrait'),
13
+ currentCursor = $bindable('default'),
14
+ canvasAPI = $bindable<any>(null),
15
+ lastFrameData = $bindable<any>(null),
16
+ isConnected = $bindable(false),
17
+ latencyMs = $bindable<number>(0),
18
+ isStreamReady = $bindable(false), // Exposed: true when first frame received
19
+ isNavigating = $bindable(false), // Track if page is navigating (from parent)
20
+ isReconnecting = $bindable(false), // Track if reconnecting after navigation (prevents loading overlay)
21
+
22
+ // Callbacks for interactions
23
+ onInteraction = $bindable<(action: any) => void>(() => {}),
24
+ onCursorUpdate = $bindable<(cursor: string) => void>(() => {}),
25
+ onFrameUpdate = $bindable<(data: any) => void>(() => {}),
26
+ onStatsUpdate = $bindable<(stats: BrowserWebCodecsStreamStats | null) => void>(() => {}),
27
+ onRequestScreencastRefresh = $bindable<() => void>(() => {}) // Called when stream is stuck
28
+ } = $props();
29
+
30
+ // WebCodecs service instance
31
+ let webCodecsService: BrowserWebCodecsService | null = null;
32
+ let isWebCodecsActive = $state(false);
33
+ let activeStreamingSessionId: string | null = null; // Track which session is currently streaming
34
+ let isStartingStream = false; // Prevent concurrent start attempts
35
+ let lastStartRequestId: string | null = null; // Track the last start request to prevent duplicates
36
+
37
+ let canvasElement = $state<HTMLCanvasElement | undefined>();
38
+ let setupCanvasTimeout: ReturnType<typeof setTimeout> | undefined;
39
+
40
+ // Health check and recovery - EVENT-DRIVEN, not timeout-based
41
+ let healthCheckInterval: ReturnType<typeof setInterval> | undefined;
42
+ let initialFrameCheckInterval: ReturnType<typeof setInterval> | undefined;
43
+ let lastFrameTime = 0;
44
+ let consecutiveFailures = $state(0); // Made reactive for UI
45
+ let hasReceivedFirstFrame = $state(false); // Made reactive for UI
46
+ let isStreamStarting = $state(false); // Track when stream is being started
47
+ let isRecovering = $state(false); // Track recovery attempts
48
+ let connectionFailed = $state(false); // Track if connection actually failed (not just slow)
49
+ let hasRequestedScreencastRefresh = false; // Track if we've already requested refresh for this stream
50
+ let navigationJustCompleted = false; // Track if navigation just completed (for fast refresh)
51
+
52
+ // Recovery is only triggered by ACTUAL failures, not timeouts
53
+ // - ICE connection failed
54
+ // - WebCodecs connection closed unexpectedly
55
+ // - Explicit errors
56
+ const MAX_CONSECUTIVE_FAILURES = 2;
57
+ const HEALTH_CHECK_INTERVAL = 2000; // Check every 2 seconds for connection health
58
+ const FRAME_CHECK_INTERVAL = 500; // Check for first frame every 500ms (just for UI update, not recovery)
59
+ const STUCK_STREAM_TIMEOUT = 5000; // Fallback: Request screencast refresh after 5 seconds of connected but no frame
60
+ const NAVIGATION_FAST_REFRESH_DELAY = 500; // Fast refresh after navigation: 500ms
61
+
62
+ // Sync isStreamReady with hasReceivedFirstFrame for parent component
63
+ $effect(() => {
64
+ isStreamReady = hasReceivedFirstFrame;
65
+ });
66
+
67
+ // Watch projectId changes and recreate WebCodecs service
68
+ let lastProjectId = '';
69
+ $effect(() => {
70
+ const currentProjectId = projectId;
71
+
72
+ // Project changed - destroy and recreate service
73
+ if (lastProjectId && currentProjectId && lastProjectId !== currentProjectId) {
74
+ debug.log('webcodecs', `🔄 Project changed (${lastProjectId} → ${currentProjectId}), destroying old WebCodecs service`);
75
+
76
+ // Destroy old service
77
+ if (webCodecsService) {
78
+ webCodecsService.destroy();
79
+ webCodecsService = null;
80
+ activeStreamingSessionId = null;
81
+ isWebCodecsActive = false;
82
+ }
83
+ }
84
+
85
+ lastProjectId = currentProjectId;
86
+ });
87
+
88
+ // Sync navigation state with webCodecsService
89
+ // This prevents recovery when DataChannel closes during navigation
90
+ $effect(() => {
91
+ if (webCodecsService) {
92
+ webCodecsService.setNavigating(isNavigating);
93
+ if (isNavigating) {
94
+ debug.log('webcodecs', 'Navigation started - recovery will be suppressed');
95
+ }
96
+ }
97
+ });
98
+
99
+ // Convert CSS cursor values to canvas cursor styles
100
+ function mapCursorStyle(browserCursor: string): string {
101
+ const cursorMap: Record<string, string> = {
102
+ 'default': 'default',
103
+ 'auto': 'default',
104
+ 'pointer': 'pointer',
105
+ 'text': 'text',
106
+ 'wait': 'wait',
107
+ 'crosshair': 'crosshair',
108
+ 'help': 'help',
109
+ 'move': 'move',
110
+ 'n-resize': 'n-resize',
111
+ 's-resize': 's-resize',
112
+ 'e-resize': 'e-resize',
113
+ 'w-resize': 'w-resize',
114
+ 'ne-resize': 'ne-resize',
115
+ 'nw-resize': 'nw-resize',
116
+ 'se-resize': 'se-resize',
117
+ 'sw-resize': 'sw-resize',
118
+ 'ew-resize': 'ew-resize',
119
+ 'ns-resize': 'ns-resize',
120
+ 'nesw-resize': 'nesw-resize',
121
+ 'nwse-resize': 'nwse-resize',
122
+ 'grab': 'grab',
123
+ 'grabbing': 'grabbing',
124
+ 'not-allowed': 'not-allowed',
125
+ 'no-drop': 'no-drop',
126
+ 'copy': 'copy',
127
+ 'alias': 'alias',
128
+ 'context-menu': 'context-menu',
129
+ 'cell': 'cell',
130
+ 'vertical-text': 'vertical-text',
131
+ 'all-scroll': 'all-scroll',
132
+ 'col-resize': 'col-resize',
133
+ 'row-resize': 'row-resize',
134
+ 'zoom-in': 'zoom-in',
135
+ 'zoom-out': 'zoom-out'
136
+ };
137
+
138
+ return cursorMap[browserCursor] || 'default';
139
+ }
140
+
141
+ // Update canvas cursor style
142
+ function updateCanvasCursor(newCursor: string) {
143
+ if (canvasElement && newCursor !== currentCursor) {
144
+ const mappedCursor = mapCursorStyle(newCursor);
145
+ canvasElement.style.cursor = mappedCursor;
146
+ currentCursor = newCursor;
147
+ onCursorUpdate(newCursor);
148
+ }
149
+ }
150
+
151
+ // Interactive canvas functions
152
+ async function sendInteraction(action: any) {
153
+ if (!sessionId) return;
154
+ onInteraction(action);
155
+ }
156
+
157
+ // Utility function to convert canvas display coordinates to browser coordinates
158
+ function getCanvasCoordinates(event: MouseEvent | TouchEvent, canvas: HTMLCanvasElement): { x: number, y: number } {
159
+ const rect = canvas.getBoundingClientRect();
160
+ const scaleX = canvas.width / rect.width;
161
+ const scaleY = canvas.height / rect.height;
162
+
163
+ let clientX: number, clientY: number;
164
+
165
+ if (event instanceof MouseEvent) {
166
+ clientX = event.clientX;
167
+ clientY = event.clientY;
168
+ } else {
169
+ // Touch event - use first touch
170
+ const touch = event.touches[0] || event.changedTouches[0];
171
+ clientX = touch.clientX;
172
+ clientY = touch.clientY;
173
+ }
174
+
175
+ const x = (clientX - rect.left) * scaleX;
176
+ const y = (clientY - rect.top) * scaleY;
177
+
178
+ return {
179
+ x: Math.round(x),
180
+ y: Math.round(y)
181
+ };
182
+ }
183
+
184
+
185
+ function handleCanvasMouseMove(event: MouseEvent, canvas: HTMLCanvasElement) {
186
+ if (!sessionId) return;
187
+
188
+ const coords = getCanvasCoordinates(event, canvas);
189
+
190
+ if (isMouseDown && dragStartPos) {
191
+ dragCurrentPos = { x: coords.x, y: coords.y };
192
+
193
+ const dragDistance = Math.sqrt(
194
+ Math.pow(coords.x - dragStartPos.x, 2) + Math.pow(coords.y - dragStartPos.y, 2)
195
+ );
196
+
197
+ // Start drag when distance exceeds threshold
198
+ if (dragDistance > 10) {
199
+ // Send mousedown on first drag detection
200
+ if (!dragStarted) {
201
+ sendInteraction({
202
+ type: 'mousedown',
203
+ x: dragStartPos.x,
204
+ y: dragStartPos.y,
205
+ button: event.button === 2 ? 'right' : 'left'
206
+ });
207
+ dragStarted = true;
208
+ }
209
+
210
+ isDragging = true;
211
+ // Send mousemove to continue dragging (mouse is already down)
212
+ sendInteraction({
213
+ type: 'mousemove',
214
+ x: coords.x,
215
+ y: coords.y
216
+ });
217
+ }
218
+ } else if (!isMouseDown) {
219
+ sendInteraction({
220
+ type: 'mousemove',
221
+ x: coords.x,
222
+ y: coords.y
223
+ });
224
+ }
225
+ }
226
+
227
+ function handleCanvasDoubleClick(event: MouseEvent, canvas: HTMLCanvasElement) {
228
+ if (!sessionId) return;
229
+ const coords = getCanvasCoordinates(event, canvas);
230
+ sendInteraction({ type: 'doubleclick', x: coords.x, y: coords.y });
231
+ }
232
+
233
+ function handleCanvasRightClick(event: MouseEvent, canvas: HTMLCanvasElement) {
234
+ event.preventDefault();
235
+ if (!sessionId) return;
236
+ const coords = getCanvasCoordinates(event, canvas);
237
+ sendInteraction({ type: 'rightclick', x: coords.x, y: coords.y });
238
+ }
239
+
240
+ function handleCanvasWheel(event: WheelEvent, canvas: HTMLCanvasElement) {
241
+ event.preventDefault();
242
+ if (!sessionId) return;
243
+ sendInteraction({ type: 'scroll', deltaX: event.deltaX, deltaY: event.deltaY });
244
+ }
245
+
246
+ function handleCanvasKeydown(event: KeyboardEvent) {
247
+ if (!sessionId) return;
248
+
249
+ // Prevent default for all keyboard events to avoid affecting parent page
250
+ // This prevents Ctrl+A, Ctrl+C, arrow keys, etc. from affecting the parent
251
+ event.preventDefault();
252
+ event.stopPropagation();
253
+
254
+ const isNavigationKey = ['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', 'Tab', 'Enter', 'Escape'].includes(event.key);
255
+ const isModifierKey = event.ctrlKey || event.metaKey || event.altKey || event.shiftKey;
256
+
257
+ if (isNavigationKey) {
258
+ sendInteraction({
259
+ type: 'keynav',
260
+ key: event.key,
261
+ ctrlKey: event.ctrlKey,
262
+ metaKey: event.metaKey,
263
+ altKey: event.altKey,
264
+ shiftKey: event.shiftKey
265
+ });
266
+ } else if (event.key.length === 1 && !isModifierKey) {
267
+ sendInteraction({ type: 'type', text: event.key, delay: 50 });
268
+ } else {
269
+ sendInteraction({
270
+ type: 'key',
271
+ key: event.key,
272
+ ctrlKey: event.ctrlKey,
273
+ metaKey: event.metaKey,
274
+ altKey: event.altKey,
275
+ shiftKey: event.shiftKey
276
+ });
277
+ }
278
+ }
279
+
280
+ // State variables for drag-drop functionality
281
+ let isDragging = $state(false);
282
+ let isMouseDown = $state(false);
283
+ let dragStartPos = $state<{x: number, y: number} | null>(null);
284
+ let dragCurrentPos = $state<{x: number, y: number} | null>(null);
285
+ let mouseDownTime = $state(0);
286
+ let dragStarted = $state(false); // Track if we've sent mousedown for drag
287
+
288
+ function handleCanvasMouseDown(event: MouseEvent, canvas: HTMLCanvasElement) {
289
+ if (!sessionId) return;
290
+
291
+ const coords = getCanvasCoordinates(event, canvas);
292
+
293
+ isMouseDown = true;
294
+ mouseDownTime = Date.now();
295
+ dragStartPos = { x: coords.x, y: coords.y };
296
+ dragCurrentPos = { x: coords.x, y: coords.y };
297
+ dragStarted = false; // Reset drag started flag
298
+ }
299
+
300
+ function handleCanvasMouseUp(event: MouseEvent, canvas: HTMLCanvasElement) {
301
+ if (!sessionId || !isMouseDown) return;
302
+
303
+ const coords = getCanvasCoordinates(event, canvas);
304
+
305
+ if (dragStartPos) {
306
+ // If drag was started (mousedown was sent), send mouseup
307
+ if (dragStarted) {
308
+ sendInteraction({
309
+ type: 'mouseup',
310
+ x: coords.x,
311
+ y: coords.y,
312
+ button: event.button === 2 ? 'right' : 'left'
313
+ });
314
+ } else {
315
+ // No drag occurred, this is a click
316
+ // IMPORTANT: Only send click for left mouse button (button === 0)
317
+ // Right-click (button === 2) is handled by contextmenu event
318
+ if (event.button === 0) {
319
+ sendInteraction({ type: 'click', x: dragStartPos.x, y: dragStartPos.y });
320
+ }
321
+ }
322
+ }
323
+
324
+ isMouseDown = false;
325
+ isDragging = false;
326
+ dragStartPos = null;
327
+ dragCurrentPos = null;
328
+ dragStarted = false;
329
+ }
330
+
331
+ function setupCanvasInternal() {
332
+ if (!sessionInfo) return;
333
+
334
+ // IMPORTANT: Use props as primary source of truth, fallback to sessionInfo
335
+ // Props are reactive and updated when user changes device/rotation
336
+ // sessionInfo may be stale (snapshot from launch time)
337
+ const currentDevice: DeviceSize = deviceSize || sessionInfo?.deviceSize || 'laptop';
338
+ const currentRotation: Rotation = rotation || sessionInfo?.rotation || 'landscape';
339
+
340
+ // Use getViewportDimensions helper for consistent viewport calculation
341
+ // This ensures portrait = height > width, landscape = width > height
342
+ const { width: canvasWidth, height: canvasHeight } = getViewportDimensions(currentDevice, currentRotation);
343
+
344
+ debug.log('webcodecs', `setupCanvasInternal: device=${currentDevice}, rotation=${currentRotation}, canvas=${canvasWidth}x${canvasHeight}`);
345
+
346
+ // Get scale from parent (BrowserPreviewContainer calculates this)
347
+ // This is provided via previewDimensions binding
348
+ const currentScale = 1; // We keep canvas at original size, scaling handled by CSS
349
+
350
+ if (canvasElement) {
351
+ // Canvas dimensions stay at original viewport size
352
+ // Scaling is handled by CSS transform in parent container
353
+ if (canvasElement.width === canvasWidth && canvasElement.height === canvasHeight) {
354
+ return;
355
+ }
356
+
357
+ canvasElement.width = canvasWidth;
358
+ canvasElement.height = canvasHeight;
359
+ canvasElement.style.width = '100%';
360
+ canvasElement.style.height = '100%';
361
+ // Use same background as loading overlay to avoid flash of black
362
+ // This will be covered by overlay until stream is ready anyway
363
+ canvasElement.style.backgroundColor = 'transparent';
364
+ canvasElement.style.cursor = 'default';
365
+
366
+ // Get context with low-latency optimizations
367
+ const ctx = canvasElement.getContext('2d', {
368
+ alpha: false, // No transparency needed - faster
369
+ desynchronized: true, // Low latency rendering hint
370
+ willReadFrequently: false // We won't read pixels back
371
+ });
372
+
373
+ // Fill with neutral gray (works for both light/dark mode)
374
+ // This matches the loading overlay background roughly
375
+ if (ctx) {
376
+ ctx.imageSmoothingEnabled = true;
377
+ ctx.imageSmoothingQuality = 'low'; // Faster rendering
378
+ ctx.fillStyle = '#f1f5f9'; // slate-100 - neutral light color
379
+ ctx.fillRect(0, 0, canvasElement.width, canvasElement.height);
380
+ }
381
+ }
382
+ }
383
+
384
+ function setupCanvas() {
385
+ if (setupCanvasTimeout) {
386
+ clearTimeout(setupCanvasTimeout);
387
+ }
388
+ setupCanvasTimeout = setTimeout(() => {
389
+ setupCanvasInternal();
390
+ }, 5);
391
+ }
392
+
393
+ // Start WebCodecs streaming
394
+ async function startStreaming() {
395
+ if (!sessionId || !canvasElement) return;
396
+
397
+ // Prevent concurrent start attempts
398
+ if (isStartingStream) {
399
+ debug.log('webcodecs', 'Already starting stream, skipping duplicate call');
400
+ return;
401
+ }
402
+
403
+ // If already streaming same session, skip
404
+ if (isWebCodecsActive && activeStreamingSessionId === sessionId) {
405
+ debug.log('webcodecs', 'Already streaming same session, skipping');
406
+ return;
407
+ }
408
+
409
+ // Prevent duplicate requests for same session
410
+ const requestId = `${sessionId}-${Date.now()}`;
411
+ if (lastStartRequestId && lastStartRequestId.startsWith(sessionId)) {
412
+ debug.log('webcodecs', `Duplicate start request for ${sessionId}, skipping`);
413
+ return;
414
+ }
415
+ lastStartRequestId = requestId;
416
+
417
+ isStartingStream = true;
418
+ isStreamStarting = true; // Show loading overlay
419
+ hasReceivedFirstFrame = false; // Reset first frame state
420
+
421
+ try {
422
+ // If streaming a different session, stop first
423
+ if (isWebCodecsActive && activeStreamingSessionId !== sessionId) {
424
+ debug.log('webcodecs', `Session mismatch (active: ${activeStreamingSessionId}, requested: ${sessionId}), stopping old stream first`);
425
+ await stopStreaming();
426
+ // Small delay to ensure cleanup is complete
427
+ await new Promise(resolve => setTimeout(resolve, 100));
428
+ }
429
+
430
+ // Create WebCodecs service if not exists
431
+ if (!webCodecsService) {
432
+ if (!projectId) {
433
+ debug.error('webcodecs', 'Cannot start streaming: projectId is required');
434
+ isStartingStream = false;
435
+ return;
436
+ }
437
+ webCodecsService = new BrowserWebCodecsService(projectId);
438
+
439
+ // Setup error handler
440
+ webCodecsService.setErrorHandler((error: Error) => {
441
+ debug.error('webcodecs', 'Error:', error);
442
+ isStartingStream = false;
443
+ connectionFailed = true;
444
+ });
445
+
446
+ // Setup connection change handler
447
+ webCodecsService.setConnectionChangeHandler((connected: boolean) => {
448
+ isWebCodecsActive = connected;
449
+ isConnected = connected;
450
+ if (!connected) {
451
+ activeStreamingSessionId = null;
452
+ }
453
+ });
454
+
455
+ // Setup connection FAILED handler - this triggers recovery
456
+ // Only called on actual failures (ICE failed, connection failed)
457
+ // NOT called on timeouts or slow loading
458
+ webCodecsService.setConnectionFailedHandler(() => {
459
+ debug.warn('webcodecs', 'Connection failed - attempting recovery');
460
+ connectionFailed = true;
461
+ attemptRecovery();
462
+ });
463
+
464
+ // Setup navigation reconnect handler - FAST path without delay
465
+ // Called when DataChannel closes during navigation, backend already restarted
466
+ webCodecsService.setNavigationReconnectHandler(() => {
467
+ debug.log('webcodecs', '🚀 Navigation reconnect - fast path (no delay)');
468
+ fastReconnect();
469
+ });
470
+
471
+ // Setup reconnecting start handler - fires IMMEDIATELY when DataChannel closes during navigation
472
+ // This ensures isReconnecting is set before the 700ms delay, keeping progress bar visible
473
+ webCodecsService.setReconnectingStartHandler(() => {
474
+ debug.log('webcodecs', '🔄 Reconnecting state started (immediate)');
475
+ isReconnecting = true;
476
+ });
477
+
478
+ // Setup stats handler
479
+ webCodecsService.setStatsHandler((stats: BrowserWebCodecsStreamStats) => {
480
+ onStatsUpdate(stats);
481
+ });
482
+
483
+ // Setup cursor change handler
484
+ webCodecsService.setOnCursorChange((cursor: string) => {
485
+ updateCanvasCursor(cursor);
486
+ });
487
+ }
488
+
489
+ // Start streaming with retry for session not ready cases
490
+ debug.log('webcodecs', `Starting streaming for session: ${sessionId}`);
491
+
492
+ let success = false;
493
+ let retries = 0;
494
+ const maxRetries = 5; // Increased for rapid tab switching
495
+ const retryDelay = 500; // Increased delay for backend cleanup
496
+
497
+ while (!success && retries < maxRetries) {
498
+ try {
499
+ success = await webCodecsService.startStreaming(sessionId, canvasElement);
500
+ if (success) {
501
+ isWebCodecsActive = true;
502
+ isConnected = true;
503
+ activeStreamingSessionId = sessionId;
504
+ consecutiveFailures = 0; // Reset failure counter on success
505
+ startHealthCheck(); // Start monitoring for stuck streams
506
+ debug.log('webcodecs', 'Streaming started successfully');
507
+ break;
508
+ }
509
+ } catch (error: any) {
510
+ // Check if it's a retriable error - might just need more time
511
+ const isRetriable = error?.message?.includes('not found') ||
512
+ error?.message?.includes('invalid') ||
513
+ error?.message?.includes('Failed to start') ||
514
+ error?.message?.includes('No offer');
515
+
516
+ if (isRetriable) {
517
+ retries++;
518
+ if (retries < maxRetries) {
519
+ debug.log('webcodecs', `Streaming not ready, retrying in ${retryDelay}ms (${retries}/${maxRetries})`);
520
+ await new Promise(resolve => setTimeout(resolve, retryDelay));
521
+ } else {
522
+ debug.error('webcodecs', 'Max retries reached, streaming still not ready');
523
+ // Don't throw - just fail silently and let user retry manually
524
+ break;
525
+ }
526
+ } else {
527
+ // Other errors - don't retry, just log
528
+ debug.error('webcodecs', 'Streaming error:', error);
529
+ break;
530
+ }
531
+ }
532
+ }
533
+ } finally {
534
+ isStartingStream = false;
535
+ isStreamStarting = false; // Hide "Launching browser..." (but may still show "Connecting..." until first frame)
536
+ }
537
+ }
538
+
539
+ // Clear canvas to prevent showing stale frames
540
+ // Use light neutral color that works with loading overlay
541
+ function clearCanvas() {
542
+ if (canvasElement) {
543
+ const ctx = canvasElement.getContext('2d');
544
+ if (ctx) {
545
+ ctx.fillStyle = '#f1f5f9'; // slate-100 - same as setup, works with overlay
546
+ ctx.fillRect(0, 0, canvasElement.width, canvasElement.height);
547
+ }
548
+ }
549
+ }
550
+
551
+ // EVENT-DRIVEN health check - no timeout-based recovery
552
+ // We only check for first frame to update UI, not to trigger recovery
553
+ // Recovery is triggered by actual connection failures (ICE failed, connection closed)
554
+ // skipFirstFrameReset: When true, don't reset hasReceivedFirstFrame (used during fast reconnect to keep overlay stable)
555
+ function startHealthCheck(skipFirstFrameReset = false) {
556
+ // Stop existing intervals without resetting hasReceivedFirstFrame if skipFirstFrameReset is true
557
+ stopHealthCheck(skipFirstFrameReset);
558
+ lastFrameTime = Date.now();
559
+ if (!skipFirstFrameReset) {
560
+ hasReceivedFirstFrame = false;
561
+ }
562
+ connectionFailed = false;
563
+ hasRequestedScreencastRefresh = false; // Reset for new stream
564
+
565
+ const startTime = Date.now();
566
+
567
+ // Check for first frame periodically (for UI update only, NOT recovery)
568
+ initialFrameCheckInterval = setInterval(() => {
569
+ if (!isWebCodecsActive || !sessionId) {
570
+ return;
571
+ }
572
+
573
+ const stats = webCodecsService?.getStats();
574
+ const now = Date.now();
575
+ const elapsed = now - startTime;
576
+
577
+ // Log connection state periodically for debugging
578
+ if (elapsed > 0 && elapsed % 5000 < FRAME_CHECK_INTERVAL) {
579
+ debug.log('webcodecs', `Status: connected=${stats?.isConnected}, firstFrame=${stats?.firstFrameRendered}, elapsed=${elapsed}ms`);
580
+ }
581
+
582
+ // Check if we received the first frame
583
+ if (stats && stats.firstFrameRendered) {
584
+ debug.log('webcodecs', `First frame rendered after ${elapsed}ms`);
585
+ hasReceivedFirstFrame = true;
586
+ lastFrameTime = now;
587
+ consecutiveFailures = 0;
588
+ connectionFailed = false;
589
+ hasRequestedScreencastRefresh = false; // Reset on success
590
+
591
+ // Reset reconnecting state after successful frame reception
592
+ // This completes the fast reconnect cycle
593
+ // Add small delay to allow page to render a bit more before hiding overlay
594
+ if (isReconnecting) {
595
+ debug.log('webcodecs', 'First frame received during reconnect, will reset isReconnecting after delay');
596
+ setTimeout(() => {
597
+ debug.log('webcodecs', 'Resetting isReconnecting after first frame + delay');
598
+ isReconnecting = false;
599
+ }, 300); // 300ms delay to let page render more
600
+ }
601
+
602
+ // Stop initial check, start regular health check
603
+ if (initialFrameCheckInterval) {
604
+ clearInterval(initialFrameCheckInterval);
605
+ initialFrameCheckInterval = undefined;
606
+ }
607
+ startRegularHealthCheck();
608
+ return;
609
+ }
610
+
611
+ // FAST REFRESH AFTER NAVIGATION: If navigation just completed and we're
612
+ // connected but no frame, trigger refresh quickly (don't wait 5 seconds)
613
+ if (navigationJustCompleted && stats?.isConnected && !stats?.firstFrameRendered && elapsed >= NAVIGATION_FAST_REFRESH_DELAY && !hasRequestedScreencastRefresh) {
614
+ debug.log('webcodecs', `Navigation completed, fast-refreshing screencast (connected but no frame for ${elapsed}ms)`);
615
+ hasRequestedScreencastRefresh = true;
616
+ navigationJustCompleted = false;
617
+ onRequestScreencastRefresh();
618
+ return; // Skip regular stuck check
619
+ }
620
+
621
+ // STUCK STREAM DETECTION (FALLBACK): If connected but no first frame for too long,
622
+ // request screencast refresh (hot-swap) to restart CDP screencast.
623
+ // This handles cases where WebRTC is connected but CDP frames aren't flowing.
624
+ if (stats?.isConnected && !stats?.firstFrameRendered && elapsed >= STUCK_STREAM_TIMEOUT && !hasRequestedScreencastRefresh) {
625
+ debug.warn('webcodecs', `Stream appears stuck (connected but no frame for ${elapsed}ms), requesting screencast refresh`);
626
+ hasRequestedScreencastRefresh = true;
627
+ onRequestScreencastRefresh();
628
+ }
629
+
630
+ }, FRAME_CHECK_INTERVAL);
631
+ }
632
+
633
+ // Regular health check (after first frame received)
634
+ // Only monitors connection health, doesn't trigger timeout-based recovery
635
+ function startRegularHealthCheck() {
636
+ if (healthCheckInterval) return; // Already running
637
+
638
+ healthCheckInterval = setInterval(() => {
639
+ if (!isWebCodecsActive || !sessionId) {
640
+ return;
641
+ }
642
+
643
+ const stats = webCodecsService?.getStats();
644
+ const now = Date.now();
645
+
646
+ // Update last frame time if we're receiving frames
647
+ if (stats && (stats.firstFrameRendered || stats.videoFramesReceived > 0)) {
648
+ lastFrameTime = now;
649
+ consecutiveFailures = 0;
650
+ }
651
+
652
+ // NO TIMEOUT-BASED RECOVERY
653
+ // We only log for debugging purposes
654
+ // Recovery is triggered by actual connection state changes (handled in WebCodecs service)
655
+
656
+ }, HEALTH_CHECK_INTERVAL);
657
+ }
658
+
659
+ // Stop health check intervals
660
+ // skipFirstFrameReset: When true, don't reset hasReceivedFirstFrame (used during navigation reconnect)
661
+ function stopHealthCheck(skipFirstFrameReset = false) {
662
+ if (initialFrameCheckInterval) {
663
+ clearInterval(initialFrameCheckInterval);
664
+ initialFrameCheckInterval = undefined;
665
+ }
666
+ if (healthCheckInterval) {
667
+ clearInterval(healthCheckInterval);
668
+ healthCheckInterval = undefined;
669
+ }
670
+ // Only reset hasReceivedFirstFrame if not skipping (preserves overlay during navigation)
671
+ if (!skipFirstFrameReset) {
672
+ hasReceivedFirstFrame = false;
673
+ }
674
+ }
675
+
676
+ // Attempt to recover stuck stream
677
+ async function attemptRecovery() {
678
+ if (isStartingStream || isRecovering) {
679
+ debug.log('webcodecs', 'Recovery skipped - already starting or recovering');
680
+ return;
681
+ }
682
+
683
+ consecutiveFailures++;
684
+ debug.log('webcodecs', `Recovery attempt ${consecutiveFailures}/${MAX_CONSECUTIVE_FAILURES} for session ${sessionId}`);
685
+
686
+ if (consecutiveFailures > MAX_CONSECUTIVE_FAILURES) {
687
+ debug.error('webcodecs', 'Max recovery attempts reached, giving up');
688
+ isRecovering = false;
689
+ await stopStreaming();
690
+ return;
691
+ }
692
+
693
+ // Stop and restart streaming
694
+ try {
695
+ isRecovering = true; // Show "Reconnecting..." overlay
696
+ hasReceivedFirstFrame = false; // Reset for recovery
697
+ await stopStreaming();
698
+ lastStartRequestId = null; // Clear to allow new start request
699
+ await new Promise(resolve => setTimeout(resolve, 500)); // Wait for cleanup
700
+ await startStreaming();
701
+ } catch (error) {
702
+ debug.error('webcodecs', 'Recovery failed:', error);
703
+ } finally {
704
+ isRecovering = false;
705
+ }
706
+ }
707
+
708
+ // Fast reconnect after navigation - NO DELAY because backend already restarted
709
+ // Uses reconnectToExistingStream which does NOT tell backend to stop
710
+ async function fastReconnect() {
711
+ if (isStartingStream || isRecovering) {
712
+ debug.log('webcodecs', 'Fast reconnect skipped - already starting or recovering');
713
+ return;
714
+ }
715
+
716
+ if (!sessionId || !canvasElement || !webCodecsService) {
717
+ debug.warn('webcodecs', 'Fast reconnect skipped - missing session, canvas, or service');
718
+ return;
719
+ }
720
+
721
+ debug.log('webcodecs', `🚀 Fast reconnect for session ${sessionId} (reconnect only, no backend stop)`);
722
+
723
+ try {
724
+ isRecovering = true;
725
+ isStartingStream = true;
726
+
727
+ // Set isReconnecting to prevent loading overlay during reconnect
728
+ // This ensures the last frame stays visible instead of "Loading preview..."
729
+ isReconnecting = true;
730
+
731
+ // Don't reset hasReceivedFirstFrame - keep showing last frame during reconnect
732
+
733
+ // Use reconnectToExistingStream which does NOT stop backend streaming
734
+ const success = await webCodecsService.reconnectToExistingStream(sessionId, canvasElement);
735
+
736
+ if (success) {
737
+ isWebCodecsActive = true;
738
+ isConnected = true;
739
+ activeStreamingSessionId = sessionId;
740
+ consecutiveFailures = 0;
741
+ startHealthCheck(true); // Skip resetting hasReceivedFirstFrame to keep overlay stable
742
+ debug.log('webcodecs', '✅ Fast reconnect successful');
743
+ } else {
744
+ throw new Error('Reconnect returned false');
745
+ }
746
+ } catch (error) {
747
+ debug.error('webcodecs', 'Fast reconnect failed:', error);
748
+ // Fall back to regular recovery on failure
749
+ consecutiveFailures++;
750
+ isStartingStream = false;
751
+ isReconnecting = false; // Reset on failure
752
+ attemptRecovery();
753
+ } finally {
754
+ isRecovering = false;
755
+ isStartingStream = false;
756
+ // Note: isReconnecting will be reset when first frame is received
757
+ }
758
+ }
759
+
760
+ // Stop WebCodecs streaming
761
+ async function stopStreaming() {
762
+ stopHealthCheck(); // Stop health monitoring
763
+ if (webCodecsService) {
764
+ await webCodecsService.stopStreaming();
765
+ isWebCodecsActive = false;
766
+ isConnected = false;
767
+ latencyMs = 0;
768
+ activeStreamingSessionId = null;
769
+ isStartingStream = false;
770
+ lastStartRequestId = null; // Clear to allow new requests
771
+ // Note: Don't reset hasReceivedFirstFrame here - let startStreaming do it
772
+ // This prevents flashing when switching tabs
773
+ // Clear canvas to prevent stale frames, BUT keep last frame during navigation
774
+ if (!isNavigating) {
775
+ clearCanvas();
776
+ } else {
777
+ debug.log('webcodecs', 'Skipping canvas clear during navigation - keeping last frame');
778
+ }
779
+ }
780
+ }
781
+
782
+ // Reactive setup when sessionInfo changes
783
+ $effect(() => {
784
+ if (sessionInfo && canvasElement) {
785
+ setupCanvasInternal();
786
+ }
787
+ });
788
+
789
+ // Track deviceSize and rotation changes to update canvas dimensions
790
+ // This is critical for hot-swap viewport changes without reconnection
791
+ $effect(() => {
792
+ if (canvasElement && sessionInfo) {
793
+ // Access reactive values to track changes
794
+ const currentDevice = deviceSize;
795
+ const currentRotation = rotation;
796
+
797
+ debug.log('webcodecs', `Device/rotation changed: ${currentDevice}/${currentRotation}, reconfiguring canvas`);
798
+ setupCanvasInternal();
799
+ }
800
+ });
801
+
802
+ // Start/restart streaming when session is ready
803
+ // This handles both initial start and session changes (viewport switch, etc.)
804
+ $effect(() => {
805
+ if (sessionId && canvasElement && sessionInfo) {
806
+ // Skip during fast reconnect - fastReconnect() handles this case
807
+ if (isReconnecting) {
808
+ debug.log('webcodecs', 'Skipping streaming effect - fast reconnect in progress');
809
+ return;
810
+ }
811
+
812
+ // Check if we need to start or restart streaming
813
+ const needsStreaming = !isWebCodecsActive || activeStreamingSessionId !== sessionId;
814
+
815
+ if (needsStreaming) {
816
+ // Clear canvas immediately when session changes to prevent stale frames
817
+ if (activeStreamingSessionId !== sessionId) {
818
+ clearCanvas();
819
+ hasReceivedFirstFrame = false; // Reset to show loading overlay
820
+ }
821
+
822
+ // Stop existing streaming first if session changed
823
+ // This ensures clean state before starting new stream
824
+ const doStartStreaming = async () => {
825
+ if (activeStreamingSessionId && activeStreamingSessionId !== sessionId) {
826
+ debug.log('webcodecs', `Session changed from ${activeStreamingSessionId} to ${sessionId}, stopping old stream first`);
827
+ await stopStreaming();
828
+ // Wait a bit for cleanup
829
+ await new Promise(resolve => setTimeout(resolve, 100));
830
+ }
831
+ await startStreaming();
832
+ };
833
+
834
+ // Longer delay to ensure backend session is fully ready
835
+ // This is especially important during viewport/device change
836
+ // when session is being recreated
837
+ const timeout = setTimeout(() => {
838
+ doStartStreaming();
839
+ }, 200);
840
+
841
+ return () => clearTimeout(timeout);
842
+ }
843
+ }
844
+ });
845
+
846
+ // Cleanup when sessionId is cleared
847
+ $effect(() => {
848
+ if (!sessionId && isWebCodecsActive) {
849
+ hasReceivedFirstFrame = false; // Reset loading state
850
+ stopStreaming();
851
+ }
852
+ });
853
+
854
+ // Setup event listeners when canvas is ready
855
+ $effect(() => {
856
+ if (canvasElement) {
857
+ const canvas = canvasElement;
858
+
859
+ canvas.addEventListener('dblclick', (e) => handleCanvasDoubleClick(e, canvas));
860
+ canvas.addEventListener('contextmenu', (e) => handleCanvasRightClick(e, canvas));
861
+ canvas.addEventListener('wheel', (e) => handleCanvasWheel(e, canvas), { passive: false });
862
+ canvas.addEventListener('keydown', handleCanvasKeydown);
863
+ canvas.addEventListener('mousedown', (e) => handleCanvasMouseDown(e, canvas));
864
+ canvas.addEventListener('mouseup', (e) => handleCanvasMouseUp(e, canvas));
865
+
866
+ let lastMoveTime = 0;
867
+ const handleMouseMove = (e: MouseEvent) => {
868
+ const now = Date.now();
869
+ // Low-end optimized throttle: reduced CPU usage
870
+ // 32ms hover = ~30fps, 16ms drag = ~60fps
871
+ const throttleMs = isDragging ? 16 : 32;
872
+ if (now - lastMoveTime >= throttleMs) {
873
+ lastMoveTime = now;
874
+ handleCanvasMouseMove(e, canvas);
875
+ }
876
+ };
877
+ canvas.addEventListener('mousemove', handleMouseMove);
878
+
879
+ canvas.addEventListener('mousedown', () => {
880
+ canvas.focus();
881
+ });
882
+
883
+ canvas.addEventListener('touchstart', (e) => handleTouchStart(e, canvas), { passive: false });
884
+ canvas.addEventListener('touchmove', (e) => handleTouchMove(e, canvas), { passive: false });
885
+ canvas.addEventListener('touchend', (e) => handleTouchEnd(e, canvas), { passive: false });
886
+
887
+ const handleMouseLeave = () => {
888
+ if (isMouseDown) {
889
+ // If drag was started, send mouseup before resetting
890
+ if (dragStarted) {
891
+ sendInteraction({
892
+ type: 'mouseup',
893
+ x: dragCurrentPos?.x || dragStartPos?.x || 0,
894
+ y: dragCurrentPos?.y || dragStartPos?.y || 0,
895
+ button: 'left'
896
+ });
897
+ }
898
+ isMouseDown = false;
899
+ isDragging = false;
900
+ dragStartPos = null;
901
+ dragCurrentPos = null;
902
+ dragStarted = false;
903
+ }
904
+ };
905
+ canvas.addEventListener('mouseleave', handleMouseLeave);
906
+
907
+ return () => {
908
+ canvas.removeEventListener('dblclick', (e) => handleCanvasDoubleClick(e, canvas));
909
+ canvas.removeEventListener('contextmenu', (e) => handleCanvasRightClick(e, canvas));
910
+ canvas.removeEventListener('wheel', (e) => handleCanvasWheel(e, canvas));
911
+ canvas.removeEventListener('keydown', handleCanvasKeydown);
912
+ canvas.removeEventListener('mousedown', (e) => handleCanvasMouseDown(e, canvas));
913
+ canvas.removeEventListener('mouseup', (e) => handleCanvasMouseUp(e, canvas));
914
+ canvas.removeEventListener('mousemove', handleMouseMove);
915
+ canvas.removeEventListener('touchstart', (e) => handleTouchStart(e, canvas));
916
+ canvas.removeEventListener('touchmove', (e) => handleTouchMove(e, canvas));
917
+ canvas.removeEventListener('touchend', (e) => handleTouchEnd(e, canvas));
918
+ };
919
+ }
920
+ });
921
+
922
+ // Touch event handlers
923
+ function handleTouchStart(event: TouchEvent, canvas: HTMLCanvasElement) {
924
+ if (!sessionId || event.touches.length === 0) return;
925
+ event.preventDefault();
926
+
927
+ const coords = getCanvasCoordinates(event, canvas);
928
+ isMouseDown = true;
929
+ mouseDownTime = Date.now();
930
+ dragStartPos = { x: coords.x, y: coords.y };
931
+ dragCurrentPos = { x: coords.x, y: coords.y };
932
+ dragStarted = false; // Reset drag started flag
933
+ }
934
+
935
+ function handleTouchMove(event: TouchEvent, canvas: HTMLCanvasElement) {
936
+ if (!sessionId || event.touches.length === 0 || !isMouseDown || !dragStartPos) return;
937
+ event.preventDefault();
938
+
939
+ const coords = getCanvasCoordinates(event, canvas);
940
+ dragCurrentPos = { x: coords.x, y: coords.y };
941
+
942
+ const dragDistance = Math.sqrt(
943
+ Math.pow(coords.x - dragStartPos.x, 2) + Math.pow(coords.y - dragStartPos.y, 2)
944
+ );
945
+
946
+ if (dragDistance > 15) {
947
+ // Send mousedown on first drag detection
948
+ if (!dragStarted) {
949
+ sendInteraction({
950
+ type: 'mousedown',
951
+ x: dragStartPos.x,
952
+ y: dragStartPos.y,
953
+ button: 'left'
954
+ });
955
+ dragStarted = true;
956
+ }
957
+
958
+ isDragging = true;
959
+ // Send mousemove to continue dragging (mouse is already down)
960
+ sendInteraction({
961
+ type: 'mousemove',
962
+ x: coords.x,
963
+ y: coords.y
964
+ });
965
+ }
966
+ }
967
+
968
+ function handleTouchEnd(event: TouchEvent, canvas: HTMLCanvasElement) {
969
+ if (!sessionId || !isMouseDown) return;
970
+ event.preventDefault();
971
+
972
+ if (dragStartPos) {
973
+ const endPos = dragCurrentPos || dragStartPos;
974
+
975
+ // If drag was started (mousedown was sent), send mouseup
976
+ if (dragStarted) {
977
+ sendInteraction({
978
+ type: 'mouseup',
979
+ x: endPos.x,
980
+ y: endPos.y,
981
+ button: 'left'
982
+ });
983
+ } else {
984
+ // No drag occurred, this is a tap/click
985
+ sendInteraction({ type: 'click', x: dragStartPos.x, y: dragStartPos.y });
986
+ }
987
+ }
988
+
989
+ isMouseDown = false;
990
+ isDragging = false;
991
+ dragStartPos = null;
992
+ dragCurrentPos = null;
993
+ dragStarted = false;
994
+ }
995
+
996
+ function getCanvasElement() {
997
+ return canvasElement;
998
+ }
999
+
1000
+ // Notify canvas that navigation has completed
1001
+ // This triggers fast reconnection if connection was lost during navigation
1002
+ async function notifyNavigationComplete() {
1003
+ debug.log('webcodecs', 'Navigation complete notification received');
1004
+ navigationJustCompleted = true;
1005
+
1006
+ // If connection was lost during navigation, trigger fast reconnection
1007
+ // Backend has already restarted streaming, just need to reconnect frontend
1008
+ if (webCodecsService && !webCodecsService.getConnectionStatus() && sessionId) {
1009
+ debug.log('webcodecs', 'Connection lost during navigation - triggering fast reconnection');
1010
+
1011
+ // Reset navigation state to allow normal error handling after reconnect
1012
+ webCodecsService.setNavigating(false);
1013
+
1014
+ // Small delay to ensure backend has restarted streaming
1015
+ await new Promise(resolve => setTimeout(resolve, 200));
1016
+
1017
+ // Restart streaming (this will reconnect to the new peer)
1018
+ lastStartRequestId = null; // Clear to allow new start request
1019
+ await startStreaming();
1020
+ }
1021
+ }
1022
+
1023
+ // Expose API methods to parent component
1024
+ $effect(() => {
1025
+ canvasAPI = {
1026
+ updateCanvasCursor,
1027
+ setupCanvas,
1028
+ getCanvasElement,
1029
+ // Streaming control
1030
+ startStreaming,
1031
+ stopStreaming,
1032
+ isActive: () => isWebCodecsActive,
1033
+ getStats: () => webCodecsService?.getStats() ?? null,
1034
+ getLatency: () => latencyMs,
1035
+ // Navigation handling
1036
+ notifyNavigationComplete
1037
+ };
1038
+ });
1039
+
1040
+ onDestroy(() => {
1041
+ stopHealthCheck(); // Stop health monitoring
1042
+ if (webCodecsService) {
1043
+ webCodecsService.destroy();
1044
+ webCodecsService = null;
1045
+ }
1046
+ activeStreamingSessionId = null;
1047
+ isStartingStream = false;
1048
+ lastStartRequestId = null;
1049
+ });
1050
+ </script>
1051
+
1052
+ <!-- Canvas - loading overlay is handled by parent PreviewContainer -->
1053
+ <canvas
1054
+ bind:this={canvasElement}
1055
+ class="w-full h-full object-contain"
1056
+ tabindex="0"
1057
+ style="cursor: default;"
1058
+ ></canvas>