@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,308 @@
1
+ import { join, extname } from 'path';
2
+ import { readFileWithEncoding, isTextFile } from '$shared/utils/file-type-detection';
3
+
4
+ import { debug } from '$shared/utils/logger';
5
+
6
+ // Bun-compatible readdir implementation (cross-platform)
7
+ async function readdir(path: string): Promise<string[]> {
8
+ let proc;
9
+ if (process.platform === 'win32') {
10
+ proc = Bun.spawn(['cmd', '/c', 'dir', '/b', path], { stdout: 'pipe', stderr: 'ignore' });
11
+ } else {
12
+ proc = Bun.spawn(['ls', '-1', path], { stdout: 'pipe', stderr: 'ignore' });
13
+ }
14
+ const result = await new Response(proc.stdout).text();
15
+ // Split and clean up, removing \r characters for Windows compatibility
16
+ return result.trim().split(/\r?\n/).map(line => line.trim()).filter(Boolean);
17
+ }
18
+
19
+ // Return types
20
+ export interface FileTreeNode {
21
+ name: string;
22
+ type: 'file' | 'directory';
23
+ path: string;
24
+ size?: number;
25
+ modified: string;
26
+ extension?: string;
27
+ children?: FileTreeNode[];
28
+ error?: string;
29
+ }
30
+
31
+ // Helper function to build file tree
32
+ export async function buildFileTree(
33
+ rootPath: string,
34
+ maxDepth: number = 3,
35
+ currentDepth: number = 0,
36
+ expandedPaths?: Set<string>
37
+ ): Promise<FileTreeNode | null> {
38
+ try {
39
+ const file = Bun.file(rootPath);
40
+ const stats = await file.stat();
41
+ const name = rootPath.split(/[/\\]/).pop() || 'root';
42
+
43
+ if (stats.isFile()) {
44
+ return {
45
+ name,
46
+ type: 'file',
47
+ path: rootPath,
48
+ size: stats.size,
49
+ modified: stats.mtime.toISOString(),
50
+ extension: extname(rootPath)
51
+ };
52
+ }
53
+
54
+ if (stats.isDirectory()) {
55
+ try {
56
+ const items = await readdir(rootPath);
57
+ const children: FileTreeNode[] = [];
58
+
59
+ // Check if this folder should load children:
60
+ // 1. Initial load (depth 0) - always load
61
+ // 2. Folder is in expandedPaths - load recursively
62
+ const shouldLoadChildren = currentDepth < 1 || (expandedPaths && expandedPaths.has(rootPath));
63
+
64
+ if (shouldLoadChildren) {
65
+ for (const item of items.slice(0, 100)) { // Limit to 100 items per directory
66
+ const itemPath = join(rootPath, item);
67
+
68
+ // Show all files and directories
69
+ try {
70
+ const itemFile = Bun.file(itemPath);
71
+ const itemStats = await itemFile.stat();
72
+
73
+ if (itemStats.isFile()) {
74
+ children.push({
75
+ name: item,
76
+ type: 'file',
77
+ path: itemPath,
78
+ size: itemStats.size,
79
+ modified: itemStats.mtime.toISOString(),
80
+ extension: extname(item)
81
+ });
82
+ } else if (itemStats.isDirectory()) {
83
+ // Check if this directory should be expanded
84
+ const isExpanded = expandedPaths && expandedPaths.has(itemPath);
85
+
86
+ if (isExpanded) {
87
+ // Recursively load children for expanded folders
88
+ const subTree = await buildFileTree(itemPath, maxDepth, currentDepth + 1, expandedPaths);
89
+ if (subTree) {
90
+ children.push(subTree);
91
+ }
92
+ } else {
93
+ // For collapsed directories, just add basic info without loading children
94
+ children.push({
95
+ name: item,
96
+ type: 'directory',
97
+ path: itemPath,
98
+ modified: itemStats.mtime.toISOString(),
99
+ // Add empty children array to indicate it can be expanded
100
+ children: []
101
+ });
102
+ }
103
+ }
104
+ } catch (error) {
105
+ debug.debug('file', `Cannot access ${itemPath}:`, error);
106
+ }
107
+ }
108
+ }
109
+
110
+ // Sort children: directories first, then files
111
+ children.sort((a, b) => {
112
+ if (a.type !== b.type) {
113
+ return a.type === 'directory' ? -1 : 1;
114
+ }
115
+ return a.name.localeCompare(b.name);
116
+ });
117
+
118
+ return {
119
+ name,
120
+ type: 'directory',
121
+ path: rootPath,
122
+ children,
123
+ modified: stats.mtime.toISOString()
124
+ };
125
+ } catch {
126
+ // Permission denied or other error reading directory
127
+ return {
128
+ name,
129
+ type: 'directory',
130
+ path: rootPath,
131
+ error: 'Permission denied',
132
+ modified: stats.mtime.toISOString()
133
+ };
134
+ }
135
+ }
136
+
137
+ return null;
138
+ } catch (error) {
139
+ debug.error('file', `Error building file tree for ${rootPath}:`, error);
140
+ return null;
141
+ }
142
+ }
143
+
144
+ // Helper function to list directory contents
145
+ export async function listDirectoryContents(dirPath: string): Promise<FileTreeNode[]> {
146
+ // Use stat directly instead of Bun.file for directory checking
147
+ const dirFile = Bun.file(dirPath);
148
+ const stats = await dirFile.stat();
149
+
150
+ if (!stats.isDirectory()) {
151
+ throw new Error('Path is not a directory');
152
+ }
153
+
154
+ const items = await readdir(dirPath);
155
+ const children: FileTreeNode[] = [];
156
+
157
+ // Show all files and directories
158
+ for (const item of items.slice(0, 100)) { // Limit to 100 items per directory
159
+
160
+ const itemPath = join(dirPath, item);
161
+
162
+ try {
163
+ const itemFile = Bun.file(itemPath);
164
+ const itemStats = await itemFile.stat();
165
+
166
+ if (itemStats.isFile()) {
167
+ children.push({
168
+ name: item,
169
+ type: 'file',
170
+ path: itemPath,
171
+ size: itemStats.size,
172
+ modified: itemStats.mtime.toISOString(),
173
+ extension: extname(item)
174
+ });
175
+ } else if (itemStats.isDirectory()) {
176
+ children.push({
177
+ name: item,
178
+ type: 'directory',
179
+ path: itemPath,
180
+ modified: itemStats.mtime.toISOString(),
181
+ // Add empty children array to indicate it can be expanded
182
+ children: []
183
+ });
184
+ }
185
+ } catch (error) {
186
+ debug.debug('file', `Cannot access ${itemPath}:`, error);
187
+ }
188
+ }
189
+
190
+ // Sort children: directories first, then files
191
+ children.sort((a, b) => {
192
+ if (a.type !== b.type) {
193
+ return a.type === 'directory' ? -1 : 1;
194
+ }
195
+ return a.name.localeCompare(b.name);
196
+ });
197
+
198
+ return children;
199
+ }
200
+
201
+ // File content data
202
+ export interface FileContentData {
203
+ content: string;
204
+ size: number;
205
+ modified: string;
206
+ extension: string;
207
+ encoding?: string;
208
+ isBinary?: boolean;
209
+ error?: string;
210
+ }
211
+
212
+ // Helper function to read file contents
213
+ export async function readFileContents(filePath: string): Promise<FileContentData> {
214
+ const file = Bun.file(filePath);
215
+ if (!(await file.exists())) {
216
+ throw new Error('File does not exist');
217
+ }
218
+
219
+ const stats = await file.stat();
220
+
221
+ // Check if it's a text file
222
+ const isText = await isTextFile(filePath);
223
+
224
+ if (!isText) {
225
+ // Binary file
226
+ const buffer = Buffer.from(await file.arrayBuffer());
227
+ return {
228
+ content: `[Binary file - ${buffer.length} bytes]`,
229
+ size: stats.size,
230
+ modified: stats.mtime.toISOString(),
231
+ extension: extname(filePath),
232
+ isBinary: true
233
+ };
234
+ }
235
+
236
+ // Text file - detect encoding and read
237
+ const { content, detectedEncoding } = await readFileWithEncoding(filePath);
238
+
239
+ return {
240
+ content,
241
+ size: stats.size,
242
+ modified: stats.mtime.toISOString(),
243
+ extension: extname(filePath),
244
+ encoding: detectedEncoding
245
+ };
246
+ }
247
+
248
+ // Helper function to search files
249
+ export async function searchFiles(rootPath: string, query: string): Promise<FileTreeNode[]> {
250
+ const results: FileTreeNode[] = [];
251
+
252
+ async function searchRecursive(dirPath: string, depth: number = 0) {
253
+ // Limit search depth to prevent infinite recursion
254
+ if (depth > 5) return;
255
+
256
+ try {
257
+ const items = await readdir(dirPath);
258
+
259
+ for (const item of items) {
260
+ // Skip common directories that shouldn't be searched
261
+ const skipDirs = ['node_modules', '.git', '.svelte-kit', 'build', 'dist', 'coverage', '.next', '.nuxt', 'target'];
262
+ if (skipDirs.includes(item)) continue;
263
+
264
+ const itemPath = join(dirPath, item);
265
+
266
+ try {
267
+ const itemFile = Bun.file(itemPath);
268
+ const stats = await itemFile.stat();
269
+
270
+ // Check if item name matches query (case insensitive)
271
+ if (item.toLowerCase().includes(query.toLowerCase())) {
272
+ results.push({
273
+ name: item,
274
+ type: stats.isDirectory() ? 'directory' : 'file',
275
+ path: itemPath,
276
+ size: stats.isFile() ? stats.size : undefined,
277
+ modified: stats.mtime.toISOString(),
278
+ extension: stats.isFile() ? extname(item) : undefined
279
+ });
280
+ }
281
+
282
+ // Recursively search in directories
283
+ if (stats.isDirectory()) {
284
+ await searchRecursive(itemPath, depth + 1);
285
+ }
286
+ } catch (error) {
287
+ // Skip items we can't access
288
+ debug.debug('file', `Cannot access ${itemPath}:`, error);
289
+ }
290
+ }
291
+ } catch (error) {
292
+ debug.debug('file', `Cannot read directory ${dirPath}:`, error);
293
+ }
294
+ }
295
+
296
+ await searchRecursive(rootPath);
297
+
298
+ // Sort results: directories first, then files, both alphabetically
299
+ results.sort((a, b) => {
300
+ if (a.type !== b.type) {
301
+ return a.type === 'directory' ? -1 : 1;
302
+ }
303
+ return a.name.localeCompare(b.name);
304
+ });
305
+
306
+ // Limit results to prevent overwhelming the UI
307
+ return results.slice(0, 100);
308
+ }
@@ -0,0 +1,383 @@
1
+ /**
2
+ * File Watcher Service
3
+ *
4
+ * Real-time file system watcher using Node's fs.watch (compatible with Bun)
5
+ * Features:
6
+ * - Per-project watcher management
7
+ * - Debounced events to prevent spam
8
+ * - Automatic cleanup on unwatch
9
+ * - Cross-platform support (Windows/Unix)
10
+ */
11
+
12
+ import { watch, type FSWatcher, existsSync } from 'node:fs';
13
+ import { stat } from 'node:fs/promises';
14
+ import { join, relative, normalize, sep } from 'node:path';
15
+ import { ws } from '$backend/lib/utils/ws';
16
+ import { debug } from '$shared/utils/logger';
17
+ import type { FileChange } from '$shared/types/filesystem';
18
+
19
+ /**
20
+ * Debounce configuration
21
+ */
22
+ const DEBOUNCE_MS = 300; // Debounce file change events
23
+
24
+ /**
25
+ * Directories to ignore when watching
26
+ */
27
+ const IGNORED_DIRS = new Set([
28
+ 'node_modules',
29
+ '.git',
30
+ '.svelte-kit',
31
+ 'dist',
32
+ 'build',
33
+ '.next',
34
+ '.nuxt',
35
+ '.output',
36
+ '__pycache__',
37
+ '.pytest_cache',
38
+ 'coverage',
39
+ '.nyc_output',
40
+ '.turbo',
41
+ '.cache',
42
+ '.temp',
43
+ '.tmp',
44
+ 'vendor'
45
+ ]);
46
+
47
+ /**
48
+ * Files to ignore
49
+ */
50
+ const IGNORED_FILES = new Set([
51
+ '.DS_Store',
52
+ 'Thumbs.db',
53
+ '.gitkeep',
54
+ '.gitignore~'
55
+ ]);
56
+
57
+ /**
58
+ * Git state files to watch for external git operations
59
+ */
60
+ const GIT_STATE_FILES = ['index', 'HEAD', 'MERGE_HEAD', 'REBASE_HEAD'];
61
+ const GIT_DEBOUNCE_MS = 500;
62
+
63
+ /**
64
+ * Watcher instance for a project
65
+ */
66
+ interface ProjectWatcher {
67
+ watcher: FSWatcher;
68
+ projectPath: string;
69
+ projectId: string;
70
+ debounceTimer: ReturnType<typeof setTimeout> | null;
71
+ pendingChanges: Map<string, FileChange>;
72
+ subWatchers: Map<string, FSWatcher>;
73
+ gitWatcher: FSWatcher | null;
74
+ gitRefsWatcher: FSWatcher | null;
75
+ gitDebounceTimer: ReturnType<typeof setTimeout> | null;
76
+ }
77
+
78
+ /**
79
+ * File Watcher Manager
80
+ * Manages file watchers per project
81
+ */
82
+ class FileWatcherManager {
83
+ private watchers = new Map<string, ProjectWatcher>();
84
+
85
+ /**
86
+ * Start watching a project directory
87
+ */
88
+ async startWatching(projectId: string, projectPath: string): Promise<boolean> {
89
+ // Already watching this project
90
+ if (this.watchers.has(projectId)) {
91
+ debug.log('file', `Already watching project: ${projectId}`);
92
+ return true;
93
+ }
94
+
95
+ try {
96
+ // Normalize path
97
+ const normalizedPath = normalize(projectPath);
98
+
99
+ // Verify path exists and is a directory
100
+ const pathStat = await stat(normalizedPath);
101
+ if (!pathStat.isDirectory()) {
102
+ debug.error('file', `Path is not a directory: ${normalizedPath}`);
103
+ return false;
104
+ }
105
+
106
+ // Create main watcher for project root
107
+ const watcher = watch(normalizedPath, { recursive: true }, (eventType, filename) => {
108
+ if (filename) {
109
+ this.handleFileChange(projectId, normalizedPath, filename, eventType);
110
+ }
111
+ });
112
+
113
+ // Handle watcher errors
114
+ watcher.on('error', (error) => {
115
+ debug.error('file', `Watcher error for project ${projectId}:`, error);
116
+ // Try to emit error to clients
117
+ ws.emit.project(projectId, 'files:watch-error', {
118
+ projectId,
119
+ error: error.message || 'File watcher error'
120
+ });
121
+ });
122
+
123
+ // Store watcher instance
124
+ const projectWatcher: ProjectWatcher = {
125
+ watcher,
126
+ projectPath: normalizedPath,
127
+ projectId,
128
+ debounceTimer: null,
129
+ pendingChanges: new Map(),
130
+ subWatchers: new Map(),
131
+ gitWatcher: null,
132
+ gitRefsWatcher: null,
133
+ gitDebounceTimer: null
134
+ };
135
+ this.watchers.set(projectId, projectWatcher);
136
+
137
+ // Start git state watcher (for external git operations)
138
+ this.startGitWatcher(projectId, normalizedPath);
139
+
140
+ debug.log('file', `Started watching project: ${projectId} at ${normalizedPath}`);
141
+ return true;
142
+ } catch (error) {
143
+ debug.error('file', `Failed to start watching project ${projectId}:`, error);
144
+ return false;
145
+ }
146
+ }
147
+
148
+ /**
149
+ * Stop watching a project directory
150
+ */
151
+ stopWatching(projectId: string): boolean {
152
+ const projectWatcher = this.watchers.get(projectId);
153
+ if (!projectWatcher) {
154
+ debug.log('file', `Not watching project: ${projectId}`);
155
+ return false;
156
+ }
157
+
158
+ try {
159
+ // Close main watcher
160
+ projectWatcher.watcher.close();
161
+
162
+ // Close all sub-watchers
163
+ for (const subWatcher of projectWatcher.subWatchers.values()) {
164
+ subWatcher.close();
165
+ }
166
+
167
+ // Close git watchers
168
+ projectWatcher.gitWatcher?.close();
169
+ projectWatcher.gitRefsWatcher?.close();
170
+
171
+ // Clear debounce timers
172
+ if (projectWatcher.debounceTimer) {
173
+ clearTimeout(projectWatcher.debounceTimer);
174
+ }
175
+ if (projectWatcher.gitDebounceTimer) {
176
+ clearTimeout(projectWatcher.gitDebounceTimer);
177
+ }
178
+
179
+ // Remove from map
180
+ this.watchers.delete(projectId);
181
+
182
+ debug.log('file', `Stopped watching project: ${projectId}`);
183
+ return true;
184
+ } catch (error) {
185
+ debug.error('file', `Error stopping watcher for project ${projectId}:`, error);
186
+ return false;
187
+ }
188
+ }
189
+
190
+ /**
191
+ * Check if a project is being watched
192
+ */
193
+ isWatching(projectId: string): boolean {
194
+ return this.watchers.has(projectId);
195
+ }
196
+
197
+ /**
198
+ * Get all watched project IDs
199
+ */
200
+ getWatchedProjects(): string[] {
201
+ return Array.from(this.watchers.keys());
202
+ }
203
+
204
+ /**
205
+ * Handle file change event
206
+ */
207
+ private async handleFileChange(
208
+ projectId: string,
209
+ projectPath: string,
210
+ filename: string,
211
+ eventType: string
212
+ ): Promise<void> {
213
+ const projectWatcher = this.watchers.get(projectId);
214
+ if (!projectWatcher) return;
215
+
216
+ // Normalize filename to use forward slashes for consistency
217
+ const normalizedFilename = filename.replace(/\\/g, '/');
218
+
219
+ // Check if should be ignored
220
+ if (this.shouldIgnore(normalizedFilename)) {
221
+ return;
222
+ }
223
+
224
+ // Build full path
225
+ const fullPath = join(projectPath, filename);
226
+
227
+ // Determine change type
228
+ let changeType: 'created' | 'modified' | 'deleted';
229
+ if (eventType === 'change') {
230
+ changeType = 'modified';
231
+ } else {
232
+ // 'rename' event — check if file exists to distinguish create from delete
233
+ try {
234
+ await stat(fullPath);
235
+ changeType = 'created';
236
+ } catch {
237
+ changeType = 'deleted';
238
+ }
239
+ }
240
+
241
+ // Create file change object
242
+ const fileChange: FileChange = {
243
+ path: fullPath,
244
+ type: changeType,
245
+ timestamp: new Date().toISOString()
246
+ };
247
+
248
+ // Add to pending changes (using path as key to dedupe)
249
+ projectWatcher.pendingChanges.set(fullPath, fileChange);
250
+
251
+ // Debounce: clear existing timer and set new one
252
+ if (projectWatcher.debounceTimer) {
253
+ clearTimeout(projectWatcher.debounceTimer);
254
+ }
255
+
256
+ projectWatcher.debounceTimer = setTimeout(() => {
257
+ this.flushPendingChanges(projectId);
258
+ }, DEBOUNCE_MS);
259
+ }
260
+
261
+ /**
262
+ * Check if a file/directory should be ignored
263
+ */
264
+ private shouldIgnore(filename: string): boolean {
265
+ const parts = filename.split('/');
266
+
267
+ // Check each path segment
268
+ for (const part of parts) {
269
+ if (IGNORED_DIRS.has(part) || IGNORED_FILES.has(part)) {
270
+ return true;
271
+ }
272
+ // Ignore hidden files and directories (except .env files)
273
+ if (part.startsWith('.') && !part.startsWith('.env')) {
274
+ return true;
275
+ }
276
+ }
277
+
278
+ return false;
279
+ }
280
+
281
+
282
+ /**
283
+ * Flush pending changes to clients
284
+ */
285
+ private flushPendingChanges(projectId: string): void {
286
+ const projectWatcher = this.watchers.get(projectId);
287
+ if (!projectWatcher || projectWatcher.pendingChanges.size === 0) return;
288
+
289
+ // Convert pending changes to array
290
+ const changes = Array.from(projectWatcher.pendingChanges.values());
291
+
292
+ // Clear pending changes
293
+ projectWatcher.pendingChanges.clear();
294
+ projectWatcher.debounceTimer = null;
295
+
296
+ // Emit changes to users currently viewing the project
297
+ ws.emit.project(projectId, 'files:changed', {
298
+ projectId,
299
+ changes,
300
+ timestamp: Date.now()
301
+ });
302
+
303
+ debug.log(
304
+ 'file',
305
+ `Emitted ${changes.length} file changes for project ${projectId}`
306
+ );
307
+ }
308
+
309
+ /**
310
+ * Start watching .git directory for external git operations
311
+ * Watches: .git/index (staging), .git/HEAD (branch switch), .git/refs/ (branches/tags)
312
+ */
313
+ private startGitWatcher(projectId: string, projectPath: string): void {
314
+ const projectWatcher = this.watchers.get(projectId);
315
+ if (!projectWatcher) return;
316
+
317
+ const gitDir = join(projectPath, '.git');
318
+ if (!existsSync(gitDir)) return;
319
+
320
+ try {
321
+ // Watch .git/ root for index, HEAD, MERGE_HEAD changes
322
+ projectWatcher.gitWatcher = watch(gitDir, (eventType, filename) => {
323
+ if (!filename) return;
324
+ const normalized = filename.replace(/\\/g, '/');
325
+ if (GIT_STATE_FILES.includes(normalized)) {
326
+ this.emitGitChanged(projectId);
327
+ }
328
+ });
329
+ projectWatcher.gitWatcher.on('error', () => {
330
+ // Silently ignore git watcher errors
331
+ });
332
+
333
+ // Watch .git/refs/ for branch/tag create/delete
334
+ const refsDir = join(gitDir, 'refs');
335
+ if (existsSync(refsDir)) {
336
+ projectWatcher.gitRefsWatcher = watch(refsDir, { recursive: true }, (_eventType, _filename) => {
337
+ this.emitGitChanged(projectId);
338
+ });
339
+ projectWatcher.gitRefsWatcher.on('error', () => {
340
+ // Silently ignore refs watcher errors
341
+ });
342
+ }
343
+
344
+ debug.log('file', `Started git watcher for project: ${projectId}`);
345
+ } catch (err) {
346
+ debug.warn('file', `Failed to start git watcher for project ${projectId}:`, err);
347
+ }
348
+ }
349
+
350
+ /**
351
+ * Debounced emit of git:changed event
352
+ */
353
+ private emitGitChanged(projectId: string): void {
354
+ const projectWatcher = this.watchers.get(projectId);
355
+ if (!projectWatcher) return;
356
+
357
+ if (projectWatcher.gitDebounceTimer) {
358
+ clearTimeout(projectWatcher.gitDebounceTimer);
359
+ }
360
+
361
+ projectWatcher.gitDebounceTimer = setTimeout(() => {
362
+ projectWatcher.gitDebounceTimer = null;
363
+ ws.emit.project(projectId, 'git:changed', {
364
+ projectId,
365
+ timestamp: Date.now()
366
+ });
367
+ debug.log('file', `Emitted git:changed for project ${projectId}`);
368
+ }, GIT_DEBOUNCE_MS);
369
+ }
370
+
371
+ /**
372
+ * Stop all watchers (cleanup)
373
+ */
374
+ stopAll(): void {
375
+ for (const projectId of this.watchers.keys()) {
376
+ this.stopWatching(projectId);
377
+ }
378
+ debug.log('file', 'Stopped all file watchers');
379
+ }
380
+ }
381
+
382
+ // Export singleton instance
383
+ export const fileWatcher = new FileWatcherManager();