@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,843 @@
1
+ <script lang="ts">
2
+ // import { onMount } from 'svelte';
3
+ import Button from './Button.svelte';
4
+ import Icon from '$frontend/lib/components/common/Icon.svelte';
5
+ import Modal from './Modal.svelte';
6
+ import Dialog from './Dialog.svelte';
7
+ import { debug } from '$shared/utils/logger';
8
+ import ws from '$frontend/lib/utils/ws';
9
+ import { settings } from '$frontend/lib/stores/features/settings.svelte';
10
+
11
+ interface FileItem {
12
+ name: string;
13
+ type: 'file' | 'directory';
14
+ path: string;
15
+ modified?: string;
16
+ children?: FileItem[];
17
+ error?: string;
18
+ }
19
+
20
+ interface Props {
21
+ isOpen: boolean;
22
+ onClose: () => void;
23
+ onSelect: (folderPath: string, folderName: string) => void;
24
+ currentProjectPath?: string;
25
+ }
26
+
27
+ let { isOpen = $bindable(), onClose, onSelect, currentProjectPath }: Props = $props();
28
+
29
+ let currentPath = $state('');
30
+ let items: FileItem[] = $state([]);
31
+ let loading = $state(false);
32
+ let showLoadingSpinner = $state(false);
33
+ let error = $state('');
34
+ let selectedPath = $state('');
35
+ let manualPath = $state('');
36
+ let availableDrives: FileItem[] = $state([]);
37
+ let recentProjects: string[] = $state([]);
38
+ let loadingTimeout: number | null = null;
39
+ let showCreateFolder = $state(false);
40
+ let newFolderName = $state('');
41
+ let showDeleteFolder = $state(false);
42
+ let folderToDelete: FileItem | null = $state(null);
43
+ let deleteFolderConfirmName = $state('');
44
+
45
+ // Derived: whether directory access is restricted
46
+ const hasRestrictions = $derived(settings.allowedBasePaths && settings.allowedBasePaths.length > 0);
47
+
48
+ // Detect backend OS from current path (drive letter = Windows)
49
+ const isWindows = $derived(/^[A-Za-z]:/.test(currentPath));
50
+
51
+ // OS-appropriate placeholder for the path input
52
+ const pathPlaceholder = $derived(
53
+ isWindows ? 'e.g. C:\\Users\\username' : 'e.g. /home/username'
54
+ );
55
+
56
+ // Normalize path separators to forward slash and strip trailing slashes
57
+ function normalizePath(p: string): string {
58
+ let n = p.replace(/\\/g, '/');
59
+ // Keep trailing slash only for drive roots like "C:/"
60
+ if (n.length > 1 && !n.match(/^[A-Za-z]:\/$/)) {
61
+ n = n.replace(/\/+$/, '');
62
+ }
63
+ return n;
64
+ }
65
+
66
+ // Compare two paths, case-insensitively on Windows paths
67
+ function pathsEqual(a: string, b: string): boolean {
68
+ const na = normalizePath(a);
69
+ const nb = normalizePath(b);
70
+ // Windows paths start with a drive letter
71
+ if (/^[A-Za-z]:/.test(na) || /^[A-Za-z]:/.test(nb)) {
72
+ return na.toLowerCase() === nb.toLowerCase();
73
+ }
74
+ return na === nb;
75
+ }
76
+
77
+ // Check if a path is within a base path
78
+ function isWithinBase(path: string, base: string): boolean {
79
+ const np = normalizePath(path);
80
+ const nb = normalizePath(base);
81
+ const isWindows = /^[A-Za-z]:/.test(np) || /^[A-Za-z]:/.test(nb);
82
+ if (isWindows) {
83
+ const npl = np.toLowerCase();
84
+ const nbl = nb.toLowerCase();
85
+ return npl === nbl || npl.startsWith(nbl + '/');
86
+ }
87
+ return np === nb || np.startsWith(nb + '/');
88
+ }
89
+
90
+ // Check if a path is accessible (within allowed base paths)
91
+ function isPathAllowed(path: string): boolean {
92
+ if (!settings.allowedBasePaths || settings.allowedBasePaths.length === 0) return true;
93
+ return settings.allowedBasePaths.some(base => isWithinBase(path, base));
94
+ }
95
+
96
+ // Check if current path is at the restriction boundary (cannot go up)
97
+ const atRestrictionBoundary = $derived(
98
+ hasRestrictions && settings.allowedBasePaths.some(base => pathsEqual(currentPath, base))
99
+ );
100
+
101
+ // Get available drives/mount points for all platforms
102
+ async function loadAvailableDrives() {
103
+ try {
104
+ const data = await ws.http('files:browse-path', { path: 'drives' });
105
+ if (data.children) {
106
+ availableDrives = data.children as FileItem[];
107
+ }
108
+ } catch {
109
+ debug.warn('session', 'Failed to get available drives/mount points');
110
+ availableDrives = [];
111
+ }
112
+ }
113
+
114
+ // Get recent projects from database
115
+ async function loadRecentProjects() {
116
+ try {
117
+ const projects = await ws.http('projects:list', {});
118
+ if (projects) {
119
+ // Extract paths from projects, sorted by last_opened_at DESC
120
+ recentProjects = projects.map((project: any) => project.path);
121
+ }
122
+ } catch {
123
+ debug.warn('session', 'Failed to get recent projects');
124
+ recentProjects = [];
125
+ }
126
+ }
127
+
128
+ // Get user's home directory or current working directory
129
+ async function getInitialPath(): Promise<string> {
130
+ // If restrictions are set, start at the first allowed base path
131
+ if (settings.allowedBasePaths && settings.allowedBasePaths.length > 0) {
132
+ return settings.allowedBasePaths[0];
133
+ }
134
+
135
+ try {
136
+ const data = await ws.http('files:browse-path', { path: 'home' });
137
+ if (data.path) {
138
+ return data.path;
139
+ }
140
+ } catch {
141
+ debug.warn('session', 'Failed to get home directory, using current working directory');
142
+ }
143
+
144
+ // Fallback to current working directory
145
+ try {
146
+ const data = await ws.http('files:browse-path', { path: '.' });
147
+ if (data.path) {
148
+ return data.path;
149
+ }
150
+ } catch {
151
+ debug.warn('session', 'Failed to get current directory');
152
+ }
153
+
154
+ // Platform-specific fallback
155
+ if (typeof window !== 'undefined') {
156
+ if (navigator.userAgent.includes('Windows')) {
157
+ return 'C:\\';
158
+ } else {
159
+ // Unix-like systems (Linux, macOS, etc.)
160
+ return '/';
161
+ }
162
+ }
163
+
164
+ // Final fallback
165
+ return '/';
166
+ }
167
+
168
+ // Check if a folder path is a recent project
169
+ function isRecentProject(folderPath: string): boolean {
170
+ return recentProjects.includes(folderPath);
171
+ }
172
+
173
+ // Check if a folder path is the active project
174
+ function isActiveProject(folderPath: string): boolean {
175
+ return currentProjectPath !== undefined && currentProjectPath === folderPath;
176
+ }
177
+
178
+ // Navigate to a specific common location
179
+ async function navigateToLocation(location: 'home' | 'cwd' | 'drives' | string) {
180
+ if (location === 'home') {
181
+ const path = await getInitialPath();
182
+ loadDirectory(path);
183
+ } else if (location === 'cwd') {
184
+ loadDirectory('.');
185
+ } else if (location === 'drives') {
186
+ loadDirectory('drives');
187
+ } else {
188
+ // Normalize path separators to match backend OS.
189
+ // Use currentPath to detect OS if available, else detect from the path itself.
190
+ let path = location;
191
+ const backendIsWindows = currentPath
192
+ ? /^[A-Za-z]:/.test(currentPath)
193
+ : /^[A-Za-z]:/.test(path);
194
+ if (backendIsWindows) {
195
+ path = path.replace(/\//g, '\\');
196
+ } else {
197
+ path = path.replace(/\\/g, '/');
198
+ }
199
+ loadDirectory(path);
200
+ }
201
+ }
202
+
203
+ async function loadDirectory(path: string) {
204
+ loading = true;
205
+ error = '';
206
+
207
+ // Clear any existing timeout
208
+ if (loadingTimeout) {
209
+ clearTimeout(loadingTimeout);
210
+ loadingTimeout = null;
211
+ }
212
+
213
+ // Only show loading spinner after 150ms to prevent flickering for fast operations
214
+ loadingTimeout = setTimeout(() => {
215
+ if (loading) {
216
+ showLoadingSpinner = true;
217
+ }
218
+ }, 150) as unknown as number;
219
+
220
+ try {
221
+ // ws.http() now unwraps response - returns data directly or throws error
222
+ const fileData = await ws.http('files:browse-path', { path });
223
+
224
+ currentPath = fileData.path;
225
+
226
+ // Enforce access restrictions
227
+ if (!isPathAllowed(currentPath)) {
228
+ error = `Access restricted. Allowed paths: ${settings.allowedBasePaths.join(', ')}`;
229
+ items = [];
230
+ return;
231
+ }
232
+
233
+ // Auto-select current directory when loading
234
+ selectedPath = fileData.path;
235
+
236
+ if (fileData.type === 'directory' && fileData.children) {
237
+ // Filter to show only directories and common project files
238
+ items = (fileData.children as FileItem[])
239
+ .sort((a, b) => {
240
+ // Directories first, then files
241
+ if (a.type !== b.type) {
242
+ return a.type === 'directory' ? -1 : 1;
243
+ }
244
+ return a.name.localeCompare(b.name);
245
+ });
246
+ } else {
247
+ items = [];
248
+ }
249
+ } catch (err) {
250
+ error = err instanceof Error ? err.message : 'Failed to load directory';
251
+ items = [];
252
+ } finally {
253
+ loading = false;
254
+ showLoadingSpinner = false;
255
+
256
+ // Clear timeout if still pending
257
+ if (loadingTimeout) {
258
+ clearTimeout(loadingTimeout);
259
+ loadingTimeout = null;
260
+ }
261
+ }
262
+ }
263
+
264
+ function navigateToParent() {
265
+ // Special case: if we're viewing drives list
266
+ if (currentPath === 'drives') {
267
+ return; // Can't go higher than drives list
268
+ }
269
+
270
+ // Handle both Windows and Unix paths properly
271
+ const pathSeparator = currentPath.includes('\\') ? '\\' : '/';
272
+ const pathParts = currentPath.split(pathSeparator).filter(part => part !== '');
273
+
274
+ if (pathParts.length === 0) {
275
+ // Already at root, can't go higher
276
+ return;
277
+ }
278
+
279
+ if (pathParts.length === 1) {
280
+ // We're at a drive root like "C:" on Windows, go to drives list
281
+ if (pathSeparator === '\\' && pathParts[0].match(/^[A-Z]:$/)) {
282
+ navigateToLocation('drives');
283
+ return;
284
+ }
285
+ // On Unix, we're at root
286
+ loadDirectory('/');
287
+ return;
288
+ }
289
+
290
+ // Remove last part and rejoin
291
+ pathParts.pop();
292
+ let parentPath = pathParts.join(pathSeparator);
293
+
294
+ // Ensure proper format
295
+ if (pathSeparator === '\\') {
296
+ // Windows: ensure we have drive letter with backslash
297
+ if (pathParts.length === 1 && pathParts[0].match(/^[A-Z]:$/)) {
298
+ parentPath = pathParts[0] + '\\';
299
+ } else if (!parentPath.endsWith('\\')) {
300
+ parentPath = pathParts.join(pathSeparator) + '\\';
301
+ }
302
+ } else {
303
+ // Unix: ensure we start with /
304
+ if (!parentPath.startsWith('/')) {
305
+ parentPath = '/' + parentPath;
306
+ }
307
+ }
308
+
309
+ loadDirectory(parentPath);
310
+ }
311
+
312
+ function navigateToFolder(folderPath: string) {
313
+ loadDirectory(folderPath);
314
+ // Note: selectedPath is now auto-set in loadDirectory
315
+ }
316
+
317
+ function selectFolder(folderPath: string) {
318
+ selectedPath = folderPath;
319
+ }
320
+
321
+ function confirmSelection() {
322
+ if (selectedPath) {
323
+ // Handle both Windows and Unix paths properly
324
+ const pathSeparator = selectedPath.includes('\\') ? '\\' : '/';
325
+ const folderName = selectedPath.split(pathSeparator).pop() || selectedPath;
326
+ onSelect(selectedPath, folderName);
327
+ }
328
+ }
329
+
330
+
331
+ function navigateToManualPath() {
332
+ if (manualPath.trim()) {
333
+ let path = manualPath.trim();
334
+ // Normalize path separators to match backend OS
335
+ if (isWindows) {
336
+ path = path.replace(/\//g, '\\');
337
+ } else {
338
+ path = path.replace(/\\/g, '/');
339
+ }
340
+ loadDirectory(path);
341
+ }
342
+ }
343
+
344
+ function selectCurrentFolder() {
345
+ if (currentPath) {
346
+ selectFolder(currentPath);
347
+ }
348
+ }
349
+
350
+ function handleManualPathKeydown(event: KeyboardEvent) {
351
+ if (event.key === 'Enter') {
352
+ navigateToManualPath();
353
+ }
354
+ }
355
+
356
+ function handleKeydown(event: KeyboardEvent) {
357
+ if (event.key === 'Escape') {
358
+ if (showCreateFolder) {
359
+ showCreateFolder = false;
360
+ newFolderName = '';
361
+ } else {
362
+ onClose();
363
+ }
364
+ }
365
+ }
366
+
367
+ async function createNewFolder() {
368
+ if (!newFolderName.trim()) return;
369
+
370
+ const folderPath = currentPath.includes('\\')
371
+ ? `${currentPath}\\${newFolderName.trim()}`
372
+ : `${currentPath}/${newFolderName.trim()}`;
373
+
374
+ try {
375
+ // Create folder via WebSocket
376
+ const data = await ws.http('files:create-directory', {
377
+ dirPath: folderPath
378
+ });
379
+
380
+ // Store current path before clearing dialog state
381
+ const pathToReload = currentPath;
382
+
383
+ // Close create folder dialog
384
+ showCreateFolder = false;
385
+ newFolderName = '';
386
+
387
+ // Add a small delay to ensure file system operation is complete
388
+ await new Promise(resolve => setTimeout(resolve, 100));
389
+
390
+ // Reload current directory using the main loadDirectory function
391
+ if (pathToReload) {
392
+ loadDirectory(pathToReload);
393
+ } else {
394
+ // Fallback to current path if somehow it's empty
395
+ const fallbackPath = currentPath || '.';
396
+ loadDirectory(fallbackPath);
397
+ }
398
+
399
+ // Select the new folder after reload starts
400
+ selectFolder(folderPath);
401
+ } catch (err) {
402
+ error = err instanceof Error ? err.message : 'Failed to create folder';
403
+ }
404
+ }
405
+
406
+ function openDeleteDialog(item: FileItem) {
407
+ folderToDelete = item;
408
+ deleteFolderConfirmName = '';
409
+ showDeleteFolder = true;
410
+ }
411
+
412
+ async function deleteFolder() {
413
+ if (!folderToDelete || deleteFolderConfirmName !== folderToDelete.name) return;
414
+
415
+ try {
416
+ const folderPath = folderToDelete.path;
417
+
418
+ // Delete folder via WebSocket
419
+ await ws.http('files:delete', {
420
+ filePath: folderPath,
421
+ force: true // Allow deletion of non-empty directories
422
+ });
423
+
424
+ // Store current path before clearing dialog state
425
+ const pathToReload = currentPath;
426
+
427
+ // Clear selection if deleted folder was selected
428
+ if (selectedPath === folderPath) {
429
+ selectedPath = pathToReload;
430
+ }
431
+
432
+ // Close delete dialog
433
+ showDeleteFolder = false;
434
+ folderToDelete = null;
435
+ deleteFolderConfirmName = '';
436
+
437
+ // Add a small delay to ensure file system operation is complete
438
+ await new Promise(resolve => setTimeout(resolve, 100));
439
+
440
+ // Reload current directory using the main loadDirectory function
441
+ if (pathToReload) {
442
+ loadDirectory(pathToReload);
443
+ } else {
444
+ // Fallback to current path if somehow it's empty
445
+ const fallbackPath = currentPath || '.';
446
+ loadDirectory(fallbackPath);
447
+ }
448
+ } catch (err) {
449
+ error = err instanceof Error ? err.message : 'Failed to delete folder';
450
+ }
451
+ }
452
+
453
+ // Initialize when dialog opens
454
+ $effect(() => {
455
+ if (isOpen) {
456
+ // Load available drives/mount points for all platforms
457
+ loadAvailableDrives();
458
+
459
+ // Load recent projects from database
460
+ loadRecentProjects();
461
+
462
+ getInitialPath().then(path => {
463
+ loadDirectory(path);
464
+ });
465
+ }
466
+ });
467
+
468
+ // Sync manualPath with currentPath
469
+ $effect(() => {
470
+ manualPath = currentPath;
471
+ });
472
+
473
+ // Cleanup function to clear timeout when component unmounts
474
+ $effect(() => {
475
+ return () => {
476
+ if (loadingTimeout) {
477
+ clearTimeout(loadingTimeout);
478
+ loadingTimeout = null;
479
+ }
480
+ };
481
+ });
482
+ </script>
483
+
484
+ <svelte:window onkeydown={handleKeydown} />
485
+
486
+ <Modal
487
+ bind:isOpen={isOpen}
488
+ onClose={onClose}
489
+ size="xl"
490
+ className=""
491
+ >
492
+ {#snippet header()}
493
+ <div class="bg-slate-50 dark:bg-slate-800">
494
+ <div class="flex items-center justify-between px-4 md:px-6 pt-4 mb-2">
495
+ <div>
496
+ <h2 class="text-lg font-semibold text-slate-900 dark:text-slate-100">
497
+ Select Project Folder
498
+ </h2>
499
+ </div>
500
+ <button
501
+ type="button"
502
+ class="p-2 rounded-lg text-slate-500 hover:text-slate-700 dark:text-slate-400 dark:hover:text-slate-200 hover:bg-slate-100 dark:hover:bg-slate-800 transition-colors"
503
+ onclick={onClose}
504
+ aria-label="Close modal"
505
+ >
506
+ <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
507
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
508
+ </svg>
509
+ </button>
510
+ </div>
511
+
512
+ <!-- Path navigation -->
513
+ <div class="px-6 pb-4">
514
+ <div class="flex items-center space-x-2 text-sm">
515
+ <button
516
+ onclick={navigateToParent}
517
+ disabled={currentPath === '/' || loading || atRestrictionBoundary}
518
+ class="flex p-2 rounded-lg hover:bg-slate-100 dark:hover:bg-slate-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
519
+ aria-label="Go to parent directory"
520
+ >
521
+ <Icon name="lucide:arrow-up" class="text-slate-600 dark:text-slate-300" />
522
+ </button>
523
+
524
+ <input
525
+ bind:value={manualPath}
526
+ onkeydown={handleManualPathKeydown}
527
+ class="flex-1 font-mono text-slate-700 dark:text-slate-200 bg-white dark:bg-slate-800 px-3 py-2 rounded-lg border border-slate-200 dark:border-slate-600 focus:outline-none focus:ring-2 focus:ring-violet-500/20 focus:border-violet-500 dark:focus:border-violet-400"
528
+ placeholder={pathPlaceholder}
529
+ />
530
+ <button
531
+ onclick={navigateToManualPath}
532
+ disabled={!manualPath.trim()}
533
+ class="flex p-3 rounded-lg bg-violet-500 dark:bg-violet-600 hover:bg-violet-600 dark:hover:bg-violet-700 text-white disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
534
+ aria-label="Navigate to path"
535
+ >
536
+ <Icon name="lucide:arrow-right" class="text-slate-100" />
537
+ </button>
538
+
539
+ {#if showLoadingSpinner}
540
+ <div class="animate-spin rounded-full h-4 w-4 border-2 border-violet-500 border-t-transparent"></div>
541
+ {/if}
542
+ </div>
543
+
544
+ <!-- Quick actions -->
545
+ <div class="flex items-center justify-between mt-3">
546
+ <div class="flex items-center gap-2 flex-wrap">
547
+ {#if hasRestrictions}
548
+ <!-- Restricted mode: show allowed base paths as quick access -->
549
+ {#each settings.allowedBasePaths as basePath (basePath)}
550
+ <button
551
+ onclick={() => navigateToLocation(basePath)}
552
+ class="px-3 py-1.5 text-xs rounded-lg bg-slate-200 dark:bg-slate-700 hover:bg-slate-300 dark:hover:bg-slate-600 text-slate-700 dark:text-slate-300 transition-colors"
553
+ title="Go to {basePath}"
554
+ >
555
+ <Icon name="lucide:folder-lock" class="inline mr-1" />
556
+ {basePath.split(/[/\\]/).filter(Boolean).pop() || basePath}
557
+ </button>
558
+ {/each}
559
+ {:else}
560
+ <!-- Normal mode: home, system, drive buttons -->
561
+ <button
562
+ onclick={() => navigateToLocation('home')}
563
+ class="px-3 py-1.5 text-xs rounded-lg bg-slate-200 dark:bg-slate-700 hover:bg-slate-300 dark:hover:bg-slate-600 text-slate-700 dark:text-slate-300 transition-colors"
564
+ title="Go to home directory"
565
+ >
566
+ <Icon name="lucide:house" class="inline mr-1" />
567
+ Home
568
+ </button>
569
+ <button
570
+ onclick={() => navigateToLocation('drives')}
571
+ class="px-3 py-1.5 text-xs rounded-lg bg-slate-200 dark:bg-slate-700 hover:bg-slate-300 dark:hover:bg-slate-600 text-slate-700 dark:text-slate-300 transition-colors"
572
+ title="Browse system locations"
573
+ >
574
+ <Icon name="lucide:monitor" class="inline mr-1" />
575
+ System
576
+ </button>
577
+
578
+ <!-- Dynamic location buttons (all platforms) -->
579
+ {#if availableDrives.length > 0}
580
+ {#each availableDrives as location (location.path)}
581
+ <button
582
+ onclick={() => navigateToLocation(location.path)}
583
+ class="px-3 py-1.5 text-xs rounded-lg bg-slate-200 dark:bg-slate-700 hover:bg-slate-300 dark:hover:bg-slate-600 text-slate-700 dark:text-slate-300 transition-colors"
584
+ title="Go to {location.name}"
585
+ >
586
+ {#if location.path.match(/^[A-Z]:\\/i)}
587
+ <!-- Windows drive -->
588
+ <Icon name="lucide:hard-drive" class="inline mr-1" />
589
+ {location.path.replace(/\\/g, '')}
590
+ {:else if location.path === '/'}
591
+ <!-- Unix root -->
592
+ <Icon name="lucide:folder-root" class="inline mr-1" />
593
+ /
594
+ {:else if location.path.startsWith('/Volumes')}
595
+ <!-- macOS volume -->
596
+ <Icon name="lucide:disc" class="inline mr-1" />
597
+ {location.path.split('/').pop()}
598
+ {:else if location.path.includes('/mnt') || location.path.includes('/media')}
599
+ <!-- Linux mount point -->
600
+ <Icon name="lucide:hard-drive" class="inline mr-1" />
601
+ {location.path.split('/').pop()}
602
+ {:else}
603
+ <!-- Generic location -->
604
+ <Icon name="lucide:folder" class="inline mr-1" />
605
+ {location.path.split('/').pop() || location.name}
606
+ {/if}
607
+ </button>
608
+ {/each}
609
+ {/if}
610
+ {/if}
611
+ </div>
612
+
613
+ <div class="flex items-center space-x-2">
614
+ <button
615
+ onclick={() => showCreateFolder = true}
616
+ class="px-3 py-1.5 text-xs rounded-lg bg-slate-200 dark:bg-slate-700 hover:bg-slate-300 dark:hover:bg-slate-600 text-slate-700 dark:text-slate-300 transition-colors"
617
+ title="Create new folder in current directory"
618
+ >
619
+ <Icon name="lucide:folder-plus" class="inline sm:mr-1" />
620
+ <span class="hidden sm:inline">New Folder</span>
621
+ </button>
622
+ <!-- <button
623
+ onclick={selectCurrentFolder}
624
+ disabled={!currentPath}
625
+ class="px-3 py-1.5 text-xs rounded-lg bg-slate-200 dark:bg-slate-700 hover:bg-slate-300 dark:hover:bg-slate-600 text-slate-700 dark:text-slate-300 transition-colors"
626
+ >
627
+ <Icon name="lucide:check" class="inline mr-1" />
628
+ Use Current Folder
629
+ </button> -->
630
+ </div>
631
+ </div>
632
+ </div>
633
+ </div>
634
+ {/snippet}
635
+
636
+ {#snippet children()}
637
+ <!-- Scrollable directory contents -->
638
+ {#if error}
639
+ <div class="flex items-center justify-center py-12">
640
+ <div class="text-center">
641
+ <Icon name="lucide:triangle-alert" class="text-4xl text-red-500 mx-auto mb-4" />
642
+ <p class="text-red-600 dark:text-red-400 font-medium">Error Loading Directory</p>
643
+ <p class="text-sm text-slate-600 dark:text-slate-400 mt-2">{error}</p>
644
+ <Button
645
+ onclick={() => loadDirectory(currentPath)}
646
+ variant="outline"
647
+ size="sm"
648
+ class="mt-4"
649
+ >
650
+ Try Again
651
+ </Button>
652
+ </div>
653
+ </div>
654
+ {:else if showLoadingSpinner && items.length === 0}
655
+ <div class="flex items-center justify-center py-12">
656
+ <div class="text-center">
657
+ <div class="animate-spin rounded-full h-8 w-8 border-2 border-violet-500 border-t-transparent mx-auto mb-4"></div>
658
+ <p class="text-slate-600 dark:text-slate-400">Loading directory...</p>
659
+ </div>
660
+ </div>
661
+ {:else if items.length === 0}
662
+ <div class="flex items-center justify-center py-12">
663
+ <div class="text-center">
664
+ <Icon name="lucide:folder-x" class="text-4xl text-slate-400 mx-auto mb-4" />
665
+ <p class="text-slate-600 dark:text-slate-400">No folders found</p>
666
+ <p class="text-sm text-slate-500 dark:text-slate-500 mt-2">This directory doesn't contain any subdirectories</p>
667
+ </div>
668
+ </div>
669
+ {:else}
670
+ <div class="space-y-2 transition-opacity duration-300 {loading ? 'opacity-75' : 'opacity-100'}">
671
+ {#each items as item (item.path)}
672
+ <div
673
+ class="flex items-center space-x-3 py-3 px-4 rounded-xl border transition-all duration-200 cursor-pointer {selectedPath === item.path
674
+ ? 'bg-violet-50 dark:bg-violet-900/20 border-violet-200 dark:border-violet-700'
675
+ : isActiveProject(item.path)
676
+ ? 'bg-violet-50/50 dark:bg-violet-900/10 border-violet-300 dark:border-violet-700/50 hover:bg-violet-100/50 dark:hover:bg-violet-900/20'
677
+ : isRecentProject(item.path)
678
+ ? 'bg-green-50 dark:bg-green-900/10 border-green-200 dark:border-green-800/50 hover:bg-green-100 dark:hover:bg-green-900/20'
679
+ : 'bg-slate-50 dark:bg-slate-800/30 border-slate-200 dark:border-slate-700 hover:bg-slate-100 dark:hover:bg-slate-800/50'}"
680
+ onclick={() => {
681
+ if (item.type === 'directory') {
682
+ navigateToFolder(item.path);
683
+ }
684
+ }}
685
+ role="button"
686
+ tabindex="0"
687
+ onkeydown={(e) => {
688
+ if (e.key === 'Enter') {
689
+ if (item.type === 'directory') {
690
+ navigateToFolder(item.path);
691
+ }
692
+ }
693
+ }}
694
+ >
695
+ <div class="flex-shrink-0 relative">
696
+ {#if item.type === 'directory'}
697
+ <Icon
698
+ name="lucide:folder"
699
+ class="text-xl {selectedPath === item.path
700
+ ? 'text-violet-600 dark:text-violet-400'
701
+ : isActiveProject(item.path)
702
+ ? 'text-violet-600 dark:text-violet-400'
703
+ : isRecentProject(item.path)
704
+ ? 'text-green-600 dark:text-green-400'
705
+ : 'text-slate-500 dark:text-slate-400'}"
706
+ />
707
+ {#if isActiveProject(item.path)}
708
+ <div class="absolute -top-1 -right-1">
709
+ <Icon
710
+ name="lucide:star"
711
+ class="text-xs text-violet-600 dark:text-violet-400 bg-white dark:bg-slate-800 rounded-full p-0.5"
712
+ />
713
+ </div>
714
+ {:else if isRecentProject(item.path)}
715
+ <div class="absolute -top-1 -right-1">
716
+ <Icon
717
+ name="lucide:clock"
718
+ class="text-xs text-green-600 dark:text-green-400 bg-white dark:bg-slate-800 rounded-full p-0.5"
719
+ />
720
+ </div>
721
+ {/if}
722
+ {:else}
723
+ <Icon
724
+ name="lucide:file-text"
725
+ class="text-xl text-slate-500 dark:text-slate-400"
726
+ />
727
+ {/if}
728
+ </div>
729
+ <div class="flex-1 min-w-0">
730
+ <div class="flex items-center gap-2">
731
+ <p class="font-medium text-slate-900 dark:text-slate-100 truncate">
732
+ {item.name}
733
+ </p>
734
+ {#if isActiveProject(item.path)}
735
+ <span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-violet-100 dark:bg-violet-900/30 text-violet-800 dark:text-violet-300 border border-violet-200 dark:border-violet-800">
736
+ Active Project
737
+ </span>
738
+ {:else if isRecentProject(item.path)}
739
+ <span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-green-100 dark:bg-green-900/30 text-green-800 dark:text-green-300 border border-green-200 dark:border-green-800">
740
+ Recent Project
741
+ </span>
742
+ {/if}
743
+ </div>
744
+ {#if item.modified}
745
+ <p class="text-xs text-slate-500 dark:text-slate-400">
746
+ Modified {new Date(item.modified).toLocaleDateString()}
747
+ </p>
748
+ {/if}
749
+ </div>
750
+ {#if item.type === 'directory'}
751
+ <div class="flex-shrink-0 flex items-center gap-2">
752
+ <button
753
+ onclick={(e) => {
754
+ e.stopPropagation();
755
+ openDeleteDialog(item);
756
+ }}
757
+ class="flex p-1.5 rounded-lg hover:bg-red-100 dark:hover:bg-red-900/20 transition-colors group"
758
+ title="Delete folder"
759
+ aria-label="Delete {item.name}"
760
+ >
761
+ <Icon
762
+ name="lucide:trash-2"
763
+ class="text-sm text-slate-400 dark:text-slate-500 group-hover:text-red-500 dark:group-hover:text-red-400"
764
+ />
765
+ </button>
766
+ <Icon
767
+ name="lucide:chevron-right"
768
+ class="text-sm text-slate-400 dark:text-slate-500"
769
+ />
770
+ </div>
771
+ {/if}
772
+ </div>
773
+ {/each}
774
+ </div>
775
+ {/if}
776
+ {/snippet}
777
+
778
+ {#snippet footer()}
779
+ <div class="flex items-end justify-between flex-wrap sm:flex-nowrap gap-3 w-full">
780
+ {#if selectedPath}
781
+ <div class="w-full">
782
+ <p class="text-sm text-slate-600 dark:text-slate-400 mb-2">Selected folder:</p>
783
+ <div class="font-mono text-sm bg-slate-50 dark:bg-slate-800 px-3 py-2 rounded-lg border border-slate-200 dark:border-slate-600 text-slate-700 dark:text-slate-200 break-all">
784
+ {selectedPath}
785
+ </div>
786
+ </div>
787
+ {/if}
788
+
789
+ <div class="flex items-center justify-between gap-3 flex-none w-full sm:w-auto">
790
+ <!-- <div class="flex items-center gap-3 ml-auto"> -->
791
+ <!-- <Button variant="outline" onclick={onClose}>
792
+ Cancel
793
+ </Button> -->
794
+ <Button
795
+ onclick={confirmSelection}
796
+ disabled={!selectedPath}
797
+ class="disabled:opacity-50 disabled:cursor-not-allowed w-full sm:w-auto"
798
+ >
799
+ Select Project
800
+ </Button>
801
+ <!-- </div> -->
802
+ </div>
803
+ </div>
804
+ {/snippet}
805
+ </Modal>
806
+
807
+ <!-- Create Folder Dialog -->
808
+ <Dialog
809
+ bind:isOpen={showCreateFolder}
810
+ onClose={() => {
811
+ showCreateFolder = false;
812
+ newFolderName = '';
813
+ }}
814
+ title="Create New Folder"
815
+ type="info"
816
+ message={`Create a new folder in: ${currentPath}`}
817
+ bind:inputValue={newFolderName}
818
+ inputPlaceholder="Enter folder name"
819
+ confirmText="Create"
820
+ cancelText="Cancel"
821
+ showCancel={true}
822
+ onConfirm={createNewFolder}
823
+ />
824
+
825
+ <!-- Delete Folder Dialog -->
826
+ <Dialog
827
+ bind:isOpen={showDeleteFolder}
828
+ onClose={() => {
829
+ showDeleteFolder = false;
830
+ folderToDelete = null;
831
+ deleteFolderConfirmName = '';
832
+ }}
833
+ title="Delete Folder"
834
+ type="warning"
835
+ message={`This action cannot be undone. To delete "${folderToDelete?.name || ''}", type the folder name exactly as shown:`}
836
+ bind:inputValue={deleteFolderConfirmName}
837
+ inputPlaceholder="Type folder name to confirm"
838
+ confirmText="Delete"
839
+ cancelText="Cancel"
840
+ showCancel={true}
841
+ confirmDisabled={deleteFolderConfirmName !== folderToDelete?.name}
842
+ onConfirm={deleteFolder}
843
+ />